diff --git a/resources/opcodes.json b/resources/opcodes.json index add7f12..1f37cff 100644 --- a/resources/opcodes.json +++ b/resources/opcodes.json @@ -239,6 +239,16 @@ "name": "UnkResponse2", "opcode": 772, "size": 8 + }, + { + "name": "InventorySlotDiscard", + "opcode": 851, + "size": 48 + }, + { + "name": "InventorySlotDiscardFin", + "opcode": 854, + "size": 16 } ], "ClientZoneIpcType": [ diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index 4cabb6e..93b4b64 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -36,6 +36,10 @@ use tokio::sync::mpsc::{Receiver, UnboundedReceiver, UnboundedSender, channel, u use tokio::sync::oneshot; use tokio::task::JoinHandle; +use kawari::{ + INVENTORY_ACTION_ACK_GENERAL, /*INVENTORY_ACTION_ACK_SHOP,*/ INVENTORY_ACTION_DISCARD, +}; + fn spawn_main_loop() -> (ServerHandle, JoinHandle<()>) { let (send, recv) = channel(64); @@ -789,7 +793,80 @@ async fn client_loop( ClientZoneIpcData::ItemOperation(action) => { tracing::info!("Client is modifying inventory! {action:#?}"); + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::InventoryActionAck, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::InventoryActionAck { + sequence: action.context_id, + action_type: INVENTORY_ACTION_ACK_GENERAL as u16, + }, + ..Default::default() + }; + + connection + .send_segment(PacketSegment { + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, + segment_type: SegmentType::Ipc, + data: SegmentData::Ipc { data: ipc }, + }) + .await; + if action.operation_type == INVENTORY_ACTION_DISCARD { + tracing::info!("Player is discarding from their inventory!"); + let sequence = 0; // TODO: How is this decided? It seems to be a sequence value but it's not sent by the client! Perhaps it's a 'lifetime-of-the-character' value that simply gets increased for every inventory action ever taken? + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::InventorySlotDiscard, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::InventorySlotDiscard { + unk1: sequence, + operation_type: action.operation_type, + src_actor_id: action.src_actor_id, + src_storage_id: action.src_storage_id, + src_container_index: action.src_container_index, + src_stack: action.src_stack, + src_catalog_id: action.src_catalog_id, + dst_actor_id: 0xE0000000, + dst_storage_id: 0xFFFF, + dst_container_index: 0xFFFF, + dst_catalog_id: 0x0000FFFF, + }, + ..Default::default() + }; + + connection + .send_segment(PacketSegment { + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, + segment_type: SegmentType::Ipc, + data: SegmentData::Ipc { data: ipc }, + }) + .await; + + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::InventorySlotDiscard, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::InventorySlotDiscardFin { + unk1: sequence, + unk2: sequence, // yes, this repeats, it's not a copy paste error + unk3: 0x90, + unk4: 0x200, + }, + ..Default::default() + }; + + connection + .send_segment(PacketSegment { + source_actor: connection.player_data.actor_id, + target_actor: connection.player_data.actor_id, + segment_type: SegmentType::Ipc, + data: SegmentData::Ipc { data: ipc }, + }) + .await; + } + connection.player_data.inventory.process_action(action); + + // TODO: This seems incorrect, the server wasn't observed to send updates here, but if we don't then the client doesn't realize the items have been modified connection.send_inventory(true).await; } // TODO: Likely rename this opcode if non-gil shops also use this same opcode diff --git a/src/ipc/zone/actor_control.rs b/src/ipc/zone/actor_control.rs index 70441c5..1e89d20 100644 --- a/src/ipc/zone/actor_control.rs +++ b/src/ipc/zone/actor_control.rs @@ -109,6 +109,16 @@ pub enum ActorControlCategory { #[brw(pad_before = 2)] // padding emote: u32, }, + #[brw(magic = 0x1ffu16)] + EventRelatedUnk1 { + #[brw(pad_before = 2)] // padding + unk1: u32, + }, + #[brw(magic = 0x200u16)] + EventRelatedUnk2 { + #[brw(pad_before = 2)] // padding + unk1: u32, + }, Unknown { category: u16, #[brw(pad_before = 2)] // padding diff --git a/src/ipc/zone/mod.rs b/src/ipc/zone/mod.rs index c7838ae..d52321b 100644 --- a/src/ipc/zone/mod.rs +++ b/src/ipc/zone/mod.rs @@ -92,6 +92,7 @@ use crate::common::ObjectTypeId; use crate::common::Position; use crate::common::read_string; use crate::common::write_string; +use crate::inventory::ContainerType; use crate::opcodes::ClientZoneIpcType; use crate::opcodes::ServerZoneIpcType; use crate::packet::IPC_HEADER_SIZE; @@ -347,6 +348,42 @@ pub enum ServerZoneIpcData { #[brw(pad_after = 7)] unk1: u8, }, + #[br(pre_assert(*magic == ServerZoneIpcType::InventorySlotDiscard))] + InventorySlotDiscard { + /// This is later reused in InventorySlotDiscardFin, so it might be some sort of sequence or context id, but it's not the one sent by the client + unk1: u32, + /// Same as the one sent by the client, not the one that the server responds with in inventoryactionack! + operation_type: u8, + #[br(pad_before = 3)] + src_actor_id: u32, + src_storage_id: ContainerType, + src_container_index: u16, + #[br(pad_before = 2)] + src_stack: u32, + src_catalog_id: u32, + + /// This is all static as far as I can tell, across two captures and a bunch of discards these never changed + /// seems to always be 3758096384 / E0 00 00 00 + dst_actor_id: u32, + /// seems to always be 65535/0xFFFF + dst_storage_id: u16, + /// seems to always be 65535/0xFFFF + dst_container_index: u16, + /// seems to always be 0x0000FFFF + #[br(pad_after = 8)] + dst_catalog_id: u32, + }, + #[br(pre_assert(*magic == ServerZoneIpcType::InventorySlotDiscardFin))] + InventorySlotDiscardFin { + /// Same value as unk1 in InventorySlotDiscard + unk1: u32, + /// Repeated unk1 value? + unk2: u32, + /// Unknown, seems to always be 0x00000090 + unk3: u32, + /// Unknown, seems to always be 0x00000200 + unk4: u32, + }, Unknown { #[br(count = size - 32)] unk: Vec, diff --git a/src/lib.rs b/src/lib.rs index c48488d..70a6173 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,3 +87,11 @@ pub const INVENTORY_ACTION_MOVE: u8 = 146; /// The operation opcode/type when moving an item to a slot occupied by another in the inventory. pub const INVENTORY_ACTION_EXCHANGE: u8 = 147; + +/// The server's acknowledgement of a shop item being purchased. +pub const INVENTORY_ACTION_ACK_SHOP: u8 = 6; + +/// The server's acknowledgement of the client modifying their inventory. +/// In the past, many more values were used according to Sapphire: +/// https://github.com/SapphireServer/Sapphire/blob/044bff026c01b4cc3a37cbc9b0881fadca3fc477/src/common/Common.h#L83 +pub const INVENTORY_ACTION_ACK_GENERAL: u8 = 7; diff --git a/src/world/server.rs b/src/world/server.rs index 7a99fa0..aac5d4a 100644 --- a/src/world/server.rs +++ b/src/world/server.rs @@ -323,6 +323,24 @@ pub async fn server_main_loop(mut recv: Receiver) -> Result<(), std::i to_remove.push(id); } } + + if let ClientTriggerCommand::EventRelatedUnk { .. } = &trigger.trigger + { + let msg = FromServer::ActorControlSelf(ActorControlSelf { + category: ActorControlCategory::EventRelatedUnk1 { unk1: 1 }, + }); + + if handle.send(msg).is_err() { + to_remove.push(id); + } + let msg = FromServer::ActorControlSelf(ActorControlSelf { + category: ActorControlCategory::EventRelatedUnk2 { unk1: 0 }, + }); + + if handle.send(msg).is_err() { + to_remove.push(id); + } + } continue; }