1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-07-09 23:47:46 +00:00

Implement items costing money now (#94)

-Includes an extra check for trying to bypass the client-side
-Update dependencies
-Include a message that selling isn't supported yet
-Display a message indicating an item was bought
This commit is contained in:
thedax 2025-07-01 21:21:47 -04:00 committed by GitHub
parent 927c093915
commit 9bed7595cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 70 additions and 19 deletions

6
Cargo.lock generated
View file

@ -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",

View file

@ -309,6 +309,7 @@ common_events = {
-- NPC shops that accept gil for purchasing items
generic_gil_shops = {
262157, -- Tanie <Florist>, New Gridania
262197, -- Gerulf <Independent Culinarian>, Limsa Lominsa: The Lower Decks
263220, -- Neon <Air-wheeler dealer>, Solution Nine
}

View file

@ -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 = "<Error loading item name!>".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;
}

View file

@ -187,6 +187,23 @@ impl GameData {
None
}
pub fn get_item_name(&mut self, item_id: u32) -> Option<String> {
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<u16> {
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<i32> {
/// 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
}
}