nuke the tests and the dead code
data/ is runtime state, keep it out of git.
This commit is contained in:
parent
812052bc3e
commit
20a75dbb25
17 changed files with 3 additions and 1320 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
/target
|
/target
|
||||||
|
/data
|
||||||
*.json.tmp
|
*.json.tmp
|
||||||
docs/protocol.md
|
docs/protocol.md
|
||||||
|
|
|
||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -81,8 +81,6 @@ name = "gateway"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"auth-service",
|
|
||||||
"game-service",
|
|
||||||
"logic",
|
"logic",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"next_low": 2,
|
|
||||||
"accounts": [
|
|
||||||
{
|
|
||||||
"high": 0,
|
|
||||||
"low": 1,
|
|
||||||
"pass_token": "KIzXwApNr3fe6Yn0SOtv1gm9kOFIZnl3CAFT92EQ",
|
|
||||||
"created_at": 1787460399,
|
|
||||||
"last_seen_at": 1787460399,
|
|
||||||
"session_count": 1,
|
|
||||||
"play_time_seconds": 0,
|
|
||||||
"banned": false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,182 +0,0 @@
|
||||||
{
|
|
||||||
"profiles": [
|
|
||||||
{
|
|
||||||
"account": {
|
|
||||||
"high": 0,
|
|
||||||
"low": 1
|
|
||||||
},
|
|
||||||
"name": "Scroll1",
|
|
||||||
"name_set_by_user": false,
|
|
||||||
"exp_level": 1,
|
|
||||||
"exp_points": 0,
|
|
||||||
"gold": 5026,
|
|
||||||
"diamonds": 1000,
|
|
||||||
"free_diamonds": 0,
|
|
||||||
"trophies": 0,
|
|
||||||
"arena": {
|
|
||||||
"table": 54,
|
|
||||||
"name": "Arena1"
|
|
||||||
},
|
|
||||||
"chest_slot_count": 4,
|
|
||||||
"deck": [
|
|
||||||
{
|
|
||||||
"card": {
|
|
||||||
"table": 26,
|
|
||||||
"name": "Knight"
|
|
||||||
},
|
|
||||||
"level_index": 0,
|
|
||||||
"count": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"card": {
|
|
||||||
"table": 26,
|
|
||||||
"name": "Archer"
|
|
||||||
},
|
|
||||||
"level_index": 0,
|
|
||||||
"count": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"card": {
|
|
||||||
"table": 26,
|
|
||||||
"name": "Goblins"
|
|
||||||
},
|
|
||||||
"level_index": 0,
|
|
||||||
"count": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"card": {
|
|
||||||
"table": 26,
|
|
||||||
"name": "Giant"
|
|
||||||
},
|
|
||||||
"level_index": 0,
|
|
||||||
"count": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"card": {
|
|
||||||
"table": 26,
|
|
||||||
"name": "Pekka"
|
|
||||||
},
|
|
||||||
"level_index": 0,
|
|
||||||
"count": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"card": {
|
|
||||||
"table": 26,
|
|
||||||
"name": "Minions"
|
|
||||||
},
|
|
||||||
"level_index": 0,
|
|
||||||
"count": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"card": {
|
|
||||||
"table": 26,
|
|
||||||
"name": "Balloon"
|
|
||||||
},
|
|
||||||
"level_index": 0,
|
|
||||||
"count": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"card": {
|
|
||||||
"table": 26,
|
|
||||||
"name": "Witch"
|
|
||||||
},
|
|
||||||
"level_index": 0,
|
|
||||||
"count": 10
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"collection": [
|
|
||||||
{
|
|
||||||
"card": {
|
|
||||||
"table": 26,
|
|
||||||
"name": "Barbarians"
|
|
||||||
},
|
|
||||||
"level_index": 0,
|
|
||||||
"count": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"card": {
|
|
||||||
"table": 26,
|
|
||||||
"name": "Golem"
|
|
||||||
},
|
|
||||||
"level_index": 0,
|
|
||||||
"count": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"card": {
|
|
||||||
"table": 26,
|
|
||||||
"name": "Skeletons"
|
|
||||||
},
|
|
||||||
"level_index": 0,
|
|
||||||
"count": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"card": {
|
|
||||||
"table": 26,
|
|
||||||
"name": "Valkyrie"
|
|
||||||
},
|
|
||||||
"level_index": 0,
|
|
||||||
"count": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"card": {
|
|
||||||
"table": 26,
|
|
||||||
"name": "SkeletonArmy"
|
|
||||||
},
|
|
||||||
"level_index": 0,
|
|
||||||
"count": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"card": {
|
|
||||||
"table": 26,
|
|
||||||
"name": "Bomber"
|
|
||||||
},
|
|
||||||
"level_index": 0,
|
|
||||||
"count": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"card": {
|
|
||||||
"table": 26,
|
|
||||||
"name": "Musketeer"
|
|
||||||
},
|
|
||||||
"level_index": 0,
|
|
||||||
"count": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"card": {
|
|
||||||
"table": 26,
|
|
||||||
"name": "BabyDragon"
|
|
||||||
},
|
|
||||||
"level_index": 0,
|
|
||||||
"count": 10
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"card": {
|
|
||||||
"table": 26,
|
|
||||||
"name": "Prince"
|
|
||||||
},
|
|
||||||
"level_index": 0,
|
|
||||||
"count": 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"card": {
|
|
||||||
"table": 26,
|
|
||||||
"name": "SpearGoblins"
|
|
||||||
},
|
|
||||||
"level_index": 1,
|
|
||||||
"count": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"battle_count": 0,
|
|
||||||
"win_count": 0,
|
|
||||||
"lose_count": 0,
|
|
||||||
"npc_win_count": 1,
|
|
||||||
"npc_lose_count": 0,
|
|
||||||
"three_crown_wins": 0,
|
|
||||||
"tutorials_finished": true,
|
|
||||||
"gold_resource": {
|
|
||||||
"table": 5,
|
|
||||||
"name": "Gold"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,194 +0,0 @@
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use logic::{table, LogicDataTables};
|
|
||||||
use titan::GlobalId;
|
|
||||||
struct TempTree {
|
|
||||||
root: PathBuf,
|
|
||||||
}
|
|
||||||
impl TempTree {
|
|
||||||
fn new(label: &str) -> Self {
|
|
||||||
let root = std::env::temp_dir().join(format!(
|
|
||||||
"scroll-data-{label}-{}-{:?}",
|
|
||||||
std::process::id(),
|
|
||||||
std::thread::current().id()
|
|
||||||
));
|
|
||||||
let _ = std::fs::remove_dir_all(&root);
|
|
||||||
std::fs::create_dir_all(root.join("csv_logic")).expect("create tree");
|
|
||||||
Self { root }
|
|
||||||
}
|
|
||||||
fn write(&self, relative: &str, contents: &str) {
|
|
||||||
let path = self.root.join(relative);
|
|
||||||
if let Some(parent) = path.parent() {
|
|
||||||
std::fs::create_dir_all(parent).expect("create parent");
|
|
||||||
}
|
|
||||||
std::fs::write(path, contents).expect("write fixture");
|
|
||||||
}
|
|
||||||
fn path(&self) -> &Path {
|
|
||||||
&self.root
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Drop for TempTree {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
let _ = std::fs::remove_dir_all(&self.root);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn fixture(label: &str) -> TempTree {
|
|
||||||
let tree = TempTree::new(label);
|
|
||||||
tree.write(
|
|
||||||
"csv_logic/spells_characters.csv",
|
|
||||||
concat!(
|
|
||||||
"Name,Rarity,ManaCost\n",
|
|
||||||
"string,string,int\n",
|
|
||||||
"Knight,Common,3\n",
|
|
||||||
"Archers,Common,3\n",
|
|
||||||
"Giant,Rare,5\n"
|
|
||||||
),
|
|
||||||
);
|
|
||||||
tree.write(
|
|
||||||
"csv_logic/spells_buildings.csv",
|
|
||||||
concat!(
|
|
||||||
"Name,Rarity,ManaCost\n",
|
|
||||||
"string,string,int\n",
|
|
||||||
"Cannon,Common,3\n",
|
|
||||||
"Tesla,Common,4\n"
|
|
||||||
),
|
|
||||||
);
|
|
||||||
tree.write(
|
|
||||||
"csv_logic/arenas.csv",
|
|
||||||
concat!(
|
|
||||||
"Name,Arena,TrophyLimit\n",
|
|
||||||
"string,int,int\n",
|
|
||||||
"TrainingCamp,0,0\n",
|
|
||||||
"Arena1,1,400\n"
|
|
||||||
),
|
|
||||||
);
|
|
||||||
tree.write(
|
|
||||||
"csv_logic/resources.csv",
|
|
||||||
concat!(
|
|
||||||
"Name,Cap\n",
|
|
||||||
"string,int\n",
|
|
||||||
"Gold,1000000\n",
|
|
||||||
"Diamonds,0\n",
|
|
||||||
"FreeGold,0\n"
|
|
||||||
),
|
|
||||||
);
|
|
||||||
tree
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn tables_are_indexed_by_the_client_table_ids() {
|
|
||||||
let tree = fixture("ids");
|
|
||||||
let tables = LogicDataTables::load_from_dir(tree.path()).expect("load");
|
|
||||||
assert!(tables.is_loaded());
|
|
||||||
assert_eq!(tables.loaded_files().len(), 4);
|
|
||||||
assert!(!tables.skipped_files().is_empty());
|
|
||||||
assert_eq!(
|
|
||||||
tables.table(table::SPELLS_CHARACTERS).map(|t| t.count()),
|
|
||||||
Some(3)
|
|
||||||
);
|
|
||||||
assert_eq!(tables.table(table::ARENAS).map(|t| t.count()), Some(2));
|
|
||||||
assert!(tables.table(table::ACHIEVEMENTS).is_none());
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn names_resolve_to_the_global_id_that_travels_on_the_wire() {
|
|
||||||
let tree = fixture("names");
|
|
||||||
let tables = LogicDataTables::load_from_dir(tree.path()).expect("load");
|
|
||||||
assert_eq!(
|
|
||||||
tables.global_id_by_name(table::SPELLS, "Knight"),
|
|
||||||
Some(GlobalId::new(table::SPELLS_CHARACTERS, 0))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
tables.global_id_by_name(table::SPELLS, "Giant"),
|
|
||||||
Some(GlobalId::new(table::SPELLS_CHARACTERS, 2))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
tables.global_id_by_name(table::SPELLS, "Tesla"),
|
|
||||||
Some(GlobalId::new(table::SPELLS_BUILDINGS, 1))
|
|
||||||
);
|
|
||||||
assert_eq!(tables.global_id_by_name(table::SPELLS, "Golem"), None);
|
|
||||||
assert_eq!(
|
|
||||||
tables.global_id_by_name(table::ARENAS, "Arena1"),
|
|
||||||
Some(GlobalId::new(table::ARENAS, 1))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
tables.global_id_by_name(table::RESOURCES, "Gold"),
|
|
||||||
Some(GlobalId::new(table::RESOURCES, 0))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn a_global_id_resolves_back_to_its_row() {
|
|
||||||
let tree = fixture("roundtrip");
|
|
||||||
let tables = LogicDataTables::load_from_dir(tree.path()).expect("load");
|
|
||||||
let knight = tables
|
|
||||||
.data_by_id(GlobalId::new(table::SPELLS_CHARACTERS, 0))
|
|
||||||
.expect("knight");
|
|
||||||
assert_eq!(knight.name(), "Knight");
|
|
||||||
assert_eq!(knight.int("ManaCost"), 3);
|
|
||||||
assert_eq!(knight.string("Rarity"), "Common");
|
|
||||||
assert_eq!(knight.global_id(), GlobalId::new(table::SPELLS_CHARACTERS, 0));
|
|
||||||
assert!(tables
|
|
||||||
.data_by_id(GlobalId::new(table::SPELLS_CHARACTERS, 99))
|
|
||||||
.is_none());
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn an_empty_directory_is_reported_rather_than_silently_accepted() {
|
|
||||||
let tree = TempTree::new("empty");
|
|
||||||
let error = LogicDataTables::load_from_dir(tree.path()).expect_err("must fail");
|
|
||||||
assert!(error.to_string().contains("no data table"));
|
|
||||||
assert!(!LogicDataTables::empty().is_loaded());
|
|
||||||
}
|
|
||||||
fn shipped_assets() -> Option<PathBuf> {
|
|
||||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
||||||
.join("../../assets")
|
|
||||||
.canonicalize()
|
|
||||||
.ok()?;
|
|
||||||
root.join("csv_logic/spells_characters.csv").is_file().then_some(root)
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn data_references_round_trip_whether_or_not_the_tables_are_installed() {
|
|
||||||
use titan::Payload;
|
|
||||||
let raw = logic::LogicDataRef::of(table::SPELLS_CHARACTERS, 3);
|
|
||||||
let bytes = raw.to_bytes().expect("encode");
|
|
||||||
let decoded = logic::LogicDataRef::from_bytes(&bytes).expect("decode");
|
|
||||||
assert_eq!(decoded, raw);
|
|
||||||
assert_eq!(decoded.global_id(), Some(GlobalId::new(table::SPELLS_CHARACTERS, 3)));
|
|
||||||
let empty = logic::LogicDataRef::None;
|
|
||||||
let bytes = empty.to_bytes().expect("encode");
|
|
||||||
assert_eq!(bytes, vec![0]);
|
|
||||||
assert!(logic::LogicDataRef::from_bytes(&bytes).expect("decode").is_none());
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn installed_tables_turn_references_into_typed_data_classes() {
|
|
||||||
let Some(root) = shipped_assets() else {
|
|
||||||
eprintln!("assets/ is absent, skipping the typed data class check");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let tables = LogicDataTables::load_from_dir(&root).expect("load");
|
|
||||||
logic::LogicDataTables::install(std::sync::Arc::new(tables));
|
|
||||||
let knight = logic::LogicDataRef::spell("Knight");
|
|
||||||
assert!(knight.is_resolved());
|
|
||||||
assert_eq!(knight.name(), "Knight");
|
|
||||||
assert_eq!(knight.to_string(), "Knight");
|
|
||||||
assert_eq!(
|
|
||||||
knight.global_id(),
|
|
||||||
Some(GlobalId::new(table::SPELLS_CHARACTERS, 0))
|
|
||||||
);
|
|
||||||
let spell = knight.as_spell().expect("spell data");
|
|
||||||
assert_eq!(spell.rarity(), "Common");
|
|
||||||
assert_eq!(spell.mana_cost(), 3);
|
|
||||||
assert!(knight.as_arena().is_none());
|
|
||||||
let arena = logic::LogicDataRef::arena("TrainingCamp");
|
|
||||||
let arena_data = arena.as_arena().expect("arena data");
|
|
||||||
assert!(arena_data.is_training_camp());
|
|
||||||
assert_eq!(arena_data.trophy_limit(), 0);
|
|
||||||
let card = logic::model::LogicSpell::card(knight.clone(), 0, 10);
|
|
||||||
assert_eq!(card.mana_cost(), 3);
|
|
||||||
assert_eq!(card.level(), 1);
|
|
||||||
let rarity = card.rarity().expect("rarity data");
|
|
||||||
assert_eq!(rarity.name(), "Common");
|
|
||||||
assert_eq!(rarity.level_count(), 12);
|
|
||||||
assert_eq!(rarity.upgrade_cost(0), 5);
|
|
||||||
let chest = logic::LogicDataRef::treasure_chest("Silver")
|
|
||||||
.as_treasure_chest()
|
|
||||||
.expect("chest data");
|
|
||||||
assert!(chest.total_time_taken_seconds() > 0);
|
|
||||||
assert!(chest.max_gold() >= chest.min_gold());
|
|
||||||
}
|
|
||||||
|
|
@ -1,163 +0,0 @@
|
||||||
use logic::model::{
|
|
||||||
ChestSource, LogicAllianceInfo, LogicChest, LogicClientAvatar, LogicClientHome, LogicDataSlot,
|
|
||||||
LogicSpell, LogicSpellCollection, LogicSpellDeck, LogicTimer,
|
|
||||||
};
|
|
||||||
use logic::{DataId, LogicScrollMessageFactory, OwnHomeDataMessage};
|
|
||||||
use titan::{ByteStreamReader, ByteStreamWriter, LogicLong, MessageFactory, MessageMeta, Payload};
|
|
||||||
fn sample_message() -> OwnHomeDataMessage {
|
|
||||||
let deck = LogicSpellDeck::from_cards((0..8).map(|index| {
|
|
||||||
LogicSpell::card(DataId::character_card(index), index % 3, 10 + index)
|
|
||||||
}));
|
|
||||||
let collection = LogicSpellCollection::from_spells(
|
|
||||||
(0..12)
|
|
||||||
.map(|index| LogicSpell::card(DataId::character_card(index), 1, index))
|
|
||||||
.collect(),
|
|
||||||
);
|
|
||||||
let mut home = LogicClientHome {
|
|
||||||
home_id: LogicLong::new(0, 42),
|
|
||||||
chest_id_counter: 7,
|
|
||||||
free_chest_collect_count: 3,
|
|
||||||
donation_capacity_limit_timer: LogicTimer::started(86_400, 1_000),
|
|
||||||
donated_capacity: 12,
|
|
||||||
decks: vec![deck, LogicSpellDeck::default()],
|
|
||||||
spell_collection: collection,
|
|
||||||
selected_deck_index: 0,
|
|
||||||
chest_slots: vec![
|
|
||||||
Some(LogicChest::locked(
|
|
||||||
DataId::treasure_chest(0),
|
|
||||||
1,
|
|
||||||
0,
|
|
||||||
ChestSource::Battle,
|
|
||||||
)),
|
|
||||||
None,
|
|
||||||
Some(LogicChest {
|
|
||||||
unlock_timer: Some(LogicTimer::started(3_600, 500)),
|
|
||||||
..LogicChest::locked(DataId::treasure_chest(1), 2, 2, ChestSource::Crown)
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
free_chest_timer: LogicTimer::started(14_400, 100),
|
|
||||||
donation_cooldown_timer: LogicTimer::idle(),
|
|
||||||
free_chest: Some(LogicChest::locked(
|
|
||||||
DataId::treasure_chest(2),
|
|
||||||
3,
|
|
||||||
-1,
|
|
||||||
ChestSource::Free,
|
|
||||||
)),
|
|
||||||
purchased_chest: None,
|
|
||||||
crowns_towards_crown_chest: 4,
|
|
||||||
star_chest_timer_pending: true,
|
|
||||||
star_chest_timer: LogicTimer::idle(),
|
|
||||||
crown_chest: None,
|
|
||||||
request_spells_cooldown_timer: LogicTimer::idle(),
|
|
||||||
finished_tutorials: [0; 8],
|
|
||||||
opened_pages: 0b1011,
|
|
||||||
last_shown_level_up: 2,
|
|
||||||
arena: DataId::arena(1),
|
|
||||||
shop_seed: -1,
|
|
||||||
shop_weekday_index: 3,
|
|
||||||
shop_weekday_index_seen: 3,
|
|
||||||
shop_timer: LogicTimer::started(600, 900),
|
|
||||||
shop_spells: vec![
|
|
||||||
LogicDataSlot::new(DataId::character_card(4), 1),
|
|
||||||
LogicDataSlot::new(DataId::other_card(2), 0),
|
|
||||||
],
|
|
||||||
share_replay_cooldown_timer: LogicTimer::idle(),
|
|
||||||
send_team_mail_cooldown_timer: LogicTimer::idle(),
|
|
||||||
free_chest_diamond_index: 1,
|
|
||||||
crown_chest_diamond_index: 2,
|
|
||||||
card_usage_stats_battle_count: 25,
|
|
||||||
royal_tv_replay_version_major: 2,
|
|
||||||
royal_tv_replay_version_build: 1306,
|
|
||||||
royal_tv_replay_version_content: 8,
|
|
||||||
seen_replay_ids: vec![5, -9, 1_000_000],
|
|
||||||
card_request_message: Some("need arrows".to_owned()),
|
|
||||||
requested_cards: vec![DataId::other_card(1), DataId::character_card(3)],
|
|
||||||
};
|
|
||||||
home.set_tutorial_finished(0);
|
|
||||||
home.set_tutorial_finished(37);
|
|
||||||
let mut avatar = LogicClientAvatar {
|
|
||||||
avatar_id: LogicLong::new(0, 42),
|
|
||||||
account_id: LogicLong::new(0, 42),
|
|
||||||
home_id: LogicLong::new(0, 42),
|
|
||||||
name: "BladeWise".to_owned(),
|
|
||||||
name_set_by_user: true,
|
|
||||||
name_change_state: -1,
|
|
||||||
arena: DataId::arena(1),
|
|
||||||
score: 1_337,
|
|
||||||
legacy_flag: false,
|
|
||||||
commodities: Default::default(),
|
|
||||||
diamonds: 500,
|
|
||||||
free_diamonds: 25,
|
|
||||||
exp_points: 90,
|
|
||||||
exp_level: 6,
|
|
||||||
cumulative_purchased_diamonds: 0,
|
|
||||||
alliance: Some(LogicAllianceInfo {
|
|
||||||
alliance_id: LogicLong::new(0, 9),
|
|
||||||
name: "Scroll Devs".to_owned(),
|
|
||||||
badge: DataId::alliance_badge(16_000_000),
|
|
||||||
role: 2,
|
|
||||||
}),
|
|
||||||
battle_count: 40,
|
|
||||||
win_count: 22,
|
|
||||||
lose_count: 18,
|
|
||||||
win_loose_streak: 3,
|
|
||||||
npc_win_count: 4,
|
|
||||||
npc_lose_count: 0,
|
|
||||||
arena_before_last_npc_match: true,
|
|
||||||
};
|
|
||||||
avatar
|
|
||||||
.commodities
|
|
||||||
.set(0, vec![LogicDataSlot::new(DataId::resource(0), 12_500)]);
|
|
||||||
avatar.commodities.set(
|
|
||||||
1,
|
|
||||||
vec![LogicDataSlot::new(DataId::character_card(0), 3)],
|
|
||||||
);
|
|
||||||
OwnHomeDataMessage::new(home, avatar, 0x5EED)
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn own_home_data_roundtrips_byte_for_byte() {
|
|
||||||
let message = sample_message();
|
|
||||||
let bytes = message.to_bytes().unwrap();
|
|
||||||
let decoded = OwnHomeDataMessage::from_bytes(&bytes).unwrap();
|
|
||||||
assert_eq!(decoded, message);
|
|
||||||
assert_eq!(decoded.to_bytes().unwrap(), bytes);
|
|
||||||
let mut reader = ByteStreamReader::new(&bytes);
|
|
||||||
OwnHomeDataMessage::decode(&mut reader).unwrap();
|
|
||||||
assert_eq!(reader.remaining(), 0);
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn own_home_data_starts_with_the_raw_big_endian_home_id() {
|
|
||||||
let message = sample_message();
|
|
||||||
let bytes = message.to_bytes().unwrap();
|
|
||||||
assert_eq!(&bytes[0..8], &[0, 0, 0, 0, 0, 0, 0, 42]);
|
|
||||||
assert_eq!(bytes[8], 7);
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn message_metadata_matches_the_client_definition() {
|
|
||||||
assert_eq!(OwnHomeDataMessage::MESSAGE_TYPE, 24101);
|
|
||||||
assert_eq!(OwnHomeDataMessage::MESSAGE_VERSION, 0);
|
|
||||||
assert_eq!(OwnHomeDataMessage::NAME, "OwnHomeDataMessage");
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn the_factory_decodes_registered_messages() {
|
|
||||||
let registry = LogicScrollMessageFactory::registry();
|
|
||||||
let message = sample_message();
|
|
||||||
let bytes = message.to_bytes().unwrap();
|
|
||||||
let decoded = registry.create(24101, 0, &bytes).unwrap();
|
|
||||||
assert_eq!(decoded.message_type(), 24101);
|
|
||||||
assert_eq!(decoded.name(), "OwnHomeDataMessage");
|
|
||||||
let concrete = decoded
|
|
||||||
.as_any()
|
|
||||||
.downcast_ref::<OwnHomeDataMessage>()
|
|
||||||
.expect("downcast");
|
|
||||||
assert_eq!(concrete, &message);
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn empty_optionals_only_cost_a_single_packed_bit() {
|
|
||||||
let mut writer = ByteStreamWriter::new();
|
|
||||||
let home = LogicClientHome::default();
|
|
||||||
home.encode(&mut writer).unwrap();
|
|
||||||
let bytes = writer.into_inner();
|
|
||||||
let decoded = LogicClientHome::from_bytes(&bytes).unwrap();
|
|
||||||
assert_eq!(decoded, home);
|
|
||||||
}
|
|
||||||
|
|
@ -21,5 +21,3 @@ async-trait = { workspace = true }
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "game-service"
|
name = "game-service"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ impl HomeModeRegistry {
|
||||||
pub async fn close(&self, account: AccountRef) -> Option<Arc<Mutex<HomeMode>>> {
|
pub async fn close(&self, account: AccountRef) -> Option<Arc<Mutex<HomeMode>>> {
|
||||||
self.sessions.write().await.remove(&account)
|
self.sessions.write().await.remove(&account)
|
||||||
}
|
}
|
||||||
pub async fn len(&self) -> usize {
|
pub async fn session_count(&self) -> usize {
|
||||||
self.sessions.read().await.len()
|
self.sessions.read().await.len()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ impl GameApi for GameService {
|
||||||
}
|
}
|
||||||
async fn disconnect(&self, account: AccountRef) -> RpcResult<()> {
|
async fn disconnect(&self, account: AccountRef) -> RpcResult<()> {
|
||||||
self.persist(account).await?;
|
self.persist(account).await?;
|
||||||
let sessions = self.sessions.len().await;
|
let sessions = self.sessions.session_count().await;
|
||||||
tracing::debug!(%account, sessions, "session closed");
|
tracing::debug!(%account, sessions, "session closed");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use game_service::{CardRef, Catalog, DataSelector};
|
|
||||||
use logic::table;
|
|
||||||
fn assets_root() -> Option<PathBuf> {
|
|
||||||
let root = Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
||||||
.join("../../assets")
|
|
||||||
.canonicalize()
|
|
||||||
.ok()?;
|
|
||||||
root.join("csv_logic/spells_characters.csv").is_file().then_some(root)
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn the_shipped_client_tables_load_and_resolve_by_name() {
|
|
||||||
let Some(root) = assets_root() else {
|
|
||||||
eprintln!("assets/ is absent, skipping the shipped catalog check");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let catalog = Catalog::load(Some(&root));
|
|
||||||
assert!(catalog.is_loaded(), "catalog failed to load from {root:?}");
|
|
||||||
let tables = catalog.tables();
|
|
||||||
assert_eq!(tables.loaded_files().len(), 44);
|
|
||||||
assert!(tables.skipped_files().is_empty(), "{:?}", tables.skipped_files());
|
|
||||||
assert_eq!(
|
|
||||||
tables.global_id_by_name(table::ARENAS, "TrainingCamp"),
|
|
||||||
Some(titan::GlobalId::new(table::ARENAS, 0))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
tables.global_id_by_name(table::RESOURCES, "Gold"),
|
|
||||||
Some(titan::GlobalId::new(table::RESOURCES, 1))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
tables.global_id_by_name(table::RESOURCES, "Diamonds"),
|
|
||||||
Some(titan::GlobalId::new(table::RESOURCES, 0))
|
|
||||||
);
|
|
||||||
let knight = tables.spell_by_name("Knight").expect("Knight");
|
|
||||||
assert_eq!(knight.global_id().class_id, table::SPELLS_CHARACTERS);
|
|
||||||
assert_eq!(knight.global_id().instance_id, 0);
|
|
||||||
let fireball = tables.spell_by_name("Fireball").expect("Fireball");
|
|
||||||
assert_eq!(fireball.global_id().class_id, table::SPELLS_OTHER);
|
|
||||||
let cannon = tables.spell_by_name("Cannon").expect("Cannon");
|
|
||||||
assert_eq!(cannon.global_id().class_id, table::SPELLS_BUILDINGS);
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn continuation_rows_build_the_per_level_arrays() {
|
|
||||||
let Some(root) = assets_root() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let catalog = Catalog::load(Some(&root));
|
|
||||||
let tables = catalog.tables();
|
|
||||||
let common = tables
|
|
||||||
.data_by_name(table::RARITIES, "Common")
|
|
||||||
.expect("Common rarity");
|
|
||||||
assert!(
|
|
||||||
common.array_size() > 1,
|
|
||||||
"rarities.csv uses continuation rows for the upgrade curve"
|
|
||||||
);
|
|
||||||
assert_eq!(common.int_at("UpgradeCost", 0), 5);
|
|
||||||
assert_eq!(common.int_at("UpgradeCost", 1), 20);
|
|
||||||
assert_eq!(common.int("LevelCount"), 12);
|
|
||||||
assert_eq!(
|
|
||||||
common.int_at("UpgradeCost", 999),
|
|
||||||
common.int_at("UpgradeCost", common.array_size() - 1),
|
|
||||||
"reading past the end clamps to the last level"
|
|
||||||
);
|
|
||||||
let newbie = tables
|
|
||||||
.treasure_chest_by_name("Newbie1")
|
|
||||||
.expect("Newbie1 chest");
|
|
||||||
assert_eq!(newbie.string("Arena"), "TrainingCamp");
|
|
||||||
let row = newbie.row().expect("row");
|
|
||||||
let guaranteed = newbie
|
|
||||||
.csv_table()
|
|
||||||
.column_index("GuaranteedSpells")
|
|
||||||
.expect("column");
|
|
||||||
assert_eq!(row.value_count(guaranteed), 2);
|
|
||||||
assert_eq!(row.string_at(guaranteed, 0), "Knight");
|
|
||||||
assert_eq!(row.string_at(guaranteed, 1), "Arrows");
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn the_default_starter_profile_resolves_against_the_real_tables() {
|
|
||||||
let Some(root) = assets_root() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let catalog = Catalog::load(Some(&root));
|
|
||||||
let starter = game_service::StarterProfile::default();
|
|
||||||
for card in starter.deck.iter().chain(starter.extra_collection.iter()) {
|
|
||||||
assert!(
|
|
||||||
catalog.resolve_card(card).is_some(),
|
|
||||||
"starter card {card:?} does not resolve"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let gold = catalog.resolve_resource(&DataSelector::named("Gold"));
|
|
||||||
assert_eq!(gold.name(), "Gold");
|
|
||||||
assert_eq!(
|
|
||||||
gold.global_id(),
|
|
||||||
Some(titan::GlobalId::new(table::RESOURCES, 1))
|
|
||||||
);
|
|
||||||
let arena = catalog.resolve_arena(&starter.arena);
|
|
||||||
assert_eq!(arena.table_index(), Some(table::ARENAS));
|
|
||||||
assert!(catalog.resolve_card(&CardRef::named("NotACard")).is_none());
|
|
||||||
}
|
|
||||||
|
|
@ -20,8 +20,3 @@ async-trait = { workspace = true }
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "gateway"
|
name = "gateway"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
auth-service = { workspace = true }
|
|
||||||
game-service = { workspace = true }
|
|
||||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "net", "io-util", "sync", "time", "fs"] }
|
|
||||||
|
|
|
||||||
|
|
@ -1,331 +0,0 @@
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
|
||||||
use auth_service::{AuthConfig, AuthService};
|
|
||||||
use game_service::{GameConfig, GameService};
|
|
||||||
use gateway::{Backends, GatewayConfig};
|
|
||||||
use game_service::{CardRef, DataSelector, StarterProfile};
|
|
||||||
use logic::{message_type, table, KeepAliveMessage, LoginMessage, LoginOkMessage, OwnHomeDataMessage};
|
|
||||||
use titan::GlobalId;
|
|
||||||
use titan::crypto::SessionCipher;
|
|
||||||
use titan::{FrameCodec, FrameHeader, MessageFrame, Payload, HEADER_LEN};
|
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
||||||
use tokio::net::TcpStream;
|
|
||||||
struct FakeClient {
|
|
||||||
stream: TcpStream,
|
|
||||||
cipher: SessionCipher,
|
|
||||||
}
|
|
||||||
impl FakeClient {
|
|
||||||
async fn connect(port: u16) -> Self {
|
|
||||||
let stream = TcpStream::connect(("127.0.0.1", port)).await.expect("connect");
|
|
||||||
stream.set_nodelay(true).expect("nodelay");
|
|
||||||
Self {
|
|
||||||
stream,
|
|
||||||
cipher: SessionCipher::scroll_rc4(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async fn send<M: Payload + titan::MessageMeta>(&mut self, message: &M) {
|
|
||||||
let payload = message.to_bytes().expect("encode");
|
|
||||||
let cipher_text = self.cipher.encrypt_outbound(&payload).expect("encrypt");
|
|
||||||
let frame = MessageFrame::new(M::MESSAGE_TYPE, M::MESSAGE_VERSION, cipher_text);
|
|
||||||
let bytes = FrameCodec::encode(&frame).expect("frame");
|
|
||||||
self.stream.write_all(&bytes).await.expect("write");
|
|
||||||
self.stream.flush().await.expect("flush");
|
|
||||||
}
|
|
||||||
async fn close(self) {
|
|
||||||
drop(self.stream);
|
|
||||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
|
||||||
}
|
|
||||||
async fn receive(&mut self) -> (u16, Vec<u8>) {
|
|
||||||
let mut header_bytes = [0u8; HEADER_LEN];
|
|
||||||
tokio::time::timeout(
|
|
||||||
Duration::from_secs(5),
|
|
||||||
self.stream.read_exact(&mut header_bytes),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("header timeout")
|
|
||||||
.expect("header read");
|
|
||||||
let header = FrameHeader::parse(&header_bytes);
|
|
||||||
let mut payload = vec![0u8; header.payload_len];
|
|
||||||
tokio::time::timeout(Duration::from_secs(5), self.stream.read_exact(&mut payload))
|
|
||||||
.await
|
|
||||||
.expect("payload timeout")
|
|
||||||
.expect("payload read");
|
|
||||||
let plain = self.cipher.decrypt_inbound(&payload).expect("decrypt");
|
|
||||||
(header.message_type, plain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
struct TestServer {
|
|
||||||
port: u16,
|
|
||||||
directory: PathBuf,
|
|
||||||
}
|
|
||||||
impl Drop for TestServer {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
for attempt in 0..5 {
|
|
||||||
if std::fs::remove_dir_all(&self.directory).is_ok() && !self.directory.exists() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if attempt < 4 {
|
|
||||||
std::thread::sleep(Duration::from_millis(20));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async fn start_server(label: &str) -> TestServer {
|
|
||||||
start_server_with(label, |config| config).await
|
|
||||||
}
|
|
||||||
async fn start_server_with(
|
|
||||||
label: &str,
|
|
||||||
customise: impl FnOnce(GameConfig) -> GameConfig,
|
|
||||||
) -> TestServer {
|
|
||||||
let directory = std::env::temp_dir().join(format!("scroll-test-{label}-{}", std::process::id()));
|
|
||||||
let _ = std::fs::remove_dir_all(&directory);
|
|
||||||
std::fs::create_dir_all(&directory).expect("temp dir");
|
|
||||||
let auth = AuthService::bootstrap(AuthConfig {
|
|
||||||
store_path: directory.join("accounts.json"),
|
|
||||||
..AuthConfig::default()
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("auth");
|
|
||||||
let game = GameService::bootstrap(customise(GameConfig {
|
|
||||||
store_path: directory.join("players.json"),
|
|
||||||
..GameConfig::default()
|
|
||||||
}))
|
|
||||||
.await
|
|
||||||
.expect("game");
|
|
||||||
let config = GatewayConfig {
|
|
||||||
listen: "127.0.0.1:0".to_owned(),
|
|
||||||
..GatewayConfig::default()
|
|
||||||
};
|
|
||||||
let listener = gateway::bind(&config).await.expect("bind");
|
|
||||||
let port = listener.local_addr().expect("addr").port();
|
|
||||||
let backends = Backends::new(auth, game);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let _ = gateway::run_on(listener, config, Arc::clone(&backends)).await;
|
|
||||||
});
|
|
||||||
TestServer { port, directory }
|
|
||||||
}
|
|
||||||
fn write_card_catalog(root: &Path) {
|
|
||||||
std::fs::create_dir_all(root.join("csv_logic")).expect("csv dir");
|
|
||||||
std::fs::write(
|
|
||||||
root.join("csv_logic/spells_characters.csv"),
|
|
||||||
concat!(
|
|
||||||
"Name,Rarity,ManaCost\n",
|
|
||||||
"string,string,int\n",
|
|
||||||
"Knight,Common,3\n",
|
|
||||||
"Archers,Common,3\n",
|
|
||||||
"Giant,Rare,5\n"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.expect("write characters");
|
|
||||||
std::fs::write(
|
|
||||||
root.join("csv_logic/spells_other.csv"),
|
|
||||||
concat!(
|
|
||||||
"Name,Rarity,ManaCost\n",
|
|
||||||
"string,string,int\n",
|
|
||||||
"Fireball,Rare,4\n",
|
|
||||||
"Arrows,Common,3\n"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.expect("write other");
|
|
||||||
std::fs::write(
|
|
||||||
root.join("csv_logic/arenas.csv"),
|
|
||||||
concat!(
|
|
||||||
"Name,Arena,TrophyLimit\n",
|
|
||||||
"string,int,int\n",
|
|
||||||
"TrainingCamp,0,0\n",
|
|
||||||
"Arena1,1,400\n"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.expect("write arenas");
|
|
||||||
std::fs::write(
|
|
||||||
root.join("csv_logic/resources.csv"),
|
|
||||||
concat!("Name,Cap\n", "string,int\n", "Gold,1000000\n", "Diamonds,0\n"),
|
|
||||||
)
|
|
||||||
.expect("write resources");
|
|
||||||
}
|
|
||||||
fn fresh_login() -> LoginMessage {
|
|
||||||
LoginMessage {
|
|
||||||
client_major_version: 2,
|
|
||||||
client_minor_version: 0,
|
|
||||||
client_build: 1306,
|
|
||||||
resource_sha: Some("0".repeat(40)),
|
|
||||||
udid: Some("integration-test".to_owned()),
|
|
||||||
device: Some("iPhone8,1".to_owned()),
|
|
||||||
os_version: Some("12.5.7".to_owned()),
|
|
||||||
preferred_device_language: "EN".to_owned(),
|
|
||||||
..LoginMessage::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[tokio::test]
|
|
||||||
async fn a_fresh_client_reaches_the_lobby() {
|
|
||||||
let server = start_server("lobby").await;
|
|
||||||
let mut client = FakeClient::connect(server.port).await;
|
|
||||||
client.send(&fresh_login()).await;
|
|
||||||
let (message_type, payload) = client.receive().await;
|
|
||||||
assert_eq!(message_type, message_type::LOGIN_OK);
|
|
||||||
let login_ok = LoginOkMessage::from_bytes(&payload).expect("decode login ok");
|
|
||||||
assert_eq!(login_ok.server_major_version, 2);
|
|
||||||
assert_eq!(login_ok.server_build, 1306);
|
|
||||||
assert!(!login_ok.account_id.is_zero());
|
|
||||||
assert_eq!(login_ok.pass_token.as_deref().map(str::len), Some(40));
|
|
||||||
assert_eq!(login_ok.account_id, login_ok.home_id);
|
|
||||||
let (message_type, payload) = client.receive().await;
|
|
||||||
assert_eq!(message_type, message_type::OWN_HOME_DATA);
|
|
||||||
let home_data = OwnHomeDataMessage::from_bytes(&payload).expect("decode own home data");
|
|
||||||
assert_eq!(home_data.home.home_id, login_ok.home_id);
|
|
||||||
assert_eq!(home_data.avatar.account_id, login_ok.account_id);
|
|
||||||
assert_eq!(home_data.home.decks.len(), 1);
|
|
||||||
assert_eq!(home_data.home.decks[0].filled_slot_count(), 8);
|
|
||||||
assert_eq!(home_data.home.spell_collection.spells.len(), 8);
|
|
||||||
assert_eq!(home_data.home.chest_slots.len(), 4);
|
|
||||||
assert_eq!(home_data.avatar.diamonds, 1_000);
|
|
||||||
assert_eq!(home_data.avatar.exp_level, 1);
|
|
||||||
assert_eq!(home_data.avatar.name, format!("Scroll{}", login_ok.account_id.low));
|
|
||||||
client.send(&KeepAliveMessage::default()).await;
|
|
||||||
let (message_type, payload) = client.receive().await;
|
|
||||||
assert_eq!(message_type, message_type::KEEP_ALIVE_SERVER);
|
|
||||||
assert!(payload.is_empty());
|
|
||||||
client.close().await;
|
|
||||||
}
|
|
||||||
#[tokio::test]
|
|
||||||
async fn a_fresh_avatar_does_not_trigger_the_first_tutorial_battle() {
|
|
||||||
let server = start_server("tutorial").await;
|
|
||||||
let mut client = FakeClient::connect(server.port).await;
|
|
||||||
client.send(&fresh_login()).await;
|
|
||||||
let _ = client.receive().await;
|
|
||||||
let (message_type, payload) = client.receive().await;
|
|
||||||
assert_eq!(message_type, message_type::OWN_HOME_DATA);
|
|
||||||
let home_data = OwnHomeDataMessage::from_bytes(&payload).expect("decode");
|
|
||||||
let avatar = &home_data.avatar;
|
|
||||||
assert!(
|
|
||||||
avatar.npc_lose_count != 0 || avatar.npc_win_count != 0,
|
|
||||||
"LogicClientAvatar::shouldGoToFirstTutorialBattle returns true when both npc counters \
|
|
||||||
are zero, which sends InitState into GameMode(1) instead of the home screen"
|
|
||||||
);
|
|
||||||
client.close().await;
|
|
||||||
}
|
|
||||||
#[tokio::test]
|
|
||||||
async fn reconnecting_with_the_issued_pass_token_keeps_the_account() {
|
|
||||||
let server = start_server("reconnect").await;
|
|
||||||
let (account_id, pass_token) = {
|
|
||||||
let mut client = FakeClient::connect(server.port).await;
|
|
||||||
client.send(&fresh_login()).await;
|
|
||||||
let (_, payload) = client.receive().await;
|
|
||||||
let login_ok = LoginOkMessage::from_bytes(&payload).expect("decode");
|
|
||||||
let _ = client.receive().await;
|
|
||||||
let credentials = (login_ok.account_id, login_ok.pass_token.expect("token"));
|
|
||||||
client.close().await;
|
|
||||||
credentials
|
|
||||||
};
|
|
||||||
let mut client = FakeClient::connect(server.port).await;
|
|
||||||
client
|
|
||||||
.send(&LoginMessage {
|
|
||||||
account_id,
|
|
||||||
pass_token: Some(pass_token.clone()),
|
|
||||||
..fresh_login()
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let (message_type, payload) = client.receive().await;
|
|
||||||
assert_eq!(message_type, message_type::LOGIN_OK);
|
|
||||||
let login_ok = LoginOkMessage::from_bytes(&payload).expect("decode");
|
|
||||||
assert_eq!(login_ok.account_id, account_id);
|
|
||||||
assert_eq!(login_ok.pass_token.as_deref(), Some(pass_token.as_str()));
|
|
||||||
assert!(login_ok.session_count >= 2);
|
|
||||||
let (message_type, _) = client.receive().await;
|
|
||||||
assert_eq!(message_type, message_type::OWN_HOME_DATA);
|
|
||||||
client.close().await;
|
|
||||||
}
|
|
||||||
#[tokio::test]
|
|
||||||
async fn a_wrong_pass_token_is_rejected_with_login_failed() {
|
|
||||||
let server = start_server("rejected").await;
|
|
||||||
let account_id = {
|
|
||||||
let mut client = FakeClient::connect(server.port).await;
|
|
||||||
client.send(&fresh_login()).await;
|
|
||||||
let (_, payload) = client.receive().await;
|
|
||||||
let login_ok = LoginOkMessage::from_bytes(&payload).expect("decode");
|
|
||||||
let _ = client.receive().await;
|
|
||||||
let account = login_ok.account_id;
|
|
||||||
client.close().await;
|
|
||||||
account
|
|
||||||
};
|
|
||||||
let mut client = FakeClient::connect(server.port).await;
|
|
||||||
client
|
|
||||||
.send(&LoginMessage {
|
|
||||||
account_id,
|
|
||||||
pass_token: Some("not-the-issued-token".to_owned()),
|
|
||||||
..fresh_login()
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let (message_type, payload) = client.receive().await;
|
|
||||||
assert_eq!(message_type, message_type::LOGIN_FAILED);
|
|
||||||
let failure = logic::LoginFailedMessage::from_bytes(&payload).expect("decode");
|
|
||||||
assert_eq!(failure.error_code, 1);
|
|
||||||
client.close().await;
|
|
||||||
}
|
|
||||||
#[tokio::test]
|
|
||||||
async fn go_home_reissues_the_lobby_snapshot() {
|
|
||||||
let server = start_server("gohome").await;
|
|
||||||
let mut client = FakeClient::connect(server.port).await;
|
|
||||||
client.send(&fresh_login()).await;
|
|
||||||
let _ = client.receive().await;
|
|
||||||
let (_, first) = client.receive().await;
|
|
||||||
client
|
|
||||||
.send(&logic::GoHomeMessage {
|
|
||||||
from_offer_wall: false,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let (message_type, second) = client.receive().await;
|
|
||||||
assert_eq!(message_type, message_type::OWN_HOME_DATA);
|
|
||||||
assert_eq!(first, second);
|
|
||||||
client.close().await;
|
|
||||||
}
|
|
||||||
#[tokio::test]
|
|
||||||
async fn a_starter_profile_can_name_its_cards_when_the_csv_catalog_is_present() {
|
|
||||||
let catalog_root =
|
|
||||||
std::env::temp_dir().join(format!("scroll-test-cards-{}", std::process::id()));
|
|
||||||
let _ = std::fs::remove_dir_all(&catalog_root);
|
|
||||||
write_card_catalog(&catalog_root);
|
|
||||||
let root_for_config = catalog_root.clone();
|
|
||||||
let server = start_server_with("catalog", move |config| GameConfig {
|
|
||||||
csv_root: Some(root_for_config),
|
|
||||||
starter: StarterProfile {
|
|
||||||
arena: DataSelector::named("Arena1"),
|
|
||||||
gold_resource: DataSelector::named("Gold"),
|
|
||||||
deck: vec![
|
|
||||||
CardRef::named("Knight"),
|
|
||||||
CardRef::named("Archers"),
|
|
||||||
CardRef::named("Fireball"),
|
|
||||||
CardRef::named("Arrows"),
|
|
||||||
],
|
|
||||||
extra_collection: vec![CardRef::named("Giant"), CardRef::named("NotACard")],
|
|
||||||
..StarterProfile::default()
|
|
||||||
},
|
|
||||||
..config
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
let mut client = FakeClient::connect(server.port).await;
|
|
||||||
client.send(&fresh_login()).await;
|
|
||||||
let _ = client.receive().await;
|
|
||||||
let (message_type, payload) = client.receive().await;
|
|
||||||
assert_eq!(message_type, message_type::OWN_HOME_DATA);
|
|
||||||
let home_data = OwnHomeDataMessage::from_bytes(&payload).expect("decode");
|
|
||||||
let deck = &home_data.home.decks[0];
|
|
||||||
assert_eq!(deck.filled_slot_count(), 4);
|
|
||||||
assert_eq!(
|
|
||||||
deck.slots[0].as_ref().expect("knight").data.global_id(),
|
|
||||||
Some(GlobalId::new(table::SPELLS_CHARACTERS, 0))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
deck.slots[2].as_ref().expect("fireball").data.global_id(),
|
|
||||||
Some(GlobalId::new(table::SPELLS_OTHER, 0))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
home_data.home.arena.global_id(),
|
|
||||||
Some(GlobalId::new(table::ARENAS, 1))
|
|
||||||
);
|
|
||||||
assert_eq!(home_data.avatar.arena, home_data.home.arena);
|
|
||||||
assert_eq!(home_data.home.spell_collection.spells.len(), 1);
|
|
||||||
client.close().await;
|
|
||||||
let _ = std::fs::remove_dir_all(&catalog_root);
|
|
||||||
}
|
|
||||||
|
|
@ -11,7 +11,3 @@ tracing = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "net", "io-util", "sync", "time"] }
|
|
||||||
async-trait = { workspace = true }
|
|
||||||
|
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
use std::sync::Arc;
|
|
||||||
use service_rpc::base64;
|
|
||||||
use service_rpc::{
|
|
||||||
serve, AccountRef, GameRequest, GameResponse, HomeRequestKind, RpcClient, RpcResult, RpcService,
|
|
||||||
WireMessage,
|
|
||||||
};
|
|
||||||
use tokio::net::TcpListener;
|
|
||||||
struct EchoGame;
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl RpcService for EchoGame {
|
|
||||||
type Request = GameRequest;
|
|
||||||
type Response = GameResponse;
|
|
||||||
fn service_name(&self) -> &'static str {
|
|
||||||
"echo-game"
|
|
||||||
}
|
|
||||||
async fn call(&self, request: GameRequest) -> RpcResult<GameResponse> {
|
|
||||||
match request {
|
|
||||||
GameRequest::LoadHome { account, .. } => Ok(GameResponse::messages(vec![
|
|
||||||
WireMessage::new(24101, 0, vec![account.low as u8, 0xFF, 0x00, 0x7F]),
|
|
||||||
])),
|
|
||||||
_ => Ok(GameResponse::Empty),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[tokio::test]
|
|
||||||
async fn requests_round_trip_over_the_length_prefixed_transport() {
|
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind");
|
|
||||||
let endpoint = listener.local_addr().expect("addr").to_string();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let _ = serve(listener, Arc::new(EchoGame)).await;
|
|
||||||
});
|
|
||||||
let client: RpcClient<GameRequest, GameResponse> = RpcClient::new(endpoint);
|
|
||||||
let response = client
|
|
||||||
.call(&GameRequest::LoadHome {
|
|
||||||
account: AccountRef::new(0, 9),
|
|
||||||
kind: HomeRequestKind::Login,
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("call");
|
|
||||||
let messages = response.into_messages();
|
|
||||||
assert_eq!(messages.len(), 1);
|
|
||||||
assert_eq!(messages[0].message_type, 24101);
|
|
||||||
assert_eq!(messages[0].payload, vec![9, 0xFF, 0x00, 0x7F]);
|
|
||||||
let empty = client
|
|
||||||
.call(&GameRequest::Disconnect {
|
|
||||||
account: AccountRef::new(0, 9),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.expect("call");
|
|
||||||
assert_eq!(empty, GameResponse::Empty);
|
|
||||||
}
|
|
||||||
#[tokio::test]
|
|
||||||
async fn payloads_survive_base64_encoding() {
|
|
||||||
for length in 0..64usize {
|
|
||||||
let payload: Vec<u8> = (0..length).map(|index| (index * 7 % 251) as u8).collect();
|
|
||||||
let encoded = base64::encode(&payload);
|
|
||||||
assert_eq!(base64::decode(&encoded).expect("decode"), payload);
|
|
||||||
}
|
|
||||||
assert_eq!(base64::encode(b"scroll"), "c2Nyb2xs");
|
|
||||||
assert_eq!(base64::decode("c2Nyb2xs").unwrap(), b"scroll");
|
|
||||||
}
|
|
||||||
|
|
@ -11,5 +11,3 @@ titan-derive = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
|
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
use titan::csv::{ColumnType, CsvNode, CsvReader};
|
|
||||||
const SPELLS: &str = concat!(
|
|
||||||
"Name,Rarity,Mana,SummonCharacter,Damage,Enabled\n",
|
|
||||||
"string,string,int,string,int,boolean\n",
|
|
||||||
"Knight,Common,3,Knight,75,true\n",
|
|
||||||
",,,,82,\n",
|
|
||||||
",,,,90,\n",
|
|
||||||
"Archers,Common,3,Archer,42,true\n",
|
|
||||||
",,,,46,\n",
|
|
||||||
"\"Mini P.E.K.K.A\",Rare,4,MiniPekka,325,false\n"
|
|
||||||
);
|
|
||||||
#[test]
|
|
||||||
fn the_first_two_lines_declare_names_and_types() {
|
|
||||||
let table = CsvReader::parse("spells_characters.csv", SPELLS).expect("parse");
|
|
||||||
assert_eq!(table.column_count(), 6);
|
|
||||||
assert_eq!(table.column_name(0), "Name");
|
|
||||||
assert_eq!(table.column_type(0), Some(ColumnType::String));
|
|
||||||
assert_eq!(table.column_type(2), Some(ColumnType::Int));
|
|
||||||
assert_eq!(table.column_type(5), Some(ColumnType::Boolean));
|
|
||||||
assert_eq!(table.column_index("Damage"), Some(4));
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn a_row_opens_only_when_the_first_column_is_populated() {
|
|
||||||
let table = CsvReader::parse("spells_characters.csv", SPELLS).expect("parse");
|
|
||||||
assert_eq!(table.row_count(), 3);
|
|
||||||
assert_eq!(table.row_at(0).unwrap().string(0), "Knight");
|
|
||||||
assert_eq!(table.row_at(1).unwrap().string(0), "Archers");
|
|
||||||
assert_eq!(table.row_at(2).unwrap().string(0), "Mini P.E.K.K.A");
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn continuation_lines_extend_the_per_level_arrays() {
|
|
||||||
let table = CsvReader::parse("spells_characters.csv", SPELLS).expect("parse");
|
|
||||||
let knight = table.row_at(0).expect("knight");
|
|
||||||
assert_eq!(knight.array_size(), 3);
|
|
||||||
assert_eq!(knight.value_count(4), 3);
|
|
||||||
assert_eq!(knight.int_at(4, 0), 75);
|
|
||||||
assert_eq!(knight.int_at(4, 1), 82);
|
|
||||||
assert_eq!(knight.int_at(4, 2), 90);
|
|
||||||
assert_eq!(knight.scaled_int(4, 9), 90);
|
|
||||||
assert_eq!(knight.int(2), 3);
|
|
||||||
assert!(knight.boolean(5));
|
|
||||||
let archers = table.row_at(1).expect("archers");
|
|
||||||
assert_eq!(archers.value_count(4), 2);
|
|
||||||
assert!(!table.row_at(2).unwrap().boolean(5));
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn rows_are_addressable_by_their_first_column() {
|
|
||||||
let table = CsvReader::parse("spells_characters.csv", SPELLS).expect("parse");
|
|
||||||
assert_eq!(table.row_index_of("Archers"), Some(1));
|
|
||||||
assert_eq!(
|
|
||||||
table.row_by_name("Mini P.E.K.K.A").map(|row| row.int(2)),
|
|
||||||
Some(4)
|
|
||||||
);
|
|
||||||
assert!(table.row_by_name("Golem").is_none());
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn quoted_cells_keep_their_commas() {
|
|
||||||
let source = concat!(
|
|
||||||
"Name,Text\n",
|
|
||||||
"string,string\n",
|
|
||||||
"Greeting,\"hello, world\"\n",
|
|
||||||
"Quote,\"say \"\"hi\"\"\"\n"
|
|
||||||
);
|
|
||||||
let table = CsvReader::parse("hints.csv", source).expect("parse");
|
|
||||||
assert_eq!(table.row_at(0).unwrap().string(1), "hello, world");
|
|
||||||
assert_eq!(table.row_at(1).unwrap().string(1), "say \"hi\"");
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn missing_trailing_cells_decode_as_empty() {
|
|
||||||
let source = concat!("Name,A,B\n", "string,int,int\n", "Row,5\n");
|
|
||||||
let table = CsvReader::parse("t.csv", source).expect("parse");
|
|
||||||
let row = table.row_at(0).expect("row");
|
|
||||||
assert_eq!(row.int(1), 5);
|
|
||||||
assert_eq!(row.int(2), 0);
|
|
||||||
assert!(row.value(2).expect("value").is_empty());
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn an_unknown_column_type_is_rejected() {
|
|
||||||
let source = concat!("Name,A\n", "string,float\n", "Row,1\n");
|
|
||||||
let error = CsvReader::parse("bad.csv", source).expect_err("must fail");
|
|
||||||
assert!(error.to_string().contains("float"));
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn a_node_wraps_the_table_with_its_file_identity() {
|
|
||||||
let node = CsvNode::load("csv_logic/spells_characters.csv", SPELLS).expect("load");
|
|
||||||
assert_eq!(node.name(), "spells_characters");
|
|
||||||
assert_eq!(node.file_name(), "csv_logic/spells_characters.csv");
|
|
||||||
assert_eq!(node.table().row_count(), 3);
|
|
||||||
assert_eq!(node.node_count(), 0);
|
|
||||||
}
|
|
||||||
|
|
@ -1,168 +0,0 @@
|
||||||
use titan::crypto::{Rc4Encrypter, StreamEncrypter};
|
|
||||||
use titan::{ByteStreamReader, ByteStreamWriter, FrameCodec, FrameHeader, MessageFrame, HEADER_LEN};
|
|
||||||
fn roundtrip_vint(value: i32) {
|
|
||||||
let mut writer = ByteStreamWriter::new();
|
|
||||||
writer.write_vint(value);
|
|
||||||
let bytes = writer.into_inner();
|
|
||||||
let mut reader = ByteStreamReader::new(&bytes);
|
|
||||||
assert_eq!(reader.read_vint().unwrap(), value, "vint {value} roundtrip");
|
|
||||||
assert_eq!(reader.remaining(), 0, "vint {value} length");
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn vint_roundtrips_across_every_width() {
|
|
||||||
let boundaries = [
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
63,
|
|
||||||
64,
|
|
||||||
0x1FFF,
|
|
||||||
0x2000,
|
|
||||||
0x000F_FFFF,
|
|
||||||
0x0010_0000,
|
|
||||||
0x07FF_FFFF,
|
|
||||||
0x0800_0000,
|
|
||||||
i32::MAX,
|
|
||||||
-1,
|
|
||||||
-63,
|
|
||||||
-64,
|
|
||||||
-8191,
|
|
||||||
-8192,
|
|
||||||
-1_048_575,
|
|
||||||
-1_048_576,
|
|
||||||
-134_217_727,
|
|
||||||
-134_217_728,
|
|
||||||
i32::MIN,
|
|
||||||
];
|
|
||||||
for value in boundaries {
|
|
||||||
roundtrip_vint(value);
|
|
||||||
}
|
|
||||||
let mut value: i64 = -2_000_000;
|
|
||||||
while value < 2_000_000 {
|
|
||||||
roundtrip_vint(value as i32);
|
|
||||||
value += 9_973;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn vint_encodes_to_the_expected_byte_widths() {
|
|
||||||
let cases = [
|
|
||||||
(0i32, 1usize),
|
|
||||||
(63, 1),
|
|
||||||
(64, 2),
|
|
||||||
(0x1FFF, 2),
|
|
||||||
(0x2000, 3),
|
|
||||||
(0x000F_FFFF, 3),
|
|
||||||
(0x0010_0000, 4),
|
|
||||||
(0x07FF_FFFF, 4),
|
|
||||||
(0x0800_0000, 5),
|
|
||||||
(-63, 1),
|
|
||||||
(-64, 2),
|
|
||||||
(-8191, 2),
|
|
||||||
(-8192, 3),
|
|
||||||
(-1_048_575, 3),
|
|
||||||
(-1_048_576, 4),
|
|
||||||
(-134_217_727, 4),
|
|
||||||
(-134_217_728, 5),
|
|
||||||
];
|
|
||||||
for (value, width) in cases {
|
|
||||||
let mut writer = ByteStreamWriter::new();
|
|
||||||
writer.write_vint(value);
|
|
||||||
assert_eq!(writer.len(), width, "width of {value}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn booleans_are_bit_packed_until_another_primitive_is_written() {
|
|
||||||
let mut writer = ByteStreamWriter::new();
|
|
||||||
for bit in 0..8 {
|
|
||||||
writer.write_boolean(bit % 2 == 0);
|
|
||||||
}
|
|
||||||
writer.write_boolean(true);
|
|
||||||
writer.write_vint(7);
|
|
||||||
writer.write_boolean(true);
|
|
||||||
let bytes = writer.into_inner();
|
|
||||||
assert_eq!(bytes[0], 0b0101_0101);
|
|
||||||
assert_eq!(bytes[1], 0b0000_0001);
|
|
||||||
assert_eq!(bytes[2], 7);
|
|
||||||
assert_eq!(bytes[3], 0b0000_0001);
|
|
||||||
let mut reader = ByteStreamReader::new(&bytes);
|
|
||||||
for bit in 0..8 {
|
|
||||||
assert_eq!(reader.read_boolean().unwrap(), bit % 2 == 0);
|
|
||||||
}
|
|
||||||
assert!(reader.read_boolean().unwrap());
|
|
||||||
assert_eq!(reader.read_vint().unwrap(), 7);
|
|
||||||
assert!(reader.read_boolean().unwrap());
|
|
||||||
assert_eq!(reader.remaining(), 0);
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn strings_carry_a_big_endian_length_prefix() {
|
|
||||||
let mut writer = ByteStreamWriter::new();
|
|
||||||
writer.write_string(Some("scroll")).unwrap();
|
|
||||||
writer.write_string(None).unwrap();
|
|
||||||
writer.write_string_reference("").unwrap();
|
|
||||||
let bytes = writer.into_inner();
|
|
||||||
assert_eq!(&bytes[0..4], &[0, 0, 0, 6]);
|
|
||||||
assert_eq!(&bytes[4..10], b"scroll");
|
|
||||||
assert_eq!(&bytes[10..14], &[0xFF, 0xFF, 0xFF, 0xFF]);
|
|
||||||
assert_eq!(&bytes[14..18], &[0, 0, 0, 0]);
|
|
||||||
let mut reader = ByteStreamReader::new(&bytes);
|
|
||||||
assert_eq!(reader.read_string().unwrap().as_deref(), Some("scroll"));
|
|
||||||
assert_eq!(reader.read_string().unwrap(), None);
|
|
||||||
assert_eq!(reader.read_string_reference().unwrap(), "");
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn frame_header_is_seven_bytes_big_endian() {
|
|
||||||
let frame = MessageFrame::new(24101, 0, vec![1, 2, 3]);
|
|
||||||
let encoded = FrameCodec::encode(&frame).unwrap();
|
|
||||||
assert_eq!(encoded.len(), HEADER_LEN + 3);
|
|
||||||
assert_eq!(&encoded[0..HEADER_LEN], &[0x5E, 0x25, 0, 0, 3, 0, 0]);
|
|
||||||
let mut header_bytes = [0u8; HEADER_LEN];
|
|
||||||
header_bytes.copy_from_slice(&encoded[0..HEADER_LEN]);
|
|
||||||
let header = FrameHeader::parse(&header_bytes);
|
|
||||||
assert_eq!(header.message_type, 24101);
|
|
||||||
assert_eq!(header.payload_len, 3);
|
|
||||||
assert_eq!(header.message_version, 0);
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn rc4_streams_are_symmetric_and_stateful() {
|
|
||||||
let mut client_to_server = Rc4Encrypter::scroll_default();
|
|
||||||
let mut server_side = Rc4Encrypter::scroll_default();
|
|
||||||
let first = client_to_server.encrypt(b"LoginMessage payload").unwrap();
|
|
||||||
let second = client_to_server.encrypt(b"KeepAliveMessage").unwrap();
|
|
||||||
assert_eq!(server_side.decrypt(&first).unwrap(), b"LoginMessage payload");
|
|
||||||
assert_eq!(server_side.decrypt(&second).unwrap(), b"KeepAliveMessage");
|
|
||||||
}
|
|
||||||
#[test]
|
|
||||||
fn rc4_matches_a_reference_implementation_with_the_supercell_keystream_discard() {
|
|
||||||
fn reference(key: &[u8], nonce: &[u8], length: usize) -> Vec<u8> {
|
|
||||||
let mut seed = key.to_vec();
|
|
||||||
seed.extend_from_slice(nonce);
|
|
||||||
let mut state: Vec<u8> = (0..=255u8).collect();
|
|
||||||
let mut j: u8 = 0;
|
|
||||||
for i in 0..256usize {
|
|
||||||
j = j.wrapping_add(state[i]).wrapping_add(seed[i % seed.len()]);
|
|
||||||
state.swap(i, j as usize);
|
|
||||||
}
|
|
||||||
let mut i: u8 = 0;
|
|
||||||
let mut j: u8 = 0;
|
|
||||||
let next = |state: &mut Vec<u8>, i: &mut u8, j: &mut u8| {
|
|
||||||
*i = i.wrapping_add(1);
|
|
||||||
*j = j.wrapping_add(state[*i as usize]);
|
|
||||||
state.swap(*i as usize, *j as usize);
|
|
||||||
let index = state[*i as usize].wrapping_add(state[*j as usize]);
|
|
||||||
state[index as usize]
|
|
||||||
};
|
|
||||||
for _ in 0..seed.len() {
|
|
||||||
next(&mut state, &mut i, &mut j);
|
|
||||||
}
|
|
||||||
(0..length)
|
|
||||||
.map(|_| next(&mut state, &mut i, &mut j))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
let expected = reference(
|
|
||||||
titan::crypto::SCROLL_RC4_KEY.as_bytes(),
|
|
||||||
titan::crypto::SCROLL_RC4_NONCE.as_bytes(),
|
|
||||||
32,
|
|
||||||
);
|
|
||||||
let mut cipher = Rc4Encrypter::scroll_default();
|
|
||||||
let produced = cipher.encrypt(&[0u8; 32]).unwrap();
|
|
||||||
assert_eq!(produced, expected);
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue