diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index 7ad1a6b..3939f48 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -802,21 +802,14 @@ async fn client_loop( result = game_data.get_gilshop_item(*event_id, *item_index as u16); } - if let Some((item_id, price_mid)) = result { - if connection.player_data.inventory.currency.gil.quantity >= price_mid as u32 { + if let Some(item_info) = result { + if connection.player_data.inventory.currency.gil.quantity >= item_info.price_mid as u32 { // TODO: send the proper response packets! - connection.player_data.inventory.currency.gil.quantity -= price_mid as u32; - connection.player_data.inventory.add_in_next_free_slot(Item::new(1, item_id as u32)); + connection.player_data.inventory.currency.gil.quantity -= item_info.price_mid as u32; + connection.player_data.inventory.add_in_next_free_slot(Item::new(1, item_info.id as u32)); connection.send_inventory(false).await; // TODO: send an actual system notice, this is just a placeholder to provide feedback that the player actually bought something. - let result; - { - let mut game_data = connection.gamedata.lock().unwrap(); - result = game_data.get_item_name(item_id as u32); - } - let fallback = "".to_string(); - let item_name = result.unwrap_or(fallback); - connection.send_message(&format!("You obtained one or more items: {} (id: {})!", item_name, item_id)).await; + connection.send_message(&format!("You obtained one or more items: {} (id: {})!", item_info.name, item_info.id)).await; } else { connection.send_message("Insufficient gil to buy item. Nice try bypassing the client-side check!").await; } diff --git a/src/common/gamedata.rs b/src/common/gamedata.rs index a516d28..0fbb0e5 100644 --- a/src/common/gamedata.rs +++ b/src/common/gamedata.rs @@ -30,6 +30,28 @@ impl Default for GameData { } } +/// Struct detailing various information about an item, pulled from the Items sheet. +#[derive(Default)] +pub struct ItemInfo { + /// The item's textual name. + pub name: String, + /// The item's id number. + pub id: u32, + /// The item's price, when sold by an NPC. + pub price_mid: u32, + /// The item's price, when sold to an NPC by the player. + pub price_low: u32, + /// The item's equip category. + pub equip_category: u8, + pub primary_model_id: u64, +} + +#[derive(Debug)] +pub enum ItemInfoQuery { + ById(u32), + ByName(String), +} + impl GameData { pub fn new() -> Self { let config = get_config(); @@ -98,20 +120,86 @@ impl GameData { }) } + /// Gets various information from the Item sheet. + pub fn get_item_info(&mut self, query: ItemInfoQuery) -> Option { + let mut result = None; + 'outer: for page in &self.item_pages { + match query { + ItemInfoQuery::ById(ref query_item_id) => { + if let Some(row) = page.get_row(*query_item_id) { + let ExcelRowKind::SingleRow(item_row) = row else { + panic!("Expected a single row!"); + }; + result = Some((item_row, query_item_id)); + break 'outer; + } + } + + ItemInfoQuery::ByName(ref query_item_name) => { + for row in &page.rows { + let ExcelRowKind::SingleRow(single_row) = &row.kind else { + panic!("Expected a single row!"); + }; + + let physis::exd::ColumnData::String(item_name) = &single_row.columns[9] + else { + panic!("Unexpected type!"); + }; + + if !item_name + .to_lowercase() + .contains(&query_item_name.to_lowercase()) + { + continue; + } + + result = Some((single_row.clone(), &row.row_id)); + break 'outer; + } + } + } + } + + if let Some((matched_row, item_id)) = result { + let physis::exd::ColumnData::String(name) = &matched_row.columns[9] else { + panic!("Unexpected type!"); + }; + + let physis::exd::ColumnData::UInt8(equip_category) = &matched_row.columns[17] else { + panic!("Unexpected type!"); + }; + + let physis::exd::ColumnData::UInt32(price_mid) = &matched_row.columns[25] else { + panic!("Unexpected type!"); + }; + + let physis::exd::ColumnData::UInt32(price_low) = &matched_row.columns[26] else { + panic!("Unexpected type!"); + }; + + let physis::exd::ColumnData::UInt64(primary_model_id) = &matched_row.columns[47] else { + panic!("Unexpected type!"); + }; + + let item_info = ItemInfo { + id: *item_id, + name: name.to_string(), + price_mid: *price_mid, + price_low: *price_low, + equip_category: *equip_category, + primary_model_id: *primary_model_id, + }; + + return Some(item_info); + } + + None + } + /// Gets the primary model ID for a given item ID pub fn get_primary_model_id(&mut self, item_id: u32) -> Option { - for page in &self.item_pages { - if let Some(row) = page.get_row(item_id) { - let ExcelRowKind::SingleRow(item_row) = row else { - panic!("Expected a single row!") - }; - - let physis::exd::ColumnData::UInt64(id) = &item_row.columns[47] else { - panic!("Unexpected type!"); - }; - - return Some(*id); - } + if let Some(item_info) = self.get_item_info(ItemInfoQuery::ById(item_id)) { + return Some(item_info.primary_model_id); } None @@ -160,50 +248,6 @@ impl GameData { Some(value.clone()) } - /// Find an item's equip category and id by name, if it exists. - pub fn get_item_by_name(&mut self, name: &str) -> Option<(u8, u32)> { - for page in &self.item_pages { - for row in &page.rows { - let ExcelRowKind::SingleRow(single_row) = &row.kind else { - panic!("Expected a single row!") - }; - - let physis::exd::ColumnData::String(item_name) = &single_row.columns[9] else { - panic!("Unexpected type!"); - }; - - if !item_name.to_lowercase().contains(&name.to_lowercase()) { - continue; - } - - let physis::exd::ColumnData::UInt8(equip_category) = &single_row.columns[17] else { - panic!("Unexpected type!"); - }; - - return Some((*equip_category, row.row_id)); - } - } - - None - } - - pub fn get_item_name(&mut self, item_id: u32) -> Option { - for page in &self.item_pages { - if let Some(row) = page.get_row(item_id) { - let ExcelRowKind::SingleRow(item_row) = row else { - panic!("Expected a single row!") - }; - - let physis::exd::ColumnData::String(item_name) = &item_row.columns[9] else { - panic!("Unexpected type!") - }; - - return Some(item_name.clone()); - } - } - None - } - /// Turn an equip slot category id into a slot for the equipped inventory pub fn get_equipslot_category(&mut self, equipslot_id: u8) -> Option { let sheet = EquipSlotCategorySheet::read_from(&mut self.game_data, Language::None)?; @@ -349,24 +393,12 @@ impl GameData { } /// Gets the item and its cost from the specified shop. - pub fn get_gilshop_item(&mut self, gilshop_id: u32, index: u16) -> Option<(i32, i32)> { + pub fn get_gilshop_item(&mut self, gilshop_id: u32, index: u16) -> Option { let sheet = GilShopItemSheet::read_from(&mut self.game_data, Language::None)?; let row = sheet.get_subrow(gilshop_id, index)?; let item_id = row.Item().into_i32()?; - for page in &self.item_pages { - if let Some(row) = page.get_row(*item_id as u32) { - let ExcelRowKind::SingleRow(item_row) = row else { - panic!("Expected a single row!") - }; - let physis::exd::ColumnData::UInt32(price_mid) = &item_row.columns[25] else { - panic!("Unexpected type!") - }; - - return Some((*item_id, *price_mid as i32)); - } - } - None + self.get_item_info(ItemInfoQuery::ById(*item_id as u32)) } } diff --git a/src/common/mod.rs b/src/common/mod.rs index 4d223d9..f99c517 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -12,7 +12,7 @@ pub use position::Position; mod gamedata; pub use gamedata::GameData; -pub use gamedata::TerritoryNameKind; +pub use gamedata::{ItemInfo, ItemInfoQuery, TerritoryNameKind}; pub mod workdefinitions; diff --git a/src/world/chat_handler.rs b/src/world/chat_handler.rs index 102c633..3f74f07 100644 --- a/src/world/chat_handler.rs +++ b/src/world/chat_handler.rs @@ -1,4 +1,5 @@ use crate::{ + common::ItemInfoQuery, inventory::{Item, Storage}, ipc::zone::{ChatMessage, GameMasterRank}, world::ToServer, @@ -57,15 +58,19 @@ impl ChatHandler { { let mut gamedata = connection.gamedata.lock().unwrap(); - if let Some((equip_category, id)) = gamedata.get_item_by_name(name) { - let slot = gamedata.get_equipslot_category(equip_category).unwrap(); + if let Some(item_info) = + gamedata.get_item_info(ItemInfoQuery::ByName(name.to_string())) + { + let slot = gamedata + .get_equipslot_category(item_info.equip_category) + .unwrap(); connection .player_data .inventory .equipped .get_slot_mut(slot) - .id = id; + .id = item_info.id; connection .player_data .inventory @@ -84,11 +89,13 @@ impl ChatHandler { { let mut gamedata = connection.gamedata.lock().unwrap(); - if let Some((_, id)) = gamedata.get_item_by_name(name) { + if let Some(item_info) = + gamedata.get_item_info(ItemInfoQuery::ByName(name.to_string())) + { connection .player_data .inventory - .add_in_next_free_slot(Item::new(1, id)); + .add_in_next_free_slot(Item::new(1, item_info.id)); } }