1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-04-20 06:37:45 +00:00

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.
This commit is contained in:
Joshua Goins 2025-03-31 23:23:29 -04:00
parent 32b6840e38
commit 822d1f5139
8 changed files with 114 additions and 37 deletions

View file

@ -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 `181`, Event `1245185` plays the Limsa opening sequence
* Territory `182`, Event `1245187` plays the Ul'dah opening sequence * Territory `182`, Event `1245187` plays the Ul'dah opening sequence
* Territory `183`, Event `1245186` plays the Gridania opening sequence * Territory `183`, Event `1245186` plays the Gridania opening sequence
### GM commands ### GM commands
These GM commands are implemented in the FFXIV protocol, but only some of them are implemented. 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 <id>`: Changes to the specified territory * `//gm teri <id>`: Changes to the specified territory
* `//gm weather <id>`: Changes the weather * `//gm weather <id>`: Changes the weather
* `//gm wireframe`: Toggle wireframe rendering for the environment * `//gm wireframe`: Toggle wireframe rendering for the environment
* `//gm item <id>`: Gives yourself an item. This can only place a single item in the first page of your inventory currently.

Binary file not shown.

View file

@ -655,6 +655,11 @@ async fn client_loop(
ActorControlCategory::ToggleWireframeRendering(), ActorControlCategory::ToggleWireframeRendering(),
}) })
.await, .await,
GameMasterCommandType::GiveItem => {
connection.inventory.extra_slot.id = *arg;
connection.inventory.extra_slot.quantity = 1;
connection.send_inventory(false).await;
}
} }
} }
ClientZoneIpcData::EnterZoneLine { ClientZoneIpcData::EnterZoneLine {

View file

@ -448,7 +448,37 @@ impl ZoneConnection {
} }
pub async fn send_inventory(&mut self, send_appearance_update: bool) { 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; let equipped = self.inventory.equipped;
@ -490,6 +520,27 @@ impl ZoneConnection {
send_slot(13, &equipped.soul_crystal).await; 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 // inform the client they have items equipped
{ {
let ipc = ServerZoneIpcSegment { let ipc = ServerZoneIpcSegment {
@ -498,6 +549,7 @@ impl ZoneConnection {
data: ServerZoneIpcData::ContainerInfo(ContainerInfo { data: ServerZoneIpcData::ContainerInfo(ContainerInfo {
container: ContainerType::Equipped, container: ContainerType::Equipped,
num_items: self.inventory.equipped.num_items(), num_items: self.inventory.equipped.num_items(),
sequence: 1,
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()

View file

@ -5,7 +5,7 @@ use physis::{
use crate::config::get_config; use crate::config::get_config;
use super::ipc::InventoryModify; use super::ipc::{ContainerType, InventoryModify};
#[derive(Default, Copy, Clone)] #[derive(Default, Copy, Clone)]
pub struct Item { pub struct Item {
@ -52,10 +52,30 @@ impl EquippedContainer {
+ self.left_ring.quantity + self.left_ring.quantity
+ self.soul_crystal.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 struct Inventory {
pub equipped: EquippedContainer, pub equipped: EquippedContainer,
pub extra_slot: Item, // WIP for inventory pages
} }
impl Default for Inventory { impl Default for Inventory {
@ -68,6 +88,7 @@ impl Inventory {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
equipped: EquippedContainer::default(), equipped: EquippedContainer::default(),
extra_slot: Item::default()
} }
} }
@ -116,26 +137,15 @@ impl Inventory {
pub fn process_action(&mut self, action: &InventoryModify) { pub fn process_action(&mut self, action: &InventoryModify) {
// equipped // equipped
if action.src_storage_id == 1000 { if action.src_storage_id == ContainerType::Equipped {
let slot = match action.src_container_index { let src_slot = self.equipped.get_slot(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?!?"),
};
// it only unequips for now, doesn't move the item // 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;
} }
} }
} }

View file

@ -11,6 +11,7 @@ pub enum ContainerType {
Inventory2 = 2, Inventory2 = 2,
Inventory3 = 3, Inventory3 = 3,
Equipped = 1000, Equipped = 1000,
ArmouryBody = 3202,
} }
#[binrw] #[binrw]
@ -19,7 +20,7 @@ pub enum ContainerType {
pub struct ContainerInfo { pub struct ContainerInfo {
pub sequence: u32, pub sequence: u32,
pub num_items: u32, pub num_items: u32,
#[brw(pad_after = 2)] // not used #[brw(pad_size_to = 4)]
pub container: ContainerType, pub container: ContainerType,
pub start_or_finish: u32, pub start_or_finish: u32,
} }

View file

@ -1,20 +1,27 @@
use binrw::binrw; use binrw::binrw;
use super::ContainerType;
#[binrw] #[binrw]
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct InventoryModify { pub struct InventoryModify {
pub context_id: u32, pub context_id: u32,
pub operation_type: u8, pub operation_type: u16,
#[brw(pad_before = 3)]
#[brw(pad_before = 2)]
pub src_actor_id: u32, pub src_actor_id: u32,
pub src_storage_id: u32, #[brw(pad_size_to = 4)]
pub src_container_index: i16, pub src_storage_id: ContainerType,
#[brw(pad_before = 4)] pub src_container_index: u16,
#[brw(pad_before = 2)]
pub src_stack: u32, pub src_stack: u32,
pub src_catalog_id: u32, pub src_catalog_id: u32,
pub dst_actor_id: u32, pub dst_actor_id: u32,
pub dst_storage_id: u32, #[brw(pad_size_to = 4)]
pub dst_container_index: i16, pub dst_storage_id: ContainerType,
pub dst_container_index: u16,
#[brw(pad_before = 2)]
pub dst_stack: u32, pub dst_stack: u32,
pub dst_catalog_id: u32, pub dst_catalog_id: u32,
} }
@ -36,16 +43,16 @@ mod tests {
let mut buffer = Cursor::new(&buffer); let mut buffer = Cursor::new(&buffer);
let modify_inventory = InventoryModify::read_le(&mut buffer).unwrap(); let modify_inventory = InventoryModify::read_le(&mut buffer).unwrap();
assert_eq!(modify_inventory.context_id, 0x10000002); assert_eq!(modify_inventory.context_id, 0x10000000);
assert_eq!(modify_inventory.operation_type, 70); assert_eq!(modify_inventory.operation_type, 572);
assert_eq!(modify_inventory.src_actor_id, 0); assert_eq!(modify_inventory.src_actor_id, 0);
assert_eq!(modify_inventory.src_storage_id, 1000); assert_eq!(modify_inventory.src_storage_id, ContainerType::Equipped);
assert_eq!(modify_inventory.src_container_index, 4); assert_eq!(modify_inventory.src_container_index, 3);
assert_eq!(modify_inventory.src_stack, 0); assert_eq!(modify_inventory.src_stack, 1);
assert_eq!(modify_inventory.src_catalog_id, 0); assert_eq!(modify_inventory.src_catalog_id, 0);
assert_eq!(modify_inventory.dst_actor_id, 209911808); assert_eq!(modify_inventory.dst_actor_id, 0);
assert_eq!(modify_inventory.dst_storage_id, 0); assert_eq!(modify_inventory.dst_storage_id, ContainerType::ArmouryBody);
assert_eq!(modify_inventory.dst_container_index, 96); assert_eq!(modify_inventory.dst_container_index, 0);
assert_eq!(modify_inventory.dst_stack, 0); assert_eq!(modify_inventory.dst_stack, 0);
assert_eq!(modify_inventory.dst_catalog_id, 0); assert_eq!(modify_inventory.dst_catalog_id, 0);
} }

View file

@ -136,6 +136,7 @@ pub enum GameMasterCommandType {
ToggleInvisibility = 0xD, ToggleInvisibility = 0xD,
ToggleWireframe = 0x26, ToggleWireframe = 0x26,
ChangeTerritory = 0x58, ChangeTerritory = 0x58,
GiveItem = 0xC8,
} }
#[binrw] #[binrw]