diff --git a/src/ship/items/bank.rs b/src/ship/items/bank.rs index b130a79..aa98796 100644 --- a/src/ship/items/bank.rs +++ b/src/ship/items/bank.rs @@ -2,6 +2,9 @@ use crate::ship::items::ClientItemId; use libpso::character::character;//::InventoryItem; use crate::entity::item::{ItemEntityId, ItemDetail}; use crate::entity::item::tool::Tool; +use crate::ship::items::inventory::{InventoryItemHandle, InventoryItem}; + +const BANK_CAPACITY: usize = 200; #[derive(Debug, Clone)] pub struct IndividualBankItem { @@ -21,6 +24,15 @@ impl StackedBankItem { pub fn count(&self) -> usize { self.entity_ids.len() } + + pub fn take_entity_ids(&mut self, amount: usize) -> Option> { + if amount <= self.count() { + Some(self.entity_ids.drain(..amount).collect()) + } + else { + None + } + } } #[derive(Debug, Clone)] @@ -39,7 +51,7 @@ impl std::cmp::PartialEq for BankItem { let self_value = u32::from_be_bytes(self_bytes); let other_value = u32::from_be_bytes(other_bytes); - + self_value.eq(&other_value) } } @@ -58,7 +70,7 @@ impl std::cmp::PartialOrd for BankItem { let self_value = u32::from_be_bytes(self_bytes); let other_value = u32::from_be_bytes(other_bytes); - + self_value.partial_cmp(&other_value) } } @@ -75,7 +87,7 @@ impl std::cmp::Ord for BankItem { let self_value = u32::from_le_bytes(self_bytes); let other_value = u32::from_le_bytes(other_bytes); - + self_value.cmp(&other_value) } } @@ -123,22 +135,62 @@ impl BankItem { } } -pub struct CharacterBank(Vec); + +pub struct BankItemHandle<'a> { + bank: &'a mut CharacterBank, + index: usize +} + +impl<'a> BankItemHandle<'a> { + pub fn item(&'a self) -> Option<&'a BankItem> { + self.bank.items.get(self.index) + } + + pub fn item_mut(&mut self) -> Option<&mut BankItem> { + self.bank.items.get_mut(self.index) + } + + pub fn remove_from_bank(self) { + self.bank.items.remove(self.index); + } +} + +pub struct CharacterBank { + item_id_counter: u32, + items: Vec +} impl CharacterBank { pub fn new(mut items: Vec) -> CharacterBank { items.sort(); - CharacterBank(items) + CharacterBank { + item_id_counter: 0, + items: items, + } } pub fn initialize_item_ids(&mut self, base_item_id: u32) { - for (i, item) in self.0.iter_mut().enumerate() { + for (i, item) in self.items.iter_mut().enumerate() { item.set_item_id(ClientItemId(base_item_id + i as u32)); } + self.item_id_counter = base_item_id + self.items.len() as u32 + 1; + } + + pub fn get_item_handle_by_id<'a>(&'a mut self, item_id: ClientItemId) -> Option> { + let (index, _) = self.items.iter() + .enumerate() + .filter(|(_, item)| { + item.item_id() == item_id + }) + .nth(0)?; + Some(BankItemHandle { + bank: self, + index: index, + }) } - + pub fn as_client_bank_items(&self) -> character::Bank { - self.0.iter() + self.items.iter() .enumerate() .fold(character::Bank::default(), |mut bank, (slot, item)| { bank.item_count = (slot + 1) as u32; @@ -152,7 +204,7 @@ impl CharacterBank { } pub fn as_client_bank_request(&self) -> Vec { - self.0.iter() + self.items.iter() .map(|item| { let bytes = item.as_client_bytes(); let mut data1 = [0; 12]; @@ -179,7 +231,65 @@ impl CharacterBank { } pub fn count(&self) -> usize { - self.0.len() + self.items.len() + } + + pub fn deposit_item(&mut self, mut inventory_item: InventoryItemHandle, amount: usize) -> Option<&BankItem> { + let remove = match inventory_item.item_mut()? { + InventoryItem::Individual(individual_inventory_item) => { + if self.items.len() >= BANK_CAPACITY { + return None + } + self.items.push(BankItem::Individual(IndividualBankItem { + entity_id: individual_inventory_item.entity_id, + item_id: individual_inventory_item.item_id, + item: individual_inventory_item.item.clone(), + })); + true + }, + InventoryItem::Stacked(stacked_inventory_item) => { + let existing_bank_item = self.items.iter_mut() + .find_map(|item| { + if let BankItem::Stacked(stacked_bank_item) = item { + if stacked_bank_item.tool == stacked_inventory_item.tool { + return Some(stacked_bank_item) + } + } + None + }); + + match existing_bank_item { + Some(stacked_bank_item) => { + if stacked_bank_item.count() + stacked_inventory_item.count() > stacked_inventory_item.tool.max_stack() { + return None + } + + let mut deposited_entity_ids = stacked_inventory_item.take_entity_ids(amount)?; + stacked_bank_item.entity_ids.append(&mut deposited_entity_ids); + } + None => { + if self.items.len() >= BANK_CAPACITY { + return None + } + let deposited_entity_ids = stacked_inventory_item.take_entity_ids(amount)?; + + self.item_id_counter += 1; + self.items.push(BankItem::Stacked(StackedBankItem { + entity_ids: deposited_entity_ids, + item_id: ClientItemId(self.item_id_counter), + tool: stacked_inventory_item.tool, + })) + } + } + stacked_inventory_item.count() == 0 + } + }; + + if remove { + inventory_item.remove_from_inventory(); + } + + self.items.last() } } diff --git a/src/ship/items/floor.rs b/src/ship/items/floor.rs index 80bc582..4e1f3f2 100644 --- a/src/ship/items/floor.rs +++ b/src/ship/items/floor.rs @@ -228,9 +228,9 @@ impl RoomFloorItems { 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 { + if let ItemDetail::Tool(tool) = consumed_item.item() { self.0.push(FloorItem::Stacked(StackedFloorItem { - entity_ids: consumed_item.entity_ids, + entity_ids: consumed_item.entity_ids(), item_id: new_item_id, tool: tool, map_area: item_drop_location.0, diff --git a/src/ship/items/inventory.rs b/src/ship/items/inventory.rs index c831dcf..b0cd347 100644 --- a/src/ship/items/inventory.rs +++ b/src/ship/items/inventory.rs @@ -1,11 +1,13 @@ use std::cmp::Ordering; use thiserror::Error; use libpso::character::character;//::InventoryItem; -use crate::entity::item::{ItemEntityId, ItemDetail}; +use crate::entity::item::{ItemEntityId, ItemDetail, ItemType}; use crate::entity::item::tool::Tool; -use crate::ship::items::ClientItemId; +use crate::ship::items::{ClientItemId, BankItem, BankItemHandle}; use crate::ship::items::floor::{IndividualFloorItem, StackedFloorItem}; +const INVENTORY_CAPACITY: usize = 30; + #[derive(Debug, Clone)] pub struct InventorySlot(pub usize); @@ -29,6 +31,15 @@ impl StackedInventoryItem { pub fn count(&self) -> usize { self.entity_ids.len() } + + pub fn take_entity_ids(&mut self, amount: usize) -> Option> { + if amount <= self.count() { + Some(self.entity_ids.drain(..amount).collect()) + } + else { + None + } + } } #[derive(Debug, Clone)] @@ -68,6 +79,18 @@ impl InventoryItem { } } + pub fn item_type(&self) -> ItemType { + match self { + InventoryItem::Individual(individual_inventory_item) => { + individual_inventory_item.item.item_type() + }, + InventoryItem::Stacked(stacked_inventory_item) => { + ItemType::Tool(stacked_inventory_item.tool.tool) + } + } + } + + // TOOD: delete? pub fn are_same_stackable_tool(&self, other_stacked_item: &StackedFloorItem) -> bool { match self { InventoryItem::Stacked(self_stacked_item) => { @@ -78,6 +101,7 @@ impl InventoryItem { } } + // TOOD: delete? pub fn can_combine_stacks(&self, other_stacked_item: &StackedFloorItem) -> bool { match self { InventoryItem::Stacked(self_stacked_item) => { @@ -90,6 +114,7 @@ impl InventoryItem { } // TODO: result + // TOOD: delete? pub fn combine_stacks(&mut self, other_stacked_item: &mut StackedFloorItem) { match self { InventoryItem::Stacked(self_stacked_item) => { @@ -163,11 +188,46 @@ pub enum InventoryItemConsumeError { InvalidAmount, } -pub struct ConsumedItem { +pub struct IndividualConsumedItem { + pub entity_id: ItemEntityId, + pub item: ItemDetail, +} + +pub struct StackedConsumedItem { pub entity_ids: Vec, - pub item: ItemDetail + pub tool: Tool } +pub enum ConsumedItem { + Individual(IndividualConsumedItem), + Stacked(StackedConsumedItem), +} + +impl ConsumedItem { + pub fn entity_ids(&self) -> Vec { + match self { + ConsumedItem::Individual(individual_consumed_item) => { + vec![individual_consumed_item.entity_id] + }, + ConsumedItem::Stacked(stacked_consumed_item) => { + stacked_consumed_item.entity_ids.clone() + } + } + } + + pub fn item(&self) -> ItemDetail { + match self { + ConsumedItem::Individual(individual_consumed_item) => { + individual_consumed_item.item.clone() + }, + ConsumedItem::Stacked(stacked_consumed_item) => { + ItemDetail::Tool(stacked_consumed_item.tool) + } + } + } +} + + pub struct InventoryItemHandle<'a> { inventory: &'a mut CharacterInventory, slot: usize, @@ -175,37 +235,41 @@ pub struct InventoryItemHandle<'a> { impl<'a> InventoryItemHandle<'a> { pub fn item(&'a self) -> Option<&'a InventoryItem> { - self.inventory.0.get(self.slot) + self.inventory.items.get(self.slot) + } + + pub fn item_mut(&mut self) -> Option<&mut InventoryItem> { + self.inventory.items.get_mut(self.slot) } pub fn remove_from_inventory(self) { - self.inventory.0.remove(self.slot); + self.inventory.items.remove(self.slot); } pub fn consume(self, amount: usize) -> Result { enum RemoveMethod { EntireThing(ConsumedItem), - Partial(ItemDetail), + Partial(Tool), } - let inventory_item = self.inventory.0.get(self.slot).ok_or(InventoryItemConsumeError::InconsistentState)?; + let inventory_item = self.inventory.items.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], + RemoveMethod::EntireThing(ConsumedItem::Individual(IndividualConsumedItem { + entity_id: 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 { + RemoveMethod::EntireThing(ConsumedItem::Stacked(StackedConsumedItem { entity_ids: stacked_inventory_item.entity_ids.clone(), - item: ItemDetail::Tool(stacked_inventory_item.tool), - }) + tool: stacked_inventory_item.tool, + })) }, Ordering::Greater => { - RemoveMethod::Partial(ItemDetail::Tool(stacked_inventory_item.tool)) + RemoveMethod::Partial(stacked_inventory_item.tool) }, Ordering::Less => { return Err(InventoryItemConsumeError::InvalidAmount) @@ -216,11 +280,11 @@ impl<'a> InventoryItemHandle<'a> { match remove_method { RemoveMethod::EntireThing(consumed_item) => { - self.inventory.0.remove(self.slot); + self.inventory.items.remove(self.slot); Ok(consumed_item) }, - RemoveMethod::Partial(item_detail) => { - let entity_ids = self.inventory.0.get_mut(self.slot) + RemoveMethod::Partial(tool) => { + let entity_ids = self.inventory.items.get_mut(self.slot) .and_then(|item| { if let InventoryItem::Stacked(stacked_inventory_item) = item { Some(stacked_inventory_item.entity_ids.drain(..amount).collect::>()) @@ -230,10 +294,10 @@ impl<'a> InventoryItemHandle<'a> { } }) .ok_or(InventoryItemConsumeError::InvalidAmount)?; - Ok(ConsumedItem { + Ok(ConsumedItem::Stacked(StackedConsumedItem { entity_ids: entity_ids, - item: item_detail, - }) + tool: tool, + })) } } } @@ -243,21 +307,28 @@ impl<'a> InventoryItemHandle<'a> { #[derive(Debug)] -pub struct CharacterInventory(Vec); +pub struct CharacterInventory { + item_id_counter: u32, + items: Vec, +} impl CharacterInventory { pub fn new(items: Vec) -> CharacterInventory { - CharacterInventory(items) + CharacterInventory{ + item_id_counter: 0, + items: items, + } } pub fn initialize_item_ids(&mut self, base_item_id: u32) { - for (i, item) in self.0.iter_mut().enumerate() { + for (i, item) in self.items.iter_mut().enumerate() { item.set_item_id(ClientItemId(base_item_id + i as u32)); } + self.item_id_counter = base_item_id + self.items.len() as u32 + 1; } pub fn as_client_inventory_items(&self) -> [character::InventoryItem; 30] { - self.0.iter() + self.items.iter() .enumerate() .fold([character::InventoryItem::default(); 30], |mut inventory, (slot, item)| { let bytes = item.as_client_bytes(); @@ -273,15 +344,15 @@ impl CharacterInventory { } pub fn slot(&self, slot: usize) -> Option<&InventoryItem> { - self.0.get(slot) + self.items.get(slot) } pub fn count(&self) -> usize { - self.0.len() + self.items.len() } pub fn get_item_handle_by_id<'a>(&'a mut self, item_id: ClientItemId) -> Option> { - let (slot, _) = self.0.iter() + let (slot, _) = self.items.iter() .enumerate() .filter(|(_, item)| { item.item_id() == item_id @@ -294,7 +365,7 @@ impl CharacterInventory { } pub fn get_item_by_id(&self, item_id: ClientItemId) -> Option<&InventoryItem> { - self.0.iter() + self.items.iter() .filter(|item| { item.item_id() == item_id }) @@ -302,14 +373,14 @@ impl CharacterInventory { } pub fn take_item_by_id(&mut self, item_id: ClientItemId) -> Option { - self.0 + self.items .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); + self.items.push(item); Ok(()) } @@ -319,14 +390,14 @@ impl CharacterInventory { return None; } - self.0.push(InventoryItem::Individual(IndividualInventoryItem { + self.items.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() { + if let Some(InventoryItem::Individual(new_item)) = self.items.last() { Some((new_item, InventorySlot(self.count()))) } else { @@ -336,7 +407,7 @@ impl CharacterInventory { // 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() + let existing_stack_position = self.items.iter() .position(|inventory_item| { if let InventoryItem::Stacked(stacked_inventory_item) = inventory_item { if stacked_inventory_item.tool == floor_item.tool { @@ -347,7 +418,7 @@ impl CharacterInventory { }); if let Some(existing_stack_position) = existing_stack_position { - if let Some(InventoryItem::Stacked(stacked_item)) = self.0.get_mut(existing_stack_position) { + if let Some(InventoryItem::Stacked(stacked_item)) = self.items.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))) @@ -367,8 +438,8 @@ impl CharacterInventory { tool: floor_item.tool, }); - self.0.push(new_stacked_item); - if let Some(InventoryItem::Stacked(new_item)) = self.0.last() { + self.items.push(new_stacked_item); + if let Some(InventoryItem::Stacked(new_item)) = self.items.last() { Some((new_item, InventorySlot(self.count()))) } else { @@ -376,5 +447,69 @@ impl CharacterInventory { } } } + + pub fn withdraw_item(&mut self, mut bank_item: BankItemHandle, amount: usize) -> Option<(&InventoryItem, usize)> { + let (remove, slot) = match bank_item.item_mut()? { + BankItem::Individual(individual_bank_item) => { + if self.items.len() >= INVENTORY_CAPACITY { + return None + } + self.items.push(InventoryItem::Individual(IndividualInventoryItem { + entity_id: individual_bank_item.entity_id, + item_id: individual_bank_item.item_id, + item: individual_bank_item.item.clone(), + equipped: false, + })); + (true, self.count()) + }, + BankItem::Stacked(stacked_bank_item) => { + let existing_inventory_item = self.items.iter_mut() + .enumerate() + .find_map(|(index, item)| { + if let InventoryItem::Stacked(stacked_inventory_item) = item { + if stacked_inventory_item.tool == stacked_inventory_item.tool { + return Some((index, stacked_inventory_item)) + } + } + None + }); + + let slot = match existing_inventory_item { + Some((slot, stacked_inventory_item)) => { + if stacked_inventory_item.count() + stacked_bank_item.count() > stacked_bank_item.tool.max_stack() { + return None + } + + let mut withdrawn_entity_ids = stacked_bank_item.take_entity_ids(amount)?; + stacked_inventory_item.entity_ids.append(&mut withdrawn_entity_ids); + slot + } + None => { + if self.items.len() >= INVENTORY_CAPACITY { + return None + } + let withdrawn_entity_ids = stacked_bank_item.take_entity_ids(amount)?; + + self.item_id_counter += 1; // oh no + self.items.push(InventoryItem::Stacked(StackedInventoryItem { + entity_ids: withdrawn_entity_ids, + item_id: ClientItemId(self.item_id_counter), + tool: stacked_bank_item.tool, + })); + self.count() + } + }; + (stacked_bank_item.count() == 0, slot) + } + }; + + if remove { + bank_item.remove_from_bank(); + } + + self.items.last().map(|item| { + (item, slot) + }) + } } diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs index e586e69..69cd03b 100644 --- a/src/ship/items/manager.rs +++ b/src/ship/items/manager.rs @@ -32,15 +32,18 @@ pub enum ItemManagerError { Idunnoman, CouldNotSplitItem(ClientItemId), CouldNotDropMeseta, + InvalidBankName(BankName), NotEnoughTools(Tool, usize, usize), // have, expected InventoryItemConsumeError(#[from] InventoryItemConsumeError), + BankFull, } pub struct ItemManager { id_counter: u32, character_inventory: HashMap, - character_bank: HashMap>, + //character_bank: HashMap>, + character_bank: HashMap, character_floor: HashMap, character_room: HashMap, @@ -101,7 +104,8 @@ impl ItemManager { acc }); - let bank_items = items.into_iter() + // TODO: not using BankName anymore, clean this up + let mut bank_items = items.into_iter() .filter_map(|item| { match item.location { ItemLocation::Bank{name, ..} => Some((item.id, item.item, name)), @@ -158,7 +162,7 @@ impl ItemManager { .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); + self.character_bank.insert(character.id, bank_items.remove(&BankName("".to_string())).unwrap_or(CharacterBank::new(Vec::new()))); } pub fn add_character_to_room(&mut self, room_id: RoomId, character: &CharacterEntity, area_client: AreaClient) { @@ -166,7 +170,7 @@ impl ItemManager { 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())); + 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); @@ -192,11 +196,20 @@ impl ItemManager { 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 + .ok_or(ItemManagerError::NoCharacter(character.id))?) + //.get(&BankName("".to_string())) + //.ok_or(ItemManagerError::InvalidBankName(BankName("".to_string())))?) } + /*pub fn get_character_bank_mut(&mut self, character: &CharacterEntity) -> Result<&CharacterBank, ItemManagerError> { + Ok(self.character_bank + .get_mut(&character.id) + .ok_or(ItemManagerError::NoCharacter(character.id))? + .entry(BankName("".to_string())) + .or_insert(CharacterBank::new(Vec::new()))) + //.ok_or(ItemManagerError::InvalidBankName(BankName("".to_string())))?) + }*/ + pub fn remove_character_from_room(&mut self, character: &CharacterEntity) { self.character_inventory.remove(&character.id); self.character_floor.remove(&character.id); @@ -256,7 +269,6 @@ impl ItemManager { }, 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)) => { @@ -501,29 +513,86 @@ impl ItemManager { 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 { + for entity_id in consumed_item.entity_ids() { entity_gateway.change_item_location(&entity_id, ItemLocation::Consumed).await; } - Ok(consumed_item.item) + Ok(consumed_item.item()) } pub async fn player_deposits_item(&mut self, - _entity_gateway: &mut EG, - _character: &CharacterEntity, - _item_id: ClientItemId, - _amount: usize) + entity_gateway: &mut EG, + character: &CharacterEntity, + item_id: ClientItemId, + amount: usize) -> Result<(), ItemManagerError> { + let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; + let bank = self.character_bank + .get_mut(&character.id) + .ok_or(ItemManagerError::NoCharacter(character.id))?; + + let item_to_deposit = inventory.get_item_handle_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?; + let bank_item = bank.deposit_item(item_to_deposit, amount).ok_or(ItemManagerError::Idunnoman)?; + + match bank_item { + BankItem::Individual(individual_bank_item) => { + entity_gateway.change_item_location(&individual_bank_item.entity_id, + ItemLocation::Bank { + character_id: character.id, + name: BankName("".to_string()) + }).await; + }, + BankItem::Stacked(stacked_bank_item) => { + for entity_id in &stacked_bank_item.entity_ids { + entity_gateway.change_item_location(entity_id, + ItemLocation::Bank { + character_id: character.id, + name: BankName("".to_string()) + }).await; + } + } + } + Ok(()) } pub async fn player_withdraws_item(&mut self, - _entity_gateway: &mut EG, - _character: &CharacterEntity, - _item_id: ClientItemId, - _amount: usize) - -> Result<(), ItemManagerError> { - Ok(()) + entity_gateway: &mut EG, + character: &CharacterEntity, + item_id: ClientItemId, + amount: usize) + -> Result<&InventoryItem, ItemManagerError> { + + let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; + let bank = self.character_bank + .get_mut(&character.id) + .ok_or(ItemManagerError::NoCharacter(character.id))?; + + let item_to_withdraw = bank.get_item_handle_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?; + let inventory_item = inventory.withdraw_item(item_to_withdraw, amount).ok_or(ItemManagerError::Idunnoman)?; + + match inventory_item { + (InventoryItem::Individual(individual_inventory_item), slot) => { + entity_gateway.change_item_location(&individual_inventory_item.entity_id, + ItemLocation::Inventory { + character_id: character.id, + slot: slot, + equipped: false, + }).await; + }, + (InventoryItem::Stacked(stacked_inventory_item), slot) => { + for entity_id in &stacked_inventory_item.entity_ids { + entity_gateway.change_item_location(entity_id, + ItemLocation::Inventory { + character_id: character.id, + slot: slot, + equipped: false, + }).await; + } + } + } + + Ok(inventory_item.0) } } diff --git a/src/ship/packet/builder/message.rs b/src/ship/packet/builder/message.rs index 331b833..69ad26c 100644 --- a/src/ship/packet/builder/message.rs +++ b/src/ship/packet/builder/message.rs @@ -2,7 +2,7 @@ use libpso::packet::messages::*; use libpso::packet::ship::*; use crate::common::leveltable::CharacterStats; use crate::ship::ship::{ShipError}; -use crate::ship::items::{StackedFloorItem, FloorItem, CharacterBank}; +use crate::ship::items::{ClientItemId, InventoryItem, StackedFloorItem, FloorItem, CharacterBank}; use crate::ship::location::AreaClient; use std::convert::TryInto; @@ -37,6 +37,18 @@ pub fn create_item(area_client: AreaClient, item: &FloorItem) -> Result Result { + let bytes = item.as_client_bytes(); + Ok(CreateItem { + client: area_client.local_client.id(), + target: 0, + item_data: bytes[0..12].try_into()?, + item_id: item.item_id().0, + item_data2: bytes[12..16].try_into()?, + unknown: 0, + }) +} + pub fn remove_item_from_floor(area_client: AreaClient, item: &FloorItem) -> Result { Ok(RemoveItemFromFloor { client: area_client.local_client.id(), @@ -118,3 +130,12 @@ pub fn bank_item_list(bank: &CharacterBank) -> BankItemList { items: bank.as_client_bank_request() } } + +pub fn player_no_longer_has_item(area_client: AreaClient, item_id: ClientItemId, amount: u32) -> PlayerNoLongerHasItem { + PlayerNoLongerHasItem { + client: area_client.local_client.id(), + target: 0, + item_id: item_id.0, + amount: amount, + } +} diff --git a/src/ship/packet/handler/direct_message.rs b/src/ship/packet/handler/direct_message.rs index 275097c..18edad8 100644 --- a/src/ship/packet/handler/direct_message.rs +++ b/src/ship/packet/handler/direct_message.rs @@ -72,45 +72,6 @@ where let clients_in_area = client_location.get_clients_in_room(room_id).map_err(|err| -> ClientLocationError { err.into() })?; - /* - let item_drop_packets = clients_in_area.into_iter() - .filter_map(|area_client| { - room.drop_table.get_drop(&monster.map_area, &monster.monster).map(|item_drop_type| { - warn!("drop is? {:?}", item_drop_type); - (area_client, item_drop_type) - }) - }) - .map(|(area_client, item_drop_type)| async { - let item_drop = ItemDrop { - map_area: monster.map_area, - x: request_item.x, - y: request_item.y, - z: request_item.z, - item: item_drop_type, - }; - let client = clients.get_mut(&area_client.client).ok_or(ShipError::ClientNotFound(area_client.client))?; - let floor_item = item_manager.enemy_drop_item_on_local_floor(entity_gateway, &client.character, item_drop).await.unwrap(); // TODO: unwrap - let item_drop_msg = builder::message::item_drop(request_item.client, request_item.target, &floor_item)?; - - // I am not able to manually specify a closure return type when also using the async keyword - let result: Result<(ClientId, SendShipPacket), ShipError> = Ok((area_client.client, SendShipPacket::Message(Message::new(GameMessage::ItemDrop(item_drop_msg))))); - result - }) - .map(|item_drop_pkt| async { - item_drop_pkt.await - }); - - let item_drop_packets = join_all(item_drop_packets).await.into_iter() - .filter_map(|item_drop_pkt| { - // TODO: log errors here - item_drop_pkt.ok() - }); - - //.collect::>(); // TODO: can EntityGateway be Sync? - - Ok(Box::new(item_drop_packets)) - */ - let client_and_drop = clients_in_area.into_iter() .filter_map(|area_client| { room.drop_table.get_drop(&monster.map_area, &monster.monster).map(|item_drop_type| { @@ -209,41 +170,6 @@ EG: EntityGateway let clients_in_area = client_location.get_clients_in_room(room_id).map_err(|err| -> ClientLocationError { err.into() })?; - /*let item_drop_packets = clients_in_area.into_iter() - .filter_map(|area_client| { - room.drop_table.get_box_drop(&box_object.map, &box_object).map(|item_drop_type| { - warn!("drop is? {:?}", item_drop_type); - (area_client, item_drop_type) - }) - }) - .map(async move |(area_client, item_drop_type)| -> Result<_, ShipError> { - let item_drop = ItemDrop { - map_area: box_object.map, - x: box_drop_request.x, - y: 0.0, - z: box_drop_request.z, - item: item_drop_type, - }; - let client = clients.get_mut(&area_client.client).ok_or(ShipError::ClientNotFound(area_client.client))?; - let floor_item = item_manager.enemy_drop_item_on_local_floor(entity_gateway, &client.character, item_drop).await.unwrap(); // TODO: unwrap - let item_drop_msg = builder::message::item_drop(box_drop_request.client, box_drop_request.target, &floor_item)?; - Ok((area_client.client, SendShipPacket::Message(Message::new(GameMessage::ItemDrop(item_drop_msg))))) - }) - /*.filter_map(|item_drop_pkt| { - // TODO: log errors here - item_drop_pkt.ok() - }) - .collect::>(); // TODO: can EntityGateway be Sync?*/ - ; - let item_drop_packets = join_all(item_drop_packets).await.into_iter() - .filter_map(|item_drop_pkt| { - // TODO: log errors here - item_drop_pkt.ok() - }); - - Ok(Box::new(item_drop_packets)) - */ - let client_and_drop = clients_in_area.into_iter() .filter_map(|area_client| { room.drop_table.get_box_drop(&box_object.map, &box_object).map(|item_drop_type| { @@ -287,6 +213,7 @@ pub async fn send_bank_list(id: ClientId, pub async fn bank_interaction(id: ClientId, bank_interaction: &BankInteraction, entity_gateway: &mut EG, + client_location: &ClientLocation, clients: &mut Clients, item_manager: &mut ItemManager) -> Result + Send>, ShipError> @@ -294,32 +221,51 @@ where EG: EntityGateway { let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; - match bank_interaction.action { + let area_client = client_location.get_local_client(id).map_err(|err| -> ClientLocationError { err.into() })?; + let other_clients_in_area = client_location.get_client_neighbors(id).map_err(|err| -> ClientLocationError { err.into() })?; + let bank_action_pkts = 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; + if client.character.meseta > bank_interaction.meseta_amount && (bank_interaction.meseta_amount + client.character.bank_meseta) <= 999999 { + client.character.meseta -= bank_interaction.meseta_amount; + client.character.bank_meseta += bank_interaction.meseta_amount; entity_gateway.save_character(&client.character).await; } + Vec::new() } 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?; + let player_no_longer_has_item = builder::message::player_no_longer_has_item(area_client, ClientItemId(bank_interaction.item_id), bank_interaction.item_amount as u32); + vec![SendShipPacket::Message(Message::new(GameMessage::PlayerNoLongerHasItem(player_no_longer_has_item)))] } }, 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; + client.character.bank_meseta -= bank_interaction.meseta_amount; entity_gateway.save_character(&client.character).await; } + Vec::new() } 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?; + let item_added_to_inventory = item_manager.player_withdraws_item(entity_gateway, &client.character, ClientItemId(bank_interaction.item_id), bank_interaction.item_amount as usize).await?; + let item_created = builder::message::create_withdrawn_inventory_item(area_client, &item_added_to_inventory)?; + vec![SendShipPacket::Message(Message::new(GameMessage::CreateItem(item_created)))] } }, - _ => {} - } - Ok(Box::new(None.into_iter())) + _ => { + Vec::new() + } + }; + + Ok(Box::new(other_clients_in_area.into_iter() + .map(move |c| { + bank_action_pkts.clone().into_iter() + .map(move |pkt| { + (c.client, pkt) + }) + }) + .flatten() + )) } diff --git a/src/ship/packet/handler/message.rs b/src/ship/packet/handler/message.rs index 5b2622f..edcc249 100644 --- a/src/ship/packet/handler/message.rs +++ b/src/ship/packet/handler/message.rs @@ -117,7 +117,7 @@ pub fn drop_coordinates(id: ClientId, } pub async fn split_item_stack(id: ClientId, - split_item_stack: &PlayerSplitItemStack, + no_longer_has_item: &PlayerNoLongerHasItem, entity_gateway: &mut EG, client_location: &ClientLocation, clients: &mut Clients, @@ -131,12 +131,12 @@ where let room_id = client_location.get_room(id).map_err(|err| -> ClientLocationError { err.into() })?; let drop_location = client.item_drop_location.ok_or(ShipError::ItemDropLocationNotSet)?; - if drop_location.item_id.0 != split_item_stack.item_id { - return Err(ShipError::DropInvalidItemId(split_item_stack.item_id)); + if drop_location.item_id.0 != no_longer_has_item.item_id { + return Err(ShipError::DropInvalidItemId(no_longer_has_item.item_id)); } - 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?; + if no_longer_has_item.item_id == 0xFFFFFFFF { + let dropped_meseta = item_manager.player_drops_meseta_on_shared_floor(entity_gateway, &mut client.character, drop_location, no_longer_has_item.amount as u32).await?; let dropped_meseta_pkt = builder::message::drop_split_meseta_stack(area_client, &dropped_meseta)?; client.item_drop_location = None; @@ -148,7 +148,7 @@ where }))) } else { - 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 = item_manager.player_drops_partial_stack_on_shared_floor(entity_gateway, &client.character, drop_location.item_id, drop_location, no_longer_has_item.amount as usize).await?; let dropped_item_pkt = builder::message::drop_split_stack(area_client, dropped_item)?; client.item_drop_location = None; diff --git a/src/ship/ship.rs b/src/ship/ship.rs index 571a440..69c0870 100644 --- a/src/ship/ship.rs +++ b/src/ship/ship.rs @@ -283,8 +283,8 @@ impl ShipServerState { GameMessage::DropCoordinates(drop_coordinates) => { handler::message::drop_coordinates(id, drop_coordinates, &self.client_location, &mut self.clients, &self.rooms) }, - GameMessage::PlayerSplitItemStack(split_item_stack) => { - handler::message::split_item_stack(id, split_item_stack, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.item_manager).await + GameMessage::PlayerNoLongerHasItem(no_longer_has_item) => { + handler::message::split_item_stack(id, no_longer_has_item, &mut self.entity_gateway, &mut self.client_location, &mut self.clients, &mut self.item_manager).await }, GameMessage::PlayerChangedMap(_) | GameMessage::PlayerChangedMap2(_) | GameMessage::TellOtherPlayerMyLocation(_) | GameMessage::PlayerWarpingToFloor(_) | GameMessage::PlayerTeleported(_) | GameMessage::PlayerStopped(_) | @@ -327,7 +327,7 @@ impl ShipServerState { 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 + handler::direct_message::bank_interaction(id, bank_interaction, &mut self.entity_gateway, &self.client_location, &mut self.clients, &mut self.item_manager).await }, _ => { let cmsg = msg.clone(); diff --git a/tests/test_bank.rs b/tests/test_bank.rs index c355ef2..eb93867 100644 --- a/tests/test_bank.rs +++ b/tests/test_bank.rs @@ -41,7 +41,7 @@ async fn test_bank_items_sent_in_character_login() { 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] )); } @@ -197,7 +197,6 @@ async fn test_request_bank_items_sorted() { 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 @@ -208,12 +207,1562 @@ async fn test_request_bank_items_sorted() { } -//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 +#[async_std::test] +async fn test_deposit_individual_item() { + 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::Saber, + grind: 0, + special: None, + attrs: [None, None, None], + tekked: true, + } + ), + location: item::ItemLocation::Inventory { + character_id: char1.id, + slot: 0, + equipped: false, + } + }).await; + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Handgun, + grind: 0, + special: None, + attrs: [None, None, None], + tekked: true, + } + ), + location: item::ItemLocation::Inventory { + character_id: char1.id, + slot: 1, + equipped: false, + } + }).await; + + let mut ship = ShipServerState::new(entity_gateway.clone()); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + log_in_char(&mut ship, ClientId(2), "a2", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + join_lobby(&mut ship, ClientId(2)).await; + create_room(&mut ship, ClientId(1), "room", "").await; + join_room(&mut ship, ClientId(2), 0).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankInteraction(BankInteraction { + client: 0, + target: 0, + item_id: 0x10001, + action: 0, + item_amount: 0, + meseta_amount: 0, + unknown: 0, + })))).await.unwrap().collect::>(); + + assert!(packets.len() == 1); + assert!(matches!(&packets[0], (ClientId(2), SendShipPacket::Message(Message {msg: GameMessage::PlayerNoLongerHasItem(player_no_longer_has_item)})) + if player_no_longer_has_item.item_id == 0x10001 + && player_no_longer_has_item.amount == 0 + )); + + let items = entity_gateway.get_items_by_character(&char1).await; + let bank_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Bank {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(bank_item_ids == vec![item::ItemEntityId(2)]); +} + +#[async_std::test] +async fn test_deposit_stacked_item() { + 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::Inventory { + character_id: char1.id, + slot: 0, + equipped: false, + } + }).await; + } + + let mut ship = ShipServerState::new(entity_gateway.clone()); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + log_in_char(&mut ship, ClientId(2), "a2", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + join_lobby(&mut ship, ClientId(2)).await; + create_room(&mut ship, ClientId(1), "room", "").await; + join_room(&mut ship, ClientId(2), 0).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankInteraction(BankInteraction { + client: 0, + target: 0, + item_id: 0x10000, + action: 0, + item_amount: 3, + meseta_amount: 0, + unknown: 0, + })))).await.unwrap().collect::>(); + + assert!(packets.len() == 1); + assert!(matches!(&packets[0], (ClientId(2), SendShipPacket::Message(Message {msg: GameMessage::PlayerNoLongerHasItem(player_no_longer_has_item)})) + if player_no_longer_has_item.item_id == 0x10000 + && player_no_longer_has_item.amount == 3 + )); + + let items = entity_gateway.get_items_by_character(&char1).await; + let bank_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Bank {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(bank_item_ids == vec![item::ItemEntityId(1), item::ItemEntityId(2), item::ItemEntityId(3)]); +} + +#[async_std::test] +async fn test_deposit_partial_stacked_item() { + 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::Inventory { + character_id: char1.id, + slot: 0, + equipped: false, + } + }).await; + } + + let mut ship = ShipServerState::new(entity_gateway.clone()); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + log_in_char(&mut ship, ClientId(2), "a2", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + join_lobby(&mut ship, ClientId(2)).await; + create_room(&mut ship, ClientId(1), "room", "").await; + join_room(&mut ship, ClientId(2), 0).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankInteraction(BankInteraction { + client: 0, + target: 0, + item_id: 0x10000, + action: 0, + item_amount: 2, + meseta_amount: 0, + unknown: 0, + })))).await.unwrap().collect::>(); + + assert!(packets.len() == 1); + assert!(matches!(&packets[0], (ClientId(2), SendShipPacket::Message(Message {msg: GameMessage::PlayerNoLongerHasItem(player_no_longer_has_item)})) + if player_no_longer_has_item.item_id == 0x10000 + && player_no_longer_has_item.amount == 2 + )); + + let items = entity_gateway.get_items_by_character(&char1).await; + let bank_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Bank {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(bank_item_ids == vec![item::ItemEntityId(1), item::ItemEntityId(2)]); + + let inventory_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Inventory {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(inventory_item_ids == vec![item::ItemEntityId(3)]); +} + + +#[async_std::test] +async fn test_deposit_stacked_item_with_stack_already_in_bank() { + 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..2 { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ), + location: item::ItemLocation::Inventory { + character_id: char1.id, + slot: 0, + equipped: false, + } + }).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("".into()), + } + }).await; + } + + let mut ship = ShipServerState::new(entity_gateway.clone()); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + log_in_char(&mut ship, ClientId(2), "a2", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + join_lobby(&mut ship, ClientId(2)).await; + create_room(&mut ship, ClientId(1), "room", "").await; + join_room(&mut ship, ClientId(2), 0).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankInteraction(BankInteraction { + client: 0, + target: 0, + item_id: 0x10000, + action: 0, + item_amount: 2, + meseta_amount: 0, + unknown: 0, + })))).await.unwrap().collect::>(); + + assert!(packets.len() == 1); + assert!(matches!(&packets[0], (ClientId(2), SendShipPacket::Message(Message {msg: GameMessage::PlayerNoLongerHasItem(player_no_longer_has_item)})) + if player_no_longer_has_item.item_id == 0x10000 + && player_no_longer_has_item.amount == 2 + )); + + let items = entity_gateway.get_items_by_character(&char1).await; + let bank_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Bank {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(bank_item_ids == vec![item::ItemEntityId(1), item::ItemEntityId(2), item::ItemEntityId(3), item::ItemEntityId(4)]); +} + +#[async_std::test] +async fn test_deposit_stacked_item_with_full_stack_in_bank() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + + for _ in 0..2 { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ), + location: item::ItemLocation::Inventory { + character_id: char1.id, + slot: 0, + equipped: false, + } + }).await; + } + + for _ in 0..10 { + 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("".into()), + } + }).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; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankInteraction(BankInteraction { + client: 0, + target: 0, + item_id: 0x10000, + action: 0, + item_amount: 2, + meseta_amount: 0, + unknown: 0, + })))).await; + + assert!(packets.is_err()); + + let items = entity_gateway.get_items_by_character(&char1).await; + let bank_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Bank {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(bank_item_ids.len() == 10); + + let inventory_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Inventory {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(inventory_item_ids == vec![item::ItemEntityId(1), item::ItemEntityId(2)]); +} + +#[async_std::test] +async fn test_deposit_individual_item_in_full_bank() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "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::Inventory { + character_id: char1.id, + slot: 0, + equipped: false, + } + }).await; + + for _ in 0..200 { + 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; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankInteraction(BankInteraction { + client: 0, + target: 0, + item_id: 0x10000, + action: 0, + item_amount: 0, + meseta_amount: 0, + unknown: 0, + })))).await; + + assert!(packets.is_err()); + + let items = entity_gateway.get_items_by_character(&char1).await; + let bank_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Bank {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(bank_item_ids.len() == 200); + + let inventory_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Inventory {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(inventory_item_ids == vec![item::ItemEntityId(1)]); +} + +#[async_std::test] +async fn test_deposit_stacked_item_in_full_bank() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + + for _ in 0..2 { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ), + location: item::ItemLocation::Inventory { + character_id: char1.id, + slot: 0, + equipped: false, + } + }).await; + } + + for _ in 0..200 { + 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; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankInteraction(BankInteraction { + client: 0, + target: 0, + item_id: 0x10000, + action: 0, + item_amount: 2, + meseta_amount: 0, + unknown: 0, + })))).await; + + assert!(packets.is_err()); + + let items = entity_gateway.get_items_by_character(&char1).await; + let bank_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Bank {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(bank_item_ids.len() == 200); + + let inventory_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Inventory {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(inventory_item_ids == vec![item::ItemEntityId(1), item::ItemEntityId(2)]); +} + +#[async_std::test] +async fn test_deposit_stacked_item_in_full_bank_with_partial_stack() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + + for _ in 0..2 { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ), + location: item::ItemLocation::Inventory { + character_id: char1.id, + slot: 0, + equipped: false, + } + }).await; + } + + for _ in 0..199 { + 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; + } + + for _ in 0..2 { + 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; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankInteraction(BankInteraction { + client: 0, + target: 0, + item_id: 0x10000, + action: 0, + item_amount: 2, + meseta_amount: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + let items = entity_gateway.get_items_by_character(&char1).await; + let bank_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Bank {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(bank_item_ids.len() == 203); + + let inventory_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Inventory {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(inventory_item_ids.len() == 0); +} + +#[async_std::test] +async fn test_deposit_meseta() { + let mut entity_gateway = InMemoryGateway::new(); + + let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + char1.meseta = 300; + entity_gateway.save_character(&char1).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; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankInteraction(BankInteraction { + client: 0, + target: 0, + item_id: 0xFFFFFFFF, + action: 0, + item_amount: 0, + meseta_amount: 23, + unknown: 0, + })))).await.unwrap().for_each(drop); + + let characters = entity_gateway.get_characters_by_user(&user1).await; + let char = characters[0].as_ref().unwrap(); + assert!(char.meseta == 277); + assert!(char.bank_meseta == 23); +} + +#[async_std::test] +async fn test_deposit_too_much_meseta() { + let mut entity_gateway = InMemoryGateway::new(); + + let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + char1.meseta = 300; + char1.bank_meseta = 999980; + entity_gateway.save_character(&char1).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; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankInteraction(BankInteraction { + client: 0, + target: 0, + item_id: 0xFFFFFFFF, + action: 0, + item_amount: 0, + meseta_amount: 23, + unknown: 0, + })))).await.unwrap().for_each(drop); + + let characters = entity_gateway.get_characters_by_user(&user1).await; + let char = characters[0].as_ref().unwrap(); + assert!(char.meseta == 300); + assert!(char.bank_meseta == 999980); +} + + +#[async_std::test] +async fn test_deposit_meseta_when_bank_is_maxed() { + let mut entity_gateway = InMemoryGateway::new(); + + let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + char1.meseta = 300; + char1.bank_meseta = 999999; + entity_gateway.save_character(&char1).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; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankInteraction(BankInteraction { + client: 0, + target: 0, + item_id: 0xFFFFFFFF, + action: 0, + item_amount: 0, + meseta_amount: 23, + unknown: 0, + })))).await.unwrap().for_each(drop); + + let characters = entity_gateway.get_characters_by_user(&user1).await; + let char = characters[0].as_ref().unwrap(); + assert!(char.meseta == 300); + assert!(char.bank_meseta == 999999); +} + + +#[async_std::test] +async fn test_withdraw_individual_item() { + 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::Saber, + 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; + log_in_char(&mut ship, ClientId(2), "a2", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + join_lobby(&mut ship, ClientId(2)).await; + create_room(&mut ship, ClientId(1), "room", "").await; + join_room(&mut ship, ClientId(2), 0).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankInteraction(BankInteraction { + client: 0, + target: 0, + item_id: 0x20000, + action: 1, + item_amount: 0, + meseta_amount: 0, + unknown: 0, + })))).await.unwrap().collect::>(); + + assert!(packets.len() == 1); + assert!(matches!(&packets[0], (ClientId(2), SendShipPacket::Message(Message {msg: GameMessage::CreateItem(create_item)})) + if create_item.item_id == 0x20000 + )); + + let items = entity_gateway.get_items_by_character(&char1).await; + let inventory_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Inventory{..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(inventory_item_ids == vec![item::ItemEntityId(1)]); +} + +#[async_std::test] +async fn test_withdraw_stacked_item() { + 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; + log_in_char(&mut ship, ClientId(2), "a2", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + join_lobby(&mut ship, ClientId(2)).await; + create_room(&mut ship, ClientId(1), "room", "").await; + join_room(&mut ship, ClientId(2), 0).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankInteraction(BankInteraction { + client: 0, + target: 0, + item_id: 0x20000, + action: 1, + item_amount: 3, + meseta_amount: 0, + unknown: 0, + })))).await.unwrap().collect::>(); + + assert!(packets.len() == 1); + assert!(matches!(&packets[0], (ClientId(2), SendShipPacket::Message(Message {msg: GameMessage::CreateItem(create_item)})) + if create_item.item_id == 0x10002 + )); + + let items = entity_gateway.get_items_by_character(&char1).await; + let inventory_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Inventory {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(inventory_item_ids == vec![item::ItemEntityId(1), item::ItemEntityId(2), item::ItemEntityId(3)]); +} + +#[async_std::test] +async fn test_withdraw_partial_stacked_item() { + 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("".into()) + } + }).await; + } + + let mut ship = ShipServerState::new(entity_gateway.clone()); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + log_in_char(&mut ship, ClientId(2), "a2", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + join_lobby(&mut ship, ClientId(2)).await; + create_room(&mut ship, ClientId(1), "room", "").await; + join_room(&mut ship, ClientId(2), 0).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankInteraction(BankInteraction { + client: 0, + target: 0, + item_id: 0x20000, + action: 1, + item_amount: 2, + meseta_amount: 0, + unknown: 0, + })))).await.unwrap().collect::>(); + + assert!(packets.len() == 1); + assert!(matches!(&packets[0], (ClientId(2), SendShipPacket::Message(Message {msg: GameMessage::CreateItem(create_item)})) + if create_item.item_id == 0x10002 + )); + + let items = entity_gateway.get_items_by_character(&char1).await; + let bank_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Bank {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(bank_item_ids == vec![item::ItemEntityId(3)]); + + let inventory_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Inventory {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(inventory_item_ids == vec![item::ItemEntityId(1), item::ItemEntityId(2)]); +} + +#[async_std::test] +async fn test_withdraw_stacked_item_with_stack_already_in_inventory() { + 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..2 { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ), + location: item::ItemLocation::Inventory { + character_id: char1.id, + slot: 0, + equipped: false, + } + }).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("".into()), + } + }).await; + } + + let mut ship = ShipServerState::new(entity_gateway.clone()); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + log_in_char(&mut ship, ClientId(2), "a2", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + join_lobby(&mut ship, ClientId(2)).await; + create_room(&mut ship, ClientId(1), "room", "").await; + join_room(&mut ship, ClientId(2), 0).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankInteraction(BankInteraction { + client: 0, + target: 0, + item_id: 0x20000, + action: 1, + item_amount: 2, + meseta_amount: 0, + unknown: 0, + })))).await.unwrap().collect::>(); + + assert!(packets.len() == 1); + assert!(matches!(&packets[0], (ClientId(2), SendShipPacket::Message(Message {msg: GameMessage::CreateItem(create_item)})) + if create_item.item_id == 0x10000 + )); + + let items = entity_gateway.get_items_by_character(&char1).await; + let inventory_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Inventory {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(inventory_item_ids == vec![item::ItemEntityId(1), item::ItemEntityId(2), item::ItemEntityId(3), item::ItemEntityId(4)]); +} + +#[async_std::test] +async fn test_withdraw_stacked_item_with_full_stack_in_inventory() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + + for _ in 0..2 { + 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("".into()), + } + }).await; + } + + for _ in 0..10 { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ), + location: item::ItemLocation::Inventory { + character_id: char1.id, + slot: 0, + equipped: false, + } + }).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; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankInteraction(BankInteraction { + client: 0, + target: 0, + item_id: 0x20000, + action: 1, + item_amount: 2, + meseta_amount: 0, + unknown: 0, + })))).await; + + assert!(packets.is_err()); + + let items = entity_gateway.get_items_by_character(&char1).await; + let bank_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Bank {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(bank_item_ids == vec![item::ItemEntityId(1), item::ItemEntityId(2)]); + + let inventory_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Inventory {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(inventory_item_ids.len() == 10); +} + +#[async_std::test] +async fn test_withdraw_individual_item_in_full_inventory() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "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; + + for i in 0..30 { + 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::Inventory { + character_id: char1.id, + slot: i, + equipped: false, + } + }).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; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankInteraction(BankInteraction { + client: 0, + target: 0, + item_id: 0x20000, + action: 1, + item_amount: 0, + meseta_amount: 0, + unknown: 0, + })))).await; + + assert!(packets.is_err()); + + let items = entity_gateway.get_items_by_character(&char1).await; + let bank_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Bank {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(bank_item_ids == vec![item::ItemEntityId(1)]); + + let inventory_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Inventory {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(inventory_item_ids.len() == 30); +} + +#[async_std::test] +async fn test_withdraw_stacked_item_in_full_inventory() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + + for _ in 0..2 { + 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; + } + + for i in 0..30 { + 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::Inventory { + character_id: char1.id, + slot: i, + equipped: false, + } + }).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; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankInteraction(BankInteraction { + client: 0, + target: 0, + item_id: 0x20000, + action: 1, + item_amount: 2, + meseta_amount: 0, + unknown: 0, + })))).await; + + assert!(packets.is_err()); + + let items = entity_gateway.get_items_by_character(&char1).await; + let bank_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Bank {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(bank_item_ids == vec![item::ItemEntityId(1), item::ItemEntityId(2)]); + + let inventory_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Inventory {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(inventory_item_ids.len() == 30); +} + +#[async_std::test] +async fn test_withdraw_stacked_item_in_full_inventory_with_partial_stack() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + + for _ in 0..2 { + 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; + } + + for i in 0..29 { + 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::Inventory { + character_id: char1.id, + slot: i, + equipped: false, + } + }).await; + } + + for _ in 0..2 { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ), + location: item::ItemLocation::Inventory { + character_id: char1.id, + slot: 29, + equipped: false, + } + }).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; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankInteraction(BankInteraction { + client: 0, + target: 0, + item_id: 0x20000, + action: 1, + item_amount: 2, + meseta_amount: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + let items = entity_gateway.get_items_by_character(&char1).await; + let bank_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Bank {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(bank_item_ids.len() == 0); + + let inventory_item_ids = items.iter() + .filter_map(|item| { + if let item::ItemLocation::Inventory {..} = item.location { + Some(item.id) + } + else { + None + } + }) + .collect::>(); + assert!(inventory_item_ids.len() == 33); +} + +#[async_std::test] +async fn test_withdraw_meseta() { + let mut entity_gateway = InMemoryGateway::new(); + + let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + char1.bank_meseta = 300; + entity_gateway.save_character(&char1).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; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankInteraction(BankInteraction { + client: 0, + target: 0, + item_id: 0xFFFFFFFF, + action: 1, + item_amount: 0, + meseta_amount: 23, + unknown: 0, + })))).await.unwrap().for_each(drop); + + let characters = entity_gateway.get_characters_by_user(&user1).await; + let char = characters[0].as_ref().unwrap(); + assert!(char.meseta == 23); + assert!(char.bank_meseta == 277); +} + +#[async_std::test] +async fn test_withdraw_too_much_meseta() { + let mut entity_gateway = InMemoryGateway::new(); + + let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + char1.meseta = 999980; + char1.bank_meseta = 300; + entity_gateway.save_character(&char1).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; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankInteraction(BankInteraction { + client: 0, + target: 0, + item_id: 0xFFFFFFFF, + action: 1, + item_amount: 0, + meseta_amount: 23, + unknown: 0, + })))).await.unwrap().for_each(drop); + + let characters = entity_gateway.get_characters_by_user(&user1).await; + let char = characters[0].as_ref().unwrap(); + assert!(char.meseta == 999980); + assert!(char.bank_meseta == 300); +} + +#[async_std::test] +async fn test_withdraw_meseta_inventory_is_maxed() { + let mut entity_gateway = InMemoryGateway::new(); + + let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + char1.meseta = 999999; + char1.bank_meseta = 300; + entity_gateway.save_character(&char1).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; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankRequest(BankRequest { + client: 0, + target: 0, + unknown: 0, + })))).await.unwrap().for_each(drop); + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BankInteraction(BankInteraction { + client: 0, + target: 0, + item_id: 0xFFFFFFFF, + action: 1, + item_amount: 0, + meseta_amount: 23, + unknown: 0, + })))).await.unwrap().for_each(drop); + + let characters = entity_gateway.get_characters_by_user(&user1).await; + let char = characters[0].as_ref().unwrap(); + assert!(char.meseta == 999999); + assert!(char.bank_meseta == 300); +} diff --git a/tests/test_item_pickup.rs b/tests/test_item_pickup.rs index f582d25..97146f2 100644 --- a/tests/test_item_pickup.rs +++ b/tests/test_item_pickup.rs @@ -205,7 +205,7 @@ async fn test_pick_up_meseta_when_inventory_full() { z: 0.0, })))).await.unwrap().for_each(drop); - ship.handle(ClientId(2), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSplitItemStack(PlayerSplitItemStack { + ship.handle(ClientId(2), &RecvShipPacket::Message(Message::new(GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { client: 0, target: 0, item_id: 0xFFFFFFFF, @@ -445,7 +445,7 @@ async fn test_can_not_drop_more_meseta_than_is_held() { z: 0.0, })))).await.unwrap().for_each(drop); - let split_attempt = ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSplitItemStack(PlayerSplitItemStack { + let split_attempt = ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { client: 0, target: 0, item_id: 0xFFFFFFFF, @@ -565,7 +565,7 @@ async fn test_can_not_pick_up_meseta_when_full() { z: 0.0, })))).await.unwrap().for_each(drop); - ship.handle(ClientId(2), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSplitItemStack(PlayerSplitItemStack { + ship.handle(ClientId(2), &RecvShipPacket::Message(Message::new(GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { client: 0, target: 0, item_id: 0xFFFFFFFF, @@ -579,7 +579,6 @@ async fn test_can_not_pick_up_meseta_when_full() { map_area: 0, unknown: [0; 3] })))).await.unwrap().collect::>(); - println!("pkts {:?}", packets); assert!(packets.len() == 0); let characters1 = entity_gateway.get_characters_by_user(&user1).await; @@ -622,7 +621,7 @@ async fn test_meseta_caps_at_999999_when_trying_to_pick_up_more() { z: 0.0, })))).await.unwrap().for_each(drop); - ship.handle(ClientId(2), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSplitItemStack(PlayerSplitItemStack { + ship.handle(ClientId(2), &RecvShipPacket::Message(Message::new(GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { client: 0, target: 0, item_id: 0xFFFFFFFF, @@ -688,7 +687,7 @@ async fn test_player_drops_partial_stack_and_other_player_picks_it_up() { z: 0.0, })))).await.unwrap().for_each(drop); - ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerSplitItemStack(PlayerSplitItemStack { + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { client: 0, target: 0, item_id: 0x10000,