From 216778ea8bacdb2ec7de80874a1bfa7f06607731 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Tue, 1 Apr 2025 21:37:41 -0400 Subject: [PATCH] Add more inventory management Instead of one single slot available in your inventory, all four pages should be available now. Moving items around should be less buggy, and it's now possible to discard items. Items cannot stack still, and when given will always take up the next free slot. --- src/bin/kawari-world.rs | 11 ++-- src/world/chat_handler.rs | 2 +- src/world/connection.rs | 73 +++++++++++++-------- src/world/database.rs | 2 +- src/world/inventory.rs | 111 ++++++++++++++++++++++++++------ src/world/ipc/container_info.rs | 2 +- 6 files changed, 143 insertions(+), 58 deletions(-) diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index e5e16e8..a847685 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -18,8 +18,8 @@ use kawari::world::ipc::{ GameMasterRank, OnlineStatus, ServerZoneIpcData, ServerZoneIpcSegment, SocialListRequestType, }; use kawari::world::{ - Actor, ClientHandle, ClientId, EffectsBuilder, FromServer, Inventory, LuaPlayer, PlayerData, - ServerHandle, StatusEffects, ToServer, WorldDatabase, + Actor, ClientHandle, ClientId, EffectsBuilder, FromServer, Inventory, Item, LuaPlayer, + PlayerData, ServerHandle, StatusEffects, ToServer, WorldDatabase, }; use kawari::world::{ ChatHandler, Zone, ZoneConnection, @@ -615,8 +615,7 @@ async fn client_loop( }) .await, GameMasterCommandType::GiveItem => { - connection.player_data.inventory.extra_slot.id = *arg; - connection.player_data.inventory.extra_slot.quantity = 1; + connection.player_data.inventory.add_in_next_free_slot(Item { id: *arg, quantity: 1 }); connection.send_inventory(false).await; } } @@ -816,7 +815,7 @@ async fn client_loop( game_data.get_citystate(chara_make.classjob_id as u16); } - let mut inventory = Inventory::new(); + let mut inventory = Inventory::default(); // fill inventory inventory.equip_racial_items( @@ -1002,7 +1001,7 @@ async fn client_loop( connection.process_effects_list().await; // update lua player - lua_player.player_data = connection.player_data; + lua_player.player_data = connection.player_data.clone(); lua_player.status_effects = connection.status_effects.clone(); } } diff --git a/src/world/chat_handler.rs b/src/world/chat_handler.rs index fc22a50..db38d54 100644 --- a/src/world/chat_handler.rs +++ b/src/world/chat_handler.rs @@ -322,7 +322,7 @@ impl ChatHandler { "!spawnclone" => { // spawn another one of us - let player = connection.player_data; + let player = &connection.player_data; let mut common = connection .get_player_common_spawn(Some(player.position), Some(player.rotation)); diff --git a/src/world/connection.rs b/src/world/connection.rs index 1b8c818..f3fe69e 100644 --- a/src/world/connection.rs +++ b/src/world/connection.rs @@ -19,6 +19,7 @@ use crate::{ use super::{ Actor, Event, Inventory, Item, LuaPlayer, StatusEffects, WorldDatabase, Zone, + inventory::Container, ipc::{ ActorControlSelf, ActorMove, ActorSetPos, ClientZoneIpcSegment, CommonSpawn, ContainerInfo, ContainerType, DisplayFlag, Equip, InitZone, ItemInfo, NpcSpawn, ObjectKind, PlayerSubKind, @@ -27,7 +28,7 @@ use super::{ }, }; -#[derive(Debug, Default, Clone, Copy)] +#[derive(Debug, Default, Clone)] pub struct PlayerData { // Static data pub actor_id: u32, @@ -428,16 +429,25 @@ impl ZoneConnection { } pub async fn send_inventory(&mut self, send_appearance_update: bool) { - // page 1 - { - let extra_slot = self.player_data.inventory.extra_slot; + let mut sequence = 0; + + // pages + for (i, page) in self.player_data.inventory.pages.clone().iter().enumerate() { + let kind = match i { + 0 => ContainerType::Inventory0, + 1 => ContainerType::Inventory1, + 2 => ContainerType::Inventory2, + 3 => ContainerType::Inventory3, + _ => panic!("Shouldn't be anything else!"), + }; 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, + sequence, + container: kind, slot: slot_index, quantity: item.quantity, catalog_id: item.id, @@ -455,7 +465,33 @@ impl ZoneConnection { .await; }; - send_slot(0, &extra_slot).await; + for (i, slot) in page.slots.iter().enumerate() { + send_slot(i as u16, slot).await; + } + + // inform the client of container state + { + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::ContainerInfo, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::ContainerInfo(ContainerInfo { + container: kind, + num_items: page.num_items(), + sequence, + ..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; + } + + sequence += 1; } // equipped @@ -467,6 +503,7 @@ impl ZoneConnection { op_code: ServerZoneIpcType::ItemInfo, timestamp: timestamp_secs(), data: ServerZoneIpcData::ItemInfo(ItemInfo { + sequence, container: ContainerType::Equipped, slot: slot_index, quantity: item.quantity, @@ -485,6 +522,7 @@ impl ZoneConnection { .await; }; + // TODO: make containers enumerable to vastly simplify this code send_slot(0, &equipped.main_hand).await; send_slot(1, &equipped.off_hand).await; send_slot(2, &equipped.head).await; @@ -500,27 +538,6 @@ 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 { @@ -529,7 +546,7 @@ impl ZoneConnection { data: ServerZoneIpcData::ContainerInfo(ContainerInfo { container: ContainerType::Equipped, num_items: self.player_data.inventory.equipped.num_items(), - sequence: 1, + sequence, ..Default::default() }), ..Default::default() diff --git a/src/world/database.rs b/src/world/database.rs index e9c7b00..87d78eb 100644 --- a/src/world/database.rs +++ b/src/world/database.rs @@ -117,7 +117,7 @@ impl WorldDatabase { &chara_make.to_json(), 2, 132, - Inventory::new(), + Inventory::default(), ); tracing::info!("{} added to the world!", character.name); diff --git a/src/world/inventory.rs b/src/world/inventory.rs index 076eb78..ebb08ee 100644 --- a/src/world/inventory.rs +++ b/src/world/inventory.rs @@ -8,6 +8,12 @@ use crate::config::get_config; use super::ipc::{ContainerType, InventoryModify}; +// TODO: rename to storage? +pub trait Container { + fn num_items(&self) -> u32; + fn get_slot<'a>(&'a mut self, index: u16) -> &'a mut Item; +} + #[derive(Default, Copy, Clone, Serialize, Deserialize, Debug)] pub struct Item { pub quantity: u32, @@ -37,8 +43,8 @@ pub struct EquippedContainer { pub soul_crystal: Item, } -impl EquippedContainer { - pub fn num_items(&self) -> u32 { +impl Container for EquippedContainer { + fn num_items(&self) -> u32 { self.main_hand.quantity + self.off_hand.quantity + self.head.quantity @@ -54,7 +60,7 @@ impl EquippedContainer { + self.soul_crystal.quantity } - pub fn get_slot(&mut self, index: u16) -> &mut Item { + fn get_slot(&mut self, index: u16) -> &mut Item { match index { 0 => &mut self.main_hand, 1 => &mut self.off_hand, @@ -74,26 +80,45 @@ impl EquippedContainer { } } -#[derive(Debug, Clone, Copy, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct InventoryPage { + pub slots: Vec, +} + +impl InventoryPage { + fn default() -> Self { + Self { + slots: vec![Item::default(); 35], + } + } +} + +impl Container for InventoryPage { + fn num_items(&self) -> u32 { + self.slots.iter().filter(|item| item.quantity > 0).count() as u32 + } + + fn get_slot(&mut self, index: u16) -> &mut Item { + self.slots.get_mut(index as usize).unwrap() + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct Inventory { pub equipped: EquippedContainer, - pub extra_slot: Item, // WIP for inventory pages + pub pages: [InventoryPage; 4], } impl Default for Inventory { fn default() -> Self { - Self::new() + Self { + equipped: EquippedContainer::default(), + pages: std::array::from_fn(|_| InventoryPage::default()), + } } } impl Inventory { - pub fn new() -> Self { - Self { - equipped: EquippedContainer::default(), - extra_slot: Item::default(), - } - } - /// Equip the starting items for a given race pub fn equip_racial_items(&mut self, race_id: u8, gender: u8) { let config = get_config(); @@ -138,16 +163,60 @@ impl Inventory { } pub fn process_action(&mut self, action: &InventoryModify) { - // equipped - 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 + if action.operation_type == 571 { + // discard + let src_container = self.get_container(&action.src_storage_id); + let src_slot = src_container.get_slot(action.src_container_index); *src_slot = Item::default(); - } else if action.src_storage_id == ContainerType::Inventory0 { - let dst_slot = self.equipped.get_slot(action.dst_container_index); + } else { + // NOTE: only swaps items for now - *dst_slot = self.extra_slot; + let src_item; + // get the source item + { + let src_container = self.get_container(&action.src_storage_id); + let src_slot = src_container.get_slot(action.src_container_index); + src_item = src_slot.clone(); + } + + let dst_item; + // move into dst item + { + let dst_container = self.get_container(&action.dst_storage_id); + let dst_slot = dst_container.get_slot(action.dst_container_index); + + dst_item = dst_slot.clone(); + dst_slot.clone_from(&src_item); + } + + // move dst item into src slot + { + let src_container = self.get_container(&action.src_storage_id); + let src_slot = src_container.get_slot(action.src_container_index); + src_slot.clone_from(&dst_item); + } + } + } + + pub fn add_in_next_free_slot(&mut self, item: Item) { + for page in &mut self.pages { + for slot in &mut page.slots { + if slot.quantity == 0 { + slot.clone_from(&item); + return; + } + } + } + } + + fn get_container(&mut self, container_type: &ContainerType) -> &mut dyn Container { + match container_type { + ContainerType::Inventory0 => &mut self.pages[0], + ContainerType::Inventory1 => &mut self.pages[1], + ContainerType::Inventory2 => &mut self.pages[2], + ContainerType::Inventory3 => &mut self.pages[3], + ContainerType::Equipped => &mut self.equipped, + ContainerType::ArmouryBody => todo!(), } } } diff --git a/src/world/ipc/container_info.rs b/src/world/ipc/container_info.rs index 560531d..aba7341 100644 --- a/src/world/ipc/container_info.rs +++ b/src/world/ipc/container_info.rs @@ -3,7 +3,7 @@ use binrw::binrw; #[binrw] #[brw(little)] #[brw(repr = u16)] -#[derive(Debug, Clone, Default, PartialEq)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)] pub enum ContainerType { #[default] Inventory0 = 0,