From 822d1f5139f29fd857e14a7819547f30f9902910 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Mon, 31 Mar 2025 23:23:29 -0400 Subject: [PATCH] Add GM command to give items, support equipping from inventory Now with this command and support for *one* inventory slot, you can now freely equip items to your character for testing. --- USAGE.md | 3 +- resources/tests/inventory_modify.bin | Bin 112 -> 48 bytes src/bin/kawari-world.rs | 5 +++ src/world/connection.rs | 54 ++++++++++++++++++++++++++- src/world/inventory.rs | 48 ++++++++++++++---------- src/world/ipc/container_info.rs | 3 +- src/world/ipc/inventory_modify.rs | 37 ++++++++++-------- src/world/ipc/mod.rs | 1 + 8 files changed, 114 insertions(+), 37 deletions(-) diff --git a/USAGE.md b/USAGE.md index 9292a48..afdc4ad 100644 --- a/USAGE.md +++ b/USAGE.md @@ -92,7 +92,7 @@ These special debug commands start with `!` and are custom to Kawari. * Territory `181`, Event `1245185` plays the Limsa opening sequence * Territory `182`, Event `1245187` plays the Ul'dah opening sequence * Territory `183`, Event `1245186` plays the Gridania opening sequence - + ### GM commands These GM commands are implemented in the FFXIV protocol, but only some of them are implemented. @@ -100,3 +100,4 @@ These GM commands are implemented in the FFXIV protocol, but only some of them a * `//gm teri `: Changes to the specified territory * `//gm weather `: Changes the weather * `//gm wireframe`: Toggle wireframe rendering for the environment +* `//gm item `: Gives yourself an item. This can only place a single item in the first page of your inventory currently. diff --git a/resources/tests/inventory_modify.bin b/resources/tests/inventory_modify.bin index c5f00ad298310453a117aed6fdcff948892d81db..d117db0834726c00c72eb807f8d6fbab6e0662a6 100644 GIT binary patch literal 48 gcmZQzU=XljVgQ2|%nS@b#J~vQg2^TxBo2@X05|^u%m4rY literal 112 zcmZQ#U=VO)WB`K~%nS@H3=ZK?E?BIY2Sg=+*&xCJh@<97>w_>*IfDp83W#F(p!qr- MMB9ORAQpB203P=Uz5oCK diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index 1205a6d..671c96e 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -655,6 +655,11 @@ async fn client_loop( ActorControlCategory::ToggleWireframeRendering(), }) .await, + GameMasterCommandType::GiveItem => { + connection.inventory.extra_slot.id = *arg; + connection.inventory.extra_slot.quantity = 1; + connection.send_inventory(false).await; + } } } ClientZoneIpcData::EnterZoneLine { diff --git a/src/world/connection.rs b/src/world/connection.rs index b75a4ec..3d8f291 100644 --- a/src/world/connection.rs +++ b/src/world/connection.rs @@ -448,7 +448,37 @@ impl ZoneConnection { } pub async fn send_inventory(&mut self, send_appearance_update: bool) { - // item list + // page 1 + { + let extra_slot = self.inventory.extra_slot; + + let mut send_slot = async |slot_index: u16, item: &Item| { + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::ItemInfo, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::ItemInfo(ItemInfo { + container: ContainerType::Inventory0, + slot: slot_index, + quantity: item.quantity, + catalog_id: item.id, + condition: 30000, + ..Default::default() + }), + ..Default::default() + }; + + self.send_segment(PacketSegment { + source_actor: self.player_data.actor_id, + target_actor: self.player_data.actor_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; + }; + + send_slot(0, &extra_slot).await; + } + + // equipped { let equipped = self.inventory.equipped; @@ -490,6 +520,27 @@ impl ZoneConnection { send_slot(13, &equipped.soul_crystal).await; } + // inform the client of page 1 + { + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::ContainerInfo, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::ContainerInfo(ContainerInfo { + container: ContainerType::Inventory0, + num_items: 1, + ..Default::default() + }), + ..Default::default() + }; + + self.send_segment(PacketSegment { + source_actor: self.player_data.actor_id, + target_actor: self.player_data.actor_id, + segment_type: SegmentType::Ipc { data: ipc }, + }) + .await; + } + // inform the client they have items equipped { let ipc = ServerZoneIpcSegment { @@ -498,6 +549,7 @@ impl ZoneConnection { data: ServerZoneIpcData::ContainerInfo(ContainerInfo { container: ContainerType::Equipped, num_items: self.inventory.equipped.num_items(), + sequence: 1, ..Default::default() }), ..Default::default() diff --git a/src/world/inventory.rs b/src/world/inventory.rs index 2f683a9..783f772 100644 --- a/src/world/inventory.rs +++ b/src/world/inventory.rs @@ -5,7 +5,7 @@ use physis::{ use crate::config::get_config; -use super::ipc::InventoryModify; +use super::ipc::{ContainerType, InventoryModify}; #[derive(Default, Copy, Clone)] pub struct Item { @@ -52,10 +52,30 @@ impl EquippedContainer { + self.left_ring.quantity + self.soul_crystal.quantity } + + pub fn get_slot(&mut self, index: u16) -> &mut Item { + match index { + 0 => &mut self.main_hand, + 1 => &mut self.off_hand, + 2 => &mut self.head, + 3 => &mut self.body, + 4 => &mut self.hands, + 6 => &mut self.legs, + 7 => &mut self.feet, + 8 => &mut self.ears, + 9 => &mut self.neck, + 10 => &mut self.wrists, + 11 => &mut self.right_ring, + 12 => &mut self.left_ring, + 13 => &mut self.soul_crystal, + _ => panic!("Not a valid src_container_index?!?"), + } + } } pub struct Inventory { pub equipped: EquippedContainer, + pub extra_slot: Item, // WIP for inventory pages } impl Default for Inventory { @@ -68,6 +88,7 @@ impl Inventory { pub fn new() -> Self { Self { equipped: EquippedContainer::default(), + extra_slot: Item::default() } } @@ -116,26 +137,15 @@ impl Inventory { pub fn process_action(&mut self, action: &InventoryModify) { // equipped - if action.src_storage_id == 1000 { - let slot = match action.src_container_index { - 0 => &mut self.equipped.main_hand, - 1 => &mut self.equipped.off_hand, - 2 => &mut self.equipped.head, - 3 => &mut self.equipped.body, - 4 => &mut self.equipped.hands, - 6 => &mut self.equipped.legs, - 7 => &mut self.equipped.feet, - 8 => &mut self.equipped.ears, - 9 => &mut self.equipped.neck, - 10 => &mut self.equipped.wrists, - 11 => &mut self.equipped.right_ring, - 12 => &mut self.equipped.left_ring, - 13 => &mut self.equipped.soul_crystal, - _ => panic!("Not a valid src_container_index?!?"), - }; + if action.src_storage_id == ContainerType::Equipped { + let src_slot = self.equipped.get_slot(action.src_container_index); // it only unequips for now, doesn't move the item - *slot = Item::default(); + *src_slot = Item::default(); + } else if action.src_storage_id == ContainerType::Inventory0 { + let dst_slot = self.equipped.get_slot(action.dst_container_index); + + *dst_slot = self.extra_slot; } } } diff --git a/src/world/ipc/container_info.rs b/src/world/ipc/container_info.rs index 7614e48..560531d 100644 --- a/src/world/ipc/container_info.rs +++ b/src/world/ipc/container_info.rs @@ -11,6 +11,7 @@ pub enum ContainerType { Inventory2 = 2, Inventory3 = 3, Equipped = 1000, + ArmouryBody = 3202, } #[binrw] @@ -19,7 +20,7 @@ pub enum ContainerType { pub struct ContainerInfo { pub sequence: u32, pub num_items: u32, - #[brw(pad_after = 2)] // not used + #[brw(pad_size_to = 4)] pub container: ContainerType, pub start_or_finish: u32, } diff --git a/src/world/ipc/inventory_modify.rs b/src/world/ipc/inventory_modify.rs index 808c951..71c249e 100644 --- a/src/world/ipc/inventory_modify.rs +++ b/src/world/ipc/inventory_modify.rs @@ -1,20 +1,27 @@ use binrw::binrw; +use super::ContainerType; + #[binrw] #[derive(Debug, Clone, Default)] pub struct InventoryModify { pub context_id: u32, - pub operation_type: u8, - #[brw(pad_before = 3)] + pub operation_type: u16, + + #[brw(pad_before = 2)] pub src_actor_id: u32, - pub src_storage_id: u32, - pub src_container_index: i16, - #[brw(pad_before = 4)] + #[brw(pad_size_to = 4)] + pub src_storage_id: ContainerType, + pub src_container_index: u16, + #[brw(pad_before = 2)] pub src_stack: u32, pub src_catalog_id: u32, + pub dst_actor_id: u32, - pub dst_storage_id: u32, - pub dst_container_index: i16, + #[brw(pad_size_to = 4)] + pub dst_storage_id: ContainerType, + pub dst_container_index: u16, + #[brw(pad_before = 2)] pub dst_stack: u32, pub dst_catalog_id: u32, } @@ -36,16 +43,16 @@ mod tests { let mut buffer = Cursor::new(&buffer); let modify_inventory = InventoryModify::read_le(&mut buffer).unwrap(); - assert_eq!(modify_inventory.context_id, 0x10000002); - assert_eq!(modify_inventory.operation_type, 70); + assert_eq!(modify_inventory.context_id, 0x10000000); + assert_eq!(modify_inventory.operation_type, 572); assert_eq!(modify_inventory.src_actor_id, 0); - assert_eq!(modify_inventory.src_storage_id, 1000); - assert_eq!(modify_inventory.src_container_index, 4); - assert_eq!(modify_inventory.src_stack, 0); + assert_eq!(modify_inventory.src_storage_id, ContainerType::Equipped); + assert_eq!(modify_inventory.src_container_index, 3); + assert_eq!(modify_inventory.src_stack, 1); assert_eq!(modify_inventory.src_catalog_id, 0); - assert_eq!(modify_inventory.dst_actor_id, 209911808); - assert_eq!(modify_inventory.dst_storage_id, 0); - assert_eq!(modify_inventory.dst_container_index, 96); + assert_eq!(modify_inventory.dst_actor_id, 0); + assert_eq!(modify_inventory.dst_storage_id, ContainerType::ArmouryBody); + assert_eq!(modify_inventory.dst_container_index, 0); assert_eq!(modify_inventory.dst_stack, 0); assert_eq!(modify_inventory.dst_catalog_id, 0); } diff --git a/src/world/ipc/mod.rs b/src/world/ipc/mod.rs index 90caa3e..74575b9 100644 --- a/src/world/ipc/mod.rs +++ b/src/world/ipc/mod.rs @@ -136,6 +136,7 @@ pub enum GameMasterCommandType { ToggleInvisibility = 0xD, ToggleWireframe = 0x26, ChangeTerritory = 0x58, + GiveItem = 0xC8, } #[binrw]