1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-04-19 22:36:49 +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

@ -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 weather <id>`: Changes the weather
* `//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(),
})
.await,
GameMasterCommandType::GiveItem => {
connection.inventory.extra_slot.id = *arg;
connection.inventory.extra_slot.quantity = 1;
connection.send_inventory(false).await;
}
}
}
ClientZoneIpcData::EnterZoneLine {

View file

@ -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()

View file

@ -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;
}
}
}

View file

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

View file

@ -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);
}

View file

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