From 5a580149b4d92a875b4b10219ccbbb239d40803a Mon Sep 17 00:00:00 2001 From: thedax Date: Mon, 30 Jun 2025 15:21:08 -0400 Subject: [PATCH] Document some opcodes related to shops and implement a generic gil shopkeeper script (#85) Document some opcodes related to shops and implement a generic gil shopkeeper script * You can now interact with shopkeepers, and if you have enough gil, you can attempt to purchase items * Upon trying to buy items the event will auto-cancel for now, because we're missing implementations of several opcodes related to inventory management --- resources/opcodes.json | 10 +++++++ resources/scripts/events/Events.lua | 9 +++++++ .../events/common/GenericShopkeeper.lua | 27 +++++++++++++++++++ src/bin/kawari-world.rs | 15 +++++++++++ src/ipc/zone/mod.rs | 21 +++++++++++++++ 5 files changed, 82 insertions(+) create mode 100644 resources/scripts/events/common/GenericShopkeeper.lua diff --git a/resources/opcodes.json b/resources/opcodes.json index d9223f7..094cdd8 100644 --- a/resources/opcodes.json +++ b/resources/opcodes.json @@ -224,6 +224,11 @@ "name": "UnkCall", "opcode": 886, "size": 32 + }, + { + "name": "InventoryActionAck", + "opcode": 483, + "size": 16 } ], "ClientZoneIpcType": [ @@ -361,6 +366,11 @@ "name": "EventUnkRequest", "opcode": 448, "size": 16 + }, + { + "name": "GilShopTransaction", + "opcode": 108, + "size": 24 } ], "ServerLobbyIpcType": [ diff --git a/resources/scripts/events/Events.lua b/resources/scripts/events/Events.lua index 149bc98..734cea1 100644 --- a/resources/scripts/events/Events.lua +++ b/resources/scripts/events/Events.lua @@ -304,6 +304,11 @@ common_events = { -- [721620] = "GenericGemstoneTrader.lua", -- Generic Endwalker & Dawntrail in-city gemstone traders, but they do nothing when interacted with right now } +-- NPC shops that accept gil for purchasing items +generic_gil_shops = { + 263220, -- Neon , Solution Nine +} + -- Not all Hunt NPCs are spawning right now, unfortunately. generic_currency_exchange = { 1769533, -- Gold Saucer Attendant (behind counter) -> Prize Exchange (Gear) @@ -377,6 +382,10 @@ for _, event_id in pairs(generic_anetshards) do registerEvent(event_id, "events/common/GenericAethernetShard.lua") end +for _, event_id in pairs(generic_gil_shops) do + registerEvent(event_id, "events/common/GenericShopkeeper.lua") --TODO: It might be okay to combine gil shops with battle currency shops, still unclear +end + for _, event_id in pairs(generic_currency_exchange) do registerEvent(event_id, "events/common/GenericHuntCurrencyExchange.lua") --TODO: Should probably rename this since it now covers other generic currency vendors like Gold Saucer ones end diff --git a/resources/scripts/events/common/GenericShopkeeper.lua b/resources/scripts/events/common/GenericShopkeeper.lua new file mode 100644 index 0000000..f4397bc --- /dev/null +++ b/resources/scripts/events/common/GenericShopkeeper.lua @@ -0,0 +1,27 @@ +-- TODO: actually implement this menu + +-- Scene 00000: NPC greeting (usually an animation, sometimes text too?) +-- Scene 00010: Displays shop interface +-- Scene 00255: Unknown, but this was also observed when capturing gil shop transaction packets. When used standalone it softlocks. + +function onTalk(target, player) + --[[ Params observed: + Gil shops: [0, 1] + Non- shops: [1, 0] + MGP shops: [1, 100] + It's unclear what these mean since shops seem to open fine without these. + ]] + player:play_scene(target, EVENT_ID, 00000, 8192, {0}) +end + +function onReturn(scene, results, player) + if scene == 0 then + --[[ Retail sends 221 zeroes as u32s as the params to the shop cutscene, but it opens fine with a single zero u32. + Perhaps they are leftovers from earlier expansions? According to Sapphire, the params used to be significantly more complex. + Historically, it also seems cutscene 00040 was used instead of 00010 as it is now. + ]] + player:play_scene(player.id, EVENT_ID, 00010, 1 | 0x2000, {0}) + elseif scene == 10 then + player:finish_event(EVENT_ID) + end +end diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index bb78fe5..8b4a9c1 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -786,6 +786,16 @@ async fn client_loop( connection.player_data.inventory.process_action(action); connection.send_inventory(true).await; } + // 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:#?}"); + + // TODO: update the client's inventory, adjust their gil, and send the proper response packets! + connection.send_message("Shops are not implemented yet. Cancelling event...").await; + + // Cancel the event for now so the client doesn't get stuck + connection.event_finish(*event_id).await; + } ClientZoneIpcData::StartTalkEvent { actor_id, event_id } => { // load event { @@ -811,6 +821,11 @@ async fn client_loop( .await; } + /* TODO: ServerZoneIpcType::Unk18 with data [64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + * was observed to always be sent by the server upon interacting with shops. They open and function fine without + * it, but should we send it anyway, for the sake of accuracy? It's also still unclear if this + * happens for -every- NPC/actor. */ + let mut should_cancel = false; { let lua = lua.lock().unwrap(); diff --git a/src/ipc/zone/mod.rs b/src/ipc/zone/mod.rs index 1fe9580..63cc8a8 100644 --- a/src/ipc/zone/mod.rs +++ b/src/ipc/zone/mod.rs @@ -315,6 +315,12 @@ pub enum ServerZoneIpcData { #[brw(pad_after = 8)] unk3: u8, }, + #[br(pre_assert(*magic == ServerZoneIpcType::InventoryActionAck))] + InventoryActionAck { + sequence: u32, + #[brw(pad_after = 12)] + action_type: u16, + }, #[br(pre_assert(*magic == ServerZoneIpcType::UnkCall))] UnkCall { unk1: u32, @@ -455,6 +461,21 @@ pub enum ClientZoneIpcData { #[brw(pad_after = 4)] // padding event_id: u32, }, + #[br(pre_assert(*magic == ClientZoneIpcType::GilShopTransaction))] + GilShopTransaction { + event_id: u32, + /// Seems to always be 0x300000a at gil shops + unk1: u32, + /// 1 is buy, 2 is sell + buy_sell_mode: u32, + /// Index into the shopkeeper's or the player's inventory + item_index: u32, + /// Quantity of items being bought or sold + item_quantity: u32, + /// unk 2: Flags? These change quite a bit when dealing with stackable items, but are apparently always 0 when buying non-stackable + /// Observed values so far: 0xDDDDDDDD (when buying 99 of a stackable item), 0xFFFFFFFF, 0xFFE0FFD0, 0xfffefffe, 0x0000FF64 + unk2: u32, + }, #[br(pre_assert(*magic == ClientZoneIpcType::EventYieldHandler))] EventYieldHandler(EventYieldHandler<2>), #[br(pre_assert(*magic == ClientZoneIpcType::EventYieldHandler8))]