diff --git a/Cargo.lock b/Cargo.lock index 7806aa5..9ff1524 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1048,7 +1048,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "physis" version = "0.5.0" -source = "git+https://github.com/redstrate/physis#862b16b681e8593f2b327442deddc12922209531" +source = "git+https://github.com/redstrate/physis#5a5896a1261c13732ec856fbb9badbdd5da196d6" dependencies = [ "binrw", "bitflags", @@ -1124,9 +1124,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.21" +version = "0.12.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8cea6b35bcceb099f30173754403d2eba0a5dc18cea3630fccd88251909288" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" dependencies = [ "base64", "bytes", diff --git a/resources/scripts/events/Events.lua b/resources/scripts/events/Events.lua index e5e56fd..4fe8dd5 100644 --- a/resources/scripts/events/Events.lua +++ b/resources/scripts/events/Events.lua @@ -309,6 +309,7 @@ common_events = { -- NPC shops that accept gil for purchasing items generic_gil_shops = { 262157, -- Tanie , New Gridania + 262197, -- Gerulf , Limsa Lominsa: The Lower Decks 263220, -- Neon , Solution Nine } diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index 59cd7e3..7ad1a6b 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -792,23 +792,43 @@ async fn client_loop( // TODO: Likely rename this opcode if non-gil shops also use this same opcode ClientZoneIpcData::GilShopTransaction { event_id, unk1: _, buy_sell_mode, item_index, item_quantity, unk2: _ } => { tracing::info!("Client is interacting with a shop! {event_id:#?} {buy_sell_mode:#?} {item_quantity:#?} {item_index:#?}"); + const BUY: u32 = 1; + const SELL: u32 = 2; - let item_id; - { - let mut game_data = connection.gamedata.lock().unwrap(); - item_id = game_data.get_gilshop_item(*event_id, *item_index as u16); - } + if *buy_sell_mode == BUY { + let result; + { + let mut game_data = connection.gamedata.lock().unwrap(); + result = game_data.get_gilshop_item(*event_id, *item_index as u16); + } - if let Some(item_id) = item_id { - // TODO: adjust their gil, and send the proper response packets! - connection.send_message("Shops are not implemented fully yet. Giving you a free item...").await; - - connection.player_data.inventory.add_in_next_free_slot(Item::new(1, item_id as u32)); - connection.send_inventory(false).await; + if let Some((item_id, price_mid)) = result { + if connection.player_data.inventory.currency.gil.quantity >= 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.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; + } else { + connection.send_message("Insufficient gil to buy item. Nice try bypassing the client-side check!").await; + } + } else { + connection.send_message(&format!("Unable to find shop item, this is a bug in Kawari!")).await; + } + } else if *buy_sell_mode == SELL { + // TODO: Implement selling items back to shops + connection.send_message("Selling items to shops is not yet implemented. Cancelling event...").await; } else { - connection.send_message(&format!("Unable to find shop item, this is a bug in Kawari!")).await; + tracing::error!("Received unknown transaction mode {buy_sell_mode}!"); } - // Cancel the event for now so the client doesn't get stuck connection.event_finish(*event_id).await; } diff --git a/src/common/gamedata.rs b/src/common/gamedata.rs index b3e93bb..a516d28 100644 --- a/src/common/gamedata.rs +++ b/src/common/gamedata.rs @@ -187,6 +187,23 @@ impl GameData { 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)?; @@ -331,12 +348,25 @@ impl GameData { self.classjob_exp_indexes.get(classjob_id as usize).copied() } - /// Gets the item and it's cost from the specified shop. - pub fn get_gilshop_item(&mut self, gilshop_id: u32, index: u16) -> Option { + /// 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)> { 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!") + }; - row.Item().into_i32().copied() + let physis::exd::ColumnData::UInt32(price_mid) = &item_row.columns[25] else { + panic!("Unexpected type!") + }; + + return Some((*item_id, *price_mid as i32)); + } + } + None } }