diff --git a/src/entity/character.rs b/src/entity/character.rs index 80b714c..a31cfc1 100644 --- a/src/entity/character.rs +++ b/src/entity/character.rs @@ -259,6 +259,7 @@ pub struct NewCharacterEntity { pub tech_menu: CharacterTechMenu, pub meseta: u32, + pub bank_meseta: u32, } impl NewCharacterEntity { @@ -278,6 +279,7 @@ impl NewCharacterEntity { materials: CharacterMaterials::default(), tech_menu: CharacterTechMenu::new(), meseta: 0, + bank_meseta: 0, } } } @@ -303,4 +305,6 @@ pub struct CharacterEntity { pub tech_menu: CharacterTechMenu, pub meseta: u32, + // TODO: this should not be tied to the character + pub bank_meseta: u32, } diff --git a/src/entity/gateway/entitygateway.rs b/src/entity/gateway/entitygateway.rs index fda3a9c..c6324e1 100644 --- a/src/entity/gateway/entitygateway.rs +++ b/src/entity/gateway/entitygateway.rs @@ -56,6 +56,10 @@ pub trait EntityGateway: Send + Sync + Clone { unimplemented!(); } + async fn change_item_location(&mut self, _item_id: &ItemEntityId, _item_location: ItemLocation) { + unimplemented!(); + } + async fn get_items_by_character(&self, _char: &CharacterEntity) -> Vec { unimplemented!(); } diff --git a/src/entity/gateway/inmemory.rs b/src/entity/gateway/inmemory.rs index 14bfa2a..5959a39 100644 --- a/src/entity/gateway/inmemory.rs +++ b/src/entity/gateway/inmemory.rs @@ -123,6 +123,7 @@ impl EntityGateway for InMemoryGateway { materials: character.materials, tech_menu: character.tech_menu, meseta: character.meseta, + bank_meseta: character.bank_meseta, }; characters.insert(new_character.id, new_character.clone()); Some(new_character) @@ -157,6 +158,13 @@ impl EntityGateway for InMemoryGateway { items.insert(item.id, item.clone()); } + async fn change_item_location(&mut self, item_id: &ItemEntityId, item_location: ItemLocation) { + self.items.lock().unwrap().get_mut(&item_id) + .map(|item_entity| { + item_entity.location = item_location + }); + } + async fn get_items_by_character(&self, character: &CharacterEntity) -> Vec { let items = self.items.lock().unwrap(); items diff --git a/src/entity/item/mod.rs b/src/entity/item/mod.rs index 03979ff..b6add91 100644 --- a/src/entity/item/mod.rs +++ b/src/entity/item/mod.rs @@ -15,8 +15,8 @@ use crate::ship::drops::ItemDropType; pub struct ItemEntityId(pub u32); #[derive(Hash, PartialEq, Eq, Debug, Clone)] pub struct ItemId(u32); -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct BankName(String); +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct BankName(pub String); #[derive(Clone, Debug, PartialEq)] pub enum ItemLocation { @@ -27,7 +27,7 @@ pub enum ItemLocation { }, Bank { character_id: CharacterEntityId, - slot: BankName, + name: BankName, }, LocalFloor { character_id: CharacterEntityId, diff --git a/src/ship/character.rs b/src/ship/character.rs index 0c3f471..c22af29 100644 --- a/src/ship/character.rs +++ b/src/ship/character.rs @@ -1,7 +1,7 @@ use libpso::character::character; use crate::common::leveltable::CharacterStats; use crate::entity::character::CharacterEntity; -use crate::ship::items::CharacterInventory; +use crate::ship::items::{CharacterInventory, CharacterBank}; pub struct CharacterBytesBuilder<'a> { character: Option<&'a CharacterEntity>, @@ -80,7 +80,8 @@ pub struct FullCharacterBytesBuilder<'a> { character: Option<&'a CharacterEntity>, stats: Option<&'a CharacterStats>, level: Option, - inventory: Option<&'a CharacterInventory<'a>>, + inventory: Option<&'a CharacterInventory>, + bank: Option<&'a CharacterBank>, key_config: Option<&'a [u8; 0x16C]>, joystick_config: Option<&'a [u8; 0x38]>, symbol_chat: Option<&'a [u8; 1248]>, @@ -95,6 +96,7 @@ impl<'a> FullCharacterBytesBuilder<'a> { stats: None, level: None, inventory: None, + bank: None, key_config: None, joystick_config: None, symbol_chat: None, @@ -130,6 +132,13 @@ impl<'a> FullCharacterBytesBuilder<'a> { } } + pub fn bank(self, bank: &'a CharacterBank) -> FullCharacterBytesBuilder<'a> { + FullCharacterBytesBuilder { + bank: Some(bank), + ..self + } + } + pub fn key_config(self, key_config: &'a [u8; 0x16C]) -> FullCharacterBytesBuilder<'a> { FullCharacterBytesBuilder { key_config: Some(key_config), @@ -163,6 +172,7 @@ impl<'a> FullCharacterBytesBuilder<'a> { let stats = self.stats.unwrap(); let level = self.level.unwrap(); let inventory = self.inventory.unwrap(); + let bank = self.bank.unwrap(); let key_config = self.key_config.unwrap(); let joystick_config = self.joystick_config.unwrap(); let symbol_chat = self.symbol_chat.unwrap(); @@ -196,6 +206,7 @@ impl<'a> FullCharacterBytesBuilder<'a> { info_board: character.info_board.as_bytes(), symbol_chats: *symbol_chat, tech_menu: *tech_menu, + bank: bank.as_client_bank_items(), ..character::FullCharacter::default() } } diff --git a/src/ship/items.rs b/src/ship/items.rs deleted file mode 100644 index 9abef27..0000000 --- a/src/ship/items.rs +++ /dev/null @@ -1,881 +0,0 @@ -use std::collections::{HashMap, BTreeMap}; -use std::cmp::Ordering; -use thiserror::Error; -use futures::future::join_all; -use libpso::character::character;//::InventoryItem; -use crate::entity::gateway::EntityGateway; -use crate::entity::character::{CharacterEntity, CharacterEntityId}; -use crate::entity::item::{ItemEntityId, ItemEntity, ItemDetail, ItemLocation}; -use crate::entity::item::{Meseta, NewItemEntity}; -use crate::entity::item::tool::{Tool, ToolType}; -use crate::ship::map::MapArea; -use crate::ship::ship::ItemDropLocation; -use crate::ship::drops::{ItemDrop, ItemDropType}; -use crate::ship::location::{AreaClient, RoomId}; - - -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] -pub struct ActiveItemId(pub u32); - -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] -struct RoomItemId(RoomId, u32); - -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] -pub struct ClientItemId(pub u32); - -#[derive(Debug, Clone)] -pub struct IndividualInventoryItem { - pub entity_id: ItemEntityId, - pub item_id: ClientItemId, - pub item: ItemDetail, - pub equipped: bool, -} - -#[derive(Debug, Clone)] -pub struct StackedInventoryItem { - pub entity_ids: Vec, - pub item_id: ClientItemId, - pub tool: Tool, -} - -impl StackedInventoryItem { - pub fn count(&self) -> usize { - self.entity_ids.len() - } -} - -#[derive(Debug, Clone)] -pub enum InventoryItem { - Individual(IndividualInventoryItem), - Stacked(StackedInventoryItem), -} - -pub enum InventoryItemAddToError { - BothAreNotStacked, - DifferentTool, - ExceedsCapacity, -} - -impl InventoryItem { - pub fn item_id(&self) -> ClientItemId { - match self { - InventoryItem::Individual(individual_inventory_item) => { - individual_inventory_item.item_id - }, - InventoryItem::Stacked(stacked_inventory_item) => { - stacked_inventory_item.item_id - } - } - } - - pub fn set_item_id(&mut self, item_id: ClientItemId) { - match self { - InventoryItem::Individual(individual_inventory_item) => { - individual_inventory_item.item_id = item_id - }, - InventoryItem::Stacked(stacked_inventory_item) => { - stacked_inventory_item.item_id = item_id - } - } - } - - pub fn are_same_stackable_tool(&self, other_stacked_item: &StackedFloorItem) -> bool { - match self { - InventoryItem::Stacked(self_stacked_item) => { - self_stacked_item.tool == other_stacked_item.tool - && self_stacked_item.tool.is_stackable() && other_stacked_item.tool.is_stackable() - }, - _ => false - } - } - - pub fn can_combine_stacks(&self, other_stacked_item: &StackedFloorItem) -> bool { - match self { - InventoryItem::Stacked(self_stacked_item) => { - self_stacked_item.tool == other_stacked_item.tool - && self_stacked_item.tool.is_stackable() && other_stacked_item.tool.is_stackable() - && self_stacked_item.count() + other_stacked_item.count() <= self_stacked_item.tool.max_stack() - }, - _ => false - } - } - - // TODO: result - pub fn combine_stacks(&mut self, other_stacked_item: &mut StackedFloorItem) { - match self { - InventoryItem::Stacked(self_stacked_item) => { - self_stacked_item.entity_ids.append(&mut other_stacked_item.entity_ids); - }, - _ => { - } - } - } - - pub fn equipped(&self) -> bool { - match self { - InventoryItem::Individual(individual_inventory_item) => { - individual_inventory_item.equipped - }, - InventoryItem::Stacked(_) => { - false - } - } - } - - pub fn as_client_bytes(&self) -> [u8; 16] { - match self { - InventoryItem::Individual(item) => { - match &item.item { - ItemDetail::Weapon(w) => w.as_bytes(), - ItemDetail::Armor(a) => a.as_bytes(), - ItemDetail::Shield(s) => s.as_bytes(), - ItemDetail::Unit(u) => u.as_bytes(), - ItemDetail::Tool(t) => t.as_individual_bytes(), - ItemDetail::TechniqueDisk(d) => d.as_bytes(), - ItemDetail::Mag(m) => m.as_bytes(), - } - }, - InventoryItem::Stacked(item) => { - item.tool.as_stacked_bytes(item.entity_ids.len()) - }, - } - } - - pub fn can_add_to(&mut self, stacked_floor_item: &StackedFloorItem) -> Result<(), InventoryItemAddToError> { - if let InventoryItem::Stacked(stacked_inventory_item) = self { - if stacked_floor_item.tool != stacked_inventory_item.tool { - return Err(InventoryItemAddToError::DifferentTool) - } - - if stacked_floor_item.tool.tool.max_stack() < (stacked_floor_item.count() + stacked_inventory_item.count()) { - return Err(InventoryItemAddToError::ExceedsCapacity) - } - Ok(()) - } - else { - Err(InventoryItemAddToError::BothAreNotStacked) - } - } - - pub fn add_to(&mut self, mut stacked_floor_item: StackedFloorItem) -> Result<(), InventoryItemAddToError> { - self.can_add_to(&stacked_floor_item)?; - if let InventoryItem::Stacked(stacked_inventory_item) = self { - stacked_inventory_item.entity_ids.append(&mut stacked_floor_item.entity_ids); - } - Ok(()) - } -} - -#[derive(Debug, Clone)] -pub struct IndividualFloorItem { - pub entity_id: ItemEntityId, - pub item_id: ClientItemId, - pub item: ItemDetail, - pub map_area: MapArea, - pub x: f32, - pub y: f32, - pub z: f32, -} - -#[derive(Debug, Clone)] -pub struct StackedFloorItem { - pub entity_ids: Vec, - pub item_id: ClientItemId, - pub tool: Tool, - pub map_area: MapArea, - pub x: f32, - pub y: f32, - pub z: f32, -} - -impl StackedFloorItem { - pub fn count(&self) -> usize { - self.entity_ids.len() - } -} - -#[derive(Debug, Clone)] -pub struct MesetaFloorItem { - pub item_id: ClientItemId, - pub meseta: Meseta, - pub map_area: MapArea, - pub x: f32, - pub y: f32, - pub z: f32, -} - -#[derive(Debug, Clone)] -pub enum FloorItem { - Individual(IndividualFloorItem), - Stacked(StackedFloorItem), - Meseta(MesetaFloorItem), -} - -impl FloorItem { - pub fn item_id(&self) -> ClientItemId { - match self { - FloorItem::Individual(individual_floor_item) => { - individual_floor_item.item_id - }, - FloorItem::Stacked(stacked_floor_item) => { - stacked_floor_item.item_id - }, - FloorItem::Meseta(meseta_floor_item) => { - meseta_floor_item.item_id - } - } - } - - pub fn x(&self) -> f32 { - match self { - FloorItem::Individual(individual_floor_item) => { - individual_floor_item.x - }, - FloorItem::Stacked(stacked_floor_item) => { - stacked_floor_item.x - }, - FloorItem::Meseta(meseta_floor_item) => { - meseta_floor_item.x - } - } - } - - pub fn y(&self) -> f32 { - match self { - FloorItem::Individual(individual_floor_item) => { - individual_floor_item.y - }, - FloorItem::Stacked(stacked_floor_item) => { - stacked_floor_item.y - }, - FloorItem::Meseta(meseta_floor_item) => { - meseta_floor_item.y - } - } - } - - pub fn z(&self) -> f32 { - match self { - FloorItem::Individual(individual_floor_item) => { - individual_floor_item.z - }, - FloorItem::Stacked(stacked_floor_item) => { - stacked_floor_item.z - }, - FloorItem::Meseta(meseta_floor_item) => { - meseta_floor_item.z - } - } - } - - pub fn map_area(&self) -> MapArea { - match self { - FloorItem::Individual(individual_floor_item) => { - individual_floor_item.map_area - }, - FloorItem::Stacked(stacked_floor_item) => { - stacked_floor_item.map_area - }, - FloorItem::Meseta(meseta_floor_item) => { - meseta_floor_item.map_area - } - } - } - - pub fn as_client_bytes(&self) -> [u8; 16] { - match self { - FloorItem::Individual(individual_floor_item) => { - individual_floor_item.item.as_client_bytes() - }, - FloorItem::Stacked(stacked_floor_item) => { - stacked_floor_item.tool.as_stacked_bytes(stacked_floor_item.count()) - }, - FloorItem::Meseta(meseta_floor_item) => { - meseta_floor_item.meseta.as_bytes() - } - } - } -} - -#[derive(Debug)] -pub struct CharacterInventory<'a>(&'a Vec); - -// TODO: this should actually do things -impl<'a> CharacterInventory<'a> { - pub fn as_client_inventory_items(&self) -> [character::InventoryItem; 30] { - self.0.iter() - .enumerate() - .fold([character::InventoryItem::default(); 30], |mut inventory, (slot, item)| { - let bytes = item.as_client_bytes(); - inventory[slot].data1.copy_from_slice(&bytes[0..12]); - inventory[slot].data2.copy_from_slice(&bytes[12..16]); - inventory[slot].item_id = item.item_id().0; - // does this do anything? - inventory[slot].equipped = if item.equipped() { 1 } else { 0 }; - // because this actually equips the item - inventory[slot].flags |= if item.equipped() { 8 } else { 0 }; - inventory - }) - } - - pub fn slot(&self, slot: usize) -> Option<&'a InventoryItem> { - self.0.get(slot) - } - - pub fn count(&self) -> usize { - self.0.len() - } - - // TODO: function that gives out a handle to an item, allowing deletion of it from the inventory -} - -pub enum TriggerCreateItem { - Yes, - No -} - - -#[derive(Error, Debug)] -#[error("")] -pub enum ItemManagerError { - EntityGatewayError, - NoSuchItemId(ClientItemId), - NoCharacter(CharacterEntityId), - CouldNotAddToInventory(FloorItem), - //ItemBelongsToOtherPlayer, - Idunnoman, - CouldNotSplitItem(InventoryItem), - CouldNotDropMeseta, - NotEnoughTools(Tool, usize, usize), // have, expected -} - -pub struct ItemManager { - id_counter: u32, - - character_inventory: HashMap>, - character_floor: HashMap>, - - character_room: HashMap, - room_floor: HashMap>, - room_item_id_counter: HashMap ClientItemId + Send>>, -} - -impl ItemManager { - pub fn new() -> ItemManager { - ItemManager { - id_counter: 0, - character_inventory: HashMap::new(), - character_floor: HashMap::new(), - character_room: HashMap::new(), - room_floor: HashMap::new(), - room_item_id_counter: HashMap::new(), - } - } - - pub fn next_global_item_id(&mut self) -> ClientItemId { - self.id_counter += 1; - ClientItemId(self.id_counter) - } - - // TODO: Result - pub async fn load_character(&mut self, entity_gateway: &mut EG, character: &CharacterEntity) { - let items = entity_gateway.get_items_by_character(&character).await; - let inventory_items = items.into_iter() - .filter_map(|item| { - match item.location { - ItemLocation::Inventory{slot, equipped, ..} => Some((item.id, item.item, slot, equipped)), - _ => None, - } - }) - .fold(BTreeMap::new(), |mut acc, (id, item, slot, equipped)| { - if item.is_stackable() { - if let ItemDetail::Tool(tool) = item { - let inventory_item = acc.entry(slot).or_insert(InventoryItem::Stacked(StackedInventoryItem { - entity_ids: Vec::new(), - item_id: self.next_global_item_id(), - tool: tool, - })); - if let InventoryItem::Stacked(ref mut stacked_inventory_item) = inventory_item { - stacked_inventory_item.entity_ids.push(id); - } - } - } - else { - acc.insert(slot, InventoryItem::Individual(IndividualInventoryItem { - entity_id: id, - item_id: self.next_global_item_id(), - item: item, - equipped: equipped, - })); - } - - acc - }); - let inventory = inventory_items.into_iter().map(|(k, v)| v).take(30).collect(); - self.character_inventory.insert(character.id, inventory); - } - - pub fn add_character_to_room(&mut self, room_id: RoomId, character: &CharacterEntity, area_client: AreaClient) { - let base_id = ((area_client.local_client.id() as u32) << 21) | 0x10000; - let inventory = self.character_inventory.get_mut(&character.id).unwrap(); - for (i, item) in inventory.iter_mut().enumerate() { - item.set_item_id(ClientItemId(base_id + i as u32)); - } - self.character_room.insert(character.id, room_id); - self.character_floor.insert(character.id, Vec::new()); - self.room_floor.entry(room_id).or_insert(Vec::new()); - - let mut inc = 0xF0000000; - self.room_item_id_counter.entry(room_id).or_insert(Box::new(move || { - inc += 1; - ClientItemId(inc) - })); - } - - pub fn get_character_inventory(&self, character: &CharacterEntity) -> Result { - Ok(CharacterInventory(self.character_inventory.get(&character.id) - .ok_or(ItemManagerError::NoCharacter(character.id))?)) - } - - pub fn remove_character_from_room(&mut self, character: &CharacterEntity) { - self.character_inventory.remove(&character.id); - self.character_floor.remove(&character.id); - self.character_room.remove(&character.id) - .as_ref() - .map(|room| { - if self.character_room.iter().find(|(_, r)| *r == room).is_none() { - self.room_floor.remove(room); - } - }); - } - - pub fn get_inventory_item_by_id(&self, character: &CharacterEntity, item_id: ClientItemId) -> Result { - let inventory = self.character_inventory.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; - inventory.iter() - .filter(|item| { - item.item_id() == item_id - }) - .nth(0) - .ok_or(ItemManagerError::NoSuchItemId(item_id)) - .map(Clone::clone) - } - - pub fn get_floor_item_by_id(&self, character: &CharacterEntity, item_id: ClientItemId) -> Result { - let floor = self.character_floor.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; - let room = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; - let shared_floor = self.room_floor.get(room).ok_or(ItemManagerError::NoCharacter(character.id))?; - floor.iter() - .chain(shared_floor.iter()) - .filter(|item| { - item.item_id() == item_id - }) - .nth(0) - .ok_or(ItemManagerError::NoSuchItemId(item_id)) - .map(Clone::clone) - } - - pub async fn character_picks_up_item(&mut self, entity_gateway: &mut EG, character: &mut CharacterEntity, floor_item: FloorItem) -> Result { - let local_floor = self.character_floor.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; - let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; - let room_id = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; - let shared_floor = self.room_floor.get_mut(&room_id).ok_or(ItemManagerError::NoCharacter(character.id))?; - - let (floor_to_remove_from, position) = if let Some(position) = local_floor.iter().position(|item| item.item_id() == floor_item.item_id()) { - (local_floor, position) - } - else if let Some(position) = shared_floor.iter().position(|item| item.item_id() == floor_item.item_id()) { - (shared_floor, position) - } - else { - return Err(ItemManagerError::NoSuchItemId(floor_item.item_id())) - }; - - let trigger_create_item = match floor_item.clone() { - FloorItem::Individual(individual_floor_item) => { - if inventory.len() >= 30 { - return Err(ItemManagerError::CouldNotAddToInventory(floor_item)); - } - - entity_gateway.save_item(&ItemEntity { - id: individual_floor_item.entity_id, - item: individual_floor_item.item.clone(), - location: ItemLocation::Inventory { - character_id: character.id, - slot: inventory.len(), - equipped: false, - } - }).await; - - let inventory_item = InventoryItem::Individual(IndividualInventoryItem { - entity_id: individual_floor_item.entity_id, - item_id: individual_floor_item.item_id, - item: individual_floor_item.item, - equipped: false, - }); - inventory.push(inventory_item); - - TriggerCreateItem::Yes - }, - FloorItem::Stacked(mut stacked_floor_item) => { - let mut tool_in_inventory = inventory.iter_mut() - .enumerate() - .find(|(_, item)| { - item.are_same_stackable_tool(&stacked_floor_item) - }); - - match tool_in_inventory { - Some((slot, ref mut tool_in_inventory)) => { - if tool_in_inventory.can_combine_stacks(&stacked_floor_item) { - for entity_id in stacked_floor_item.entity_ids.iter() { - entity_gateway.save_item(&ItemEntity { - id: *entity_id, - item: ItemDetail::Tool(stacked_floor_item.tool), - location: ItemLocation::Inventory { - character_id: character.id, - slot: slot, - equipped: false, - } - }).await; - } - - tool_in_inventory.combine_stacks(&mut stacked_floor_item) - } - else { - return Err(ItemManagerError::CouldNotAddToInventory(floor_item)) - } - TriggerCreateItem::No - }, - None => { - let slot = inventory.len(); - for entity_id in stacked_floor_item.entity_ids.iter() { - entity_gateway.save_item(&ItemEntity { - id: *entity_id, - item: ItemDetail::Tool(stacked_floor_item.tool), - location: ItemLocation::Inventory { - character_id: character.id, - slot: slot, - equipped: false, - } - }).await; - } - - inventory.push(InventoryItem::Stacked(StackedInventoryItem { - entity_ids: stacked_floor_item.entity_ids, - item_id: stacked_floor_item.item_id, - tool: stacked_floor_item.tool, - })); - TriggerCreateItem::Yes - } - } - }, - FloorItem::Meseta(meseta_floor_item) => { - if character.meseta == 999999 { - return Err(ItemManagerError::CouldNotAddToInventory(floor_item)); - } - character.meseta = std::cmp::min(character.meseta + meseta_floor_item.meseta.0, 999999); - entity_gateway.save_character(&character).await; - TriggerCreateItem::No - } - }; - - floor_to_remove_from.remove(position); - - Ok(trigger_create_item) - } - - pub async fn enemy_drop_item_on_local_floor(&mut self, entity_gateway: &mut EG, character: &CharacterEntity, item_drop: ItemDrop) -> Result<&FloorItem, ItemManagerError> { - let room_id = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; - - enum ItemOrMeseta { - Individual(ItemDetail), - Stacked(Tool), - Meseta(Meseta) - } - - let item = match item_drop.item { - ItemDropType::Weapon(w) => ItemOrMeseta::Individual(ItemDetail::Weapon(w)), - ItemDropType::Armor(w) => ItemOrMeseta::Individual(ItemDetail::Armor(w)), - ItemDropType::Shield(w) => ItemOrMeseta::Individual(ItemDetail::Shield(w)), - ItemDropType::Unit(w) => ItemOrMeseta::Individual(ItemDetail::Unit(w)), - ItemDropType::TechniqueDisk(w) => ItemOrMeseta::Individual(ItemDetail::TechniqueDisk(w)), - ItemDropType::Mag(w) => ItemOrMeseta::Individual(ItemDetail::Mag(w)), - //ItemDropType::IndividualTool(t) => ItemOrMeseta::Individual(ItemDetail::Tool(t)), - //ItemDropType::StackedTool(t, _) => ItemOrMeseta::Stacked(t), - ItemDropType::Tool(t) if t.tool.is_stackable() => ItemOrMeseta::Stacked(t), - ItemDropType::Tool(t) if !t.tool.is_stackable() => ItemOrMeseta::Individual(ItemDetail::Tool(t)), - ItemDropType::Meseta(m) => ItemOrMeseta::Meseta(Meseta(m)), - _ => unreachable!() // rust isnt smart enough to see that the conditional on tool catches everything - }; - - let item_id = self.room_item_id_counter.get_mut(room_id).ok_or(ItemManagerError::NoCharacter(character.id))?(); - let floor_item = match item { - ItemOrMeseta::Individual(item_detail) => { - let entity = entity_gateway.create_item(NewItemEntity { - item: item_detail.clone(), - location: ItemLocation::LocalFloor { - character_id: character.id, - map_area: item_drop.map_area, - x: item_drop.x, - y: item_drop.y, - z: item_drop.z, - } - }).await.ok_or(ItemManagerError::EntityGatewayError)?; - FloorItem::Individual(IndividualFloorItem { - entity_id: entity.id, - item_id: item_id, - item: item_detail, - map_area: item_drop.map_area, - x: item_drop.x, - y: item_drop.y, - z: item_drop.z, - }) - }, - ItemOrMeseta::Stacked(tool) => { - let entity = entity_gateway.create_item(NewItemEntity { - item: ItemDetail::Tool(tool), - location: ItemLocation::LocalFloor { - character_id: character.id, - map_area: item_drop.map_area, - x: item_drop.x, - y: item_drop.y, - z: item_drop.z, - } - }).await.ok_or(ItemManagerError::EntityGatewayError)?; - FloorItem::Stacked(StackedFloorItem { - entity_ids: vec![entity.id], - item_id: item_id, - tool: tool, - map_area: item_drop.map_area, - x: item_drop.x, - y: item_drop.y, - z: item_drop.z, - }) - }, - ItemOrMeseta::Meseta(meseta) => { - FloorItem::Meseta(MesetaFloorItem { - item_id: item_id, - meseta: meseta, - map_area: item_drop.map_area, - x: item_drop.x, - y: item_drop.y, - z: item_drop.z, - }) - }, - }; - - self.character_floor.entry(character.id).or_insert(Vec::new()).push(floor_item); - self.character_floor.get(&character.id).ok_or(ItemManagerError::Idunnoman)?.last().ok_or(ItemManagerError::Idunnoman) - } - - pub async fn player_drop_item_on_shared_floor(&mut self, - entity_gateway: &mut EG, - character: &CharacterEntity, - inventory_item: InventoryItem, - item_drop_location: (MapArea, f32, f32, f32)) - -> Result<(), ItemManagerError> { - let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; - let room_id = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; - let shared_floor = self.room_floor.get_mut(&room_id).ok_or(ItemManagerError::NoCharacter(character.id))?; - - let dropped_inventory_item = inventory - .drain_filter(|i| i.item_id() == inventory_item.item_id()) - .nth(0) - .ok_or(ItemManagerError::NoSuchItemId(inventory_item.item_id()))?; - - let dropped_floor_item = match dropped_inventory_item { - InventoryItem::Individual(individual_inventory_item) => { - entity_gateway.save_item(&ItemEntity { - id: individual_inventory_item.entity_id, - item: individual_inventory_item.item.clone(), - location: ItemLocation::SharedFloor { - map_area: item_drop_location.0, - x: item_drop_location.1, - y: item_drop_location.2, - z: item_drop_location.3, - } - }).await; - - FloorItem::Individual(IndividualFloorItem { - entity_id: individual_inventory_item.entity_id, - item_id: individual_inventory_item.item_id, - item: individual_inventory_item.item, - map_area: item_drop_location.0, - x: item_drop_location.1, - y: item_drop_location.2, - z: item_drop_location.3, - }) - }, - InventoryItem::Stacked(stacked_inventory_item) => { - for entity_id in stacked_inventory_item.entity_ids.iter() { - entity_gateway.save_item(&ItemEntity { - id: *entity_id, - item: ItemDetail::Tool(stacked_inventory_item.tool), - location: ItemLocation::SharedFloor { - map_area: item_drop_location.0, - x: item_drop_location.1, - y: item_drop_location.2, - z: item_drop_location.3, - } - }).await; - } - FloorItem::Stacked(StackedFloorItem { - entity_ids: stacked_inventory_item.entity_ids, - item_id: stacked_inventory_item.item_id, - tool: stacked_inventory_item.tool, - map_area: item_drop_location.0, - x: item_drop_location.1, - y: item_drop_location.2, - z: item_drop_location.3, - }) - }, - }; - - shared_floor.push(dropped_floor_item); - Ok(()) - } - - pub async fn player_drops_meseta_on_shared_floor(&mut self, - entity_gateway: &mut EG, - character: &mut CharacterEntity, - drop_location: ItemDropLocation, - amount: u32) - -> Result { - let room_id = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; - let shared_floor = self.room_floor.get_mut(&room_id).ok_or(ItemManagerError::NoCharacter(character.id))?; - if character.meseta <= amount { - return Err(ItemManagerError::CouldNotDropMeseta) - } - character.meseta -= amount; - entity_gateway.save_character(&character).await; - - let item_id = self.room_item_id_counter.get_mut(room_id).ok_or(ItemManagerError::NoCharacter(character.id))?(); - let floor_item = FloorItem::Meseta(MesetaFloorItem { - item_id: item_id, - meseta: Meseta(amount), - map_area: drop_location.map_area, - x: drop_location.x, - y: 0.0, - z: drop_location.z, - }); - - shared_floor.push(floor_item.clone()); - Ok(floor_item) - } - - pub async fn player_drops_partial_stack_on_shared_floor(&mut self, - entity_gateway: &mut EG, - character: &CharacterEntity, - inventory_item: InventoryItem, - drop_location: ItemDropLocation, - amount: usize) - -> Result { - let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; - let room_id = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; - let shared_floor = self.room_floor.get_mut(&room_id).ok_or(ItemManagerError::NoCharacter(character.id))?; - - let item_to_split = inventory.iter_mut() - .find(|i| i.item_id() == inventory_item.item_id()) - .ok_or(ItemManagerError::NoSuchItemId(inventory_item.item_id()))?; - - - if let InventoryItem::Stacked(ref mut stacked_inventory_item) = item_to_split { - if stacked_inventory_item.entity_ids.len() <= amount { - return Err(ItemManagerError::CouldNotSplitItem(inventory_item)); - } - - let dropped_entity_ids = stacked_inventory_item.entity_ids.drain(..amount).collect::>(); - for de in dropped_entity_ids.iter() { - entity_gateway.save_item(&ItemEntity { - id: *de, - item: ItemDetail::Tool(stacked_inventory_item.tool), - location: ItemLocation::SharedFloor { - map_area: drop_location.map_area, - x: drop_location.x, - y: 0.0, - z: drop_location.z, - } - }).await - } - - let item_id = self.room_item_id_counter.get_mut(room_id).ok_or(ItemManagerError::NoCharacter(character.id))?(); - let floor_item = FloorItem::Stacked(StackedFloorItem { - entity_ids: dropped_entity_ids, - item_id: item_id, - tool: stacked_inventory_item.tool, - map_area: drop_location.map_area, - x: drop_location.x, - y: 0.0, - z: drop_location.z, - }); - shared_floor.push(floor_item.clone()); - Ok(floor_item) - } - else { - Err(ItemManagerError::CouldNotSplitItem(inventory_item)) - } - } - - pub async fn player_consumes_tool(&mut self, - entity_gateway: &mut EG, - character: &CharacterEntity, - inventory_item: InventoryItem, - amount: usize) - -> Result { - let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; - - match inventory_item.clone() { - InventoryItem::Individual(individual_inventory_item) => { - // TODO: use remove - let _used_inventory_item = inventory - .drain_filter(|i| i.item_id() == inventory_item.item_id()) - .nth(0) - .ok_or(ItemManagerError::NoSuchItemId(inventory_item.item_id()))?; - - entity_gateway.save_item(&ItemEntity { - id: individual_inventory_item.entity_id, - item: individual_inventory_item.item.clone(), - location: ItemLocation::Consumed, - }).await; - Ok(individual_inventory_item.item) - }, - InventoryItem::Stacked(stacked_inventory_item) => { - let used_entity_ids = match stacked_inventory_item.count().cmp(&amount) { - Ordering::Equal => { - let _used_inventory_item = inventory - .drain_filter(|i| i.item_id() == inventory_item.item_id()) - .nth(0) - .ok_or(ItemManagerError::NoSuchItemId(inventory_item.item_id()))?; - stacked_inventory_item.entity_ids - }, - Ordering::Greater => { - let tool_used = inventory - .iter_mut() - .find(|i| i.item_id() == stacked_inventory_item.item_id) - .ok_or(ItemManagerError::NoSuchItemId(stacked_inventory_item.item_id))?; - if let InventoryItem::Stacked(ref mut stacked_tool_used) = tool_used { - stacked_tool_used.entity_ids.drain(..amount).collect::>() - } - else { - unreachable!() - } - //stacked_inventory_item.entity_ids.iter_mut().drain(..amount).collect::>() - } - Ordering::Less => { - //return Err(ItemManagerError::NotEnoughTools(tool, held_amount, amount)) - return Err(ItemManagerError::Idunnoman) - }, - }; - - for used_entity_id in used_entity_ids { - entity_gateway.save_item(&ItemEntity { - id: used_entity_id, - item: ItemDetail::Tool(stacked_inventory_item.tool), - location: ItemLocation::Consumed, - }).await; - } - Ok(ItemDetail::Tool(stacked_inventory_item.tool)) - }, - } - - } -} diff --git a/src/ship/items/bank.rs b/src/ship/items/bank.rs new file mode 100644 index 0000000..2a91755 --- /dev/null +++ b/src/ship/items/bank.rs @@ -0,0 +1,198 @@ +use crate::ship::items::ClientItemId; +use std::collections::{HashMap, BTreeMap, BinaryHeap}; +use std::cmp::{Ordering, PartialOrd, PartialEq}; +use thiserror::Error; +use futures::future::join_all; +use libpso::character::character;//::InventoryItem; +use crate::entity::gateway::EntityGateway; +use crate::entity::character::{CharacterEntity, CharacterEntityId}; +use crate::entity::item::{ItemEntityId, ItemEntity, ItemDetail, ItemLocation, BankName}; +use crate::entity::item::{Meseta, NewItemEntity}; +use crate::entity::item::tool::{Tool, ToolType}; +use crate::ship::map::MapArea; +use crate::ship::ship::ItemDropLocation; +use crate::ship::drops::{ItemDrop, ItemDropType}; +use crate::ship::location::{AreaClient, RoomId}; + + +#[derive(Debug, Clone)] +pub struct IndividualBankItem { + pub entity_id: ItemEntityId, + pub item_id: ClientItemId, + pub item: ItemDetail, +} + +#[derive(Debug, Clone)] +pub struct StackedBankItem { + pub entity_ids: Vec, + pub item_id: ClientItemId, + pub tool: Tool, +} + +impl StackedBankItem { + pub fn count(&self) -> usize { + self.entity_ids.len() + } +} + +#[derive(Debug, Clone)] +pub enum BankItem { + Individual(IndividualBankItem), + Stacked(StackedBankItem), +} + + +impl std::cmp::PartialEq for BankItem { + fn eq(&self, other: &BankItem) -> bool { + let mut self_bytes = [0u8; 4]; + let mut other_bytes = [0u8; 4]; + self_bytes.copy_from_slice(&self.as_client_bytes()[0..4]); + other_bytes.copy_from_slice(&other.as_client_bytes()[0..4]); + + let self_value = u32::from_be_bytes(self_bytes); + let other_value = u32::from_be_bytes(other_bytes); + + self_value.eq(&other_value) + } +} + +impl std::cmp::Eq for BankItem {} + +impl std::cmp::PartialOrd for BankItem { + fn partial_cmp(&self, other: &BankItem) -> Option { + //let self_bytes = self.as_client_bytes(); + //let other_bytes = other.as_client_bytes(); + let mut self_bytes = [0u8; 4]; + let mut other_bytes = [0u8; 4]; + self_bytes.copy_from_slice(&self.as_client_bytes()[0..4]); + other_bytes.copy_from_slice(&other.as_client_bytes()[0..4]); + + + let self_value = u32::from_be_bytes(self_bytes); + let other_value = u32::from_be_bytes(other_bytes); + + self_value.partial_cmp(&other_value) + } +} + +impl std::cmp::Ord for BankItem { + fn cmp(&self, other: &BankItem) -> std::cmp::Ordering { + //let self_bytes = self.as_client_bytes(); + //let other_bytes = other.as_client_bytes(); + let mut self_bytes = [0u8; 4]; + let mut other_bytes = [0u8; 4]; + self_bytes.copy_from_slice(&self.as_client_bytes()[0..4]); + other_bytes.copy_from_slice(&other.as_client_bytes()[0..4]); + + + let self_value = u32::from_le_bytes(self_bytes); + let other_value = u32::from_le_bytes(other_bytes); + + self_value.cmp(&other_value) + } +} + +impl BankItem { + pub fn set_item_id(&mut self, item_id: ClientItemId) { + match self { + BankItem::Individual(individual_bank_item) => { + individual_bank_item.item_id = item_id + }, + BankItem::Stacked(stacked_bank_item) => { + stacked_bank_item.item_id = item_id + } + } + } + + pub fn item_id(&self) -> ClientItemId { + match self { + BankItem::Individual(individual_bank_item) => { + individual_bank_item.item_id + }, + BankItem::Stacked(stacked_bank_item) => { + stacked_bank_item.item_id + } + } + } + + pub fn as_client_bytes(&self) -> [u8; 16] { + match self { + BankItem::Individual(item) => { + match &item.item { + ItemDetail::Weapon(w) => w.as_bytes(), + ItemDetail::Armor(a) => a.as_bytes(), + ItemDetail::Shield(s) => s.as_bytes(), + ItemDetail::Unit(u) => u.as_bytes(), + ItemDetail::Tool(t) => t.as_individual_bytes(), + ItemDetail::TechniqueDisk(d) => d.as_bytes(), + ItemDetail::Mag(m) => m.as_bytes(), + } + }, + BankItem::Stacked(item) => { + item.tool.as_stacked_bytes(item.entity_ids.len()) + }, + } + } +} + +pub struct CharacterBank(Vec); + +impl CharacterBank { + pub fn new(mut items: Vec) -> CharacterBank { + items.sort(); + CharacterBank(items) + } + + pub fn initialize_item_ids(&mut self, base_item_id: u32) { + for (i, item) in self.0.iter_mut().enumerate() { + item.set_item_id(ClientItemId(base_item_id + i as u32)); + } + } + + pub fn as_client_bank_items(&self) -> character::Bank { + self.0.iter() + .enumerate() + .fold(character::Bank::default(), |mut bank, (slot, item)| { + bank.item_count = (slot + 1) as u32; + let bytes = item.as_client_bytes(); + bank.items[slot].data1.copy_from_slice(&bytes[0..12]); + bank.items[slot].data2.copy_from_slice(&bytes[12..16]); + bank.items[slot].item_id = item.item_id().0; + + bank + }) + } + + pub fn as_client_bank_request(&self) -> Vec { + self.0.iter() + .map(|item| { + let bytes = item.as_client_bytes(); + let mut data1 = [0; 12]; + let mut data2 = [0; 4]; + data1.copy_from_slice(&bytes[0..12]); + data2.copy_from_slice(&bytes[12..16]); + let amount = match item { + BankItem::Individual(_individual_bank_item) => { + 1 + }, + BankItem::Stacked(stacked_bank_item) => { + stacked_bank_item.count() + }, + }; + character::BankItem { + data1: data1, + data2: data2, + item_id: item.item_id().0, + amount: amount as u16, + flags: 1, + } + }) + .collect() + } + + pub fn count(&self) -> usize { + self.0.len() + } +} + + diff --git a/src/ship/items/floor.rs b/src/ship/items/floor.rs new file mode 100644 index 0000000..b74ae7b --- /dev/null +++ b/src/ship/items/floor.rs @@ -0,0 +1,261 @@ +use crate::ship::items::ClientItemId; +use std::collections::{HashMap, BTreeMap, BinaryHeap}; +use std::cmp::{Ordering, PartialOrd, PartialEq}; +use thiserror::Error; +use futures::future::join_all; +use libpso::character::character;//::InventoryItem; +use crate::entity::gateway::EntityGateway; +use crate::entity::character::{CharacterEntity, CharacterEntityId}; +use crate::entity::item::{ItemEntityId, ItemEntity, ItemDetail, ItemLocation, BankName}; +use crate::entity::item::{Meseta, NewItemEntity}; +use crate::entity::item::tool::{Tool, ToolType}; +use crate::ship::map::MapArea; +use crate::ship::ship::ItemDropLocation; +use crate::ship::drops::{ItemDrop, ItemDropType}; +use crate::ship::location::{AreaClient, RoomId}; +use crate::ship::items::inventory::{IndividualInventoryItem, StackedInventoryItem, InventoryItemHandle}; + + +#[derive(Debug, Clone)] +pub struct IndividualFloorItem { + pub entity_id: ItemEntityId, + pub item_id: ClientItemId, + pub item: ItemDetail, + pub map_area: MapArea, + pub x: f32, + pub y: f32, + pub z: f32, +} + +#[derive(Debug, Clone)] +pub struct StackedFloorItem { + pub entity_ids: Vec, + pub item_id: ClientItemId, + pub tool: Tool, + pub map_area: MapArea, + pub x: f32, + pub y: f32, + pub z: f32, +} + +impl StackedFloorItem { + pub fn count(&self) -> usize { + self.entity_ids.len() + } + + pub fn as_client_bytes(&self) -> [u8; 16] { + self.tool.as_stacked_bytes(self.count()) + } +} + +#[derive(Debug, Clone)] +pub struct MesetaFloorItem { + pub item_id: ClientItemId, + pub meseta: Meseta, + pub map_area: MapArea, + pub x: f32, + pub y: f32, + pub z: f32, +} + +#[derive(Debug, Clone)] +pub enum FloorItem { + Individual(IndividualFloorItem), + Stacked(StackedFloorItem), + Meseta(MesetaFloorItem), +} + +impl FloorItem { + pub fn item_id(&self) -> ClientItemId { + match self { + FloorItem::Individual(individual_floor_item) => { + individual_floor_item.item_id + }, + FloorItem::Stacked(stacked_floor_item) => { + stacked_floor_item.item_id + }, + FloorItem::Meseta(meseta_floor_item) => { + meseta_floor_item.item_id + } + } + } + + pub fn x(&self) -> f32 { + match self { + FloorItem::Individual(individual_floor_item) => { + individual_floor_item.x + }, + FloorItem::Stacked(stacked_floor_item) => { + stacked_floor_item.x + }, + FloorItem::Meseta(meseta_floor_item) => { + meseta_floor_item.x + } + } + } + + pub fn y(&self) -> f32 { + match self { + FloorItem::Individual(individual_floor_item) => { + individual_floor_item.y + }, + FloorItem::Stacked(stacked_floor_item) => { + stacked_floor_item.y + }, + FloorItem::Meseta(meseta_floor_item) => { + meseta_floor_item.y + } + } + } + + pub fn z(&self) -> f32 { + match self { + FloorItem::Individual(individual_floor_item) => { + individual_floor_item.z + }, + FloorItem::Stacked(stacked_floor_item) => { + stacked_floor_item.z + }, + FloorItem::Meseta(meseta_floor_item) => { + meseta_floor_item.z + } + } + } + + pub fn map_area(&self) -> MapArea { + match self { + FloorItem::Individual(individual_floor_item) => { + individual_floor_item.map_area + }, + FloorItem::Stacked(stacked_floor_item) => { + stacked_floor_item.map_area + }, + FloorItem::Meseta(meseta_floor_item) => { + meseta_floor_item.map_area + } + } + } + + pub fn as_client_bytes(&self) -> [u8; 16] { + match self { + FloorItem::Individual(individual_floor_item) => { + individual_floor_item.item.as_client_bytes() + }, + FloorItem::Stacked(stacked_floor_item) => { + stacked_floor_item.as_client_bytes() + }, + FloorItem::Meseta(meseta_floor_item) => { + meseta_floor_item.meseta.as_bytes() + } + } + } +} + + + +pub struct FloorItemHandle<'a> { + floor: &'a mut RoomFloorItems, + index: usize, +} + +impl<'a> FloorItemHandle<'a> { + pub fn item(&'a self) -> Option<&'a FloorItem> { + self.floor.0.get(self.index) + } + + pub fn remove_from_floor(self) { + self.floor.0.remove(self.index); + } +} + +// TODO: floors should keep track of their own item_ids +#[derive(Debug)] +pub struct RoomFloorItems(Vec); + +impl RoomFloorItems { + pub fn new() -> RoomFloorItems { + RoomFloorItems(Vec::new()) + } + + pub fn add_item(&mut self, item: FloorItem) { + self.0.push(item); + } + + pub fn get_item_by_id(&self, item_id: ClientItemId) -> Option<&FloorItem> { + self.0.iter().find(|item| item.item_id() == item_id) + } + + pub fn get_item_handle_by_id(&mut self, item_id: ClientItemId) -> Option { + let index = self.0.iter().position(|item| item.item_id() == item_id)?; + Some(FloorItemHandle { + floor: self, + index: index, + }) + } + + pub fn take_item_by_id(&mut self, item_id: ClientItemId) -> Option { + self.0 + .drain_filter(|i| i.item_id() == item_id) + .nth(0) + } + + pub fn drop_individual_inventory_item(&mut self, individual_inventory_item: IndividualInventoryItem, item_drop_location: (MapArea, f32, f32, f32)) -> &IndividualFloorItem { + self.0.push(FloorItem::Individual(IndividualFloorItem { + entity_id: individual_inventory_item.entity_id, + item_id: individual_inventory_item.item_id, + item: individual_inventory_item.item, + map_area: item_drop_location.0, + x: item_drop_location.1, + y: item_drop_location.2, + z: item_drop_location.3, + })); + + match self.0.last().unwrap() { + FloorItem::Individual(item) => item, + _ => unreachable!(), + } + } + + pub fn drop_stacked_inventory_item(&mut self, stacked_inventory_item: StackedInventoryItem, item_drop_location: (MapArea, f32, f32, f32)) -> &StackedFloorItem { + self.0.push(FloorItem::Stacked(StackedFloorItem { + entity_ids: stacked_inventory_item.entity_ids, + item_id: stacked_inventory_item.item_id, + tool: stacked_inventory_item.tool, + map_area: item_drop_location.0, + x: item_drop_location.1, + y: item_drop_location.2, + z: item_drop_location.3, + })); + + match self.0.last().unwrap() { + FloorItem::Stacked(item) => item, + _ => unreachable!(), + } + } + + // TODO: Result + // TODO: if consumed_item is not a tool items do not get placed back into inventory (should I care?) + pub fn drop_partial_stacked_inventory_item(&mut self, inventory_item: InventoryItemHandle, amount: usize, new_item_id: ClientItemId, item_drop_location: (MapArea, f32, f32, f32)) -> Option<&StackedFloorItem> { + let consumed_item = inventory_item.consume(amount).ok()?; + + if let ItemDetail::Tool(tool) = consumed_item.item { + self.0.push(FloorItem::Stacked(StackedFloorItem { + entity_ids: consumed_item.entity_ids, + item_id: new_item_id, + tool: tool, + map_area: item_drop_location.0, + x: item_drop_location.1, + y: item_drop_location.2, + z: item_drop_location.3, + })) + } + else { + return None + } + + match self.0.last().unwrap() { + FloorItem::Stacked(item) => Some(item), + _ => unreachable!(), + } + } +} diff --git a/src/ship/items/inventory.rs b/src/ship/items/inventory.rs new file mode 100644 index 0000000..84fe1f3 --- /dev/null +++ b/src/ship/items/inventory.rs @@ -0,0 +1,396 @@ +use std::collections::{HashMap, BTreeMap, BinaryHeap}; +use std::cmp::{Ordering, PartialOrd, PartialEq}; +use thiserror::Error; +use futures::future::join_all; +use libpso::character::character;//::InventoryItem; +use crate::entity::gateway::EntityGateway; +use crate::entity::character::{CharacterEntity, CharacterEntityId}; +use crate::entity::item::{ItemEntityId, ItemEntity, ItemDetail, ItemLocation, BankName}; +use crate::entity::item::{Meseta, NewItemEntity}; +use crate::entity::item::tool::{Tool, ToolType}; +use crate::ship::map::MapArea; +use crate::ship::ship::ItemDropLocation; +use crate::ship::drops::{ItemDrop, ItemDropType}; +use crate::ship::location::{AreaClient, RoomId}; +use crate::ship::items::ClientItemId; +use crate::ship::items::floor::{IndividualFloorItem, StackedFloorItem}; + + +#[derive(Debug, Clone)] +pub struct InventorySlot(pub usize); + +#[derive(Debug, Clone)] +pub struct IndividualInventoryItem { + pub entity_id: ItemEntityId, + pub item_id: ClientItemId, + pub item: ItemDetail, + pub equipped: bool, +} + +#[derive(Debug, Clone)] +pub struct StackedInventoryItem { + pub entity_ids: Vec, + pub item_id: ClientItemId, + pub tool: Tool, +} + +impl StackedInventoryItem { + pub fn count(&self) -> usize { + self.entity_ids.len() + } +} + +#[derive(Debug, Clone)] +pub enum InventoryItem { + Individual(IndividualInventoryItem), + Stacked(StackedInventoryItem), +} + +#[derive(Error, Debug, Clone)] +#[error("")] +pub enum InventoryItemAddToError { + BothAreNotStacked, + DifferentTool, + ExceedsCapacity, +} + +impl InventoryItem { + pub fn item_id(&self) -> ClientItemId { + match self { + InventoryItem::Individual(individual_inventory_item) => { + individual_inventory_item.item_id + }, + InventoryItem::Stacked(stacked_inventory_item) => { + stacked_inventory_item.item_id + } + } + } + + pub fn set_item_id(&mut self, item_id: ClientItemId) { + match self { + InventoryItem::Individual(individual_inventory_item) => { + individual_inventory_item.item_id = item_id + }, + InventoryItem::Stacked(stacked_inventory_item) => { + stacked_inventory_item.item_id = item_id + } + } + } + + pub fn are_same_stackable_tool(&self, other_stacked_item: &StackedFloorItem) -> bool { + match self { + InventoryItem::Stacked(self_stacked_item) => { + self_stacked_item.tool == other_stacked_item.tool + && self_stacked_item.tool.is_stackable() && other_stacked_item.tool.is_stackable() + }, + _ => false + } + } + + pub fn can_combine_stacks(&self, other_stacked_item: &StackedFloorItem) -> bool { + match self { + InventoryItem::Stacked(self_stacked_item) => { + self_stacked_item.tool == other_stacked_item.tool + && self_stacked_item.tool.is_stackable() && other_stacked_item.tool.is_stackable() + && self_stacked_item.count() + other_stacked_item.count() <= self_stacked_item.tool.max_stack() + }, + _ => false + } + } + + // TODO: result + pub fn combine_stacks(&mut self, other_stacked_item: &mut StackedFloorItem) { + match self { + InventoryItem::Stacked(self_stacked_item) => { + self_stacked_item.entity_ids.append(&mut other_stacked_item.entity_ids); + }, + _ => { + } + } + } + + pub fn equipped(&self) -> bool { + match self { + InventoryItem::Individual(individual_inventory_item) => { + individual_inventory_item.equipped + }, + InventoryItem::Stacked(_) => { + false + } + } + } + + pub fn as_client_bytes(&self) -> [u8; 16] { + match self { + InventoryItem::Individual(item) => { + match &item.item { + ItemDetail::Weapon(w) => w.as_bytes(), + ItemDetail::Armor(a) => a.as_bytes(), + ItemDetail::Shield(s) => s.as_bytes(), + ItemDetail::Unit(u) => u.as_bytes(), + ItemDetail::Tool(t) => t.as_individual_bytes(), + ItemDetail::TechniqueDisk(d) => d.as_bytes(), + ItemDetail::Mag(m) => m.as_bytes(), + } + }, + InventoryItem::Stacked(item) => { + item.tool.as_stacked_bytes(item.entity_ids.len()) + }, + } + } + + pub fn can_add_to(&mut self, stacked_floor_item: &StackedFloorItem) -> Result<(), InventoryItemAddToError> { + if let InventoryItem::Stacked(stacked_inventory_item) = self { + if stacked_floor_item.tool != stacked_inventory_item.tool { + return Err(InventoryItemAddToError::DifferentTool) + } + + if stacked_floor_item.tool.tool.max_stack() < (stacked_floor_item.count() + stacked_inventory_item.count()) { + return Err(InventoryItemAddToError::ExceedsCapacity) + } + Ok(()) + } + else { + Err(InventoryItemAddToError::BothAreNotStacked) + } + } + + pub fn add_to(&mut self, mut stacked_floor_item: StackedFloorItem) -> Result<(), InventoryItemAddToError> { + self.can_add_to(&stacked_floor_item)?; + if let InventoryItem::Stacked(stacked_inventory_item) = self { + stacked_inventory_item.entity_ids.append(&mut stacked_floor_item.entity_ids); + } + Ok(()) + } +} + + +#[derive(Error, Debug, Clone)] +#[error("")] +pub enum InventoryItemConsumeError { + InconsistentState, + InvalidAmount, +} + +pub struct ConsumedItem { + pub entity_ids: Vec, + pub item: ItemDetail +} + +pub struct InventoryItemHandle<'a> { + inventory: &'a mut CharacterInventory, + slot: usize, +} + +impl<'a> InventoryItemHandle<'a> { + pub fn item(&'a self) -> Option<&'a InventoryItem> { + self.inventory.0.get(self.slot) + } + + pub fn remove_from_inventory(self) { + self.inventory.0.remove(self.slot); + } + + pub fn consume(self, amount: usize) -> Result { + enum RemoveMethod { + EntireThing(ConsumedItem), + Partial(ItemDetail), + } + + let inventory_item = self.inventory.0.get(self.slot).ok_or(InventoryItemConsumeError::InconsistentState)?; + let remove_method = match inventory_item { + InventoryItem::Individual(individual_inventory_item) => { + RemoveMethod::EntireThing(ConsumedItem { + entity_ids: vec![individual_inventory_item.entity_id], + item: individual_inventory_item.item.clone() + }) + }, + InventoryItem::Stacked(stacked_inventory_item) => { + match stacked_inventory_item.count().cmp(&amount) { + Ordering::Equal => { + RemoveMethod::EntireThing(ConsumedItem { + entity_ids: stacked_inventory_item.entity_ids.clone(), + item: ItemDetail::Tool(stacked_inventory_item.tool), + }) + }, + Ordering::Greater => { + RemoveMethod::Partial(ItemDetail::Tool(stacked_inventory_item.tool)) + }, + Ordering::Less => { + return Err(InventoryItemConsumeError::InvalidAmount) + } + } + }, + }; + + match remove_method { + RemoveMethod::EntireThing(consumed_item) => { + self.inventory.0.remove(self.slot); + Ok(consumed_item) + }, + RemoveMethod::Partial(item_detail) => { + let entity_ids = self.inventory.0.get_mut(self.slot) + .and_then(|item| { + if let InventoryItem::Stacked(stacked_inventory_item) = item { + Some(stacked_inventory_item.entity_ids.drain(..amount).collect::>()) + } + else { + None + } + }) + .ok_or(InventoryItemConsumeError::InvalidAmount)?; + Ok(ConsumedItem { + entity_ids: entity_ids, + item: item_detail, + }) + } + } + } +} + + + + +#[derive(Debug)] +pub struct CharacterInventory(Vec); + +impl CharacterInventory { + pub fn new(items: Vec) -> CharacterInventory { + CharacterInventory(items) + } + + pub fn initialize_item_ids(&mut self, base_item_id: u32) { + for (i, item) in self.0.iter_mut().enumerate() { + item.set_item_id(ClientItemId(base_item_id + i as u32)); + } + } + + pub fn as_client_inventory_items(&self) -> [character::InventoryItem; 30] { + self.0.iter() + .enumerate() + .fold([character::InventoryItem::default(); 30], |mut inventory, (slot, item)| { + let bytes = item.as_client_bytes(); + inventory[slot].data1.copy_from_slice(&bytes[0..12]); + inventory[slot].data2.copy_from_slice(&bytes[12..16]); + inventory[slot].item_id = item.item_id().0; + // does this do anything? + inventory[slot].equipped = if item.equipped() { 1 } else { 0 }; + // because this actually equips the item + inventory[slot].flags |= if item.equipped() { 8 } else { 0 }; + inventory + }) + } + + pub fn slot(&self, slot: usize) -> Option<&InventoryItem> { + self.0.get(slot) + } + + pub fn count(&self) -> usize { + self.0.len() + } + + pub fn get_item_handle_by_id<'a>(&'a mut self, item_id: ClientItemId) -> Option> { + let (slot, _) = self.0.iter() + .enumerate() + .filter(|(_, item)| { + item.item_id() == item_id + }) + .nth(0)?; + Some(InventoryItemHandle { + inventory: self, + slot: slot, + }) + } + + pub fn get_item_by_id(&self, item_id: ClientItemId) -> Option<&InventoryItem> { + self.0.iter() + .filter(|item| { + item.item_id() == item_id + }) + .nth(0) + } + + pub fn take_item_by_id(&mut self, item_id: ClientItemId) -> Option { + self.0 + .drain_filter(|i| i.item_id() == item_id) + .nth(0) + } + + pub fn take_stacked_item_by_id(&mut self, item_id: ClientItemId, amount: usize) -> Option { + // TODO: amount? + self.0 + .drain_filter(|i| i.item_id() == item_id) + .nth(0) + } + + pub fn add_item(&mut self, item: InventoryItem) -> Result<(), ()> { // TODO: errors + // TODO: check slot conflict? + self.0.push(item); + Ok(()) + } + + // TODO: should these pick up functions take floor_item as mut and remove the ids? + pub fn pick_up_individual_floor_item(&mut self, floor_item: &IndividualFloorItem) -> Option<(&IndividualInventoryItem, InventorySlot)> { + if self.count() >= 30 { + return None; + } + + self.0.push(InventoryItem::Individual(IndividualInventoryItem { + entity_id: floor_item.entity_id, + item_id: floor_item.item_id, + item: floor_item.item.clone(), + equipped: false, + })); + + if let Some(InventoryItem::Individual(new_item)) = self.0.last() { + Some((new_item, InventorySlot(self.count()))) + } + else { + None + } + } + + // TODO: can be simplified using find instead of position + pub fn pick_up_stacked_floor_item(&mut self, floor_item: &StackedFloorItem) -> Option<(&StackedInventoryItem, InventorySlot)> { + let existing_stack_position = self.0.iter() + .position(|inventory_item| { + if let InventoryItem::Stacked(stacked_inventory_item) = inventory_item { + if stacked_inventory_item.tool == floor_item.tool { + return true + } + } + false + }); + + if let Some(existing_stack_position) = existing_stack_position { + if let Some(InventoryItem::Stacked(stacked_item)) = self.0.get_mut(existing_stack_position) { + if (stacked_item.count() + floor_item.count() <= stacked_item.tool.max_stack()) { + stacked_item.entity_ids.append(&mut floor_item.entity_ids.clone()); + Some((stacked_item, InventorySlot(existing_stack_position))) + } + else { + None + } + } + else { + None + } + } + else { + let new_stacked_item = InventoryItem::Stacked(StackedInventoryItem { + entity_ids: floor_item.entity_ids.clone(), + item_id: floor_item.item_id, + tool: floor_item.tool, + }); + + self.0.push(new_stacked_item); + if let Some(InventoryItem::Stacked(new_item)) = self.0.last() { + Some((new_item, InventorySlot(self.count()))) + } + else { + None + } + } + } +} + diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs new file mode 100644 index 0000000..7f3a4a4 --- /dev/null +++ b/src/ship/items/manager.rs @@ -0,0 +1,532 @@ +use crate::ship::items::ClientItemId; +use std::collections::{HashMap, BTreeMap, BinaryHeap}; +use std::cmp::{Ordering, PartialOrd, PartialEq}; +use thiserror::Error; +use futures::future::join_all; +use libpso::character::character;//::InventoryItem; +use crate::entity::gateway::EntityGateway; +use crate::entity::character::{CharacterEntity, CharacterEntityId}; +use crate::entity::item::{ItemEntityId, ItemEntity, ItemDetail, ItemLocation, BankName}; +use crate::entity::item::{Meseta, NewItemEntity}; +use crate::entity::item::tool::{Tool, ToolType}; +use crate::ship::map::MapArea; +use crate::ship::ship::ItemDropLocation; +use crate::ship::drops::{ItemDrop, ItemDropType}; +use crate::ship::location::{AreaClient, RoomId}; + +use crate::ship::items::bank::*; +use crate::ship::items::floor::*; +use crate::ship::items::inventory::*; + + +pub enum TriggerCreateItem { + Yes, + No +} + +#[derive(Error, Debug)] +#[error("")] +pub enum ItemManagerError { + EntityGatewayError, + NoSuchItemId(ClientItemId), + NoCharacter(CharacterEntityId), + CouldNotAddToInventory(ClientItemId), + //ItemBelongsToOtherPlayer, + Idunnoman, + CouldNotSplitItem(ClientItemId), + CouldNotDropMeseta, + NotEnoughTools(Tool, usize, usize), // have, expected + InventoryItemConsumeError(#[from] InventoryItemConsumeError), +} + +pub struct ItemManager { + id_counter: u32, + + character_inventory: HashMap, + character_bank: HashMap>, + character_floor: HashMap, + + character_room: HashMap, + room_floor: HashMap, + room_item_id_counter: HashMap ClientItemId + Send>>, +} + +impl ItemManager { + pub fn new() -> ItemManager { + ItemManager { + id_counter: 0, + character_inventory: HashMap::new(), + character_bank: HashMap::new(), + character_floor: HashMap::new(), + character_room: HashMap::new(), + room_floor: HashMap::new(), + room_item_id_counter: HashMap::new(), + } + } + + pub fn next_global_item_id(&mut self) -> ClientItemId { + self.id_counter += 1; + ClientItemId(self.id_counter) + } + + // TODO: Result + pub async fn load_character(&mut self, entity_gateway: &mut EG, character: &CharacterEntity) { + let items = entity_gateway.get_items_by_character(&character).await; + let inventory_items = items.clone().into_iter() + .filter_map(|item| { + match item.location { + ItemLocation::Inventory{slot, equipped, ..} => Some((item.id, item.item, slot, equipped)), + _ => None, + } + }) + .fold(BTreeMap::new(), |mut acc, (id, item, slot, equipped)| { + if item.is_stackable() { + if let ItemDetail::Tool(tool) = item { + let inventory_item = acc.entry(slot).or_insert(InventoryItem::Stacked(StackedInventoryItem { + entity_ids: Vec::new(), + item_id: self.next_global_item_id(), + tool: tool, + })); + if let InventoryItem::Stacked(ref mut stacked_inventory_item) = inventory_item { + stacked_inventory_item.entity_ids.push(id); + } + } + } + else { + acc.insert(slot, InventoryItem::Individual(IndividualInventoryItem { + entity_id: id, + item_id: self.next_global_item_id(), + item: item, + equipped: equipped, + })); + } + + acc + }); + + let bank_items = items.into_iter() + .filter_map(|item| { + match item.location { + ItemLocation::Bank{name, ..} => Some((item.id, item.item, name)), + _ => None, + } + }) + .fold(BTreeMap::new(), |mut acc, (id, item, name)| { + acc.entry(name).or_insert(Vec::new()).push((id, item)); + acc + }) + .into_iter() + .map(|(bank_name, bank_items)| { + let mut stacked_bank_items = bank_items.into_iter() + .fold(Vec::new(), |mut acc, (id, bank_item)| { + if bank_item.is_stackable() { + let existing_item = acc.iter_mut() + .find(|item| { + if let (BankItem::Stacked(stacked_bank_item), &ItemDetail::Tool(ref tool)) = (item, &bank_item) { + stacked_bank_item.tool == *tool + } + else { + false + } + }); + match existing_item { + Some(item) => { + if let BankItem::Stacked(ref mut stacked_bank_item) = item { + stacked_bank_item.entity_ids.push(id); + } + } + None => { + if let ItemDetail::Tool(tool) = bank_item { + acc.push(BankItem::Stacked(StackedBankItem { + entity_ids: vec![id], + item_id: self.next_global_item_id(), + tool: tool, + })); + } + }, + } + } + else { + acc.push(BankItem::Individual(IndividualBankItem { + entity_id: id, + item_id: self.next_global_item_id(), + item: bank_item, + })); + } + + acc + }); + (bank_name, CharacterBank::new(stacked_bank_items)) + }) + .collect::>(); + let inventory = CharacterInventory::new(inventory_items.into_iter().map(|(_k, v)| v).take(30).collect()); + self.character_inventory.insert(character.id, inventory); + self.character_bank.insert(character.id, bank_items); + } + + pub fn add_character_to_room(&mut self, room_id: RoomId, character: &CharacterEntity, area_client: AreaClient) { + let base_inventory_id = ((area_client.local_client.id() as u32) << 21) | 0x10000; + let inventory = self.character_inventory.get_mut(&character.id).unwrap(); + inventory.initialize_item_ids(base_inventory_id); + let base_bank_id = ((area_client.local_client.id() as u32) << 21) | 0x20000; + let default_bank = self.character_bank.get_mut(&character.id).unwrap().get_mut(&BankName("".to_string())); + match default_bank { + Some(default_bank) => { + default_bank.initialize_item_ids(base_bank_id); + }, + None => {}, + } + self.character_room.insert(character.id, room_id); + self.character_floor.insert(character.id, RoomFloorItems::new()); + self.room_floor.entry(room_id).or_insert(RoomFloorItems::new()); + + let mut inc = 0xF0000000; + self.room_item_id_counter.entry(room_id).or_insert(Box::new(move || { + inc += 1; + ClientItemId(inc) + })); + } + + pub fn get_character_inventory(&self, character: &CharacterEntity) -> Result<&CharacterInventory, ItemManagerError> { + Ok(self.character_inventory.get(&character.id) + .ok_or(ItemManagerError::NoCharacter(character.id))?) + } + + pub fn get_character_bank(&self, character: &CharacterEntity) -> Result<&CharacterBank, ItemManagerError> { + Ok(self.character_bank + .get(&character.id) + .ok_or(ItemManagerError::NoCharacter(character.id))? + .get(&BankName("".to_string())) + .unwrap()) // TODO: make an error + } + + pub fn remove_character_from_room(&mut self, character: &CharacterEntity) { + self.character_inventory.remove(&character.id); + self.character_floor.remove(&character.id); + self.character_room.remove(&character.id) + .as_ref() + .map(|room| { + if self.character_room.iter().find(|(_, r)| *r == room).is_none() { + self.room_floor.remove(room); + } + }); + } + + pub fn get_floor_item_by_id(&self, character: &CharacterEntity, item_id: ClientItemId) -> Result<&FloorItem, ItemManagerError> { + let local_floor = self.character_floor.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; + let room = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; + let shared_floor = self.room_floor.get(room).ok_or(ItemManagerError::NoCharacter(character.id))?; + + local_floor.get_item_by_id(item_id) + .or_else(|| { + shared_floor.get_item_by_id(item_id) + }) + .ok_or(ItemManagerError::NoSuchItemId(item_id)) + } + + pub async fn character_picks_up_item(&mut self, entity_gateway: &mut EG, character: &mut CharacterEntity, item_id: ClientItemId) + -> Result { + let local_floor = self.character_floor.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; + let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; + let room_id = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; + let shared_floor = self.room_floor.get_mut(&room_id).ok_or(ItemManagerError::NoCharacter(character.id))?; + + let floor_item = local_floor.get_item_handle_by_id(item_id) + .or_else(|| { + shared_floor.get_item_handle_by_id(item_id) + }) + .ok_or(ItemManagerError::NoSuchItemId(item_id))?; + + let trigger_create_item = match floor_item.item() { + Some(FloorItem::Individual(individual_floor_item)) => { + let new_inventory_item = inventory.pick_up_individual_floor_item(&individual_floor_item); + match new_inventory_item { + Some((new_inventory_item, slot)) => { + entity_gateway.change_item_location( + &new_inventory_item.entity_id, + ItemLocation::Inventory { + character_id: character.id, + slot: slot.0, + equipped: false, + } + ).await; + }, + None => { + return Err(ItemManagerError::CouldNotAddToInventory(item_id)); + }, + } + TriggerCreateItem::Yes + }, + Some(FloorItem::Stacked(stacked_floor_item)) => { + let new_inventory_item = inventory.pick_up_stacked_floor_item(&stacked_floor_item); + println!("new inv item! {:?}", new_inventory_item); + + match new_inventory_item { + Some((new_inventory_item, slot)) => { + for entity_id in &new_inventory_item.entity_ids { + entity_gateway.change_item_location( + &entity_id, + ItemLocation::Inventory { + character_id: character.id, + slot: slot.0, + equipped: false, + } + ).await; + } + + if stacked_floor_item.count() != new_inventory_item.count() { + TriggerCreateItem::No + } + else { + TriggerCreateItem::Yes + } + }, + None => { + return Err(ItemManagerError::CouldNotAddToInventory(item_id)); + } + } + }, + Some(FloorItem::Meseta(meseta_floor_item)) => { + if character.meseta >= 999999 { + return Err(ItemManagerError::CouldNotAddToInventory(item_id)); + } + character.meseta = std::cmp::min(character.meseta + meseta_floor_item.meseta.0, 999999); + entity_gateway.save_character(&character).await; + TriggerCreateItem::No + }, + None => { + return Err(ItemManagerError::CouldNotAddToInventory(item_id)); + } + }; + + floor_item.remove_from_floor(); + Ok(trigger_create_item) + } + + pub async fn enemy_drop_item_on_local_floor(&mut self, entity_gateway: &mut EG, character: &CharacterEntity, item_drop: ItemDrop) -> Result<&FloorItem, ItemManagerError> { + let room_id = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; + + enum ItemOrMeseta { + Individual(ItemDetail), + Stacked(Tool), + Meseta(Meseta) + } + + let item = match item_drop.item { + ItemDropType::Weapon(w) => ItemOrMeseta::Individual(ItemDetail::Weapon(w)), + ItemDropType::Armor(w) => ItemOrMeseta::Individual(ItemDetail::Armor(w)), + ItemDropType::Shield(w) => ItemOrMeseta::Individual(ItemDetail::Shield(w)), + ItemDropType::Unit(w) => ItemOrMeseta::Individual(ItemDetail::Unit(w)), + ItemDropType::TechniqueDisk(w) => ItemOrMeseta::Individual(ItemDetail::TechniqueDisk(w)), + ItemDropType::Mag(w) => ItemOrMeseta::Individual(ItemDetail::Mag(w)), + //ItemDropType::IndividualTool(t) => ItemOrMeseta::Individual(ItemDetail::Tool(t)), + //ItemDropType::StackedTool(t, _) => ItemOrMeseta::Stacked(t), + ItemDropType::Tool(t) if t.tool.is_stackable() => ItemOrMeseta::Stacked(t), + ItemDropType::Tool(t) if !t.tool.is_stackable() => ItemOrMeseta::Individual(ItemDetail::Tool(t)), + ItemDropType::Meseta(m) => ItemOrMeseta::Meseta(Meseta(m)), + _ => unreachable!() // rust isnt smart enough to see that the conditional on tool catches everything + }; + + let item_id = self.room_item_id_counter.get_mut(room_id).ok_or(ItemManagerError::NoCharacter(character.id))?(); + let floor_item = match item { + ItemOrMeseta::Individual(item_detail) => { + let entity = entity_gateway.create_item(NewItemEntity { + item: item_detail.clone(), + location: ItemLocation::LocalFloor { + character_id: character.id, + map_area: item_drop.map_area, + x: item_drop.x, + y: item_drop.y, + z: item_drop.z, + } + }).await.ok_or(ItemManagerError::EntityGatewayError)?; + FloorItem::Individual(IndividualFloorItem { + entity_id: entity.id, + item_id: item_id, + item: item_detail, + map_area: item_drop.map_area, + x: item_drop.x, + y: item_drop.y, + z: item_drop.z, + }) + }, + ItemOrMeseta::Stacked(tool) => { + let entity = entity_gateway.create_item(NewItemEntity { + item: ItemDetail::Tool(tool), + location: ItemLocation::LocalFloor { + character_id: character.id, + map_area: item_drop.map_area, + x: item_drop.x, + y: item_drop.y, + z: item_drop.z, + } + }).await.ok_or(ItemManagerError::EntityGatewayError)?; + FloorItem::Stacked(StackedFloorItem { + entity_ids: vec![entity.id], + item_id: item_id, + tool: tool, + map_area: item_drop.map_area, + x: item_drop.x, + y: item_drop.y, + z: item_drop.z, + }) + }, + ItemOrMeseta::Meseta(meseta) => { + FloorItem::Meseta(MesetaFloorItem { + item_id: item_id, + meseta: meseta, + map_area: item_drop.map_area, + x: item_drop.x, + y: item_drop.y, + z: item_drop.z, + }) + }, + }; + + self.character_floor.entry(character.id).or_insert(RoomFloorItems::new()).add_item(floor_item); + // TODO: make these real errors + self.character_floor.get(&character.id).ok_or(ItemManagerError::Idunnoman)?.get_item_by_id(item_id).ok_or(ItemManagerError::Idunnoman) + } + + pub async fn player_drop_item_on_shared_floor(&mut self, + entity_gateway: &mut EG, + character: &CharacterEntity, + //inventory_item: InventoryItem, + item_id: ClientItemId, + item_drop_location: (MapArea, f32, f32, f32)) + -> Result<(), ItemManagerError> { + let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; + let room_id = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; + let shared_floor = self.room_floor.get_mut(&room_id).ok_or(ItemManagerError::NoCharacter(character.id))?; + + let dropped_inventory_item = inventory.take_item_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?; + + match dropped_inventory_item { + InventoryItem::Individual(individual_inventory_item) => { + let individual_floor_item = shared_floor.drop_individual_inventory_item(individual_inventory_item, item_drop_location); + entity_gateway.change_item_location( + &individual_floor_item.entity_id, + ItemLocation::SharedFloor { + map_area: item_drop_location.0, + x: item_drop_location.1, + y: item_drop_location.2, + z: item_drop_location.3, + } + ).await; + }, + InventoryItem::Stacked(stacked_inventory_item) => { + let stacked_floor_item = shared_floor.drop_stacked_inventory_item(stacked_inventory_item, item_drop_location); + for entity_id in &stacked_floor_item.entity_ids { + entity_gateway.change_item_location( + entity_id, + ItemLocation::SharedFloor { + map_area: item_drop_location.0, + x: item_drop_location.1, + y: item_drop_location.2, + z: item_drop_location.3, + } + ).await; + } + }, + } + + Ok(()) + } + + pub async fn player_drops_meseta_on_shared_floor(&mut self, + entity_gateway: &mut EG, + character: &mut CharacterEntity, + drop_location: ItemDropLocation, + amount: u32) + -> Result { + let room_id = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; + let shared_floor = self.room_floor.get_mut(&room_id).ok_or(ItemManagerError::NoCharacter(character.id))?; + if character.meseta <= amount { + return Err(ItemManagerError::CouldNotDropMeseta) + } + character.meseta -= amount; + entity_gateway.save_character(&character).await; + + let item_id = self.room_item_id_counter.get_mut(room_id).ok_or(ItemManagerError::NoCharacter(character.id))?(); + let floor_item = FloorItem::Meseta(MesetaFloorItem { + item_id: item_id, + meseta: Meseta(amount), + map_area: drop_location.map_area, + x: drop_location.x, + y: 0.0, + z: drop_location.z, + }); + + shared_floor.add_item(floor_item.clone()); + Ok(floor_item) + } + + pub async fn player_drops_partial_stack_on_shared_floor(&mut self, + entity_gateway: &mut EG, + character: &CharacterEntity, + //inventory_item: InventoryItem, + item_id: ClientItemId, + drop_location: ItemDropLocation, + amount: usize) + -> Result<&StackedFloorItem, ItemManagerError> { + let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; + let room_id = self.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; + let shared_floor = self.room_floor.get_mut(&room_id).ok_or(ItemManagerError::NoCharacter(character.id))?; + + let item_to_split = inventory.get_item_handle_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?; + + let new_item_id = self.room_item_id_counter.get_mut(room_id).ok_or(ItemManagerError::NoCharacter(character.id))?(); + let stacked_floor_item = shared_floor.drop_partial_stacked_inventory_item(item_to_split, amount, new_item_id, (drop_location.map_area, drop_location.x, 0.0, drop_location.z)) + .ok_or(ItemManagerError::CouldNotSplitItem(item_id))?; + + for entity_id in &stacked_floor_item.entity_ids { + entity_gateway.change_item_location( + entity_id, + ItemLocation::SharedFloor { + map_area: drop_location.map_area, + x: drop_location.x, + y: 0.0, + z: drop_location.z, + } + ).await; + } + + Ok(stacked_floor_item) + } + + pub async fn player_consumes_tool(&mut self, + entity_gateway: &mut EG, + character: &CharacterEntity, + item_id: ClientItemId, + amount: usize) + -> Result { + let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; + let used_item = inventory.get_item_handle_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?; + let consumed_item = used_item.consume(amount)?; + + for entity_id in consumed_item.entity_ids { + entity_gateway.change_item_location(&entity_id, + ItemLocation::Consumed).await; + } + + Ok(consumed_item.item) + } + + pub async fn player_deposits_item(&mut self, + entity_gateway: &mut EG, + character: &CharacterEntity, + item_id: ClientItemId, + amount: usize) + -> Result<(), ItemManagerError> { + Ok(()) + } + + pub async fn player_withdraws_item(&mut self, + entity_gateway: &mut EG, + character: &CharacterEntity, + item_id: ClientItemId, + amount: usize) + -> Result<(), ItemManagerError> { + Ok(()) + } +} diff --git a/src/ship/items/mod.rs b/src/ship/items/mod.rs new file mode 100644 index 0000000..00d1667 --- /dev/null +++ b/src/ship/items/mod.rs @@ -0,0 +1,16 @@ +mod bank; +mod floor; +mod inventory; +mod manager; + +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +pub struct ClientItemId(pub u32); + +//pub use inventory::InventoryItem; +//pub use floor::FloorItem; +pub use inventory::*; +pub use floor::*; +pub use bank::*; +pub use manager::*; + + diff --git a/src/ship/packet/builder/message.rs b/src/ship/packet/builder/message.rs index 83db7f8..33c1c89 100644 --- a/src/ship/packet/builder/message.rs +++ b/src/ship/packet/builder/message.rs @@ -1,7 +1,9 @@ +use libpso::character::character; use libpso::packet::messages::*; +use libpso::packet::ship::*; use crate::common::leveltable::CharacterStats; use crate::ship::ship::{ShipError}; -use crate::ship::items::{FloorItem}; +use crate::ship::items::{StackedFloorItem, FloorItem, CharacterBank}; use crate::ship::location::AreaClient; use std::convert::TryInto; @@ -48,7 +50,24 @@ pub fn remove_item_from_floor(area_client: AreaClient, item: &FloorItem) -> Resu }) } -pub fn drop_split_stack(area_client: AreaClient, item: &FloorItem) -> Result { +pub fn drop_split_stack(area_client: AreaClient, item: &StackedFloorItem) -> Result { + let item_bytes = item.as_client_bytes(); + Ok(DropSplitStack { + client: area_client.local_client.id(), + target: 0, + variety: 0, + unknown1: 0, + map_area: item.map_area.area_value(), + x: item.x, + z: item.z, + item_bytes: item_bytes[0..12].try_into()?, + item_id: item.item_id.0, + item_bytes2: item_bytes[12..16].try_into()?, + unknown2: 0, + }) +} + +pub fn drop_split_meseta_stack(area_client: AreaClient, item: &FloorItem) -> Result { let item_bytes = item.as_client_bytes(); Ok(DropSplitStack { client: area_client.local_client.id(), @@ -86,3 +105,17 @@ pub fn character_leveled_up(area_client: AreaClient, level: u32, before_stats: C lvl: level, } } + +// TOOD: meseta +pub fn bank_item_list(bank: &CharacterBank) -> BankItemList { + BankItemList { + aflag: 0, + cmd: 0xBC, + unknown: [0; 3], + size: bank.count() as u32 * 0x18 + 0x14, + checksum: 0x123434, + item_count: bank.count() as u32, + meseta: 12345, + items: bank.as_client_bank_request() + } +} diff --git a/src/ship/packet/handler/direct_message.rs b/src/ship/packet/handler/direct_message.rs index 30b4ed5..275097c 100644 --- a/src/ship/packet/handler/direct_message.rs +++ b/src/ship/packet/handler/direct_message.rs @@ -10,6 +10,10 @@ use crate::entity::gateway::EntityGateway; use libpso::utf8_to_utf16_array; use crate::ship::packet::builder; +const BANK_ACTION_DEPOSIT: u8 = 0; +const BANK_ACTION_WITHDRAW: u8 = 1; +//const BANK_ACTION_: u8 = 1; + fn send_to_client(id: ClientId, target: u8, msg: DirectMessage, client_location: &ClientLocation) -> Box + Send> { Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() @@ -148,6 +152,8 @@ where let area_client = client_location.get_local_client(id).map_err(|err| -> ClientLocationError { err.into() })?; let room_id = client_location.get_room(id).map_err(|err| -> ClientLocationError { err.into() })?; let clients_in_area = client_location.get_clients_in_room(room_id).map_err(|err| -> ClientLocationError { err.into() })?; + + // TODO: should not need to fetch the item here to construct this packet let item = item_manager.get_floor_item_by_id(&client.character, ClientItemId(pickup_item.item_id))?; let remove_item = builder::message::remove_item_from_floor(area_client, &item)?; let create_item = match item { @@ -155,7 +161,7 @@ where _ => Some(builder::message::create_item(area_client, &item)?), }; - match item_manager.character_picks_up_item(entity_gateway, &mut client.character, item).await { + match item_manager.character_picks_up_item(entity_gateway, &mut client.character, ClientItemId(pickup_item.item_id)).await { Ok(trigger_create_item) => { Ok(Box::new(Vec::new().into_iter() .chain(clients_in_area.clone().into_iter() @@ -263,3 +269,57 @@ EG: EntityGateway Ok(Box::new(item_drop_packets.into_iter())) } + + +// item_manager is not mutable in this, but for reasons I don't quite understand it requires the unique access of it to compile here +pub async fn send_bank_list(id: ClientId, + clients: &Clients, + item_manager: &mut ItemManager) + -> Result + Send>, ShipError> +{ + let client = clients.get(&id).ok_or(ShipError::ClientNotFound(id))?; + let bank_items = item_manager.get_character_bank(&client.character)?; + + let bank_items_pkt = builder::message::bank_item_list(&bank_items); + Ok(Box::new(vec![(id, SendShipPacket::BankItemList(bank_items_pkt))].into_iter())) +} + +pub async fn bank_interaction(id: ClientId, + bank_interaction: &BankInteraction, + entity_gateway: &mut EG, + clients: &mut Clients, + item_manager: &mut ItemManager) + -> Result + Send>, ShipError> +where + EG: EntityGateway +{ + let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; + match bank_interaction.action { + BANK_ACTION_DEPOSIT => { + if bank_interaction.item_id == 0xFFFFFFFF { + if client.character.meseta < bank_interaction.meseta_amount && client.character.bank_meseta <= 999999 { + client.character.meseta += bank_interaction.meseta_amount; + entity_gateway.save_character(&client.character).await; + } + } + else { + //let inventory_item = item_manager.get_inventory_item_by_id(&client.character, ClientItemId(bank_interaction.item_id))?; + item_manager.player_deposits_item(entity_gateway, &client.character, ClientItemId(bank_interaction.item_id), bank_interaction.item_amount as usize).await?; + } + }, + BANK_ACTION_WITHDRAW => { + if bank_interaction.item_id == 0xFFFFFFFF { + if client.character.meseta + bank_interaction.meseta_amount <= 999999 { + client.character.meseta += bank_interaction.meseta_amount; + entity_gateway.save_character(&client.character).await; + } + } + else { + //let bank_item = item_manager.get_bank_item_by_id(&client.character, ClientItemId(bank_interaction.item_id))?; + item_manager.player_withdraws_item(entity_gateway, &client.character, ClientItemId(bank_interaction.item_id), bank_interaction.item_amount as usize).await?; + } + }, + _ => {} + } + Ok(Box::new(None.into_iter())) +} diff --git a/src/ship/packet/handler/lobby.rs b/src/ship/packet/handler/lobby.rs index 5751f6a..7f2393c 100644 --- a/src/ship/packet/handler/lobby.rs +++ b/src/ship/packet/handler/lobby.rs @@ -22,12 +22,14 @@ pub fn block_selected(id: ClientId, let (level, stats) = level_table.get_stats_from_exp(client.character.char_class, client.character.exp); let inventory = item_manager.get_character_inventory(&client.character).unwrap(); + let bank = item_manager.get_character_bank(&client.character).unwrap(); let fc = FullCharacterBytesBuilder::new() .character(&client.character) .stats(&stats) .level(level) .inventory(&inventory) + .bank(&bank) .key_config(&client.settings.settings.key_config) .joystick_config(&client.settings.settings.joystick_config) .symbol_chat(&client.settings.settings.symbol_chats) diff --git a/src/ship/packet/handler/message.rs b/src/ship/packet/handler/message.rs index 011ebe1..7467783 100644 --- a/src/ship/packet/handler/message.rs +++ b/src/ship/packet/handler/message.rs @@ -83,8 +83,7 @@ where .as_mut() .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?; let area = MapArea::from_value(&room.mode.episode(), player_drop_item.area as u32)?; - let item = item_manager.get_inventory_item_by_id(&client.character, ClientItemId(player_drop_item.item_id))?; - item_manager.player_drop_item_on_shared_floor(entity_gateway, &client.character, item, (area, player_drop_item.x, player_drop_item.y, player_drop_item.z)).await?; + item_manager.player_drop_item_on_shared_floor(entity_gateway, &client.character, ClientItemId(player_drop_item.item_id), (area, player_drop_item.x, player_drop_item.y, player_drop_item.z)).await?; let clients_in_area = client_location.get_clients_in_room(room_id).map_err(|err| -> ClientLocationError { err.into() })?; let pdi = player_drop_item.clone(); Ok(Box::new(clients_in_area.into_iter() @@ -139,7 +138,7 @@ where if split_item_stack.item_id == 0xFFFFFFFF { let dropped_meseta = item_manager.player_drops_meseta_on_shared_floor(entity_gateway, &mut client.character, drop_location, split_item_stack.amount as u32).await?; - let dropped_meseta_pkt = builder::message::drop_split_stack(area_client, &dropped_meseta)?; + let dropped_meseta_pkt = builder::message::drop_split_meseta_stack(area_client, &dropped_meseta)?; client.item_drop_location = None; let clients_in_area = client_location.get_clients_in_room(room_id).map_err(|err| -> ClientLocationError { err.into() })?; @@ -149,10 +148,9 @@ where }))) } else { - let item_to_split = item_manager.get_inventory_item_by_id(&client.character, drop_location.item_id)?; - let dropped_item = item_manager.player_drops_partial_stack_on_shared_floor(entity_gateway, &client.character, item_to_split, drop_location, split_item_stack.amount as usize).await?; + let dropped_item = item_manager.player_drops_partial_stack_on_shared_floor(entity_gateway, &client.character, drop_location.item_id, drop_location, split_item_stack.amount as usize).await?; - let dropped_item_pkt = builder::message::drop_split_stack(area_client, &dropped_item)?; + let dropped_item_pkt = builder::message::drop_split_stack(area_client, dropped_item)?; client.item_drop_location = None; let clients_in_area = client_location.get_clients_in_room(room_id).map_err(|err| -> ClientLocationError { err.into() })?; @@ -225,8 +223,7 @@ where { let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; - let inventory_item_used = item_manager.get_inventory_item_by_id(&client.character, ClientItemId(player_use_tool.item_id))?; - let item_used_type = item_manager.player_consumes_tool(entity_gateway, &client.character, inventory_item_used, 1).await?; + let item_used_type = item_manager.player_consumes_tool(entity_gateway, &client.character, ClientItemId(player_use_tool.item_id), 1).await?; match item_used_type { ItemDetail::Weapon(_w) => { diff --git a/src/ship/ship.rs b/src/ship/ship.rs index 3094e3a..4fedf42 100644 --- a/src/ship/ship.rs +++ b/src/ship/ship.rs @@ -158,6 +158,7 @@ pub enum SendShipPacket { QuestHeader(QuestHeader), QuestChunk(QuestChunk), DoneLoadingQuest(DoneLoadingQuest), + BankItemList(BankItemList), } impl SendServerPacket for SendShipPacket { @@ -192,6 +193,7 @@ impl SendServerPacket for SendShipPacket { SendShipPacket::QuestHeader(pkt) => pkt.as_bytes(), SendShipPacket::QuestChunk(pkt) => pkt.as_bytes(), SendShipPacket::DoneLoadingQuest(pkt) => pkt.as_bytes(), + SendShipPacket::BankItemList(pkt) => pkt.as_bytes(), } } } @@ -320,7 +322,13 @@ impl ShipServerState { }, GameMessage::BoxDropRequest(box_drop_request) => { handler::direct_message::request_box_item(id, box_drop_request, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.rooms, &mut self.item_manager).await - } + }, + GameMessage::BankRequest(_bank_request) => { + handler::direct_message::send_bank_list(id, &self.clients, &mut self.item_manager).await + }, + GameMessage::BankInteraction(bank_interaction) => { + handler::direct_message::bank_interaction(id, bank_interaction, &mut self.entity_gateway, &mut self.clients, &mut self.item_manager).await + }, _ => { let cmsg = msg.clone(); Ok(Box::new(self.client_location.get_all_clients_by_client(id).unwrap().into_iter() diff --git a/tests/test_bank.rs b/tests/test_bank.rs new file mode 100644 index 0000000..5caa545 --- /dev/null +++ b/tests/test_bank.rs @@ -0,0 +1,220 @@ +use elseware::common::serverstate::{ClientId, ServerState}; +use elseware::entity::gateway::{EntityGateway, InMemoryGateway}; +use elseware::entity::item; +use elseware::ship::ship::{ShipServerState, RecvShipPacket, SendShipPacket}; +use elseware::ship::items::{ClientItemId}; + +use libpso::packet::ship::*; +use libpso::packet::messages::*; + +#[path = "common.rs"] +mod common; +use common::*; + +#[async_std::test] +async fn test_bank_items_sent_in_character_login() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, _char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Vulcan, + grind: 0, + special: None, + attrs: [None, None, None], + tekked: true, + } + ), + location: item::ItemLocation::Bank { + character_id: char1.id, + name: item::BankName("".to_string()) + } + }).await; + + let mut ship = ShipServerState::new(entity_gateway.clone()); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + + let packets = ship.handle(ClientId(1), &RecvShipPacket::MenuSelect(MenuSelect { + menu: BLOCK_MENU_ID, + item: 1, + })).await.unwrap().collect::>(); + + assert!(matches!(&packets[0], (_, SendShipPacket::FullCharacter(fc)) if fc.character.bank.items[0].data1[0..3] == [0x00, 0x08, 0x04] )); +} + +#[async_std::test] +async fn test_request_bank_items() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, _char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + for _ in 0..3 { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Vulcan, + grind: 0, + special: None, + attrs: [None, None, None], + tekked: true, + } + ), + location: item::ItemLocation::Bank { + character_id: char1.id, + name: item::BankName("".to_string()) + } + }).await; + } + + let mut ship = ShipServerState::new(entity_gateway.clone()); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + create_room(&mut ship, ClientId(1), "room", "").await; + + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().collect::>(); + + assert!(matches!(&packets[0], (_, SendShipPacket::BankItemList (bank_item_list)) + if bank_item_list.item_count == 3 + && bank_item_list.size == 0x18 * 3 + 0x14 + && bank_item_list.items[0].data1[0..3] == [0x00, 0x08, 0x04] + && bank_item_list.items[1].data1[0..3] == [0x00, 0x08, 0x04] + && bank_item_list.items[2].data1[0..3] == [0x00, 0x08, 0x04] + )); +} + + +#[async_std::test] +async fn test_request_stacked_bank_items() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, _char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + for _ in 0..3 { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool ( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ), + location: item::ItemLocation::Bank { + character_id: char1.id, + name: item::BankName("".to_string()) + } + }).await; + } + + let mut ship = ShipServerState::new(entity_gateway.clone()); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + create_room(&mut ship, ClientId(1), "room", "").await; + + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().collect::>(); + + assert!(matches!(&packets[0], (_, SendShipPacket::BankItemList (bank_item_list)) + if bank_item_list.item_count == 1 + && bank_item_list.size == 0x18 + 0x14 + && bank_item_list.items[0].data1[0..3] == [0x03, 0x00, 0x00] + && bank_item_list.items[0].amount == 3 + )); +} + + +#[async_std::test] +async fn test_request_bank_items_sorted() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, _char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Vulcan, + grind: 0, + special: None, + attrs: [None, None, None], + tekked: true, + } + ), + location: item::ItemLocation::Bank { + character_id: char1.id, + name: item::BankName("".to_string()) + } + }).await; + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool ( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ), + location: item::ItemLocation::Bank { + character_id: char1.id, + name: item::BankName("".to_string()) + } + }).await; + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Calibur, + grind: 0, + special: None, + attrs: [None, None, None], + tekked: true, + } + ), + location: item::ItemLocation::Bank { + character_id: char1.id, + name: item::BankName("".to_string()) + } + }).await; + + let mut ship = ShipServerState::new(entity_gateway.clone()); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + create_room(&mut ship, ClientId(1), "room", "").await; + + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().collect::>(); + + println!("{:?}", packets); + assert!(matches!(&packets[0], (_, SendShipPacket::BankItemList (bank_item_list)) + if bank_item_list.item_count == 3 + && bank_item_list.size == 0x18 * 3 + 0x14 + && bank_item_list.items[0].data1[0..3] == [0x00, 0x02, 0x04] + && bank_item_list.items[1].data1[0..3] == [0x00, 0x08, 0x04] + && bank_item_list.items[2].data1[0..3] == [0x03, 0x00, 0x00] + )); +} + + +//test_deposit_individual_item +//test_deposit_stacked_item +//test_deposit_stacked_item_with_stack_already_in_bank +//test_deposit_stacked_item_when_full_stack_in_bank +//test_deposit_individual_item_in_full_bank +//test_deposit_stacked_item_in_full_bank +//test_deposit_meseta +//test_deposit_too_much_meseta +//test_deposit_when_bank_has_max_meseta