diff --git a/Cargo.lock b/Cargo.lock index e6688ed..4bbde30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -543,8 +543,8 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reliquary" -version = "3.0.0" -source = "git+https://github.com/IceDynamix/reliquary?tag=v3.0.0#6ab4524b01ce7c02b731ba74877e9edb5060aa7c" +version = "3.1.0" +source = "git+https://github.com/IceDynamix/reliquary?tag=v3.1.0#0c96b3c1cdaf589bb9d48f21d5d7bd5d25cd352c" dependencies = [ "base64", "etherparse", @@ -565,6 +565,7 @@ dependencies = [ "clap", "color-eyre", "pcap", + "protobuf", "reliquary", "serde", "serde_json", @@ -576,7 +577,7 @@ dependencies = [ [[package]] name = "reliquary-proc-macro" version = "0.1.0" -source = "git+https://github.com/IceDynamix/reliquary?tag=v3.0.0#6ab4524b01ce7c02b731ba74877e9edb5060aa7c" +source = "git+https://github.com/IceDynamix/reliquary?tag=v3.1.0#0c96b3c1cdaf589bb9d48f21d5d7bd5d25cd352c" dependencies = [ "quote", "syn", diff --git a/Cargo.toml b/Cargo.toml index 66a6408..3fcf5ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ base64 = "0.22.1" clap = { version = "4.5.14", features = ["derive"] } color-eyre = "0.6.3" pcap = "2.0.0" +protobuf = "~3.4.0" # match the protobuf version used in reliquary-codegen serde = { version = "1.0.205", features = ["derive"] } serde_json = "1.0.122" tracing = "0.1.40" @@ -23,7 +24,7 @@ ureq = { version = "2.10.1", features = ["json"] } [dependencies.reliquary] git = "https://github.com/IceDynamix/reliquary" -tag = "v3.0.0" +tag = "v3.1.0" [profile.release] opt-level = "z" # optimize for size diff --git a/src/export/fribbels.rs b/src/export/fribbels.rs index df36db8..746fc30 100644 --- a/src/export/fribbels.rs +++ b/src/export/fribbels.rs @@ -7,6 +7,7 @@ use std::collections::HashMap; use base64::Engine; use base64::prelude::BASE64_STANDARD; +use protobuf::Enum; use reliquary::network::GameCommand; use reliquary::network::gen::command_id; use reliquary::network::gen::proto::Avatar::Avatar as ProtoCharacter; @@ -15,7 +16,8 @@ use reliquary::network::gen::proto::Equipment::Equipment as ProtoLightCone; use reliquary::network::gen::proto::GetAvatarDataScRsp::GetAvatarDataScRsp; use reliquary::network::gen::proto::GetBagScRsp::GetBagScRsp; use reliquary::network::gen::proto::GetMultiPathAvatarInfoScRsp::GetMultiPathAvatarInfoScRsp; -use reliquary::network::gen::proto::HeroBasicTypeInfo::HeroBasicTypeInfo; +use reliquary::network::gen::proto::MultiPathAvatarInfo::MultiPathAvatarInfo; +use reliquary::network::gen::proto::MultiPathAvatarType::MultiPathAvatarType; use reliquary::network::gen::proto::PlayerGetTokenScRsp::PlayerGetTokenScRsp; use reliquary::network::gen::proto::Relic::Relic as ProtoRelic; use reliquary::network::gen::proto::RelicAffix::RelicAffix; @@ -45,18 +47,17 @@ pub struct Export { pub struct Metadata { pub uid: Option, pub trailblazer: Option<&'static str>, - pub current_trailblazer_path: Option<&'static str>, } pub struct OptimizerExporter { database: Database, uid: Option, trailblazer: Option<&'static str>, - current_trailblazer_path: Option<&'static str>, light_cones: Vec, relics: Vec, characters: Vec, - trailblazer_characters: Vec, + multipath_characters: Vec, + multipath_base_avatars: HashMap, } impl OptimizerExporter { @@ -65,11 +66,11 @@ impl OptimizerExporter { database, uid: None, trailblazer: None, - current_trailblazer_path: None, light_cones: vec![], relics: vec![], characters: vec![], - trailblazer_characters: vec![], + multipath_characters: vec![], + multipath_base_avatars: HashMap::new(), } } @@ -77,8 +78,6 @@ impl OptimizerExporter { self.uid = Some(uid); } - // TODO: add_multipath_avatars - pub fn add_inventory(&mut self, bag: GetBagScRsp) { let mut relics: Vec = bag.relic_list.iter() .filter_map(|r| export_proto_relic(&self.database, r)) @@ -96,13 +95,48 @@ impl OptimizerExporter { } pub fn add_characters(&mut self, characters: GetAvatarDataScRsp) { - let mut characters: Vec = characters.avatar_list.iter() - .filter(|a| a.base_avatar_id < 8000) // skip trailblazer, handled in `write_hero` + let (characters, multipath_characters) = characters.avatar_list.iter() + .partition::, _>(|a| MultiPathAvatarType::from_i32(a.base_avatar_id as i32).is_none() ); + + let mut characters: Vec = characters.iter() .filter_map(|char| export_proto_character(&self.database, char)) .collect(); info!(num=characters.len(), "found characters"); self.characters.append(&mut characters); + + info!(num=multipath_characters.len(), "found multipath base avatars"); + self.multipath_base_avatars.extend(multipath_characters.into_iter().map(|c| (c.base_avatar_id, c.clone()))); + } + + pub fn add_multipath_characters(&mut self, characters: GetMultiPathAvatarInfoScRsp) { + let mut characters: Vec = characters.multi_path_avatar_info_list.iter() + .filter_map(|char| export_proto_multipath_character(&self.database, char)) + .collect(); + + // Try to find a trailblazer to determine the gender + if let Some(trailblazer) = characters.iter().find(|c| c.name == "Trailblazer") { + self.trailblazer = Some(if trailblazer.id.parse::().unwrap() % 2 == 0 { + "Stelle" + } else { + "Caelus" + }); + } + + info!(num=characters.len(), "found multipath characters"); + self.multipath_characters.append(&mut characters); + } + + pub fn finalize_multipath_characters(&mut self) { + // Fetch level & ascension + for character in self.multipath_characters.iter_mut() { + if let Some(config) = self.database.multipath_avatar_config.get(&character.id.parse().unwrap()) { + if let Some(base_avatar) = self.multipath_base_avatars.get(&config.BaseAvatarID) { + character.level = base_avatar.level; + character.ascension = base_avatar.promotion; + } + } + } } } @@ -152,8 +186,7 @@ impl Exporter for OptimizerExporter { let cmd = command.parse_proto::(); match cmd { Ok(cmd) => { - // TODO: handle multi path packets - warn!("ignored multipath characters for now, will be supported in next version"); + self.add_multipath_characters(cmd) } Err(error) => { warn!(%error, "could not parse multipath data command"); @@ -171,12 +204,12 @@ impl Exporter for OptimizerExporter { && self.uid.is_some() && !self.relics.is_empty() && !self.characters.is_empty() - && !self.trailblazer_characters.is_empty() + && !self.multipath_characters.is_empty() && !self.light_cones.is_empty() } #[instrument(skip_all)] - fn export(self) -> Self::Export { + fn export(mut self) -> Self::Export { info!("exporting collected data"); if self.trailblazer.is_none() { @@ -195,27 +228,28 @@ impl Exporter for OptimizerExporter { warn!("light cones were not recorded"); } - if self.trailblazer_characters.is_empty() { - warn!("trailblazer characters were not recorded"); + if self.multipath_characters.is_empty() { + warn!("multipath characters were not recorded"); } if self.characters.is_empty() { warn!("characters were not recorded"); } + self.finalize_multipath_characters(); + Export { source: "reliquary_archiver", build: env!("CARGO_PKG_VERSION"), - version: 3, + version: 4, metadata: Metadata { uid: self.uid, trailblazer: self.trailblazer, - current_trailblazer_path: self.current_trailblazer_path, }, light_cones: self.light_cones, relics: self.relics, characters: self.characters.into_iter() - .chain(self.trailblazer_characters) + .chain(self.multipath_characters) .collect(), } } @@ -225,6 +259,7 @@ pub struct Database { avatar_config: AvatarConfigMap, avatar_skill_tree_config: AvatarSkillTreeConfigMap, equipment_config: EquipmentConfigMap, + multipath_avatar_config: MultiplePathAvatarConfigMap, relic_config: RelicConfigMap, relic_set_config: RelicSetConfigMap, relic_main_affix_config: RelicMainAffixConfigMap, @@ -241,6 +276,7 @@ impl Database { avatar_config: Self::load_online_config(), avatar_skill_tree_config: Self::load_online_config(), equipment_config: Self::load_online_config(), + multipath_avatar_config: Self::load_online_config(), relic_config: Self::load_online_config(), relic_set_config: Self::load_online_config(), relic_main_affix_config: Self::load_online_config(), @@ -288,10 +324,8 @@ impl Database { return None; } - // trailblazer if avatar_id >= 8000 { - let path = avatar_path_lookup(self, avatar_id)?; - Some(format!("Trailblazer{}", path)) + Some("Trailblazer".to_owned()) } else { let cfg = self.avatar_config.get(&avatar_id)?; cfg.AvatarName.lookup(&self.text_map).map(|s| s.to_string()) @@ -299,27 +333,36 @@ impl Database { } } +fn format_location(avatar_id: u32) -> String { + if avatar_id == 0 { + return "".to_owned(); + } else { + return avatar_id.to_string(); + } +} + #[tracing::instrument(name = "relic", skip_all, fields(id = proto.tid))] fn export_proto_relic(db: &Database, proto: &ProtoRelic) -> Option { let relic_config = db.relic_config.get(&proto.tid)?; - let set_config = db.relic_set_config.get(&relic_config.SetID)?; + let set_id = relic_config.SetID; + let set_config = db.relic_set_config.get(&set_id)?; let main_affix_config = db.relic_main_affix_config.get(&relic_config.MainAffixGroup, &proto.main_affix_id).unwrap(); let id = proto.unique_id.to_string(); let level = proto.level; let lock = proto.is_protected; let discard = proto.is_discarded; - let set = set_config.SetName.lookup(&db.text_map) + let set_name = set_config.SetName.lookup(&db.text_map) .map(|s| s.to_string()) .unwrap_or("".to_string()); let slot = slot_type_to_export(&relic_config.Type); let rarity = relic_config.MaxLevel / 3; let mainstat = main_stat_to_export(&main_affix_config.Property).to_string(); - let location = db.lookup_avatar_name(proto.equip_avatar_id).unwrap_or("".to_string()); + let location = format_location(proto.equip_avatar_id); - debug!(rarity, set, slot, slot, mainstat, location, "detected"); + debug!(rarity, set_name, slot, slot, mainstat, location, "detected"); let substats = proto.sub_affix_list.iter() .filter_map(|substat| export_substat(db, rarity, substat)) @@ -327,7 +370,8 @@ fn export_proto_relic(db: &Database, proto: &ProtoRelic) -> Option { Some(Relic { - set, + set_id: set_id.to_string(), + name: set_name, slot, rarity, level, @@ -336,7 +380,7 @@ fn export_proto_relic(db: &Database, proto: &ProtoRelic) -> Option { location, lock, discard, - _id: id, + _uid: id, }) } @@ -362,7 +406,8 @@ fn export_substat(db: &Database, rarity: u32, substat: &RelicAffix) -> Option &'static str { #[derive(Serialize, Deserialize, Debug)] pub struct LightCone { - pub key: String, + pub id: String, + pub name: String, pub level: u32, pub ascension: u32, pub superimposition: u32, pub location: String, pub lock: bool, - pub _id: String, + pub _uid: String, } #[instrument(name = "light_cone", skip_all, fields(id = proto.tid))] fn export_proto_light_cone(db: &Database, proto: &ProtoLightCone) -> Option { let cfg = db.equipment_config.get(&proto.tid)?; - let key = cfg.EquipmentName.lookup(&db.text_map).map(|s| s.to_string())?; + let id = cfg.EquipmentID.to_string(); + let name = cfg.EquipmentName.lookup(&db.text_map).map(|s| s.to_string())?; let level = proto.level; let superimposition = proto.rank; - debug!(light_cone=key, level, superimposition, "detected"); + debug!(light_cone=name, level, superimposition, "detected"); - let location = db.lookup_avatar_name(proto.equip_avatar_id) - .unwrap_or("".to_string()); + let location = format_location(proto.equip_avatar_id); Some(LightCone { - key, + id, + name, level, ascension: proto.promotion, superimposition, location, lock: proto.is_protected, - _id: proto.unique_id.to_string(), + _uid: proto.unique_id.to_string(), }) } #[instrument(name = "character", skip_all, fields(id = proto.base_avatar_id))] fn export_proto_character(db: &Database, proto: &ProtoCharacter) -> Option { - let key = db.lookup_avatar_name(proto.base_avatar_id)?; + let id = proto.base_avatar_id; + let name = db.lookup_avatar_name(id)?; + let path = avatar_path_lookup(db, id)?.to_owned(); let level = proto.level; let eidolon = proto.rank; - debug!(character=key, level, eidolon, "detected"); + debug!(character=name, level, eidolon, "detected"); let (skills, traces) = export_skill_tree(db, &proto.skilltree_list); Some(Character { - key, + id: id.to_string(), + name, + path, level, ascension: proto.promotion, eidolon, @@ -491,20 +542,23 @@ fn export_proto_character(db: &Database, proto: &ProtoCharacter) -> Option Option { - let path = avatar_path_lookup(db, proto.basic_type.value() as u32)?; - let key = format!("Trailblazer{}", path); +fn export_proto_multipath_character(db: &Database, proto: &MultiPathAvatarInfo) -> Option { + let id = proto.avatar_id.value() as u32; + let name = db.lookup_avatar_name(id)?; + let path = avatar_path_lookup(db, id)?.to_owned(); - let span = info_span!("character", key); + let span = info_span!("character", name, path); let _enter = span.enter(); - trace!(character=key, "detected"); + trace!(character=name, path, "detected"); - let (skills, traces) = export_skill_tree(db, &proto.skill_tree_list); + let (skills, traces) = export_skill_tree(db, &proto.multi_path_skill_tree); // TODO: figure out where level/ascension is stored Some(Character { - key, + id: id.to_string(), + name, + path, level: 0, ascension: 0, eidolon: proto.rank, @@ -657,7 +711,9 @@ fn export_skill_tree(db: &Database, proto: &[ProtoSkillTree]) -> (Skills, Traces #[derive(Serialize, Deserialize, Debug)] pub struct Character { - pub key: String, + pub id: String, + pub name: String, + pub path: String, pub level: u32, pub ascension: u32, pub eidolon: u32,