From 37c19ee1b8889d5a7afebb7112861d06fc77514d Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sun, 30 Mar 2025 10:34:42 -0400 Subject: [PATCH] Start sharing the Physis GameData instances Instead of standalone functions in the common module to perform game data tasks, there is now a shared GameData struct that these functions moved to. This speeds up a few things, and can take advantage of Physis' built-in index caching. I know the current solution isn't ideal (each connection has to mutex lock to access gamedata) but it's at least better than before. --- src/bin/kawari-lobby.rs | 5 +- src/bin/kawari-world.rs | 263 ++++++++++++++++++++-------------------- src/common/gamedata.rs | 111 +++++++++++++++++ src/common/mod.rs | 110 +---------------- src/lobby/connection.rs | 4 +- 5 files changed, 251 insertions(+), 242 deletions(-) create mode 100644 src/common/gamedata.rs diff --git a/src/bin/kawari-lobby.rs b/src/bin/kawari-lobby.rs index 8ace825..c9934ef 100644 --- a/src/bin/kawari-lobby.rs +++ b/src/bin/kawari-lobby.rs @@ -1,7 +1,7 @@ +use kawari::common::GameData; use kawari::common::custom_ipc::CustomIpcData; use kawari::common::custom_ipc::CustomIpcSegment; use kawari::common::custom_ipc::CustomIpcType; -use kawari::common::get_world_name; use kawari::config::get_config; use kawari::lobby::LobbyConnection; use kawari::lobby::ipc::{ClientLobbyIpcData, ServerLobbyIpcSegment}; @@ -24,7 +24,8 @@ async fn main() { tracing::info!("Server started on {addr}"); - let world_name = get_world_name(config.world.world_id); + let mut game_data = GameData::new(); + let world_name = game_data.get_world_name(config.world.world_id); loop { let (socket, _) = listener.accept().await.unwrap(); diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index ece6a29..8370c5f 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -2,10 +2,8 @@ use std::collections::HashMap; use std::sync::{Arc, Mutex}; use kawari::common::custom_ipc::{CustomIpcData, CustomIpcSegment, CustomIpcType}; -use kawari::common::{ - Position, determine_initial_starting_zone, get_citystate, get_primary_model_id, get_world_name, -}; -use kawari::common::{get_racial_base_attributes, timestamp_secs}; +use kawari::common::{GameData, timestamp_secs}; +use kawari::common::{Position, determine_initial_starting_zone}; use kawari::config::get_config; use kawari::lobby::CharaMake; use kawari::oodle::OodleNetwork; @@ -28,8 +26,6 @@ use kawari::world::{ }; use kawari::world::{EffectsBuilder, LuaPlayer, PlayerData, StatusEffects, WorldDatabase}; use mlua::{Function, Lua}; -use physis::common::{Language, Platform}; -use physis::gamedata::GameData; use tokio::io::AsyncReadExt; use tokio::net::TcpListener; @@ -52,6 +48,7 @@ async fn main() { let database = Arc::new(WorldDatabase::new()); let lua = Arc::new(Mutex::new(Lua::new())); + let game_data = Arc::new(Mutex::new(GameData::new())); { let lua = lua.lock().unwrap(); @@ -89,6 +86,7 @@ async fn main() { let database = database.clone(); let lua = lua.clone(); + let game_data = game_data.clone(); let state = PacketState { client_key: None, @@ -113,7 +111,7 @@ async fn main() { let mut lua_player = LuaPlayer::default(); - let config = get_config(); + /*let config = get_config(); let mut game_data = GameData::from_existing(Platform::Win32, &config.game_location).unwrap(); @@ -121,7 +119,7 @@ async fn main() { let exh = game_data.read_excel_sheet_header("Action").unwrap(); let exd = game_data .read_excel_sheet("Action", &exh, Language::English, 0) - .unwrap(); + .unwrap();*/ tokio::spawn(async move { let mut buf = [0; 2056]; @@ -295,9 +293,14 @@ async fn main() { // Stats { - let attributes = get_racial_base_attributes( - chara_details.chara_make.customize.subrace, - ); + let attributes; + { + let mut game_data = game_data.lock().unwrap(); + + attributes = game_data.get_racial_base_attributes( + chara_details.chara_make.customize.subrace, + ); + } let ipc = ServerZoneIpcSegment { op_code: ServerZoneIpcType::PlayerStats, @@ -396,122 +399,106 @@ async fn main() { // send player spawn { - let ipc = ServerZoneIpcSegment { - op_code: ServerZoneIpcType::PlayerSpawn, - timestamp: timestamp_secs(), - data: ServerZoneIpcData::PlayerSpawn(PlayerSpawn { - account_id: connection.player_data.account_id, - content_id: connection.player_data.content_id, - current_world_id: config.world.world_id, - home_world_id: config.world.world_id, - gm_rank: GameMasterRank::Debug, - online_status: OnlineStatus::GameMasterBlue, - common: CommonSpawn { - class_job: connection - .player_data - .classjob_id, - name: chara_details.name, - hp_curr: connection.player_data.curr_hp, - hp_max: connection.player_data.max_hp, - mp_curr: connection.player_data.curr_mp, - mp_max: connection.player_data.max_mp, - object_kind: ObjectKind::Player( - PlayerSubKind::Player, - ), - look: chara_details.chara_make.customize, - fc_tag: "LOCAL".to_string(), - display_flags: DisplayFlag::UNK, - models: [ - get_primary_model_id( - connection - .inventory - .equipped - .head - .id, - ) - as u32, - get_primary_model_id( - connection - .inventory - .equipped - .body - .id, - ) - as u32, - get_primary_model_id( - connection - .inventory - .equipped - .hands - .id, - ) - as u32, - get_primary_model_id( - connection - .inventory - .equipped - .legs - .id, - ) - as u32, - get_primary_model_id( - connection - .inventory - .equipped - .feet - .id, - ) - as u32, - get_primary_model_id( - connection - .inventory - .equipped - .ears - .id, - ) - as u32, - get_primary_model_id( - connection - .inventory - .equipped - .neck - .id, - ) - as u32, - get_primary_model_id( - connection - .inventory - .equipped - .wrists - .id, - ) - as u32, - get_primary_model_id( - connection - .inventory - .equipped - .left_ring - .id, - ) - as u32, - get_primary_model_id( - connection - .inventory - .equipped - .right_ring - .id, - ) - as u32, - ], - pos: exit_position - .unwrap_or(Position::default()), - rotation: exit_rotation.unwrap_or(0.0), - ..Default::default() - }, + let ipc; + { + let mut game_data = game_data.lock().unwrap(); + let equipped = &connection.inventory.equipped; + + ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::PlayerSpawn, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::PlayerSpawn( + PlayerSpawn { + account_id: connection + .player_data + .account_id, + content_id: connection + .player_data + .content_id, + current_world_id: config.world.world_id, + home_world_id: config.world.world_id, + gm_rank: GameMasterRank::Debug, + online_status: + OnlineStatus::GameMasterBlue, + common: CommonSpawn { + class_job: connection + .player_data + .classjob_id, + name: chara_details.name, + hp_curr: connection + .player_data + .curr_hp, + hp_max: connection + .player_data + .max_hp, + mp_curr: connection + .player_data + .curr_mp, + mp_max: connection + .player_data + .max_mp, + object_kind: ObjectKind::Player( + PlayerSubKind::Player, + ), + look: chara_details + .chara_make + .customize, + fc_tag: "LOCAL".to_string(), + display_flags: DisplayFlag::UNK, + models: [ + game_data.get_primary_model_id( + equipped.head.id, + ) + as u32, + game_data.get_primary_model_id( + equipped.body.id, + ) + as u32, + game_data.get_primary_model_id( + equipped.hands.id, + ) + as u32, + game_data.get_primary_model_id( + equipped.legs.id, + ) + as u32, + game_data.get_primary_model_id( + equipped.feet.id, + ) + as u32, + game_data.get_primary_model_id( + equipped.ears.id, + ) + as u32, + game_data.get_primary_model_id( + equipped.neck.id, + ) + as u32, + game_data.get_primary_model_id( + equipped.wrists.id, + ) + as u32, + game_data.get_primary_model_id( + equipped.left_ring.id, + ) + as u32, + game_data.get_primary_model_id( + equipped.right_ring.id, + ) + as u32, + ], + pos: exit_position + .unwrap_or(Position::default()), + rotation: exit_rotation + .unwrap_or(0.0), + ..Default::default() + }, + ..Default::default() + }, + ), ..Default::default() - }), - ..Default::default() - }; + }; + } connection .send_segment(PacketSegment { @@ -837,10 +824,10 @@ async fn main() { ClientZoneIpcData::ActionRequest(request) => { tracing::info!("Recieved action request: {:#?}!", request); - let action_row = + /*let action_row = &exd.read_row(&exh, request.action_id).unwrap()[0]; - println!("Found action: {:#?}", action_row); + println!("Found action: {:#?}", action_row);*/ let mut effects_builder = None; @@ -858,6 +845,8 @@ async fn main() { .create_userdata_ref_mut(&mut lua_player) .unwrap(); + let config = get_config(); + let file_name = format!( "{}/{}", &config.world.scripts_location, @@ -993,8 +982,13 @@ async fn main() { let chara_make = CharaMake::from_json(chara_make_json); - let city_state = - get_citystate(chara_make.classjob_id as u16); + let city_state; + { + let mut game_data = game_data.lock().unwrap(); + + city_state = game_data + .get_citystate(chara_make.classjob_id as u16); + } let (content_id, actor_id) = database.create_player_data( name, @@ -1087,10 +1081,17 @@ async fn main() { CustomIpcData::RequestCharacterList { service_account_id } => { let config = get_config(); + let world_name; + { + let mut game_data = game_data.lock().unwrap(); + world_name = + game_data.get_world_name(config.world.world_id); + } + let characters = database.get_character_list( *service_account_id, config.world.world_id, - &get_world_name(config.world.world_id), + &world_name, ); // send response diff --git a/src/common/gamedata.rs b/src/common/gamedata.rs new file mode 100644 index 0000000..ae33f7f --- /dev/null +++ b/src/common/gamedata.rs @@ -0,0 +1,111 @@ +use physis::common::{Language, Platform}; + +use crate::{common::Attributes, config::get_config}; + +/// Convenient methods built on top of Physis to access data relevant to the server +pub struct GameData { + game_data: physis::gamedata::GameData, +} + +impl GameData { + pub fn new() -> Self { + let config = get_config(); + + Self { + game_data: physis::gamedata::GameData::from_existing( + Platform::Win32, + &config.game_location, + ) + .unwrap(), + } + } + + /// Gets the world name from an id into the World Excel sheet. + pub fn get_world_name(&mut self, world_id: u16) -> String { + let exh = self.game_data.read_excel_sheet_header("World").unwrap(); + let exd = self + .game_data + .read_excel_sheet("World", &exh, Language::None, 0) + .unwrap(); + + let world_row = &exd.read_row(&exh, world_id as u32).unwrap()[0]; + + let physis::exd::ColumnData::String(name) = &world_row.data[1] else { + panic!("Unexpected type!"); + }; + + name.clone() + } + + /// Gets the starting city-state from a given class/job id. + pub fn get_citystate(&mut self, classjob_id: u16) -> u8 { + let exh = self.game_data.read_excel_sheet_header("ClassJob").unwrap(); + let exd = self + .game_data + .read_excel_sheet("ClassJob", &exh, Language::English, 0) + .unwrap(); + + let world_row = &exd.read_row(&exh, classjob_id as u32).unwrap()[0]; + + let physis::exd::ColumnData::UInt8(town_id) = &world_row.data[33] else { + panic!("Unexpected type!"); + }; + + *town_id + } + + pub fn get_racial_base_attributes(&mut self, tribe_id: u8) -> Attributes { + // The Tribe Excel sheet only has deltas (e.g. 2 or -2) which are applied to a base 20 number... from somewhere + let base_stat = 20; + + let exh = self.game_data.read_excel_sheet_header("Tribe").unwrap(); + let exd = self + .game_data + .read_excel_sheet("Tribe", &exh, Language::English, 0) + .unwrap(); + + let tribe_row = &exd.read_row(&exh, tribe_id as u32).unwrap()[0]; + + let get_column = |column_index: usize| { + let physis::exd::ColumnData::Int8(delta) = &tribe_row.data[column_index] else { + panic!("Unexpected type!"); + }; + + *delta + }; + + Attributes { + strength: (base_stat + get_column(4)) as u32, + dexterity: (base_stat + get_column(6)) as u32, + vitality: (base_stat + get_column(5)) as u32, + intelligence: (base_stat + get_column(7)) as u32, + mind: (base_stat + get_column(8)) as u32, + } + } + + /// Gets the primary model ID for a given item ID + pub fn get_primary_model_id(&mut self, item_id: u32) -> u16 { + let exh = self.game_data.read_excel_sheet_header("Item").unwrap(); + for (i, _) in exh.pages.iter().enumerate() { + let exd = self + .game_data + .read_excel_sheet("Item", &exh, Language::English, i) + .unwrap(); + + if let Some(row) = exd.read_row(&exh, item_id) { + let item_row = &row[0]; + + let physis::exd::ColumnData::UInt64(id) = &item_row.data[47] else { + panic!("Unexpected type!"); + }; + + return *id as u16; + } + } + + // TODO: just turn this into an Option<> + tracing::warn!("Failed to get model id for {item_id}, this is most likely a bug!"); + + 0 + } +} diff --git a/src/common/mod.rs b/src/common/mod.rs index 50b87dc..c65442f 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -6,18 +6,15 @@ use std::{ mod customize_data; use binrw::binrw; pub use customize_data::CustomizeData; -use physis::{ - common::{Language, Platform}, - gamedata::GameData, -}; - -use crate::config::get_config; pub mod custom_ipc; mod position; pub use position::Position; +mod gamedata; +pub use gamedata::GameData; + #[binrw] #[brw(little)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -100,46 +97,6 @@ pub fn timestamp_msecs() -> u64 { .unwrap() } -/// Gets the world name from an id into the World Excel sheet. -pub fn get_world_name(world_id: u16) -> String { - let config = get_config(); - - let mut game_data = GameData::from_existing(Platform::Win32, &config.game_location).unwrap(); - - let exh = game_data.read_excel_sheet_header("World").unwrap(); - let exd = game_data - .read_excel_sheet("World", &exh, Language::None, 0) - .unwrap(); - - let world_row = &exd.read_row(&exh, world_id as u32).unwrap()[0]; - - let physis::exd::ColumnData::String(name) = &world_row.data[1] else { - panic!("Unexpected type!"); - }; - - name.clone() -} - -/// Gets the starting city-state from a given class/job id. -pub fn get_citystate(classjob_id: u16) -> u8 { - let config = get_config(); - - let mut game_data = GameData::from_existing(Platform::Win32, &config.game_location).unwrap(); - - let exh = game_data.read_excel_sheet_header("ClassJob").unwrap(); - let exd = game_data - .read_excel_sheet("ClassJob", &exh, Language::English, 0) - .unwrap(); - - let world_row = &exd.read_row(&exh, classjob_id as u32).unwrap()[0]; - - let physis::exd::ColumnData::UInt8(town_id) = &world_row.data[33] else { - panic!("Unexpected type!"); - }; - - *town_id -} - /// Gets the initial zone for a given city-state id pub fn determine_initial_starting_zone(citystate_id: u8) -> u16 { match citystate_id { @@ -153,35 +110,6 @@ pub fn determine_initial_starting_zone(citystate_id: u8) -> u16 { } } -/// Gets the primary model ID for a given item ID -pub fn get_primary_model_id(item_id: u32) -> u16 { - let config = get_config(); - - let mut game_data = GameData::from_existing(Platform::Win32, &config.game_location).unwrap(); - - let exh = game_data.read_excel_sheet_header("Item").unwrap(); - for (i, _) in exh.pages.iter().enumerate() { - let exd = game_data - .read_excel_sheet("Item", &exh, Language::English, i) - .unwrap(); - - if let Some(row) = exd.read_row(&exh, item_id) { - let item_row = &row[0]; - - let physis::exd::ColumnData::UInt64(id) = &item_row.data[47] else { - panic!("Unexpected type!"); - }; - - return *id as u16; - } - } - - // TODO: just turn this into an Option<> - tracing::warn!("Failed to get model id for {item_id}, this is most likely a bug!"); - - 0 -} - pub struct Attributes { pub strength: u32, pub dexterity: u32, @@ -190,38 +118,6 @@ pub struct Attributes { pub mind: u32, } -pub fn get_racial_base_attributes(tribe_id: u8) -> Attributes { - // The Tribe Excel sheet only has deltas (e.g. 2 or -2) which are applied to a base 20 number... from somewhere - let base_stat = 20; - - let config = get_config(); - - let mut game_data = GameData::from_existing(Platform::Win32, &config.game_location).unwrap(); - - let exh = game_data.read_excel_sheet_header("Tribe").unwrap(); - let exd = game_data - .read_excel_sheet("Tribe", &exh, Language::English, 0) - .unwrap(); - - let tribe_row = &exd.read_row(&exh, tribe_id as u32).unwrap()[0]; - - let get_column = |column_index: usize| { - let physis::exd::ColumnData::Int8(delta) = &tribe_row.data[column_index] else { - panic!("Unexpected type!"); - }; - - *delta - }; - - Attributes { - strength: (base_stat + get_column(4)) as u32, - dexterity: (base_stat + get_column(6)) as u32, - vitality: (base_stat + get_column(5)) as u32, - intelligence: (base_stat + get_column(7)) as u32, - mind: (base_stat + get_column(8)) as u32, - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/lobby/connection.rs b/src/lobby/connection.rs index 6e8c731..689798b 100644 --- a/src/lobby/connection.rs +++ b/src/lobby/connection.rs @@ -6,7 +6,7 @@ use crate::{ blowfish::Blowfish, common::{ custom_ipc::{CustomIpcData, CustomIpcSegment, CustomIpcType}, - get_world_name, timestamp_secs, + timestamp_secs, }, config::get_config, lobby::CharaMake, @@ -125,7 +125,7 @@ impl LobbyConnection { index: 0, flags: 0, icon: 0, - name: get_world_name(config.world.world_id), + name: self.world_name.clone(), }] .to_vec(); // add any empty boys