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

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.
This commit is contained in:
Joshua Goins 2025-04-01 21:37:41 -04:00
parent 6d1e9d4e73
commit 216778ea8b
6 changed files with 143 additions and 58 deletions

View file

@ -18,8 +18,8 @@ use kawari::world::ipc::{
GameMasterRank, OnlineStatus, ServerZoneIpcData, ServerZoneIpcSegment, SocialListRequestType, GameMasterRank, OnlineStatus, ServerZoneIpcData, ServerZoneIpcSegment, SocialListRequestType,
}; };
use kawari::world::{ use kawari::world::{
Actor, ClientHandle, ClientId, EffectsBuilder, FromServer, Inventory, LuaPlayer, PlayerData, Actor, ClientHandle, ClientId, EffectsBuilder, FromServer, Inventory, Item, LuaPlayer,
ServerHandle, StatusEffects, ToServer, WorldDatabase, PlayerData, ServerHandle, StatusEffects, ToServer, WorldDatabase,
}; };
use kawari::world::{ use kawari::world::{
ChatHandler, Zone, ZoneConnection, ChatHandler, Zone, ZoneConnection,
@ -615,8 +615,7 @@ async fn client_loop(
}) })
.await, .await,
GameMasterCommandType::GiveItem => { GameMasterCommandType::GiveItem => {
connection.player_data.inventory.extra_slot.id = *arg; connection.player_data.inventory.add_in_next_free_slot(Item { id: *arg, quantity: 1 });
connection.player_data.inventory.extra_slot.quantity = 1;
connection.send_inventory(false).await; connection.send_inventory(false).await;
} }
} }
@ -816,7 +815,7 @@ async fn client_loop(
game_data.get_citystate(chara_make.classjob_id as u16); game_data.get_citystate(chara_make.classjob_id as u16);
} }
let mut inventory = Inventory::new(); let mut inventory = Inventory::default();
// fill inventory // fill inventory
inventory.equip_racial_items( inventory.equip_racial_items(
@ -1002,7 +1001,7 @@ async fn client_loop(
connection.process_effects_list().await; connection.process_effects_list().await;
// update lua player // 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(); lua_player.status_effects = connection.status_effects.clone();
} }
} }

View file

