1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-07-13 17:07:45 +00:00

Rename inventory-related opcodes to match Karashiiro/FFXIVOpcodes (#112)

-Implement keyboard turning packet as a no-op so it'll stop clogging server logs

-Finish implementing inventory actions
This commit is contained in:
thedax 2025-07-12 17:40:22 -04:00 committed by GitHub
parent 779666a10f
commit 45ee95318c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 172 additions and 76 deletions

View file

@ -241,12 +241,12 @@
"size": 8
},
{
"name": "InventorySlotDiscard",
"name": "InventoryTransaction",
"opcode": 851,
"size": 48
},
{
"name": "InventorySlotDiscardFin",
"name": "InventoryTransactionFinish",
"opcode": 854,
"size": 16
}
@ -396,6 +396,11 @@
"name": "UnkCall2",
"opcode": 632,
"size": 8
},
{
"name": "StandardControlsPivot",
"opcode": 836,
"size": 8
}
],
"ServerLobbyIpcType": [

Binary file not shown.

View file

@ -6,7 +6,7 @@ use kawari::RECEIVE_BUFFER_SIZE;
use kawari::common::Position;
use kawari::common::{GameData, timestamp_secs};
use kawari::config::get_config;
use kawari::inventory::Item;
use kawari::inventory::{Item, ItemOperationKind};
use kawari::ipc::chat::{ServerChatIpcData, ServerChatIpcSegment};
use kawari::ipc::zone::{
ActorControlCategory, ActorControlSelf, PlayerEntry, PlayerSpawn, PlayerStatus, SocialList,
@ -36,9 +36,7 @@ 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,
};
use kawari::INVENTORY_ACTION_ACK_GENERAL;
fn spawn_main_loop() -> (ServerHandle, JoinHandle<()>) {
let (send, recv) = channel(64);
@ -810,24 +808,27 @@ async fn client_loop(
data: SegmentData::Ipc { data: ipc },
})
.await;
if action.operation_type == INVENTORY_ACTION_DISCARD {
connection.player_data.inventory.process_action(action);
if action.operation_type == ItemOperationKind::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,
op_code: ServerZoneIpcType::InventoryTransaction,
timestamp: timestamp_secs(),
data: ServerZoneIpcData::InventorySlotDiscard {
unk1: sequence,
data: ServerZoneIpcData::InventoryTransaction {
unk1: connection.player_data.item_sequence,
operation_type: action.operation_type,
src_actor_id: action.src_actor_id,
src_actor_id: connection.player_data.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,
unk2: action.src_stack,
unk3: action.src_catalog_id, // TODO: unk2 and unk3 are not being set accurately here, but the client doesn't seem to care what they are
dst_actor_id: 0,
dst_storage_id: 0xE000,
dst_container_index: u16::MAX,
dst_catalog_id: u32::MAX,
},
..Default::default()
};
@ -842,11 +843,11 @@ async fn client_loop(
.await;
let ipc = ServerZoneIpcSegment {
op_code: ServerZoneIpcType::InventorySlotDiscard,
op_code: ServerZoneIpcType::InventoryTransactionFinish,
timestamp: timestamp_secs(),
data: ServerZoneIpcData::InventorySlotDiscardFin {
unk1: sequence,
unk2: sequence, // yes, this repeats, it's not a copy paste error
data: ServerZoneIpcData::InventoryTransactionFinish {
unk1: connection.player_data.item_sequence,
unk2: connection.player_data.item_sequence, // yes, this repeats, it's not a copy paste error
unk3: 0x90,
unk4: 0x200,
},
@ -863,10 +864,7 @@ async fn client_loop(
.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;
connection.player_data.item_sequence += 1;
}
// 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: _ } => {
@ -989,6 +987,10 @@ async fn client_loop(
))
.await;
}
ClientZoneIpcData::StandardControlsPivot { .. } => {
/* No-op because we already seem to handle this, other nearby clients can see the sending player
* pivoting anyway. */
}
ClientZoneIpcData::EventUnkRequest { event_id, unk1, unk2, unk3 } => {
let ipc = ServerZoneIpcSegment {
op_code: ServerZoneIpcType::EventUnkReply,

View file

@ -1,4 +1,5 @@
use crate::common::GameData;
use binrw::binrw;
use icarus::{ClassJob::ClassJobSheet, Race::RaceSheet};
use physis::common::Language;
use serde::{Deserialize, Serialize};
@ -20,11 +21,46 @@ pub use storage::{ContainerType, Storage};
mod currency;
pub use currency::CurrencyStorage;
use crate::INVENTORY_ACTION_DISCARD;
use crate::{
INVENTORY_ACTION_COMBINE_STACK, INVENTORY_ACTION_DISCARD, INVENTORY_ACTION_EXCHANGE,
INVENTORY_ACTION_MOVE, INVENTORY_ACTION_SPLIT_STACK,
};
const MAX_NORMAL_STORAGE: usize = 35;
const MAX_LARGE_STORAGE: usize = 50;
#[binrw]
#[derive(Debug, Clone, Default, Copy, PartialEq)]
#[brw(repr = u8)]
#[repr(u8)]
pub enum ItemOperationKind {
/// The operation opcode/type when discarding an item from the inventory.
Discard = INVENTORY_ACTION_DISCARD,
#[default]
/// The operation opcode/type when moving an item to an emtpy slot in the inventory.
Move = INVENTORY_ACTION_MOVE,
/// The operation opcode/type when moving an item to a slot occupied by another in the inventory.
Exchange = INVENTORY_ACTION_EXCHANGE,
/// The operation opcode/type when splitting stacks of identical items.
SplitStack = INVENTORY_ACTION_SPLIT_STACK,
/// The operation opcode/type when combining stacks of identical items.
CombineStack = INVENTORY_ACTION_COMBINE_STACK,
}
impl TryFrom<u8> for ItemOperationKind {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
x if x == ItemOperationKind::Discard as u8 => Ok(ItemOperationKind::Discard),
x if x == ItemOperationKind::Move as u8 => Ok(ItemOperationKind::Move),
x if x == ItemOperationKind::Exchange as u8 => Ok(ItemOperationKind::Exchange),
x if x == ItemOperationKind::SplitStack as u8 => Ok(ItemOperationKind::SplitStack),
x if x == ItemOperationKind::CombineStack as u8 => Ok(ItemOperationKind::CombineStack),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Inventory {
pub equipped: EquippedStorage,
@ -171,36 +207,67 @@ impl Inventory {
}
}
/// Helper functions to reduce boilerplate
fn get_item_mut(&mut self, storage_id: ContainerType, storage_index: u16) -> &mut Item {
let container = self.get_container_mut(&storage_id);
container.get_slot_mut(storage_index)
}
fn get_item(&self, storage_id: ContainerType, storage_index: u16) -> Item {
let container = self.get_container(&storage_id);
*container.get_slot(storage_index)
}
pub fn process_action(&mut self, action: &ItemOperation) {
if action.operation_type == INVENTORY_ACTION_DISCARD {
let src_container = self.get_container_mut(&action.src_storage_id);
let src_slot = src_container.get_slot_mut(action.src_container_index);
*src_slot = Item::default();
} else {
// NOTE: only swaps items for now
let src_item;
// get the source item
{
let src_container = self.get_container_mut(&action.src_storage_id);
let src_slot = src_container.get_slot_mut(action.src_container_index);
src_item = *src_slot;
match action.operation_type {
ItemOperationKind::Discard => {
let src_item = self.get_item_mut(action.src_storage_id, action.src_container_index);
*src_item = Item::default();
}
ItemOperationKind::CombineStack => {
let src_item;
{
let original_item =
self.get_item_mut(action.src_storage_id, action.src_container_index);
src_item = *original_item;
*original_item = Item::default();
}
let dst_item;
// move into dst item
{
let dst_container = self.get_container_mut(&action.dst_storage_id);
let dst_slot = dst_container.get_slot_mut(action.dst_container_index);
let dst_item = self.get_item_mut(action.dst_storage_id, action.dst_container_index);
// TODO: We ought to check the max stack size for a given item id and disallow overflow
dst_item.quantity += src_item.quantity;
}
ItemOperationKind::SplitStack => {
let mut src_item;
{
let original_item =
self.get_item_mut(action.src_storage_id, action.src_container_index);
if original_item.quantity >= action.dst_stack {
original_item.quantity -= action.dst_stack;
src_item = *original_item;
src_item.quantity = action.dst_stack
} else {
tracing::warn!(
"Client sent a bogus split amount: {}! Rejecting item operation!",
action.dst_stack
);
return;
}
}
dst_item = *dst_slot;
let dst_item = self.get_item_mut(action.dst_storage_id, action.dst_container_index);
dst_item.clone_from(&src_item);
}
ItemOperationKind::Exchange | ItemOperationKind::Move => {
let src_item = self.get_item(action.src_storage_id, action.src_container_index);
// move src item into dst slot
let dst_slot = self.get_item_mut(action.dst_storage_id, action.dst_container_index);
let dst_item = *dst_slot;
dst_slot.clone_from(&src_item);
}
// move dst item into src slot
{
let src_container = self.get_container_mut(&action.src_storage_id);
let src_slot = src_container.get_slot_mut(action.src_container_index);
// move dst item into src slot
let src_slot = self.get_item_mut(action.src_storage_id, action.src_container_index);
src_slot.clone_from(&dst_item);
}
}

View file

@ -1,12 +1,13 @@
use binrw::binrw;
use crate::inventory::ContainerType;
use crate::inventory::ItemOperationKind;
#[binrw]
#[derive(Debug, Clone, Default)]
pub struct ItemOperation {
pub context_id: u32,
pub operation_type: u8,
pub operation_type: ItemOperationKind,
#[brw(pad_before = 3)]
pub src_actor_id: u32,
@ -44,7 +45,7 @@ mod tests {
let modify_inventory = ItemOperation::read_le(&mut buffer).unwrap();
assert_eq!(modify_inventory.context_id, 0x10000000);
assert_eq!(modify_inventory.operation_type, 60);
assert_eq!(modify_inventory.operation_type, ItemOperationKind::Move);
assert_eq!(modify_inventory.src_actor_id, 0);
assert_eq!(modify_inventory.src_storage_id, ContainerType::Equipped);
assert_eq!(modify_inventory.src_container_index, 3);

View file

@ -93,7 +93,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::inventory::{ContainerType, ItemOperationKind};
use crate::opcodes::ClientZoneIpcType;
use crate::opcodes::ServerZoneIpcType;
use crate::packet::IPC_HEADER_SIZE;
@ -384,24 +384,26 @@ 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
#[br(pre_assert(*magic == ServerZoneIpcType::InventoryTransaction))]
InventoryTransaction {
/// This is later reused in InventoryTransactionFinish, 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,
/// Same as the one sent by the client, not the one that the server responds with in InventoryActionAck!
operation_type: ItemOperationKind,
#[brw(pad_before = 3)]
src_actor_id: u32,
src_storage_id: ContainerType,
src_container_index: u16,
#[brw(pad_before = 2)]
src_stack: u32,
src_catalog_id: u32,
/// On retail, this contains very strange values that don't make sense (in regards to stack size). It's possible this field is a different size or isn't intended for stack size.
unk2: u32,
/// On retail, this contains very strange values that don't make sense (in regards to catalog id). It's possible this field is a different size or isn't intended for catalog id.
unk3: 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
/// seems to always be 0
dst_actor_id: u32,
/// seems to always be 65535/0xFFFF
/// seems to always be 57344/0xE000
dst_storage_id: u16,
/// seems to always be 65535/0xFFFF
dst_container_index: u16,
@ -409,15 +411,15 @@ pub enum ServerZoneIpcData {
#[brw(pad_after = 10)]
dst_catalog_id: u32,
},
#[br(pre_assert(*magic == ServerZoneIpcType::InventorySlotDiscardFin))]
InventorySlotDiscardFin {
/// Same value as unk1 in InventorySlotDiscard
#[br(pre_assert(*magic == ServerZoneIpcType::InventoryTransactionFinish))]
InventoryTransactionFinish {
/// Same value as unk1 in InventoryTransaction.
unk1: u32,
/// Repeated unk1 value?
/// Repeated unk1 value.
unk2: u32,
/// Unknown, seems to always be 0x00000090
/// Unknown, seems to always be 0x00000090.
unk3: u32,
/// Unknown, seems to always be 0x00000200
/// Unknown, seems to always be 0x00000200.
unk4: u32,
},
Unknown {
@ -569,6 +571,15 @@ pub enum ClientZoneIpcData {
/// Observed values so far: 0xDDDDDDDD (when buying 99 of a stackable item), 0xFFFFFFFF, 0xFFE0FFD0, 0xfffefffe, 0x0000FF64
unk2: u32,
},
/// This packet is sent by the client when they pivot left or right on standard controls.
/// It is sent once when beginning to pivot, and once when pivoting ends.
#[br(pre_assert(*magic == ClientZoneIpcType::StandardControlsPivot))]
StandardControlsPivot {
/// Set to 4 when beginning to pivot.
/// Set to 0 when pivoting ends.
#[brw(pad_after = 4)]
is_pivoting: u32,
},
#[br(pre_assert(*magic == ClientZoneIpcType::EventYieldHandler))]
EventYieldHandler(EventYieldHandler<2>),
#[br(pre_assert(*magic == ClientZoneIpcType::EventYieldHandler8))]
@ -821,15 +832,15 @@ mod tests {
ServerZoneIpcData::UnkResponse2 { unk1: 0 },
),
(
ServerZoneIpcType::InventorySlotDiscard,
ServerZoneIpcData::InventorySlotDiscard {
ServerZoneIpcType::InventoryTransaction,
ServerZoneIpcData::InventoryTransaction {
unk1: 0,
operation_type: 0,
operation_type: ItemOperationKind::Move,
src_actor_id: 0,
src_storage_id: ContainerType::Inventory0,
src_container_index: 0,
src_stack: 0,
src_catalog_id: 0,
unk2: 0,
unk3: 0,
dst_actor_id: 0,
dst_storage_id: 0,
dst_container_index: 0,
@ -837,8 +848,8 @@ mod tests {
},
),
(
ServerZoneIpcType::InventorySlotDiscardFin,
ServerZoneIpcData::InventorySlotDiscardFin {
ServerZoneIpcType::InventoryTransactionFinish,
ServerZoneIpcData::InventoryTransactionFinish {
unk1: 0,
unk2: 0,
unk3: 0,

View file

@ -82,6 +82,8 @@ pub const CLASSJOB_ARRAY_SIZE: usize = 32;
/// The maximum durability of an item.
pub const ITEM_CONDITION_MAX: u16 = 30000;
// These operation codes/types change regularly, so update them when needed!
/// The operation opcode/type when discarding an item from the inventory.
pub const INVENTORY_ACTION_DISCARD: u8 = 145;
@ -91,6 +93,12 @@ 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 operation opcode/type when splitting stacks of identical items.
pub const INVENTORY_ACTION_SPLIT_STACK: u8 = 148;
/// The operation opcode/type when combining stacks of identical items.
pub const INVENTORY_ACTION_COMBINE_STACK: u8 = 150;
/// The server's acknowledgement of a shop item being purchased.
pub const INVENTORY_ACTION_ACK_SHOP: u8 = 6;

View file

@ -83,6 +83,7 @@ pub struct PlayerData {
pub unlocks: Vec<u8>,
pub aetherytes: Vec<u8>,
pub completed_quests: Vec<u8>,
pub item_sequence: u32,
}
/// Various obsfucation-related bits like the seeds and keys for this connection.
@ -178,6 +179,7 @@ impl ZoneConnection {
self.player_data.max_hp = 100;
self.player_data.curr_mp = 10000;
self.player_data.max_mp = 10000;
self.player_data.item_sequence = 0;
tracing::info!("Client {actor_id} is initializing zone session...");