1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-04-21 23:17:45 +00:00

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.
This commit is contained in:
Joshua Goins 2025-03-30 10:34:42 -04:00
parent 68d3b07acb
commit 37c19ee1b8
5 changed files with 251 additions and 242 deletions

View file

@ -1,7 +1,7 @@
use kawari::common::GameData;
use kawari::common::custom_ipc::CustomIpcData; use kawari::common::custom_ipc::CustomIpcData;
use kawari::common::custom_ipc::CustomIpcSegment; use kawari::common::custom_ipc::CustomIpcSegment;
use kawari::common::custom_ipc::CustomIpcType; use kawari::common::custom_ipc::CustomIpcType;
use kawari::common::get_world_name;
use kawari::config::get_config; use kawari::config::get_config;
use kawari::lobby::LobbyConnection; use kawari::lobby::LobbyConnection;
use kawari::lobby::ipc::{ClientLobbyIpcData, ServerLobbyIpcSegment}; use kawari::lobby::ipc::{ClientLobbyIpcData, ServerLobbyIpcSegment};
@ -24,7 +24,8 @@ async fn main() {
tracing::info!("Server started on {addr}"); 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 { loop {
let (socket, _) = listener.accept().await.unwrap(); let (socket, _) = listener.accept().await.unwrap();

View file

@ -2,10 +2,8 @@ use std::collections::HashMap;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use kawari::common::custom_ipc::{CustomIpcData, CustomIpcSegment, CustomIpcType}; use kawari::common::custom_ipc::{CustomIpcData, CustomIpcSegment, CustomIpcType};
use kawari::common::{ use kawari::common::{GameData, timestamp_secs};
Position, determine_initial_starting_zone, get_citystate, get_primary_model_id, get_world_name, use kawari::common::{Position, determine_initial_starting_zone};
};
use kawari::common::{get_racial_base_attributes, timestamp_secs};
use kawari::config::get_config; use kawari::config::get_config;
use kawari::lobby::CharaMake; use kawari::lobby::CharaMake;
use kawari::oodle::OodleNetwork; use kawari::oodle::OodleNetwork;
@ -28,8 +26,6 @@ use kawari::world::{
}; };
use kawari::world::{EffectsBuilder, LuaPlayer, PlayerData, StatusEffects, WorldDatabase}; use kawari::world::{EffectsBuilder, LuaPlayer, PlayerData, StatusEffects, WorldDatabase};
use mlua::{Function, Lua}; use mlua::{Function, Lua};
use physis::common::{Language, Platform};
use physis::gamedata::GameData;
use tokio::io::AsyncReadExt; use tokio::io::AsyncReadExt;
use tokio::net::TcpListener; use tokio::net::TcpListener;
@ -52,6 +48,7 @@ async fn main() {
let database = Arc::new(WorldDatabase::new()); let database = Arc::new(WorldDatabase::new());
let lua = Arc::new(Mutex::new(Lua::new())); let lua = Arc::new(Mutex::new(Lua::new()));
let game_data = Arc::new(Mutex::new(GameData::new()));
{ {
let lua = lua.lock().unwrap(); let lua = lua.lock().unwrap();
@ -89,6 +86,7 @@ async fn main() {
let database = database.clone(); let database = database.clone();
let lua = lua.clone(); let lua = lua.clone();
let game_data = game_data.clone();
let state = PacketState { let state = PacketState {
client_key: None, client_key: None,
@ -113,7 +111,7 @@ async fn main() {
let mut lua_player = LuaPlayer::default(); let mut lua_player = LuaPlayer::default();
let config = get_config(); /*let config = get_config();
let mut game_data = let mut game_data =
GameData::from_existing(Platform::Win32, &config.game_location).unwrap(); 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 exh = game_data.read_excel_sheet_header("Action").unwrap();
let exd = game_data let exd = game_data
.read_excel_sheet("Action", &exh, Language::English, 0) .read_excel_sheet("Action", &exh, Language::English, 0)
.unwrap(); .unwrap();*/
tokio::spawn(async move { tokio::spawn(async move {
let mut buf = [0; 2056]; let mut buf = [0; 2056];
@ -295,9 +293,14 @@ async fn main() {
// Stats // Stats
{ {
let attributes = get_racial_base_attributes( let attributes;
chara_details.chara_make.customize.subrace, {
); let mut game_data = game_data.lock().unwrap();
attributes = game_data.get_racial_base_attributes(
chara_details.chara_make.customize.subrace,
);
}
let ipc = ServerZoneIpcSegment { let ipc = ServerZoneIpcSegment {
op_code: ServerZoneIpcType::PlayerStats, op_code: ServerZoneIpcType::PlayerStats,
@ -396,122 +399,106 @@ async fn main() {
// send player spawn // send player spawn
{ {
let ipc = ServerZoneIpcSegment { let ipc;
op_code: ServerZoneIpcType::PlayerSpawn, {
timestamp: timestamp_secs(), let mut game_data = game_data.lock().unwrap();
data: ServerZoneIpcData::PlayerSpawn(PlayerSpawn { let equipped = &connection.inventory.equipped;
account_id: connection.player_data.account_id,
content_id: connection.player_data.content_id, ipc = ServerZoneIpcSegment {
current_world_id: config.world.world_id, op_code: ServerZoneIpcType::PlayerSpawn,
home_world_id: config.world.world_id, timestamp: timestamp_secs(),
gm_rank: GameMasterRank::Debug, data: ServerZoneIpcData::PlayerSpawn(
online_status: OnlineStatus::GameMasterBlue, PlayerSpawn {
common: CommonSpawn { account_id: connection
class_job: connection .player_data
.player_data .account_id,
.classjob_id, content_id: connection
name: chara_details.name, .player_data
hp_curr: connection.player_data.curr_hp, .content_id,
hp_max: connection.player_data.max_hp, current_world_id: config.world.world_id,
mp_curr: connection.player_data.curr_mp, home_world_id: config.world.world_id,
mp_max: connection.player_data.max_mp, gm_rank: GameMasterRank::Debug,
object_kind: ObjectKind::Player( online_status:
PlayerSubKind::Player, OnlineStatus::GameMasterBlue,
), common: CommonSpawn {
look: chara_details.chara_make.customize, class_job: connection
fc_tag: "LOCAL".to_string(), .player_data
display_flags: DisplayFlag::UNK, .classjob_id,
models: [ name: chara_details.name,
get_primary_model_id( hp_curr: connection
connection .player_data
.inventory .curr_hp,
.equipped hp_max: connection
.head .player_data
.id, .max_hp,
) mp_curr: connection
as u32, .player_data
get_primary_model_id( .curr_mp,
connection mp_max: connection
.inventory .player_data
.equipped .max_mp,
.body object_kind: ObjectKind::Player(
.id, PlayerSubKind::Player,
) ),
as u32, look: chara_details
get_primary_model_id( .chara_make
connection .customize,
.inventory fc_tag: "LOCAL".to_string(),
.equipped display_flags: DisplayFlag::UNK,
.hands models: [
.id, game_data.get_primary_model_id(
) equipped.head.id,
as u32, )
get_primary_model_id( as u32,
connection game_data.get_primary_model_id(
.inventory equipped.body.id,
.equipped )
.legs as u32,
.id, game_data.get_primary_model_id(
) equipped.hands.id,
as u32, )
get_primary_model_id( as u32,
connection game_data.get_primary_model_id(
.inventory equipped.legs.id,
.equipped )
.feet as u32,
.id, game_data.get_primary_model_id(
) equipped.feet.id,
as u32, )
get_primary_model_id( as u32,
connection game_data.get_primary_model_id(
.inventory equipped.ears.id,
.equipped )
.ears as u32,
.id, game_data.get_primary_model_id(
) equipped.neck.id,
as u32, )
get_primary_model_id( as u32,
connection game_data.get_primary_model_id(
.inventory equipped.wrists.id,
.equipped )
.neck as u32,
.id, game_data.get_primary_model_id(
) equipped.left_ring.id,
as u32, )
get_primary_model_id( as u32,
connection game_data.get_primary_model_id(
.inventory equipped.right_ring.id,
.equipped )
.wrists as u32,
.id, ],
) pos: exit_position
as u32, .unwrap_or(Position::default()),
get_primary_model_id( rotation: exit_rotation
connection .unwrap_or(0.0),
.inventory ..Default::default()
.equipped },
.left_ring ..Default::default()
.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()
},
..Default::default() ..Default::default()
}), };
..Default::default() }
};
connection connection
.send_segment(PacketSegment { .send_segment(PacketSegment {
@ -837,10 +824,10 @@ async fn main() {
ClientZoneIpcData::ActionRequest(request) => { ClientZoneIpcData::ActionRequest(request) => {
tracing::info!("Recieved action request: {:#?}!", request); tracing::info!("Recieved action request: {:#?}!", request);
let action_row = /*let action_row =
&exd.read_row(&exh, request.action_id).unwrap()[0]; &exd.read_row(&exh, request.action_id).unwrap()[0];
println!("Found action: {:#?}", action_row); println!("Found action: {:#?}", action_row);*/
let mut effects_builder = None; let mut effects_builder = None;
@ -858,6 +845,8 @@ async fn main() {
.create_userdata_ref_mut(&mut lua_player) .create_userdata_ref_mut(&mut lua_player)
.unwrap(); .unwrap();
let config = get_config();
let file_name = format!( let file_name = format!(
"{}/{}", "{}/{}",
&config.world.scripts_location, &config.world.scripts_location,
@ -993,8 +982,13 @@ async fn main() {
let chara_make = CharaMake::from_json(chara_make_json); let chara_make = CharaMake::from_json(chara_make_json);
let city_state = let city_state;
get_citystate(chara_make.classjob_id as u16); {
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( let (content_id, actor_id) = database.create_player_data(
name, name,
@ -1087,10 +1081,17 @@ async fn main() {
CustomIpcData::RequestCharacterList { service_account_id } => { CustomIpcData::RequestCharacterList { service_account_id } => {
let config = get_config(); 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( let characters = database.get_character_list(
*service_account_id, *service_account_id,
config.world.world_id, config.world.world_id,
&get_world_name(config.world.world_id), &world_name,
); );
// send response // send response

111
src/common/gamedata.rs Normal file
View file

@ -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
}
}

View file

@ -6,18 +6,15 @@ use std::{
mod customize_data; mod customize_data;
use binrw::binrw; use binrw::binrw;
pub use customize_data::CustomizeData; pub use customize_data::CustomizeData;
use physis::{
common::{Language, Platform},
gamedata::GameData,
};
use crate::config::get_config;
pub mod custom_ipc; pub mod custom_ipc;
mod position; mod position;
pub use position::Position; pub use position::Position;
mod gamedata;
pub use gamedata::GameData;
#[binrw] #[binrw]
#[brw(little)] #[brw(little)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -100,46 +97,6 @@ pub fn timestamp_msecs() -> u64 {
.unwrap() .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 /// Gets the initial zone for a given city-state id
pub fn determine_initial_starting_zone(citystate_id: u8) -> u16 { pub fn determine_initial_starting_zone(citystate_id: u8) -> u16 {
match citystate_id { 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 struct Attributes {
pub strength: u32, pub strength: u32,
pub dexterity: u32, pub dexterity: u32,
@ -190,38 +118,6 @@ pub struct Attributes {
pub mind: u32, 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -6,7 +6,7 @@ use crate::{
blowfish::Blowfish, blowfish::Blowfish,
common::{ common::{
custom_ipc::{CustomIpcData, CustomIpcSegment, CustomIpcType}, custom_ipc::{CustomIpcData, CustomIpcSegment, CustomIpcType},
get_world_name, timestamp_secs, timestamp_secs,
}, },
config::get_config, config::get_config,
lobby::CharaMake, lobby::CharaMake,
@ -125,7 +125,7 @@ impl LobbyConnection {
index: 0, index: 0,
flags: 0, flags: 0,
icon: 0, icon: 0,
name: get_world_name(config.world.world_id), name: self.world_name.clone(),
}] }]
.to_vec(); .to_vec();
// add any empty boys // add any empty boys