@ -322,7 +322,7 @@ impl ChatHandler {
"!spawnclone" => { "!spawnclone" => {
// spawn another one of us // spawn another one of us
let player = connection.player_data; let player = &connection.player_data;
let mut common = connection let mut common = connection
.get_player_common_spawn(Some(player.position), Some(player.rotation)); .get_player_common_spawn(Some(player.position), Some(player.rotation));

View file

@ -19,6 +19,7 @@ use crate::{
use super::{ use super::{
Actor, Event, Inventory, Item, LuaPlayer, StatusEffects, WorldDatabase, Zone, Actor, Event, Inventory, Item, LuaPlayer, StatusEffects, WorldDatabase, Zone,
inventory::Container,
ipc::{ ipc::{
ActorControlSelf, ActorMove, ActorSetPos, ClientZoneIpcSegment, CommonSpawn, ContainerInfo, ActorControlSelf, ActorMove, ActorSetPos, ClientZoneIpcSegment, CommonSpawn, ContainerInfo,
ContainerType, DisplayFlag, Equip, InitZone, ItemInfo, NpcSpawn, ObjectKind, PlayerSubKind, 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 { pub struct PlayerData {
// Static data // Static data
pub actor_id: u32, pub actor_id: u32,
@ -428,16 +429,25 @@ impl ZoneConnection {
} }
pub async fn send_inventory(&mut self, send_appearance_update: bool) { pub async fn send_inventory(&mut self, send_appearance_update: bool) {
// page 1 let mut sequence = 0;
{
let extra_slot = self.player_data.inventory.extra_slot; // 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 mut send_slot = async |slot_index: u16, item: &Item| {
let ipc = ServerZoneIpcSegment { let ipc = ServerZoneIpcSegment {
op_code: ServerZoneIpcType::ItemInfo, op_code: ServerZoneIpcType::ItemInfo,
timestamp: timestamp_secs(), timestamp: timestamp_secs(),
data: ServerZoneIpcData::ItemInfo(ItemInfo { data: ServerZoneIpcData::ItemInfo(ItemInfo {
container: ContainerType::Inventory0, sequence,
container: kind,
slot: slot_index, slot: slot_index,
quantity: item.quantity, quantity: item.quantity,
catalog_id: item.id, catalog_id: item.id,
@ -455,7 +465,33 @@ impl ZoneConnection {
.await; .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 // equipped
@ -467,6 +503,7 @@ impl ZoneConnection {
op_code: ServerZoneIpcType::ItemInfo, op_code: ServerZoneIpcType::ItemInfo,
timestamp: timestamp_secs(), timestamp: timestamp_secs(),
data: ServerZoneIpcData::ItemInfo(ItemInfo { data: ServerZoneIpcData::ItemInfo(ItemInfo {
sequence,
container: ContainerType::Equipped, container: ContainerType::Equipped,
slot: slot_index, slot: slot_index,
quantity: item.quantity, quantity: item.quantity,
@ -485,6 +522,7 @@ impl ZoneConnection {
.await; .await;
}; };
// TODO: make containers enumerable to vastly simplify this code
send_slot(0, &equipped.main_hand).await; send_slot(0, &equipped.main_hand).await;
send_slot(1, &equipped.off_hand).await; send_slot(1, &equipped.off_hand).await;
send_slot(2, &equipped.head).await; send_slot(2, &equipped.head).await;
@ -500,27 +538,6 @@ 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 {
@ -529,7 +546,7 @@ impl ZoneConnection {
data: ServerZoneIpcData::ContainerInfo(ContainerInfo { data: ServerZoneIpcData::ContainerInfo(ContainerInfo {
container: ContainerType::Equipped, container: ContainerType::Equipped,
num_items: self.player_data.inventory.equipped.num_items(), num_items: self.player_data.inventory.equipped.num_items(),
sequence: 1, sequence,
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()

View file

@ -117,7 +117,7 @@ impl WorldDatabase {
&chara_make.to_json(), &chara_make.to_json(),
2, 2,
132, 132,
Inventory::new(), Inventory::default(),
); );
tracing::info!("{} added to the world!", character.name); tracing::info!("{} added to the world!", character.name);

View file

@ -8,6 +8,12 @@ use crate::config::get_config;
use super::ipc::{ContainerType, InventoryModify}; 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)] #[derive(Default, Copy, Clone, Serialize, Deserialize, Debug)]
pub struct Item { pub struct Item {
pub quantity: u32, pub quantity: u32,
@ -37,8 +43,8 @@ pub struct EquippedContainer {
pub soul_crystal: Item, pub soul_crystal: Item,
} }
impl EquippedContainer { impl Container for EquippedContainer {
pub fn num_items(&self) -> u32 { fn num_items(&self) -> u32 {
self.main_hand.quantity self.main_hand.quantity
+ self.off_hand.quantity + self.off_hand.quantity
+ self.head.quantity + self.head.quantity
@ -54,7 +60,7 @@ impl EquippedContainer {
+ self.soul_crystal.quantity + 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 { match index {
0 => &mut self.main_hand, 0 => &mut self.main_hand,
1 => &mut self.off_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<Item>,
}
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 struct Inventory {
pub equipped: EquippedContainer, pub equipped: EquippedContainer,
pub extra_slot: Item, // WIP for inventory pages pub pages: [InventoryPage; 4],
} }
impl Default for Inventory { impl Default for Inventory {
fn default() -> Self { fn default() -> Self {
Self::new() Self {
equipped: EquippedContainer::default(),
pages: std::array::from_fn(|_| InventoryPage::default()),
}
} }
} }
impl Inventory { impl Inventory {
pub fn new() -> Self {
Self {
equipped: EquippedContainer::default(),
extra_slot: Item::default(),
}
}
/// Equip the starting items for a given race /// Equip the starting items for a given race
pub fn equip_racial_items(&mut self, race_id: u8, gender: u8) { pub fn equip_racial_items(&mut self, race_id: u8, gender: u8) {
let config = get_config(); let config = get_config();
@ -138,16 +163,60 @@ impl Inventory {
} }
pub fn process_action(&mut self, action: &InventoryModify) { pub fn process_action(&mut self, action: &InventoryModify) {
// equipped if action.operation_type == 571 {
if action.src_storage_id == ContainerType::Equipped { // discard
let src_slot = self.equipped.get_slot(action.src_container_index); let src_container = self.get_container(&action.src_storage_id);
let src_slot = src_container.get_slot(action.src_container_index);
// it only unequips for now, doesn't move the item
*src_slot = Item::default(); *src_slot = Item::default();
} else if action.src_storage_id == ContainerType::Inventory0 { } else {
let dst_slot = self.equipped.get_slot(action.dst_container_index); // 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!(),
} }
} }
} }

View file

@ -3,7 +3,7 @@ use binrw::binrw;
#[binrw] #[binrw]
#[brw(little)] #[brw(little)]
#[brw(repr = u16)] #[brw(repr = u16)]
#[derive(Debug, Clone, Default, PartialEq)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub enum ContainerType { pub enum ContainerType {
#[default] #[default]
Inventory0 = 0, Inventory0 = 0,