mirror of
https://github.com/redstrate/Kawari.git
synced 2025-07-13 09:07:44 +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:
parent
779666a10f
commit
45ee95318c
8 changed files with 172 additions and 76 deletions
|
@ -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.
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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...");
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue