diff --git a/src/common/gamedata.rs b/src/common/gamedata.rs index 4cfa430..34996cf 100644 --- a/src/common/gamedata.rs +++ b/src/common/gamedata.rs @@ -312,6 +312,14 @@ impl GameData { self.get_weather_rate(*weather_rate_id as u32) } + + /// Gets the array index used in EXP & levels. + pub fn get_exp_array_index(&mut self, classjob_id: u16) -> Option { + let sheet = ClassJobSheet::read_from(&mut self.game_data, Language::English)?; + let row = sheet.get_row(classjob_id as u32)?; + + row.ExpArrayIndex().into_i8().copied() + } } // Simple enum for GameData::get_territory_name diff --git a/src/ipc/zone/player_setup.rs b/src/ipc/zone/player_setup.rs index 94f2ac5..129b9c3 100644 --- a/src/ipc/zone/player_setup.rs +++ b/src/ipc/zone/player_setup.rs @@ -94,7 +94,6 @@ pub struct PlayerStatus { #[br(count = UNLOCK_BITMASK_SIZE)] #[bw(pad_size_to = UNLOCK_BITMASK_SIZE)] pub unlocks: Vec, - pub unknown10e: [u8; 28], #[br(count = AETHERYTE_UNLOCK_BITMASK_SIZE)] #[bw(pad_size_to = AETHERYTE_UNLOCK_BITMASK_SIZE)] pub aetherytes: Vec, diff --git a/src/lib.rs b/src/lib.rs index 6cb8d6a..aff662b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -70,7 +70,7 @@ pub fn get_supported_expac_versions() -> HashMap<&'static str, Version<'static>> pub const OBFUSCATION_ENABLED_MODE: u8 = 41; /// The size of the unlock bitmask. -pub const UNLOCK_BITMASK_SIZE: usize = 64; +pub const UNLOCK_BITMASK_SIZE: usize = 92; /// The size of the aetheryte unlock bitmask. // TODO: this can be automatically derived from game data diff --git a/src/world/custom_ipc_handler.rs b/src/world/custom_ipc_handler.rs index c93ccf0..ef6ddf1 100644 --- a/src/world/custom_ipc_handler.rs +++ b/src/world/custom_ipc_handler.rs @@ -194,9 +194,11 @@ pub async fn handle_custom_ipc(connection: &mut ZoneConnection, data: &CustomIpc service_account_id, path, } => { + let mut game_data = connection.gamedata.lock().unwrap(); + connection .database - .import_character(*service_account_id, path); + .import_character(&mut game_data, *service_account_id, path); } CustomIpcData::RemakeCharacter { content_id, diff --git a/src/world/database.rs b/src/world/database.rs index 43bcf5e..beed42e 100644 --- a/src/world/database.rs +++ b/src/world/database.rs @@ -9,7 +9,7 @@ use crate::{ CustomizeData, GameData, Position, workdefinitions::{CharaMake, ClientSelectData, RemakeMode}, }, - inventory::Inventory, + inventory::{Inventory, Item, Storage}, ipc::{ lobby::{CharacterDetails, CharacterFlag}, zone::GameMasterRank, @@ -57,7 +57,7 @@ impl WorldDatabase { } } - pub fn import_character(&self, service_account_id: u32, path: &str) { + pub fn import_character(&self, game_data: &mut GameData, service_account_id: u32, path: &str) { tracing::info!("Importing character backup from {path}..."); let file = std::fs::File::open(path).unwrap(); @@ -75,6 +75,27 @@ impl WorldDatabase { month: i32, } + #[derive(Deserialize)] + struct ClassJobLevelValue { + level: i32, + exp: Option, + value: i32, + } + + #[derive(Deserialize)] + struct InventoryItem { + slot: i32, + quantity: u32, + condition: i32, + id: u32, + glamour_id: u32, + } + + #[derive(Deserialize)] + struct InventoryContainer { + items: Vec, + } + #[derive(Deserialize)] struct CharacterJson { name: String, @@ -82,6 +103,30 @@ impl WorldDatabase { nameday: NamedayValue, guardian: GenericValue, voice: i32, + classjob_levels: Vec, + + inventory1: InventoryContainer, + inventory2: InventoryContainer, + inventory3: InventoryContainer, + inventory4: InventoryContainer, + equipped_items: InventoryContainer, + + currency: InventoryContainer, + + armory_off_hand: InventoryContainer, + armory_head: InventoryContainer, + armory_body: InventoryContainer, + armory_hands: InventoryContainer, + armory_legs: InventoryContainer, + armory_ear: InventoryContainer, + armory_neck: InventoryContainer, + armory_wrist: InventoryContainer, + armory_rings: InventoryContainer, + armory_soul_crystal: InventoryContainer, + armory_main_hand: InventoryContainer, + + unlock_flags: Vec, + unlock_aetherytes: Vec, } let character: CharacterJson; @@ -117,8 +162,7 @@ impl WorldDatabase { unk2: 1, }; - // TODO: import inventory - self.create_player_data( + let (_, actor_id) = self.create_player_data( service_account_id, &character.name, &chara_make.to_json(), @@ -127,6 +171,97 @@ impl WorldDatabase { Inventory::default(), ); + let mut player_data = self.find_player_data(actor_id); + + // import jobs + for classjob in &character.classjob_levels { + // find the array index of the job + let index = game_data + .get_exp_array_index(classjob.value as u16) + .unwrap(); + + player_data.classjob_levels[index as usize] = classjob.level; + if let Some(exp) = classjob.exp { + player_data.classjob_exp[index as usize] = exp; + } + } + + let process_inventory_container = + |container: &InventoryContainer, target: &mut dyn Storage| { + for item in &container.items { + if item.slot as u32 > target.max_slots() { + continue; + } + *target.get_slot_mut(item.slot as u16) = Item { + quantity: item.quantity, + id: item.id, + }; + } + }; + + // import inventory + process_inventory_container(&character.inventory1, &mut player_data.inventory.pages[0]); + process_inventory_container(&character.inventory2, &mut player_data.inventory.pages[1]); + process_inventory_container(&character.inventory3, &mut player_data.inventory.pages[2]); + process_inventory_container(&character.inventory4, &mut player_data.inventory.pages[3]); + process_inventory_container( + &character.equipped_items, + &mut player_data.inventory.equipped, + ); + + process_inventory_container(&character.currency, &mut player_data.inventory.currency); + + process_inventory_container( + &character.armory_off_hand, + &mut player_data.inventory.armoury_off_hand, + ); + process_inventory_container( + &character.armory_head, + &mut player_data.inventory.armoury_head, + ); + process_inventory_container( + &character.armory_body, + &mut player_data.inventory.armoury_body, + ); + process_inventory_container( + &character.armory_hands, + &mut player_data.inventory.armoury_hands, + ); + process_inventory_container( + &character.armory_legs, + &mut player_data.inventory.armoury_legs, + ); + process_inventory_container( + &character.armory_ear, + &mut player_data.inventory.armoury_earring, + ); + process_inventory_container( + &character.armory_neck, + &mut player_data.inventory.armoury_necklace, + ); + process_inventory_container( + &character.armory_wrist, + &mut player_data.inventory.armoury_bracelet, + ); + process_inventory_container( + &character.armory_rings, + &mut player_data.inventory.armoury_rings, + ); + process_inventory_container( + &character.armory_soul_crystal, + &mut player_data.inventory.armoury_soul_crystal, + ); + process_inventory_container( + &character.armory_main_hand, + &mut player_data.inventory.armoury_main_hand, + ); + + // import unlock flags + player_data.unlocks = character.unlock_flags; + player_data.aetherytes = character.unlock_aetherytes; + + self.commit_player_data(&player_data); + tracing::info!("{} added to the world!", character.name); }