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

View file

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

View file

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

View file

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

View file

@ -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<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 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!(),
}
}
}

View file

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