mirror of
https://github.com/redstrate/Kawari.git
synced 2025-07-17 10:47:44 +00:00
When adding items to the inventory, prefer existing stacks first before selecting empty slots.
-This works for adding items to the inventory in any capacity, including debug and GM commands!
This commit is contained in:
parent
a5120bb9d6
commit
421a24aa6f
7 changed files with 115 additions and 31 deletions
|
@ -26,7 +26,7 @@ use kawari::world::{
|
|||
ClientHandle, Event, FromServer, LuaPlayer, PlayerData, ServerHandle, StatusEffects, ToServer,
|
||||
WorldDatabase, handle_custom_ipc, server_main_loop,
|
||||
};
|
||||
use kawari::{RECEIVE_BUFFER_SIZE, TITLE_UNLOCK_BITMASK_SIZE};
|
||||
use kawari::{ERR_INVENTORY_ADD_FAILED, RECEIVE_BUFFER_SIZE, TITLE_UNLOCK_BITMASK_SIZE};
|
||||
|
||||
use mlua::{Function, Lua};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
@ -895,22 +895,23 @@ async fn client_loop(
|
|||
|
||||
if let Some(item_info) = result {
|
||||
if connection.player_data.inventory.currency.gil.quantity >= *item_quantity * item_info.price_mid {
|
||||
connection.player_data.inventory.currency.gil.quantity -= *item_quantity * item_info.price_mid;
|
||||
// TODO: We need to obtain information on where the item was added, as the client needs to be told.
|
||||
// For now we hardcode it to the very first inventory slot.
|
||||
//connection.player_data.inventory.add_in_next_free_slot(Item::new(*item_quantity, item_info.id));
|
||||
connection.player_data.inventory.add_in_slot(Item::new(*item_quantity, item_info.id), &ContainerType::Inventory0, 0);
|
||||
if let Some(add_result) = connection.player_data.inventory.add_in_next_free_slot(Item::new(*item_quantity, item_info.id), item_info.stack_size) {
|
||||
connection.player_data.inventory.currency.gil.quantity -= *item_quantity * item_info.price_mid;
|
||||
connection.send_gilshop_item_update(0x07D0, 0, connection.player_data.inventory.currency.gil.quantity, 1).await;
|
||||
|
||||
connection.send_gilshop_item_update(0x07D0, 0, connection.player_data.inventory.currency.gil.quantity, 1).await;
|
||||
connection.send_inventory_ack(u32::MAX, INVENTORY_ACTION_ACK_SHOP as u16).await;
|
||||
|
||||
connection.send_inventory_ack(u32::MAX, INVENTORY_ACTION_ACK_SHOP as u16).await;
|
||||
connection.send_gilshop_item_update(add_result.container as u16, add_result.index, add_result.quantity, item_info.id).await;
|
||||
connection.send_gilshop_ack(*event_id, item_info.id, *item_quantity, item_info.price_mid).await;
|
||||
|
||||
// TODO: This is hardcoded to the first item slot in the inventory, fix this
|
||||
connection.send_gilshop_item_update(0, 0, *item_quantity, item_info.id).await;
|
||||
connection.send_gilshop_ack(*event_id, item_info.id, *item_quantity, item_info.price_mid).await;
|
||||
|
||||
let target_id = connection.player_data.target_actorid;
|
||||
connection.event_scene(&target_id, *event_id, 10, 8193, vec![1, 100]).await;
|
||||
let target_id = connection.player_data.target_actorid;
|
||||
// See GenericShopkeeper.lua for information about this scene, the flags, and the params.
|
||||
connection.event_scene(&target_id, *event_id, 10, 8193, vec![1, 100]).await;
|
||||
} else {
|
||||
tracing::error!(ERR_INVENTORY_ADD_FAILED);
|
||||
connection.send_message(ERR_INVENTORY_ADD_FAILED).await;
|
||||
connection.event_finish(*event_id).await;
|
||||
}
|
||||
} else {
|
||||
connection.send_message("Insufficient gil to buy item. Nice try bypassing the client-side check!").await;
|
||||
connection.event_finish(*event_id).await;
|
||||
|
|
|
@ -49,7 +49,10 @@ pub struct ItemInfo {
|
|||
pub price_low: u32,
|
||||
/// The item's equip category.
|
||||
pub equip_category: u8,
|
||||
/// The item's primary model id.
|
||||
pub primary_model_id: u64,
|
||||
/// The item's max stack size.
|
||||
pub stack_size: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -191,6 +194,10 @@ impl GameData {
|
|||
panic!("Unexpected type!");
|
||||
};
|
||||
|
||||
let physis::exd::ColumnData::UInt32(stack_size) = &matched_row.columns[20] else {
|
||||
panic!("Unexpected type!");
|
||||
};
|
||||
|
||||
let physis::exd::ColumnData::UInt32(price_mid) = &matched_row.columns[25] else {
|
||||
panic!("Unexpected type!");
|
||||
};
|
||||
|
@ -210,6 +217,7 @@ impl GameData {
|
|||
price_low: *price_low,
|
||||
equip_category: *equip_category,
|
||||
primary_model_id: *primary_model_id,
|
||||
stack_size: *stack_size,
|
||||
};
|
||||
|
||||
return Some(item_info);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::inventory::ContainerType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{Item, Storage};
|
||||
|
@ -5,12 +6,15 @@ use super::{Item, Storage};
|
|||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct GenericStorage<const N: usize> {
|
||||
pub slots: Vec<Item>,
|
||||
#[serde(skip)]
|
||||
pub kind: ContainerType,
|
||||
}
|
||||
|
||||
impl<const N: usize> Default for GenericStorage<N> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
slots: vec![Item::default(); N],
|
||||
kind: ContainerType::Invalid,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,6 +61,12 @@ impl TryFrom<u8> for ItemOperationKind {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct ItemDestinationInfo {
|
||||
pub container: ContainerType,
|
||||
pub index: u16,
|
||||
pub quantity: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Inventory {
|
||||
pub equipped: EquippedStorage,
|
||||
|
@ -82,9 +88,16 @@ pub struct Inventory {
|
|||
|
||||
impl Default for Inventory {
|
||||
fn default() -> Self {
|
||||
// TODO: Set the ContainerType for the others if needed?
|
||||
// Right now we only use this for adding items to the main inventory.
|
||||
let mut pages = std::array::from_fn(|_| GenericStorage::default());
|
||||
pages[0].kind = ContainerType::Inventory0;
|
||||
pages[1].kind = ContainerType::Inventory1;
|
||||
pages[2].kind = ContainerType::Inventory2;
|
||||
pages[3].kind = ContainerType::Inventory3;
|
||||
Self {
|
||||
equipped: EquippedStorage::default(),
|
||||
pages: std::array::from_fn(|_| GenericStorage::default()),
|
||||
pages,
|
||||
armoury_main_hand: GenericStorage::default(),
|
||||
armoury_head: GenericStorage::default(),
|
||||
armoury_body: GenericStorage::default(),
|
||||
|
@ -273,15 +286,44 @@ impl Inventory {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn add_in_next_free_slot(&mut self, item: Item) {
|
||||
fn add_in_empty_slot(&mut self, item: Item) -> Option<ItemDestinationInfo> {
|
||||
for page in &mut self.pages {
|
||||
for slot in &mut page.slots {
|
||||
for (slot_index, slot) in page.slots.iter_mut().enumerate() {
|
||||
if slot.quantity == 0 {
|
||||
slot.clone_from(&item);
|
||||
return;
|
||||
return Some(ItemDestinationInfo {
|
||||
container: page.kind,
|
||||
index: slot_index as u16,
|
||||
quantity: item.quantity,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn add_in_next_free_slot(
|
||||
&mut self,
|
||||
item: Item,
|
||||
stack_size: u32,
|
||||
) -> Option<ItemDestinationInfo> {
|
||||
if stack_size > 1 {
|
||||
for page in &mut self.pages {
|
||||
for (slot_index, slot) in page.slots.iter_mut().enumerate() {
|
||||
if slot.id == item.id && slot.quantity + item.quantity <= stack_size {
|
||||
slot.quantity += item.quantity;
|
||||
return Some(ItemDestinationInfo {
|
||||
container: page.kind,
|
||||
index: slot_index as u16,
|
||||
quantity: slot.quantity,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we didn't find any stacks, or the item isn't stackable, try again to find an empty inventory slot.
|
||||
self.add_in_empty_slot(item)
|
||||
}
|
||||
|
||||
pub fn add_in_slot(&mut self, item: Item, container_type: &ContainerType, index: u16) {
|
||||
|
|
|
@ -115,3 +115,7 @@ pub const INVENTORY_ACTION_ACK_SHOP: u8 = 6;
|
|||
/// In the past, many more values were used according to Sapphire:
|
||||
/// https://github.com/SapphireServer/Sapphire/blob/044bff026c01b4cc3a37cbc9b0881fadca3fc477/src/common/Common.h#L83
|
||||
pub const INVENTORY_ACTION_ACK_GENERAL: u8 = 7;
|
||||
|
||||
/// Error messages: TODO: this should probably be moved into its own universal mod/crate?
|
||||
pub const ERR_INVENTORY_ADD_FAILED: &str =
|
||||
"Unable to add item to inventory! Your inventory is full, or this is a bug in Kawari!";
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{
|
||||
ITEM_CONDITION_MAX,
|
||||
ERR_INVENTORY_ADD_FAILED, ITEM_CONDITION_MAX,
|
||||
common::ItemInfoQuery,
|
||||
inventory::{Item, Storage},
|
||||
ipc::zone::{ChatMessage, GameMasterRank},
|
||||
|
@ -80,22 +80,28 @@ impl ChatHandler {
|
|||
}
|
||||
"!item" => {
|
||||
let (_, name) = chat_message.message.split_once(' ').unwrap();
|
||||
|
||||
let mut result = None;
|
||||
{
|
||||
let mut gamedata = connection.gamedata.lock().unwrap();
|
||||
|
||||
if let Some(item_info) =
|
||||
gamedata.get_item_info(ItemInfoQuery::ByName(name.to_string()))
|
||||
{
|
||||
connection
|
||||
.player_data
|
||||
.inventory
|
||||
.add_in_next_free_slot(Item::new(1, item_info.id));
|
||||
result = connection.player_data.inventory.add_in_next_free_slot(
|
||||
Item::new(1, item_info.id),
|
||||
item_info.stack_size,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
connection.send_inventory(false).await;
|
||||
true
|
||||
if result.is_some() {
|
||||
connection.send_inventory(false).await;
|
||||
true
|
||||
} else {
|
||||
tracing::error!(ERR_INVENTORY_ADD_FAILED);
|
||||
connection.send_message(ERR_INVENTORY_ADD_FAILED).await;
|
||||
true
|
||||
}
|
||||
}
|
||||
"!reload" => {
|
||||
connection.reload_scripts();
|
||||
|
|
|
@ -10,8 +10,10 @@ use tokio::net::TcpStream;
|
|||
|
||||
use crate::{
|
||||
CLASSJOB_ARRAY_SIZE, COMPLETED_LEVEQUEST_BITMASK_SIZE, COMPLETED_QUEST_BITMASK_SIZE,
|
||||
ERR_INVENTORY_ADD_FAILED,
|
||||
common::{
|
||||
GameData, ObjectId, ObjectTypeId, Position, timestamp_secs, value_to_flag_byte_index_value,
|
||||
GameData, ItemInfoQuery, ObjectId, ObjectTypeId, Position, timestamp_secs,
|
||||
value_to_flag_byte_index_value,
|
||||
},
|
||||
config::{WorldConfig, get_config},
|
||||
inventory::{ContainerType, Inventory, Item, Storage},
|
||||
|
@ -860,10 +862,27 @@ impl ZoneConnection {
|
|||
}
|
||||
}
|
||||
Task::AddItem { id } => {
|
||||
self.player_data
|
||||
.inventory
|
||||
.add_in_next_free_slot(Item::new(1, *id));
|
||||
self.send_inventory(false).await;
|
||||
let item_info;
|
||||
{
|
||||
let mut game_data = self.gamedata.lock().unwrap();
|
||||
item_info = game_data.get_item_info(ItemInfoQuery::ById(*id));
|
||||
}
|
||||
if item_info.is_some() {
|
||||
if self
|
||||
.player_data
|
||||
.inventory
|
||||
.add_in_next_free_slot(Item::new(1, *id), item_info.unwrap().stack_size)
|
||||
.is_some()
|
||||
{
|
||||
self.send_inventory(false).await;
|
||||
} else {
|
||||
tracing::error!(ERR_INVENTORY_ADD_FAILED);
|
||||
self.send_message(ERR_INVENTORY_ADD_FAILED).await;
|
||||
}
|
||||
} else {
|
||||
tracing::error!(ERR_INVENTORY_ADD_FAILED);
|
||||
self.send_message(ERR_INVENTORY_ADD_FAILED).await;
|
||||
}
|
||||
}
|
||||
Task::CompleteAllQuests {} => {
|
||||
self.player_data.completed_quests = vec![0xFF; COMPLETED_QUEST_BITMASK_SIZE];
|
||||
|
|
Loading…
Add table
Reference in a new issue