From 571bab25347150ed8719abcd0becf16fa0e7b46c Mon Sep 17 00:00:00 2001 From: jake Date: Sat, 12 Dec 2020 19:54:47 -0700 Subject: [PATCH 01/41] print send buffers --- src/common/mainloop/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/mainloop/client.rs b/src/common/mainloop/client.rs index 32562e9..d99f1c9 100644 --- a/src/common/mainloop/client.rs +++ b/src/common/mainloop/client.rs @@ -119,7 +119,7 @@ async fn send_pkt(socket: Arc Result<(), NetworkError> { let buf = pkt.as_bytes(); - //println!("sndbuf: {:?}", buf); + trace!("[send buf] {:?}", buf); let cbuf = cipher.lock().await.encrypt(&buf)?; let mut ssock = &*socket; ssock.write_all(&cbuf).await?; From 0d30df8da64b30e884b1ac1f444c067bcf80d972 Mon Sep 17 00:00:00 2001 From: jake Date: Sat, 12 Dec 2020 19:55:27 -0700 Subject: [PATCH 02/41] start of trading --- src/entity/gateway/postgres/models.rs | 65 +++++++-- src/entity/item/mod.rs | 5 + src/ship/items/manager.rs | 18 ++- src/ship/packet/builder/trade.rs | 0 src/ship/packet/handler/mod.rs | 1 + src/ship/packet/handler/trade.rs | 181 ++++++++++++++++++++++++++ src/ship/ship.rs | 31 ++++- 7 files changed, 288 insertions(+), 13 deletions(-) create mode 100644 src/ship/packet/builder/trade.rs create mode 100644 src/ship/packet/handler/trade.rs diff --git a/src/entity/gateway/postgres/models.rs b/src/entity/gateway/postgres/models.rs index 02bd750..917aca8 100644 --- a/src/entity/gateway/postgres/models.rs +++ b/src/entity/gateway/postgres/models.rs @@ -597,32 +597,75 @@ pub enum PgItemLocationDetail { mag: u32, }, Shop, + Trade { + id: i32, + character_to: i32, + character_from: i32, + } } impl From for PgItemLocationDetail { fn from(other: ItemLocation) -> PgItemLocationDetail { match other { - ItemLocation::Inventory{character_id} => PgItemLocationDetail::Inventory{character_id: character_id.0}, - ItemLocation::Bank{character_id, name} => PgItemLocationDetail::Bank{character_id: character_id.0, name: name.0}, - ItemLocation::LocalFloor{character_id, map_area, x,y,z} => PgItemLocationDetail::LocalFloor{character_id: character_id.0, map_area, x,y,z}, - ItemLocation::SharedFloor{map_area, x,y,z} => PgItemLocationDetail::SharedFloor{map_area, x,y,z}, + ItemLocation::Inventory{character_id} => PgItemLocationDetail::Inventory{ + character_id: character_id.0 + }, + ItemLocation::Bank{character_id, name} => PgItemLocationDetail::Bank{ + character_id: character_id.0, + name: name.0 + }, + ItemLocation::LocalFloor{character_id, map_area, x,y,z} => PgItemLocationDetail::LocalFloor{ + character_id: character_id.0, + map_area, + x,y,z + }, + ItemLocation::SharedFloor{map_area, x,y,z} => PgItemLocationDetail::SharedFloor{ + map_area, + x,y,z + }, ItemLocation::Consumed => PgItemLocationDetail::Consumed, - ItemLocation::FedToMag{mag} => PgItemLocationDetail::FedToMag{mag: mag.0}, + ItemLocation::FedToMag{mag} => PgItemLocationDetail::FedToMag{ + mag: mag.0 + }, ItemLocation::Shop => PgItemLocationDetail::Shop, + ItemLocation::Trade{id, character_to, character_from} => PgItemLocationDetail::Trade { + id: id.0 as i32, + character_to: character_to.0 as i32, + character_from: character_from.0 as i32, + } } } } impl From for ItemLocation { fn from(other: PgItemLocationDetail) -> ItemLocation { - match other{ - PgItemLocationDetail::Inventory{character_id} => ItemLocation::Inventory{character_id: CharacterEntityId(character_id)}, - PgItemLocationDetail::Bank{character_id, name} => ItemLocation::Bank{character_id: CharacterEntityId(character_id), name: BankName(name)}, - PgItemLocationDetail::LocalFloor{character_id, map_area, x,y,z} => ItemLocation::LocalFloor{character_id: CharacterEntityId(character_id), map_area, x,y,z}, - PgItemLocationDetail::SharedFloor{map_area, x,y,z} => ItemLocation::SharedFloor{map_area, x,y,z}, + match other { + PgItemLocationDetail::Inventory{character_id} => ItemLocation::Inventory{ + character_id: CharacterEntityId(character_id) + }, + PgItemLocationDetail::Bank{character_id, name} => ItemLocation::Bank{ + character_id: CharacterEntityId(character_id), + name: BankName(name) + }, + PgItemLocationDetail::LocalFloor{character_id, map_area, x,y,z} => ItemLocation::LocalFloor{ + character_id: CharacterEntityId(character_id), + map_area, + x,y,z + }, + PgItemLocationDetail::SharedFloor{map_area, x,y,z} => ItemLocation::SharedFloor{ + map_area, + x,y,z + }, PgItemLocationDetail::Consumed => ItemLocation::Consumed, - PgItemLocationDetail::FedToMag{mag} => ItemLocation::FedToMag{mag: ItemEntityId(mag)}, + PgItemLocationDetail::FedToMag{mag} => ItemLocation::FedToMag{ + mag: ItemEntityId(mag) + }, PgItemLocationDetail::Shop => ItemLocation::Shop, + PgItemLocationDetail::Trade {id, character_to, character_from} => ItemLocation::Trade { + id: TradeId(id as usize), + character_to: CharacterEntityId(character_to as u32), + character_from: CharacterEntityId(character_from as u32), + } } } } diff --git a/src/entity/item/mod.rs b/src/entity/item/mod.rs index 9959f54..b4260a3 100644 --- a/src/entity/item/mod.rs +++ b/src/entity/item/mod.rs @@ -47,6 +47,11 @@ pub enum ItemLocation { mag: ItemEntityId, }, Shop, + Trade { + //id: TradeId, + character_to: CharacterEntityId, + character_from: CharacterEntityId, + }, /*Destroyed { // marks an item that has been consumed in some way }, diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs index 441c85b..d3fdf0e 100644 --- a/src/ship/items/manager.rs +++ b/src/ship/items/manager.rs @@ -8,7 +8,7 @@ use crate::entity::item::{Meseta, NewItemEntity, ItemEntity, InventoryItemEntity use crate::entity::item::tool::{Tool, ToolType}; use crate::entity::item::weapon; use crate::ship::map::MapArea; -use crate::ship::ship::ItemDropLocation; +use crate::ship::ship::{TradeItem, ItemDropLocation}; use crate::ship::drops::{ItemDrop, ItemDropType}; use crate::ship::location::{AreaClient, RoomId}; use crate::ship::shops::ShopItem; @@ -180,6 +180,11 @@ impl ItemManager { .ok_or(ItemManagerError::NoCharacter(character.id))?) } + pub fn get_character_inventory_mut<'a>(&'a mut self, character: &CharacterEntity) -> Result<&'a mut CharacterInventory, anyhow::Error> { + Ok(self.character_inventory.get_mut(&character.id) + .ok_or(ItemManagerError::NoCharacter(character.id))?) + } + pub fn get_character_bank(&self, character: &CharacterEntity) -> Result<&CharacterBank, anyhow::Error> { Ok(self.character_bank .get(&character.id) @@ -919,4 +924,15 @@ impl ItemManager { Ok(weapon) } + + pub async fn send_items_to_other_player(&mut self, + entity_gateway: &mut EG, + source_character: &CharacterEntity, + dest_character: &CharacterEntity, + items: &Vec) + -> Result<(), anyhow::Error> { + + + + } } diff --git a/src/ship/packet/builder/trade.rs b/src/ship/packet/builder/trade.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/ship/packet/handler/mod.rs b/src/ship/packet/handler/mod.rs index 02761f8..c921d28 100644 --- a/src/ship/packet/handler/mod.rs +++ b/src/ship/packet/handler/mod.rs @@ -7,3 +7,4 @@ pub mod room; pub mod settings; pub mod quest; pub mod ship; +pub mod trade; diff --git a/src/ship/packet/handler/trade.rs b/src/ship/packet/handler/trade.rs new file mode 100644 index 0000000..8f3c04f --- /dev/null +++ b/src/ship/packet/handler/trade.rs @@ -0,0 +1,181 @@ +use std::convert::TryInto; +use log::warn; +use rand::Rng; +use rand::seq::SliceRandom; +use libpso::packet::ship::*; +use libpso::packet::messages::*; +use crate::common::leveltable::CharacterLevelTable; +use crate::common::serverstate::ClientId; +use crate::ship::ship::{SendShipPacket, ShipError, Clients, Rooms, ItemShops, TradeItem}; +use crate::ship::location::{ClientLocation, ClientLocationError}; +use crate::ship::drops::ItemDrop; +use crate::ship::items::{ItemManager, ItemManagerError, ClientItemId, TriggerCreateItem, FloorItem, FloorType}; +use crate::ship::items::inventory::InventoryItem; +use crate::entity::gateway::EntityGateway; +use crate::entity::item; +use libpso::utf8_to_utf16_array; +use crate::ship::packet::builder; +use crate::ship::shops::{ShopItem, ToolShopItem, ArmorShopItem}; + +#[derive(thiserror::Error, Debug)] +#[error("")] +pub enum TradeError { + CouldNotFindTradePartner, + ClientItemIdDidNotMatchItem(ClientItemId, [u8; 16]), + InvalidStackAmount(ClientItemId, usize), + NotInTradeMenu, +} + + + +pub async fn inner_items_to_trade(id: ClientId, + items_to_trade: &ItemsToTrade, + entity_gateway: &mut EG, + client_location: &ClientLocation, + clients: &mut Clients, + item_manager: &mut ItemManager) + -> Result + Send>, anyhow::Error> +where + EG: EntityGateway +{ + let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; + let inventory = item_manager.get_character_inventory_mut(&client.character)?; + let trade_partner = client_location.get_client_neighbors(id)?; + let trade_partner = trade_partner + .iter() + .filter(|ac| { + ac.local_client.id() == items_to_trade.trade_target + }) + .nth(0) + .ok_or(TradeError::CouldNotFindTradePartner)?; + + + let item_blobs = items_to_trade.items.iter().take(items_to_trade.count as usize); + let trade_items = item_blobs + .map(|item| { + // TOOD: meseta? + let real_item = inventory.get_item_handle_by_id(ClientItemId(item.item_id)) + .ok_or(ItemManagerError::NoSuchItemId(ClientItemId(item.item_id)))?; + let real_item = real_item + .item() + .ok_or(ItemManagerError::NoSuchItemId(ClientItemId(item.item_id)))?; + let trade_item_bytes: [u8; 16] = item.item_data.iter() + .chain(item.item_data2.iter()) + .cloned().collect::>() + .try_into() + .unwrap(); + if real_item.as_client_bytes() == trade_item_bytes { + match real_item { + InventoryItem::Individual(individual_inventory_item) => { + Ok(TradeItem::Individual(individual_inventory_item.item_id)) + }, + InventoryItem::Stacked(stacked_inventory_item) => { + let amount = trade_item_bytes[5] as usize; + if amount > stacked_inventory_item.entity_ids.len() { + Ok(TradeItem::Stacked(stacked_inventory_item.item_id, amount)) + } + else { + Err(TradeError::InvalidStackAmount(stacked_inventory_item.item_id, amount).into()) + } + } + } + } + else { + Err(TradeError::ClientItemIdDidNotMatchItem(ClientItemId(item.item_id), trade_item_bytes).into()) + } + }) + .collect::, anyhow::Error>>()?; + + // TODO: check room in inventory for items + client.trade = Some((trade_partner.client, trade_items)); + + Ok(Box::new(vec![(trade_partner.client, SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {}))].into_iter())) +} + +pub async fn items_to_trade(id: ClientId, + items_to_trade_pkt: &ItemsToTrade, + entity_gateway: &mut EG, + client_location: &ClientLocation, + clients: &mut Clients, + item_manager: &mut ItemManager) + -> Result + Send>, anyhow::Error> +where + EG: EntityGateway +{ + let t = inner_items_to_trade(id, items_to_trade_pkt, entity_gateway, client_location, clients, item_manager).await; + match t { + Ok(p) => Ok(p), + Err(err) => { + let client = clients.get(&id).ok_or(ShipError::ClientNotFound(id))?; + let trade_partner = client.trade.as_ref() + .and_then(|(trade_partner, _)| { + client_location.get_local_client(*trade_partner).ok() + }) + .map(|trade_partner| { + (trade_partner.client, SendShipPacket::CancelTrade(CancelTrade {})) + }); + + log::warn!("error in trading: {:?}", err); + Ok(Box::new(vec![(id, SendShipPacket::CancelTrade(CancelTrade {}))] + .into_iter() + .chain(trade_partner.into_iter()))) + } + } +} + +// this function is a shitshow due to not thinking of what would happen if I needed more than 1 client at a time +pub async fn trade_confirmed(id: ClientId, + entity_gateway: &mut EG, + client_location: &ClientLocation, + clients: &mut Clients, + item_manager: &mut ItemManager) + -> Result + Send>, anyhow::Error> +where + EG: EntityGateway +{ + let (this_client_confirmed, other_client_id) = { + let client = clients.get(&id).ok_or(ShipError::ClientNotFound(id))?; + (client.confirmed_trade, client.trade.as_ref().ok_or(TradeError::NotInTradeMenu)?.0) + }; + let other_client_confirmed = { + let client = clients.get(&other_client_id).ok_or(ShipError::ClientNotFound(id))?; + client.confirmed_trade + }; + + { + let this_client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; + this_client.confirmed_trade = true; + } + + let both_confirmed = { + let this_client = clients.get(&id).ok_or(ShipError::ClientNotFound(id))?; + let other_client = clients.get(&other_client_id).ok_or(ShipError::ClientNotFound(id))?; + this_client.confirmed_trade && other_client.confirmed_trade + }; + + if both_confirmed { + { + let this_client = clients.get(&id).ok_or(ShipError::ClientNotFound(id))?; + let other_client = clients.get(&other_client_id).ok_or(ShipError::ClientNotFound(id))?; + + let this_character_items = &this_client.trade.as_ref().ok_or(TradeError::NotInTradeMenu)?.1; + item_manager.send_items_to_other_player(entity_gateway, &this_client.character, &other_client.character, this_character_items).await?; + + let other_character_items = &other_client.trade.as_ref().ok_or(TradeError::NotInTradeMenu)?.1; + item_manager.send_items_to_other_player(entity_gateway, &other_client.character, &this_client.character, other_character_items).await?; + } + { + let this_client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; + this_client.trade = None; + } + { + let other_client = clients.get_mut(&other_client_id).ok_or(ShipError::ClientNotFound(id))?; + other_client.trade = None; + } + + Ok(Box::new(None.into_iter())) + } + else { + Ok(Box::new(None.into_iter())) + } +} diff --git a/src/ship/ship.rs b/src/ship/ship.rs index 8091647..1478143 100644 --- a/src/ship/ship.rs +++ b/src/ship/ship.rs @@ -106,6 +106,8 @@ pub enum RecvShipPacket { SaveOptions(SaveOptions), RequestShipList(RequestShipList), RequestShipBlockList(RequestShipBlockList), + ItemsToTrade(ItemsToTrade), + TradeConfirmed(TradeConfirmed), } impl RecvServerPacket for RecvShipPacket { @@ -143,6 +145,8 @@ impl RecvServerPacket for RecvShipPacket { 0xA1 => Ok(RecvShipPacket::RequestShipBlockList(RequestShipBlockList::from_bytes(data)?)), 0xA2 => Ok(RecvShipPacket::RequestQuestList(RequestQuestList::from_bytes(data)?)), 0xAC => Ok(RecvShipPacket::DoneLoadingQuest(DoneLoadingQuest::from_bytes(data)?)), + 0xD0 => Ok(RecvShipPacket::ItemsToTrade(ItemsToTrade::from_bytes(data)?)), + 0xD2 => Ok(RecvShipPacket::TradeConfirmed(TradeConfirmed::from_bytes(data)?)), 0xE7 => Ok(RecvShipPacket::FullCharacterData(Box::new(FullCharacterData::from_bytes(data)?))), 0x1ED => Ok(RecvShipPacket::SaveOptions(SaveOptions::from_bytes(data)?)), _ => Err(PacketParseError::WrongPacketForServerType(u16::from_le_bytes([data[2], data[3]]), data.to_vec())) @@ -184,6 +188,8 @@ pub enum SendShipPacket { DoneLoadingQuest(DoneLoadingQuest), BankItemList(BankItemList), RedirectClient(RedirectClient), + AcknowledgeTrade(AcknowledgeTrade), + CancelTrade(CancelTrade), } impl SendServerPacket for SendShipPacket { @@ -221,6 +227,8 @@ impl SendServerPacket for SendShipPacket { SendShipPacket::DoneLoadingQuest(pkt) => pkt.as_bytes(), SendShipPacket::BankItemList(pkt) => pkt.as_bytes(), SendShipPacket::RedirectClient(pkt) => pkt.as_bytes(), + SendShipPacket::AcknowledgeTrade(pkt) => pkt.as_bytes(), + SendShipPacket::CancelTrade(pkt) => pkt.as_bytes(), } } } @@ -239,6 +247,14 @@ pub struct LoadingQuest { //pub quest_chunk_bin: Option>>, } + +pub enum TradeItem { + Individual(items::ClientItemId), + Stacked(items::ClientItemId, usize), + Meseta(usize), +} + + pub struct ClientState { pub user: UserAccountEntity, pub settings: UserSettingsEntity, @@ -257,6 +273,8 @@ pub struct ClientState { pub tool_shop: Vec, pub armor_shop: Vec, pub tek: Option<(items::ClientItemId, item::weapon::TekSpecialModifier, item::weapon::TekPercentModifier, i32)>, + pub trade: Option<(ClientId, Vec)>, + pub confirmed_trade: bool, } impl ClientState { @@ -277,6 +295,8 @@ impl ClientState { tool_shop: Vec::new(), armor_shop: Vec::new(), tek: None, + trade: None, + confirmed_trade: false, } } } @@ -696,7 +716,16 @@ impl ServerState for ShipServerState { }, RecvShipPacket::RequestShipBlockList(_) => { handler::ship::block_list(id, &self.name, self.blocks.0.len()) - } + }, + RecvShipPacket::ItemsToTrade(items_to_trade) => { + log::warn!("trade! {:?} {:?}", id, items_to_trade); + let block = self.blocks.with_client(id, &self.clients)?; + handler::trade::items_to_trade(id, items_to_trade, &mut self.entity_gateway, &block.client_location, &mut self.clients, &mut self.item_manager).await? + }, + RecvShipPacket::TradeConfirmed(_) => { + let block = self.blocks.with_client(id, &self.clients)?; + handler::trade::trade_confirmed(id, &mut self.entity_gateway, &block.client_location, &mut self.clients, &mut self.item_manager).await? + }, }) } From 91d381a2e2a48722325e3d44df7e9606d965d06d Mon Sep 17 00:00:00 2001 From: jake Date: Mon, 27 Sep 2021 23:05:11 -0600 Subject: [PATCH 03/41] this fixed a thing probably --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 33acf8d..2feadfe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ enum-utils = "0.1.2" derive_more = { version = "0.99.3", features = ["display"]} thiserror = "1.0.15" ages-prs = "0.1" -async-trait = "0.1.41" +async-trait = "0.1.51" lazy_static = "1.4.0" barrel = { version = "0.6.5", features = ["pg"] } refinery = { version = "0.5.0", features = ["postgres"] } From 9b78d7ed805436c3a3e44811e4b19825e8240f24 Mon Sep 17 00:00:00 2001 From: jake Date: Mon, 27 Sep 2021 23:06:21 -0600 Subject: [PATCH 04/41] add some Defaults --- src/entity/account.rs | 22 ++++++++++++++++++++- src/entity/character.rs | 42 +++++++++++++++++++++++++---------------- src/lib.rs | 1 + 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/src/entity/account.rs b/src/entity/account.rs index 9223797..3958412 100644 --- a/src/entity/account.rs +++ b/src/entity/account.rs @@ -5,7 +5,7 @@ use libpso::character::guildcard; pub const USERFLAG_NEWCHAR: u32 = 0x00000001; pub const USERFLAG_DRESSINGROOM: u32 = 0x00000002; -#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)] pub struct UserAccountId(pub u32); #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct UserSettingsId(pub u32); @@ -59,6 +59,26 @@ pub struct UserAccountEntity { pub at_ship: bool, } +impl Default for UserAccountEntity { + fn default() -> UserAccountEntity { + UserAccountEntity { + id: UserAccountId(0), + username: "".into(), + password: "".into(), + guildcard: 0xFFFFFFFF, + team_id: None, + banned_until: None, + muted_until: None, + created_at: chrono::Utc::now(), + flags: 0, + activated: false, + at_login: false, + at_character: false, + at_ship: false, + } + } +} + impl UserAccountEntity { pub fn is_currently_online(&self) -> bool { self.at_login | self.at_character | self.at_ship diff --git a/src/entity/character.rs b/src/entity/character.rs index 6785322..117fddb 100644 --- a/src/entity/character.rs +++ b/src/entity/character.rs @@ -7,8 +7,9 @@ use libpso::character::character::{DEFAULT_PALETTE_CONFIG, DEFAULT_TECH_MENU}; use crate::entity::item::tech::Technique; use crate::entity::account::UserAccountId; -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, enum_utils::FromStr, derive_more::Display, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, enum_utils::FromStr, derive_more::Display, Serialize, Deserialize, Default)] pub enum CharacterClass { + #[default] HUmar, HUnewearl, HUcast, @@ -90,8 +91,9 @@ impl CharacterClass { } -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, enum_utils::FromStr, derive_more::Display, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, enum_utils::FromStr, derive_more::Display, Serialize, Deserialize, Default)] pub enum SectionID { + #[default] Viridia, Greenill, Skyly, @@ -163,13 +165,15 @@ pub struct CharacterTechniques { pub techs: HashMap } -impl CharacterTechniques { - fn new() -> CharacterTechniques { +impl Default for CharacterTechniques { + fn default() -> CharacterTechniques { CharacterTechniques { techs: HashMap::new(), } } +} +impl CharacterTechniques { pub fn set_tech(&mut self, tech: Technique, level: TechLevel) { self.techs.insert(tech, TechLevel(level.0 - 1)); } @@ -192,13 +196,15 @@ pub struct CharacterConfig { pub raw_data: [u8; 0xE8], } -impl CharacterConfig { - fn new() -> CharacterConfig { +impl Default for CharacterConfig { + fn default() -> CharacterConfig { CharacterConfig { raw_data: DEFAULT_PALETTE_CONFIG, } } +} +impl CharacterConfig { pub fn update(&mut self, new_config: &UpdateConfig) { self.raw_data = new_config.config; } @@ -213,13 +219,15 @@ pub struct CharacterInfoboard { pub board: [u16; 172], } -impl CharacterInfoboard { - fn new() -> CharacterInfoboard { +impl Default for CharacterInfoboard { + fn default() -> CharacterInfoboard { CharacterInfoboard { board: [0; 172] } } +} +impl CharacterInfoboard { pub fn as_bytes(&self) -> [u16; 172] { self.board } @@ -239,13 +247,15 @@ pub struct CharacterTechMenu { pub tech_menu: [u8; 40], } -impl CharacterTechMenu { - fn new() -> CharacterTechMenu { +impl Default for CharacterTechMenu { + fn default() -> CharacterTechMenu { CharacterTechMenu { tech_menu: DEFAULT_TECH_MENU, } } +} +impl CharacterTechMenu { pub fn as_bytes(&self) -> [u8; 40] { self.tech_menu } @@ -262,7 +272,7 @@ pub struct CharacterMaterials { pub tp: u32, } -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)] pub struct CharacterEntityId(pub u32); #[derive(Clone)] @@ -299,12 +309,12 @@ impl NewCharacterEntity { char_class: CharacterClass::HUmar, section_id: SectionID::Viridia, appearance: CharacterAppearance::default(), - techs: CharacterTechniques::new(), - config: CharacterConfig::new(), - info_board: CharacterInfoboard::new(), + techs: CharacterTechniques::default(), + config: CharacterConfig::default(), + info_board: CharacterInfoboard::default(), guildcard: CharacterGuildCard::default(), materials: CharacterMaterials::default(), - tech_menu: CharacterTechMenu::new(), + tech_menu: CharacterTechMenu::default(), meseta: 0, bank_meseta: 0, option_flags: 0, @@ -312,7 +322,7 @@ impl NewCharacterEntity { } } -#[derive(Clone)] +#[derive(Clone, Default)] pub struct CharacterEntity { pub id: CharacterEntityId, pub user_id: UserAccountId, diff --git a/src/lib.rs b/src/lib.rs index dbf3afa..cb41223 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ #![feature(maybe_uninit_extra)] #![feature(inline_const)] #![feature(drain_filter)] +#![feature(derive_default_enum)] From 143ed7ed7806c84f2950d9a2e18cac18b3dee39a Mon Sep 17 00:00:00 2001 From: jake Date: Mon, 27 Sep 2021 23:07:21 -0600 Subject: [PATCH 05/41] add ItemTransaction struct as begining of item refactor v3 --- src/ship/items/mod.rs | 1 + src/ship/items/transaction.rs | 384 ++++++++++++++++++++++++++++++++++ 2 files changed, 385 insertions(+) create mode 100644 src/ship/items/transaction.rs diff --git a/src/ship/items/mod.rs b/src/ship/items/mod.rs index d70a7f4..7a259ae 100644 --- a/src/ship/items/mod.rs +++ b/src/ship/items/mod.rs @@ -2,6 +2,7 @@ mod bank; mod floor; pub mod inventory; mod manager; +mod transaction; pub mod use_tool; use serde::{Serialize, Deserialize}; diff --git a/src/ship/items/transaction.rs b/src/ship/items/transaction.rs new file mode 100644 index 0000000..a161a2a --- /dev/null +++ b/src/ship/items/transaction.rs @@ -0,0 +1,384 @@ +use crate::entity::gateway::EntityGateway; +use thiserror::Error; +use std::borrow::Cow; +use crate::ship::items::manager::{ItemManager, ItemManagerError}; +use std::collections::HashMap; +use crate::entity::character::{CharacterEntity, CharacterEntityId, TechLevel}; +use crate::ship::items::bank::*; +use crate::ship::items::floor::*; +use crate::ship::items::inventory::*; +use crate::ship::items::ClientItemId; +use crate::entity::gateway::GatewayError; + +use crate::ship::location::{AreaClient, RoomId}; + +#[derive(Error, Debug)] +#[error("")] +pub enum TransactionCommitError { + Gateway(#[from] GatewayError), + ItemManager(#[from] ItemManagerError), +} + +#[async_trait::async_trait] +pub trait ItemAction: std::marker::Send + std::marker::Sync { + async fn commit(&self, manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError>; +} + +pub struct ItemTransactionActions<'a, EG: EntityGateway> { + action_queue: Vec>>, + pub manager: &'a ItemManager, +} + + +impl<'a, EG: EntityGateway> ItemTransactionActions<'a, EG> { + fn new(manager: &'a ItemManager) -> ItemTransactionActions<'a, EG> { + ItemTransactionActions { + action_queue: Vec::new(), + manager + } + } + + pub fn action + 'static>(&mut self, action: A) { + //pub fn action>>>(&mut self, action: A) { + //pub fn action(&mut self, action: impl ItemAction) { + self.action_queue.push(Box::new(action)) + //self.action_queue.push(action.into()) + } +} + + +pub struct ItemTransaction<'a, T, EG: EntityGateway> { + data: T, + actions: ItemTransactionActions<'a, EG>, +} + +impl<'a, T, EG: EntityGateway> ItemTransaction<'a, T, EG> { + pub fn new(manager: &'a ItemManager, arg: T) -> ItemTransaction<'a, T, EG> { + ItemTransaction { + data: arg, + actions: ItemTransactionActions::new(manager), + } + } + + /* + pub fn act(mut self, action: fn(&mut ItemTransactionActions, &T) -> Result) -> ItemTransaction<'a, U, E, EG> { + if self.error.is_none() { + let k = action(&mut self.actions, &self.prev.unwrap()); + match k { + Ok(k) => { + ItemTransaction { + error: None, + prev: Some(k), + actions: self.actions, + } + }, + Err(err) => { + ItemTransaction { + error: Some(err), + prev: None, + actions: self.actions, + } + } + } + } + else { + ItemTransaction { + error: self.error, + prev: None, + actions: self.actions, + } + } + } +*/ + + pub fn act(mut self, action: fn(&mut ItemTransactionActions, &T) -> Result) -> FinalizedItemTransaction { + match action(&mut self.actions, &self.data) { + Ok(k) => { + FinalizedItemTransaction { + value: Ok(k), + action_queue: self.actions.action_queue, + } + }, + Err(err) => { + FinalizedItemTransaction { + value: Err(err), + action_queue: Vec::new(), + } + } + } + } + + /* + pub fn finalize(self) -> FinalizedItemTransaction { + FinalizedItemTransaction { + error: self.error, + prev: self.prev, + action_queue: self.actions.action_queue, + } + } +*/ +} + + +#[derive(Error, Debug)] +#[error("")] +pub enum TransactionError { + Action(E), + Commit(#[from] TransactionCommitError), + +} + +// this only exists to drop the ItemManager borrow of ItemTransaction so a mutable ItemTransaction can be passed in later +pub struct FinalizedItemTransaction { + value: Result, + action_queue: Vec>>, +} + +impl FinalizedItemTransaction { + pub async fn commit(self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result> { + match self.value { + Ok(value) => { + for action in self.action_queue.into_iter() { + // TODO: better handle rolling back if this ever errors out + //let result = action.item_action(item_manager).await.map_err(|err| TransactionError::Commit(err.into()))?; + //action.gateway_action(entity_gateway, result).await.map_err(|err| TransactionError::Commit(err.into()))?; + action.commit(item_manager, entity_gateway).await.map_err(|err| TransactionError::Commit(err))?; + } + Ok(value) + }, + Err(err) => Err(TransactionError::Action(err)), + } + } +} + + +#[cfg(test)] +mod test { + use super::*; + use crate::entity::account::{UserAccountId, NewUserAccountEntity, UserAccountEntity}; + use crate::entity::character::{NewCharacterEntity, CharacterEntity}; + use crate::entity::gateway::GatewayError; + use thiserror::Error; + + #[async_std::test] + async fn test_item_transaction() { + struct DummyAction1 { + name: String, + } + struct DummyAction2 { + value: u32, + } + + #[derive(Error, Debug)] + #[error("")] + enum DummyError { + Error + } + + #[derive(Default, Clone)] + struct DummyGateway { + d1_set: String, + d2_inc: u32, + } + + #[async_trait::async_trait] + impl EntityGateway for DummyGateway { + async fn create_user(&mut self, user: NewUserAccountEntity) -> Result { + self.d1_set = user.username; + Ok(UserAccountEntity::default()) + } + + async fn create_character(&mut self, char: NewCharacterEntity) -> Result { + self.d2_inc += char.slot; + Ok(CharacterEntity::default()) + } + } + + + #[async_trait::async_trait] + impl ItemAction for DummyAction1 { + async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + item_manager.id_counter = 55555; + entity_gateway.create_user(NewUserAccountEntity { + username: self.name.clone(), + ..NewUserAccountEntity::default() + }) + .await?; + Ok(()) + } + } + + #[async_trait::async_trait] + impl ItemAction for DummyAction2 { + async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + item_manager.id_counter += self.value; + entity_gateway.create_character(NewCharacterEntity { + slot: self.value, + ..NewCharacterEntity::new(UserAccountId(0)) + }) + .await?; + Ok(()) + } + } + + let mut item_manager = ItemManager::default(); + let mut entity_gateway = DummyGateway::default(); + + let result = ItemTransaction::new(&item_manager, 12) + .act(|it, k| { + it.action(DummyAction1 {name: "asdf".into()}); + it.action(DummyAction2 {value: 11}); + it.action(DummyAction2 {value: *k}); + if *k == 99 { + return Err(DummyError::Error) + } + Ok(String::from("hello")) + }) + .commit(&mut item_manager, &mut entity_gateway) + .await; + + assert!(entity_gateway.d1_set == "asdf"); + assert!(entity_gateway.d2_inc == 23); + assert!(item_manager.id_counter == 55578); + assert!(result.unwrap() == "hello"); + } + + #[async_std::test] + async fn test_item_transaction_with_action_error() { + struct DummyAction1 { + } + struct DummyAction2 { + } + + #[derive(Error, Debug, PartialEq, Eq)] + #[error("")] + enum DummyError { + Error + } + + #[derive(Default, Clone)] + struct DummyGateway { + d1_set: String, + d2_inc: u32, + } + + #[async_trait::async_trait] + impl EntityGateway for DummyGateway { + async fn create_character(&mut self, char: NewCharacterEntity) -> Result { + self.d2_inc += char.slot; + Ok(CharacterEntity::default()) + } + } + + + #[async_trait::async_trait] + impl ItemAction for DummyAction1 { + async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + entity_gateway.create_character(NewCharacterEntity { + slot: 1, + ..NewCharacterEntity::new(UserAccountId(0)) + }) + .await?; + Ok(()) + } + } + + #[async_trait::async_trait] + impl ItemAction for DummyAction2 { + async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + entity_gateway.create_character(NewCharacterEntity { + slot: 1, + ..NewCharacterEntity::new(UserAccountId(0)) + }) + .await?; + Ok(()) + } + } + + let mut item_manager = ItemManager::default(); + let mut entity_gateway = DummyGateway::default(); + + let result = ItemTransaction::new(&item_manager, 12) + .act(|it, _| -> Result<(), _> { + it.action(DummyAction1 {}); + it.action(DummyAction2 {}); + it.action(DummyAction2 {}); + Err(DummyError::Error) + }) + .commit(&mut item_manager, &mut entity_gateway) + .await; + + assert!(entity_gateway.d2_inc == 0); + assert!(matches!(result, Err(TransactionError::Action(DummyError::Error)))); + } + + #[async_std::test] + async fn test_item_transaction_with_commit_error() { + struct DummyAction1 { + } + struct DummyAction2 { + } + + #[derive(Error, Debug, PartialEq, Eq)] + #[error("")] + enum DummyError { + } + + #[derive(Default, Clone)] + struct DummyGateway { + d1_set: String, + d2_inc: u32, + } + + #[async_trait::async_trait] + impl EntityGateway for DummyGateway { + async fn create_character(&mut self, char: NewCharacterEntity) -> Result { + self.d2_inc += char.slot; + Ok(CharacterEntity::default()) + } + } + + + #[async_trait::async_trait] + impl ItemAction for DummyAction1 { + async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + entity_gateway.create_character(NewCharacterEntity { + slot: 1, + ..NewCharacterEntity::new(UserAccountId(0)) + }) + .await?; + Err(GatewayError::Error.into()) + } + } + + #[async_trait::async_trait] + impl ItemAction for DummyAction2 { + async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + entity_gateway.create_character(NewCharacterEntity { + slot: 1, + ..NewCharacterEntity::new(UserAccountId(0)) + }) + .await?; + Ok(()) + } + } + + let mut item_manager = ItemManager::default(); + let mut entity_gateway = DummyGateway::default(); + + let result = ItemTransaction::new(&item_manager, 12) + .act(|it, _| -> Result<_, DummyError> { + it.action(DummyAction1 {}); + it.action(DummyAction2 {}); + it.action(DummyAction2 {}); + Ok(()) + }) + .commit(&mut item_manager, &mut entity_gateway) + .await; + + // in an ideal world this would be 0 as rollbacks would occur + assert!(entity_gateway.d2_inc == 1); + assert!(matches!(result, Err(TransactionError::Commit(TransactionCommitError::Gateway(GatewayError::Error))))); + } +} + From 55f82b9fcef0110ab02b35e2fc3bc96f5bc7e34f Mon Sep 17 00:00:00 2001 From: jake Date: Mon, 27 Sep 2021 23:14:37 -0600 Subject: [PATCH 06/41] trace -> info --- src/common/mainloop/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/mainloop/client.rs b/src/common/mainloop/client.rs index d99f1c9..52cc503 100644 --- a/src/common/mainloop/client.rs +++ b/src/common/mainloop/client.rs @@ -156,7 +156,7 @@ where match pkt_receiver.recv_pkts().await { Ok(pkts) => { for pkt in pkts { - trace!("[recv from {:?}] {:?}", client_id, pkt); + info!("[recv from {:?}] {:?}", client_id, pkt); server_sender.send(ClientAction::Packet(client_id, pkt)).await.unwrap(); } }, @@ -194,7 +194,7 @@ where *cipher_out.lock().await = outc; } ServerStateAction::Packet(pkt) => { - trace!("[send to {:?}] {:?}", client_id, pkt); + info!("[send to {:?}] {:?}", client_id, pkt); if let Err(err) = send_pkt(socket.clone(), cipher_out.clone(), pkt).await { warn!("[client {:?} send error ] {:?}", client_id, err); } From 70a6764360f779deec0fd2c26b00fbff894bd5ff Mon Sep 17 00:00:00 2001 From: jake Date: Mon, 27 Sep 2021 23:46:03 -0600 Subject: [PATCH 07/41] make these a bit more public for tests --- src/ship/items/floor.rs | 6 ++++++ src/ship/items/manager.rs | 14 +++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/ship/items/floor.rs b/src/ship/items/floor.rs index c976048..271880c 100644 --- a/src/ship/items/floor.rs +++ b/src/ship/items/floor.rs @@ -173,10 +173,16 @@ impl RoomFloorItems { self.0.push(item); } + pub fn remove_item(&mut self, item_id: &ClientItemId) { + self.0.retain(|item| item.item_id() != *item_id); + } + + // TODO: &ClientItemId pub fn get_item_by_id(&self, item_id: ClientItemId) -> Option<&FloorItem> { self.0.iter().find(|item| item.item_id() == item_id) } + // TODO: &ClientItemId 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 { diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs index d3fdf0e..e51b412 100644 --- a/src/ship/items/manager.rs +++ b/src/ship/items/manager.rs @@ -58,16 +58,16 @@ pub enum ItemManagerError { } pub struct ItemManager { - id_counter: u32, + pub(super) id_counter: u32, - character_inventory: HashMap, + pub(self) character_inventory: HashMap, //character_bank: HashMap>, - character_bank: HashMap, - character_floor: HashMap, + pub(self) character_bank: HashMap, + pub(self) character_floor: HashMap, - character_room: HashMap, - room_floor: HashMap, - room_item_id_counter: HashMap ClientItemId + Send>>, + pub(self) character_room: HashMap, + pub(self) room_floor: HashMap, + pub(self) room_item_id_counter: HashMap ClientItemId + Send>>, } impl Default for ItemManager { From b374b63cba7122179f3bb55e2f3da6bb73e9edf0 Mon Sep 17 00:00:00 2001 From: jake Date: Mon, 27 Sep 2021 23:47:19 -0600 Subject: [PATCH 08/41] nth(0) -> next() --- src/ship/packet/handler/trade.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ship/packet/handler/trade.rs b/src/ship/packet/handler/trade.rs index 8f3c04f..a1ea277 100644 --- a/src/ship/packet/handler/trade.rs +++ b/src/ship/packet/handler/trade.rs @@ -40,13 +40,12 @@ where { let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; let inventory = item_manager.get_character_inventory_mut(&client.character)?; - let trade_partner = client_location.get_client_neighbors(id)?; - let trade_partner = trade_partner - .iter() + let trade_partner = client_location.get_client_neighbors(id)? + .into_iter() .filter(|ac| { ac.local_client.id() == items_to_trade.trade_target }) - .nth(0) + .next() .ok_or(TradeError::CouldNotFindTradePartner)?; From 42eb875f8fee9ba04cdd5427135a53f08deb69a5 Mon Sep 17 00:00:00 2001 From: jake Date: Sat, 9 Oct 2021 01:33:40 -0600 Subject: [PATCH 09/41] split create_item into individual and stacked variants --- src/ship/packet/builder/message.rs | 14 +++++++++++++- src/ship/packet/handler/direct_message.rs | 6 +++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/ship/packet/builder/message.rs b/src/ship/packet/builder/message.rs index b7d7f97..9517736 100644 --- a/src/ship/packet/builder/message.rs +++ b/src/ship/packet/builder/message.rs @@ -27,7 +27,7 @@ pub fn item_drop(client: u8, target: u8, item_drop: &FloorItem) -> Result Result { +pub fn create_individual_item(area_client: AreaClient, item_id: ClientItemId, item: &item::ItemDetail) -> Result { let bytes = item.as_client_bytes(); Ok(CreateItem { client: area_client.local_client.id(), @@ -39,6 +39,18 @@ pub fn create_item(area_client: AreaClient, item_id: ClientItemId, item: &item:: }) } +pub fn create_stacked_item(area_client: AreaClient, item_id: ClientItemId, tool: &item::tool::Tool, amount: usize) -> Result { + let bytes = tool.as_stacked_bytes(amount); + Ok(CreateItem { + client: area_client.local_client.id(), + target: 0, + item_data: bytes[0..12].try_into()?, + item_id: item_id.0, + item_data2: bytes[12..16].try_into()?, + unknown: 0, + }) +} + pub fn create_withdrawn_inventory_item(area_client: AreaClient, item: &InventoryItem) -> Result { let bytes = item.as_client_bytes(); Ok(CreateItem { diff --git a/src/ship/packet/handler/direct_message.rs b/src/ship/packet/handler/direct_message.rs index b2cb864..a2f1eda 100644 --- a/src/ship/packet/handler/direct_message.rs +++ b/src/ship/packet/handler/direct_message.rs @@ -137,8 +137,8 @@ where let (item, floor_type) = 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 { - FloorItem::Individual(individual_floor_item) => Some(builder::message::create_item(area_client, item.item_id(), &individual_floor_item.item)?), - FloorItem::Stacked(stacked_floor_item) => Some(builder::message::create_item(area_client, item.item_id(), &item::ItemDetail::Tool(stacked_floor_item.tool))?), + FloorItem::Individual(individual_floor_item) => Some(builder::message::create_individual_item(area_client, item.item_id(), &individual_floor_item.item)?), + FloorItem::Stacked(stacked_floor_item) => Some(builder::message::create_stacked_item(area_client, item.item_id(), &stacked_floor_item.tool, stacked_floor_item.count())?), FloorItem::Meseta(_) => None, //_ => Some(builder::message::create_item(area_client, &item)?), }; @@ -480,7 +480,7 @@ where }; let weapon = item_manager.replace_item_with_tekked(entity_gateway, &client.character, item_id, modifier).await?; - let create_item_pkt = builder::message::create_item(area_client, item_id, &item::ItemDetail::Weapon(weapon))?; + let create_item_pkt = builder::message::create_individual_item(area_client, item_id, &item::ItemDetail::Weapon(weapon))?; let neighbors = client_location.get_client_neighbors(id).map_err(|err| -> ClientLocationError { err.into() })?; Ok(Box::new(neighbors.into_iter() From 21df9a934018a9bb83c33d7c8eda7cf0efc25f32 Mon Sep 17 00:00:00 2001 From: jake Date: Wed, 13 Oct 2021 23:56:42 -0600 Subject: [PATCH 10/41] itemtransaction action takes boxed value --- src/ship/items/transaction.rs | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/ship/items/transaction.rs b/src/ship/items/transaction.rs index a161a2a..805f6d1 100644 --- a/src/ship/items/transaction.rs +++ b/src/ship/items/transaction.rs @@ -38,11 +38,8 @@ impl<'a, EG: EntityGateway> ItemTransactionActions<'a, EG> { } } - pub fn action + 'static>(&mut self, action: A) { - //pub fn action>>>(&mut self, action: A) { - //pub fn action(&mut self, action: impl ItemAction) { - self.action_queue.push(Box::new(action)) - //self.action_queue.push(action.into()) + pub fn action(&mut self, action: Box>) { + self.action_queue.push(action) } } @@ -226,9 +223,9 @@ mod test { let result = ItemTransaction::new(&item_manager, 12) .act(|it, k| { - it.action(DummyAction1 {name: "asdf".into()}); - it.action(DummyAction2 {value: 11}); - it.action(DummyAction2 {value: *k}); + it.action(Box::new(DummyAction1 {name: "asdf".into()})); + it.action(Box::new(DummyAction2 {value: 11})); + it.action(Box::new(DummyAction2 {value: *k})); if *k == 99 { return Err(DummyError::Error) } @@ -300,9 +297,9 @@ mod test { let result = ItemTransaction::new(&item_manager, 12) .act(|it, _| -> Result<(), _> { - it.action(DummyAction1 {}); - it.action(DummyAction2 {}); - it.action(DummyAction2 {}); + it.action(Box::new(DummyAction1 {})); + it.action(Box::new(DummyAction2 {})); + it.action(Box::new(DummyAction2 {})); Err(DummyError::Error) }) .commit(&mut item_manager, &mut entity_gateway) @@ -368,9 +365,9 @@ mod test { let result = ItemTransaction::new(&item_manager, 12) .act(|it, _| -> Result<_, DummyError> { - it.action(DummyAction1 {}); - it.action(DummyAction2 {}); - it.action(DummyAction2 {}); + it.action(Box::new(DummyAction1 {})); + it.action(Box::new(DummyAction2 {})); + it.action(Box::new(DummyAction2 {})); Ok(()) }) .commit(&mut item_manager, &mut entity_gateway) From dbb1105741e8e16242550efd7201ebb3b89ec8e0 Mon Sep 17 00:00:00 2001 From: jake Date: Wed, 13 Oct 2021 23:57:38 -0600 Subject: [PATCH 11/41] cleanup --- src/ship/items/transaction.rs | 43 ----------------------------------- 1 file changed, 43 deletions(-) diff --git a/src/ship/items/transaction.rs b/src/ship/items/transaction.rs index 805f6d1..4c385c5 100644 --- a/src/ship/items/transaction.rs +++ b/src/ship/items/transaction.rs @@ -57,37 +57,6 @@ impl<'a, T, EG: EntityGateway> ItemTransaction<'a, T, EG> { } } - /* - pub fn act(mut self, action: fn(&mut ItemTransactionActions, &T) -> Result) -> ItemTransaction<'a, U, E, EG> { - if self.error.is_none() { - let k = action(&mut self.actions, &self.prev.unwrap()); - match k { - Ok(k) => { - ItemTransaction { - error: None, - prev: Some(k), - actions: self.actions, - } - }, - Err(err) => { - ItemTransaction { - error: Some(err), - prev: None, - actions: self.actions, - } - } - } - } - else { - ItemTransaction { - error: self.error, - prev: None, - actions: self.actions, - } - } - } -*/ - pub fn act(mut self, action: fn(&mut ItemTransactionActions, &T) -> Result) -> FinalizedItemTransaction { match action(&mut self.actions, &self.data) { Ok(k) => { @@ -104,16 +73,6 @@ impl<'a, T, EG: EntityGateway> ItemTransaction<'a, T, EG> { } } } - - /* - pub fn finalize(self) -> FinalizedItemTransaction { - FinalizedItemTransaction { - error: self.error, - prev: self.prev, - action_queue: self.actions.action_queue, - } - } -*/ } @@ -137,8 +96,6 @@ impl FinalizedItemTransaction { for action in self.action_queue.into_iter() { // TODO: better handle rolling back if this ever errors out - //let result = action.item_action(item_manager).await.map_err(|err| TransactionError::Commit(err.into()))?; - //action.gateway_action(entity_gateway, result).await.map_err(|err| TransactionError::Commit(err.into()))?; action.commit(item_manager, entity_gateway).await.map_err(|err| TransactionError::Commit(err))?; } Ok(value) From 296d1cc0eacd52ccde64c2127d75e62a9da5bcdc Mon Sep 17 00:00:00 2001 From: jake Date: Wed, 13 Oct 2021 23:58:32 -0600 Subject: [PATCH 12/41] InventoryItem::{stacked, mag} --- src/ship/items/inventory.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/ship/items/inventory.rs b/src/ship/items/inventory.rs index 9589849..50d161d 100644 --- a/src/ship/items/inventory.rs +++ b/src/ship/items/inventory.rs @@ -220,6 +220,20 @@ impl InventoryItem { _ => None } } + + pub fn stacked(&self) -> Option<&StackedInventoryItem> { + match self { + InventoryItem::Stacked(ref stacked_inventory_item) => Some(stacked_inventory_item), + _ => None + } + } + + pub fn mag(&self) -> Option<&Mag> { + match self { + InventoryItem::Individual(individual_inventory_item) => individual_inventory_item.mag(), + _ => None + } + } } From 6ed0d838d28580ae28d1ba2d29ed28b5d67bf7e6 Mon Sep 17 00:00:00 2001 From: jake Date: Thu, 14 Oct 2021 00:16:22 -0600 Subject: [PATCH 13/41] add gateway function set_character_meseta --- src/entity/gateway/entitygateway.rs | 4 ++++ src/entity/gateway/inmemory.rs | 8 ++++++++ src/entity/gateway/postgres/postgres.rs | 9 +++++++++ 3 files changed, 21 insertions(+) diff --git a/src/entity/gateway/entitygateway.rs b/src/entity/gateway/entitygateway.rs index 261aadc..27cc5d5 100644 --- a/src/entity/gateway/entitygateway.rs +++ b/src/entity/gateway/entitygateway.rs @@ -115,4 +115,8 @@ pub trait EntityGateway: Send + Sync + Clone { async fn set_character_equips(&mut self, _char_id: &CharacterEntityId, _equips: &EquippedEntity) -> Result<(), GatewayError> { unimplemented!(); } + + async fn set_character_meseta(&mut self, _char_id: &CharacterEntityId, amount: usize) -> Result<(), GatewayError> { + unimplemented!(); + } } diff --git a/src/entity/gateway/inmemory.rs b/src/entity/gateway/inmemory.rs index d80dca7..4a95b52 100644 --- a/src/entity/gateway/inmemory.rs +++ b/src/entity/gateway/inmemory.rs @@ -318,4 +318,12 @@ impl EntityGateway for InMemoryGateway { equips.insert(*char_id, equipped.clone()); Ok(()) } + + async fn set_character_meseta(&mut self, char_id: &CharacterEntityId, amount: usize) -> Result<(), GatewayError> { + let mut characters = self.characters.lock().unwrap(); + if let Some(char) = characters.get_mut(&char_id) { + char.meseta = amount as u32; + } + Ok(()) + } } diff --git a/src/entity/gateway/postgres/postgres.rs b/src/entity/gateway/postgres/postgres.rs index 53609cf..4f872c4 100644 --- a/src/entity/gateway/postgres/postgres.rs +++ b/src/entity/gateway/postgres/postgres.rs @@ -597,4 +597,13 @@ impl EntityGateway for PostgresGateway { .await?; Ok(()) } + + async fn set_character_meseta(&mut self, char_id: &CharacterEntityId, amount: usize) -> Result<(), GatewayError> { + sqlx::query(r#"update player_character set meseta=$2 where id = $1"#) + .bind(char_id.0) + .bind(amount as i32) + .execute(&self.pool) + .await?; + Ok(()) + } } From 8a29a75397cc517719b31fd220b0d8ecd3edbdd1 Mon Sep 17 00:00:00 2001 From: jake Date: Thu, 14 Oct 2021 00:17:36 -0600 Subject: [PATCH 14/41] remove TODO --- src/ship/items/manager.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs index e51b412..d47eb7b 100644 --- a/src/ship/items/manager.rs +++ b/src/ship/items/manager.rs @@ -90,7 +90,6 @@ impl ItemManager { ClientItemId(self.id_counter) } - // TODO: Result pub async fn load_character(&mut self, entity_gateway: &mut EG, character: &CharacterEntity) -> Result<(), anyhow::Error> { let inventory = entity_gateway.get_character_inventory(&character.id).await?; let bank = entity_gateway.get_character_bank(&character.id, BankName("".into())).await?; From ac423fa6ba28e890b07d9717d412d3387e3b03e6 Mon Sep 17 00:00:00 2001 From: jake Date: Thu, 14 Oct 2021 00:18:51 -0600 Subject: [PATCH 15/41] change trade structure in clientstate --- src/ship/packet/handler/trade.rs | 12 ++++++++---- src/ship/ship.rs | 17 ++++++++++++++--- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/ship/packet/handler/trade.rs b/src/ship/packet/handler/trade.rs index a1ea277..1718f63 100644 --- a/src/ship/packet/handler/trade.rs +++ b/src/ship/packet/handler/trade.rs @@ -6,7 +6,7 @@ use libpso::packet::ship::*; use libpso::packet::messages::*; use crate::common::leveltable::CharacterLevelTable; use crate::common::serverstate::ClientId; -use crate::ship::ship::{SendShipPacket, ShipError, Clients, Rooms, ItemShops, TradeItem}; +use crate::ship::ship::{SendShipPacket, ShipError, Clients, Rooms, ItemShops, TradeItem, TradeState, TradeStatus}; use crate::ship::location::{ClientLocation, ClientLocationError}; use crate::ship::drops::ItemDrop; use crate::ship::items::{ItemManager, ItemManagerError, ClientItemId, TriggerCreateItem, FloorItem, FloorType}; @@ -86,7 +86,11 @@ where .collect::, anyhow::Error>>()?; // TODO: check room in inventory for items - client.trade = Some((trade_partner.client, trade_items)); + client.trade = Some(TradeState { + other_client: trade_partner.client, + items: trade_items, + status: TradeStatus::Unconfirmed + }); Ok(Box::new(vec![(trade_partner.client, SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {}))].into_iter())) } @@ -107,8 +111,8 @@ where Err(err) => { let client = clients.get(&id).ok_or(ShipError::ClientNotFound(id))?; let trade_partner = client.trade.as_ref() - .and_then(|(trade_partner, _)| { - client_location.get_local_client(*trade_partner).ok() + .and_then(|trade_state| { + client_location.get_local_client(trade_state.other_client).ok() }) .map(|trade_partner| { (trade_partner.client, SendShipPacket::CancelTrade(CancelTrade {})) diff --git a/src/ship/ship.rs b/src/ship/ship.rs index 1478143..8166936 100644 --- a/src/ship/ship.rs +++ b/src/ship/ship.rs @@ -248,12 +248,25 @@ pub struct LoadingQuest { } +#[derive(Clone)] pub enum TradeItem { Individual(items::ClientItemId), Stacked(items::ClientItemId, usize), Meseta(usize), } +#[derive(Clone, Eq, PartialEq)] +pub enum TradeStatus { + Confirmed, + Unconfirmed, +} + +#[derive(Clone)] +pub struct TradeState { + pub other_client: ClientId, + pub items: Vec, + pub status: TradeStatus, +} pub struct ClientState { pub user: UserAccountEntity, @@ -273,8 +286,7 @@ pub struct ClientState { pub tool_shop: Vec, pub armor_shop: Vec, pub tek: Option<(items::ClientItemId, item::weapon::TekSpecialModifier, item::weapon::TekPercentModifier, i32)>, - pub trade: Option<(ClientId, Vec)>, - pub confirmed_trade: bool, + pub trade: Option, } impl ClientState { @@ -296,7 +308,6 @@ impl ClientState { armor_shop: Vec::new(), tek: None, trade: None, - confirmed_trade: false, } } } From ddff55d09efbf43f4b77865f3d499595a27d94dd Mon Sep 17 00:00:00 2001 From: jake Date: Thu, 14 Oct 2021 00:20:59 -0600 Subject: [PATCH 16/41] add another item pickup test --- tests/test_item_pickup.rs | 74 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/tests/test_item_pickup.rs b/tests/test_item_pickup.rs index e54f0ec..e1600d8 100644 --- a/tests/test_item_pickup.rs +++ b/tests/test_item_pickup.rs @@ -10,6 +10,80 @@ use libpso::packet::messages::*; mod common; use common::*; +#[async_std::test] +async fn test_pick_up_individual_item() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let mut p1_inv = Vec::new(); + p1_inv.push(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, + } + }).await.unwrap()); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); + + ship.handle(ClientId(1), &RecvShipPacket::Message(Message::new(GameMessage::PlayerDropItem(PlayerDropItem { + client: 0, + target: 0, + unknown1: 0, + map_area: 0, + item_id: 0x10000, + x: 0.0, + y: 0.0, + z: 0.0, + })))).await.unwrap().for_each(drop); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 0); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); + + ship.handle(ClientId(2), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::PickupItem(PickupItem { + client: 0, + target: 0, + item_id: 0x10000, + map_area: 0, + unknown: [0; 3] + })))).await.unwrap().for_each(drop); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 0); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 1); +} + #[async_std::test] async fn test_pick_up_item_stack_of_items_already_in_inventory() { let mut entity_gateway = InMemoryGateway::default(); From d80d0fb01d20d862b5a15fd23b06c709c5f0594d Mon Sep 17 00:00:00 2001 From: jake Date: Thu, 14 Oct 2021 00:22:23 -0600 Subject: [PATCH 17/41] add some functions to Inventory --- src/ship/items/inventory.rs | 88 +++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/src/ship/items/inventory.rs b/src/ship/items/inventory.rs index 50d161d..314eeab 100644 --- a/src/ship/items/inventory.rs +++ b/src/ship/items/inventory.rs @@ -87,6 +87,18 @@ pub enum InventoryItemAddToError { pub enum InventoryAddError { } +#[derive(Debug, Clone)] +pub enum YesThereIsSpace { + NewStack, + ExistingStack, +} + +#[derive(Debug, Clone)] +pub enum SpaceForStack { + Yes(YesThereIsSpace), + No, +} + impl InventoryItem { pub fn entity_ids(&self) -> Vec { match self { @@ -427,6 +439,45 @@ impl CharacterInventory { self.items.len() } + pub fn space_for_individual_item(&self) -> bool { + self.count() < INVENTORY_CAPACITY + } + + pub fn space_for_stacked_item(&self, tool: &Tool, amount: usize) -> SpaceForStack { + let existing_item = self.items.iter() + .filter_map(|item| { + match item { + InventoryItem::Stacked(s_item) => { + Some(s_item) + }, + _ => None + } + }) + .find(|s_item| { + s_item.tool == *tool + }); + + match existing_item { + Some(item) => { + if item.count() + amount <= tool.tool.max_stack() { + SpaceForStack::Yes(YesThereIsSpace::ExistingStack) + } + else { + SpaceForStack::No + } + } + None => { + if self.count() < INVENTORY_CAPACITY { + SpaceForStack::Yes(YesThereIsSpace::NewStack) + } + else { + SpaceForStack::No + } + } + } + } + + pub fn get_item_handle_by_id(&mut self, item_id: ClientItemId) -> Option { let (slot, _) = self.items.iter() .enumerate() @@ -526,6 +577,16 @@ impl CharacterInventory { Ok(()) } + pub fn add_individual_floor_item(&mut self, floor_item: &IndividualFloorItem) -> &InventoryItem { + self.items.push(InventoryItem::Individual(IndividualInventoryItem { + entity_id: floor_item.entity_id, + item_id: floor_item.item_id, + item: floor_item.item.clone(), + })); + + self.items.last().unwrap() + } + // 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 { @@ -546,6 +607,33 @@ impl CharacterInventory { } } + pub fn add_stacked_floor_item(&mut self, floor_item: &StackedFloorItem) { + let existing_item = self.items.iter_mut() + .filter_map(|item| { + match item { + InventoryItem::Stacked(s_item) => Some(s_item), + _ => None, + } + + }) + .find(|item| { + item.tool == floor_item.tool + }); + + match existing_item { + Some(item) => { + item.entity_ids.append(&mut floor_item.entity_ids.clone()) + }, + None => { + self.items.push(InventoryItem::Stacked(StackedInventoryItem { + entity_ids: floor_item.entity_ids.clone(), + item_id: floor_item.item_id, + tool: floor_item.tool, + })); + } + } + } + // 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.items.iter() From 8e82d6c4b40d8a28df1eac6e61aab158e7ffaac4 Mon Sep 17 00:00:00 2001 From: jake Date: Thu, 14 Oct 2021 00:28:31 -0600 Subject: [PATCH 18/41] convert item pickup to new itemtransaction model --- src/ship/items/manager.rs | 267 +++++++++++++++++++++++++++----------- 1 file changed, 191 insertions(+), 76 deletions(-) diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs index d47eb7b..e25f1bc 100644 --- a/src/ship/items/manager.rs +++ b/src/ship/items/manager.rs @@ -1,10 +1,10 @@ use crate::ship::items::ClientItemId; use std::collections::HashMap; use thiserror::Error; -use crate::entity::gateway::EntityGateway; +use crate::entity::gateway::{EntityGateway, GatewayError}; use crate::entity::character::{CharacterEntity, CharacterEntityId, TechLevel}; use crate::entity::item::{ItemDetail, ItemLocation, BankName}; -use crate::entity::item::{Meseta, NewItemEntity, ItemEntity, InventoryItemEntity, BankItemEntity}; +use crate::entity::item::{Meseta, NewItemEntity, ItemEntity, ItemEntityId, InventoryItemEntity, BankItemEntity}; use crate::entity::item::tool::{Tool, ToolType}; use crate::entity::item::weapon; use crate::ship::map::MapArea; @@ -17,6 +17,7 @@ use crate::ship::items::bank::*; use crate::ship::items::floor::*; use crate::ship::items::inventory::*; use crate::ship::items::use_tool; +use crate::ship::items::transaction::{ItemTransaction, ItemAction, TransactionError, TransactionCommitError}; #[derive(PartialEq, Eq)] pub enum FloorType { @@ -35,6 +36,7 @@ pub enum ItemManagerError { EntityGatewayError, NoSuchItemId(ClientItemId), NoCharacter(CharacterEntityId), + NoRoom(RoomId), CouldNotAddToInventory(ClientItemId), //ItemBelongsToOtherPlayer, Idunnoman, @@ -53,10 +55,36 @@ pub enum ItemManagerError { CannotGetIndividualItem, InvalidSlot(u8, u8), // slots available, slot attempted NoArmorEquipped, - GatewayError(#[from] crate::entity::gateway::GatewayError), + GatewayError(#[from] GatewayError), StackedItemError(Vec), + ItemTransactionAction(Box), + InvalidTrade, } +impl std::convert::From> for ItemManagerError +where + E: std::fmt::Debug + std::marker::Send + std::marker::Sync + std::error::Error + 'static, +{ + fn from(other: TransactionError) -> ItemManagerError { + match other { + TransactionError::Action(err) => { + ItemManagerError::ItemTransactionAction(Box::new(err)) + }, + TransactionError::Commit(err) => { + match err { + TransactionCommitError::Gateway(gw) => { + ItemManagerError::GatewayError(gw) + }, + TransactionCommitError::ItemManager(im) => { + im + } + } + } + } + } +} + + pub struct ItemManager { pub(super) id_counter: u32, @@ -225,80 +253,88 @@ impl ItemManager { 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, - } - ).await?; - if new_inventory_item.mag().is_some() { - entity_gateway.change_mag_owner(&new_inventory_item.entity_id, character).await?; - } + let it = ItemTransaction::new(&self, (character, item_id)) + .act(|it, (character, item_id)| -> Result<_, ItemManagerError> { + let local_floor = it.manager.character_floor.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; + let inventory = it.manager.character_inventory.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; + let room_id = it.manager.character_room.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; + let shared_floor = it.manager.room_floor.get(room_id).ok_or(ItemManagerError::NoRoom(*room_id))?; + + let floor_item = match local_floor.get_item_by_id(*item_id) { + Some(floor_item) => { + it.action(Box::new(RemoveFromLocalFloor { + character_id: character.id, + item_id: item_id.clone() + })); + floor_item }, None => { - return Err(ItemManagerError::CouldNotAddToInventory(item_id).into()); - }, - } - TriggerCreateItem::Yes - }, - Some(FloorItem::Stacked(stacked_floor_item)) => { - let new_inventory_item = inventory.pick_up_stacked_floor_item(stacked_floor_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, - } - ).await?; + match shared_floor.get_item_by_id(*item_id) { + Some(floor_item) => { + it.action(Box::new(RemoveFromSharedFloor { + room_id: *room_id, + item_id: item_id.clone() + })); + floor_item + }, + None => { + return Err(ItemManagerError::NoSuchItemId(item_id.clone())).into() + } } + } + }; - if stacked_floor_item.count() != new_inventory_item.count() { - TriggerCreateItem::No + let create_trigger = match floor_item { + FloorItem::Individual(individual_floor_item) => { + if inventory.space_for_individual_item() { + it.action(Box::new(AddIndividualFloorItemToInventory { + character: (**character).clone(), + item: individual_floor_item.clone() + })) } else { - TriggerCreateItem::Yes + return Err(ItemManagerError::CouldNotAddToInventory(*item_id).into()); } + TriggerCreateItem::Yes }, - None => { - return Err(ItemManagerError::CouldNotAddToInventory(item_id).into()); - } - } - }, - Some(FloorItem::Meseta(meseta_floor_item)) => { - if character.meseta >= 999999 { - return Err(ItemManagerError::CouldNotAddToInventory(item_id).into()); - } - 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).into()); - } - }; + FloorItem::Stacked(stacked_floor_item) => { + match inventory.space_for_stacked_item(&stacked_floor_item.tool, stacked_floor_item.entity_ids.len()) { + SpaceForStack::Yes(YesThereIsSpace::NewStack) => { + it.action(Box::new(AddStackedFloorItemToInventory { + character_id: character.id, + item: stacked_floor_item.clone() + })); + TriggerCreateItem::Yes + }, + SpaceForStack::Yes(YesThereIsSpace::ExistingStack) => { + it.action(Box::new(AddStackedFloorItemToInventory { + character_id: character.id, + item: stacked_floor_item.clone() + })); + TriggerCreateItem::No + }, + SpaceForStack::No => { + return Err(ItemManagerError::CouldNotAddToInventory(*item_id).into()); + }, + } + }, + FloorItem::Meseta(meseta_floor_item) => { + if character.meseta >= 999999 { + return Err(ItemManagerError::CouldNotAddToInventory(*item_id).into()); + } + it.action(Box::new(AddMesetaFloorItemToInventory { + character: (**character).clone(), + item: meseta_floor_item.clone() + })); - entity_gateway.set_character_inventory(&character.id, &inventory.as_inventory_entity(&character.id)).await?; - floor_item.remove_from_floor(); - Ok(trigger_create_item) + TriggerCreateItem::No + }, + }; + Ok(create_trigger) + }); + it.commit(self, entity_gateway) + .await + .map_err(|err| err.into()) } pub async fn enemy_drop_item_on_local_floor(&mut self, entity_gateway: &mut EG, character: &CharacterEntity, item_drop: ItemDrop) -> Result<&FloorItem, anyhow::Error> { @@ -924,14 +960,93 @@ impl ItemManager { Ok(weapon) } - pub async fn send_items_to_other_player(&mut self, - entity_gateway: &mut EG, - source_character: &CharacterEntity, - dest_character: &CharacterEntity, - items: &Vec) - -> Result<(), anyhow::Error> { - - +struct RemoveFromLocalFloor { + character_id: CharacterEntityId, + item_id: ClientItemId, +} + +#[async_trait::async_trait] +impl ItemAction for RemoveFromLocalFloor { + async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + let local_floor = item_manager.character_floor.get_mut(&self.character_id).ok_or(ItemManagerError::NoCharacter(self.character_id))?; + local_floor.remove_item(&self.item_id); + Ok(()) + } +} + + +struct RemoveFromSharedFloor { + room_id: RoomId, + item_id: ClientItemId, +} + +#[async_trait::async_trait] +impl ItemAction for RemoveFromSharedFloor { + async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + let shared_floor = item_manager.room_floor.get_mut(&self.room_id).ok_or(ItemManagerError::NoRoom(self.room_id))?; + shared_floor.remove_item(&self.item_id); + Ok(()) + } +} + + +struct AddIndividualFloorItemToInventory{ + character: CharacterEntity, + item: IndividualFloorItem, +} + +#[async_trait::async_trait] +impl ItemAction for AddIndividualFloorItemToInventory { + async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + let inventory = item_manager.character_inventory.get_mut(&self.character.id).ok_or(ItemManagerError::NoCharacter(self.character.id))?; + let inv_item = inventory.add_individual_floor_item(&self.item); + + entity_gateway.change_item_location( + &self.item.entity_id, + ItemLocation::Inventory { + character_id: self.character.id, + } + ).await?; + + if inv_item.mag().is_some() { + entity_gateway.change_mag_owner(&self.item.entity_id, &self.character).await?; + } + + entity_gateway.set_character_inventory(&self.character.id, &inventory.as_inventory_entity(&self.character.id)).await?; + Ok(()) + } +} + + +struct AddStackedFloorItemToInventory{ + character_id: CharacterEntityId, + item: StackedFloorItem, +} + +#[async_trait::async_trait] +impl ItemAction for AddStackedFloorItemToInventory { + async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + let inventory = item_manager.character_inventory.get_mut(&self.character_id).ok_or(ItemManagerError::NoCharacter(self.character_id))?; + inventory.add_stacked_floor_item(&self.item); + + entity_gateway.set_character_inventory(&self.character_id, &inventory.as_inventory_entity(&self.character_id)).await?; + Ok(()) + } +} + + +struct AddMesetaFloorItemToInventory{ + character: CharacterEntity, + item: MesetaFloorItem, +} + +#[async_trait::async_trait] +impl ItemAction for AddMesetaFloorItemToInventory { + async fn commit(&self, _item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + let mut nchar = self.character.clone(); + nchar.meseta = std::cmp::min(self.character.meseta + self.item.meseta.0, 999999); + entity_gateway.save_character(&nchar).await?; + Ok(()) } } From 98260308e8f312c3851197f72f5c263ac16da817 Mon Sep 17 00:00:00 2001 From: jake Date: Thu, 14 Oct 2021 00:30:53 -0600 Subject: [PATCH 19/41] stray spaces --- src/ship/items/manager.rs | 2 +- src/ship/packet/handler/trade.rs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs index e25f1bc..47154e9 100644 --- a/src/ship/items/manager.rs +++ b/src/ship/items/manager.rs @@ -539,7 +539,7 @@ impl ItemManager { 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)?; - + if let ItemDetail::TechniqueDisk(tech_disk) = consumed_item.item() { // TODO: validate tech level in packet is in bounds [1..30] character.techs.set_tech(tech_disk.tech, TechLevel(tech_disk.level as u8)); diff --git a/src/ship/packet/handler/trade.rs b/src/ship/packet/handler/trade.rs index 1718f63..618dfa0 100644 --- a/src/ship/packet/handler/trade.rs +++ b/src/ship/packet/handler/trade.rs @@ -47,8 +47,7 @@ where }) .next() .ok_or(TradeError::CouldNotFindTradePartner)?; - - + let item_blobs = items_to_trade.items.iter().take(items_to_trade.count as usize); let trade_items = item_blobs .map(|item| { @@ -117,7 +116,7 @@ where .map(|trade_partner| { (trade_partner.client, SendShipPacket::CancelTrade(CancelTrade {})) }); - + log::warn!("error in trading: {:?}", err); Ok(Box::new(vec![(id, SendShipPacket::CancelTrade(CancelTrade {}))] .into_iter() From b80f30ef9de18fd38e98dfd0aeb23f4e8bf733a6 Mon Sep 17 00:00:00 2001 From: jake Date: Fri, 15 Oct 2021 12:17:37 -0600 Subject: [PATCH 20/41] initial actual trading logic --- src/ship/items/manager.rs | 192 +++++++++++++++++++++++++++++ src/ship/packet/builder/message.rs | 2 + src/ship/packet/handler/trade.rs | 90 +++++++++----- 3 files changed, 253 insertions(+), 31 deletions(-) diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs index 47154e9..773819c 100644 --- a/src/ship/items/manager.rs +++ b/src/ship/items/manager.rs @@ -960,6 +960,125 @@ impl ItemManager { Ok(weapon) } + pub async fn trade_items(&mut self, + entity_gateway: &mut EG, + p1: (&AreaClient, &CharacterEntity, &Vec), + p2: (&AreaClient, &CharacterEntity, &Vec)) + -> Result, anyhow::Error> { + let it = ItemTransaction::new(&self, (p1, p2)) + .act(|it, (p1, p2)| -> Result<_, anyhow::Error> { + let p1_inventory = it.manager.get_character_inventory(p1.1)?; + let p2_inventory = it.manager.get_character_inventory(p2.1)?; + + //TODO: inv-selftrade+othertrade <= 30 + //if p1_inventory + + let trade_items = [(p1, p2, p1_inventory), (p2, p1, p2_inventory)] + .map(|((src_client, dest_client, src_inventory))| { + src_client.2.iter() + .map(|item| -> Option<(Option, Vec>>)> { + match item { + TradeItem::Individual(item_id) => { + let item = src_inventory.get_item_by_id(*item_id)?.individual()?; + Some(( + Some(ItemToTrade { + add_to: *dest_client.0, + remove_from: *src_client.0, + item_id: *item_id, + item_detail: ItemToTradeDetail::Individual(item.item.clone()) + }), + vec![ + Box::new(AddIndividualItemToInventory { + character_id: dest_client.1.id, + item_id: item.entity_id, + }), + Box::new(RemoveIndividualItemFromInventory { + character_id: src_client.1.id, + item_id: item.entity_id, + }) + ] + )) + }, + TradeItem::Stacked(item_id, amount) => { + let item = src_inventory.get_item_by_id(*item_id)?.stacked()?; + if item.count() < *amount { + None + } + else { + Some(( + Some(ItemToTrade { + add_to: *dest_client.0, + remove_from: *src_client.0, + item_id: *item_id, + item_detail: ItemToTradeDetail::Stacked(item.tool, *amount) + }), + vec![ + Box::new(AddStackedItemToInventory { + character_id: dest_client.1.id, + item_ids: item.entity_ids.iter().cloned().take(*amount).collect(), + }), + Box::new(RemoveStackedItemFromInventory { + character_id: src_client.1.id, + item_ids: item.entity_ids.iter().cloned().take(*amount).collect(), + }), + ] + )) + } + }, + TradeItem::Meseta(amount) => { + Some((None, + vec![ + Box::new(AddMesetaToInventory { + character_id: dest_client.1.id, + amount: *amount, + }), + Box::new(RemoveMesetaFromInventory { + character_id: src_client.1.id, + amount: *amount, + }), + ] + )) + } + } + }) + .collect::>>() + }); + + if let [Some(p1_trades), Some(p2_trades)] = trade_items { + let (p1_item_trades, p1_item_actions): (Vec>, Vec>>>) = p1_trades.into_iter().unzip(); + let (p2_item_trades, p2_item_actions): (Vec>, Vec>>>) = p2_trades.into_iter().unzip(); + let item_trades = p1_item_trades.into_iter().flatten().chain(p2_item_trades.into_iter().flatten()); + let item_actions = p1_item_actions.into_iter().flatten().chain(p2_item_actions.into_iter().flatten()); + + for action in item_actions { + it.action(action); + } + + Ok(item_trades.collect()) + } + else { + Err(ItemManagerError::InvalidTrade.into()) + } + + }); + it.commit(self, entity_gateway) + .await + .map_err(|err| err.into()) + } +} + +pub enum ItemToTradeDetail { + Individual(ItemDetail), + Stacked(Tool, usize), +} + +pub struct ItemToTrade { + pub add_to: AreaClient, + pub remove_from: AreaClient, + pub item_id: ClientItemId, + pub item_detail: ItemToTradeDetail, +} + struct RemoveFromLocalFloor { character_id: CharacterEntityId, @@ -1050,3 +1169,76 @@ impl ItemAction for AddMesetaFloorItemToInventory { Ok(()) } } + + +struct AddIndividualItemToInventory { + character_id: CharacterEntityId, + item_id: ItemEntityId, +} + +#[async_trait::async_trait] +impl ItemAction for AddIndividualItemToInventory { + async fn commit(&self, _item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + Ok(()) + } +} + +struct AddStackedItemToInventory { + character_id: CharacterEntityId, + item_ids: Vec, +} + +#[async_trait::async_trait] +impl ItemAction for AddStackedItemToInventory { + async fn commit(&self, _item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + Ok(()) + } +} + +struct RemoveIndividualItemFromInventory { + character_id: CharacterEntityId, + item_id: ItemEntityId, +} + +#[async_trait::async_trait] +impl ItemAction for RemoveIndividualItemFromInventory { + async fn commit(&self, _item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + Ok(()) + } +} + +struct RemoveStackedItemFromInventory { + character_id: CharacterEntityId, + item_ids: Vec, +} + +#[async_trait::async_trait] +impl ItemAction for RemoveStackedItemFromInventory { + async fn commit(&self, _item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + Ok(()) + } +} + +struct AddMesetaToInventory { + character_id: CharacterEntityId, + amount: usize, +} + +#[async_trait::async_trait] +impl ItemAction for AddMesetaToInventory { + async fn commit(&self, _item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + Ok(()) + } +} + +struct RemoveMesetaFromInventory { + character_id: CharacterEntityId, + amount: usize, +} + +#[async_trait::async_trait] +impl ItemAction for RemoveMesetaFromInventory { + async fn commit(&self, _item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + Ok(()) + } +} diff --git a/src/ship/packet/builder/message.rs b/src/ship/packet/builder/message.rs index 9517736..01c434c 100644 --- a/src/ship/packet/builder/message.rs +++ b/src/ship/packet/builder/message.rs @@ -27,6 +27,7 @@ pub fn item_drop(client: u8, target: u8, item_drop: &FloorItem) -> Result Result { let bytes = item.as_client_bytes(); Ok(CreateItem { @@ -39,6 +40,7 @@ pub fn create_individual_item(area_client: AreaClient, item_id: ClientItemId, it }) } +// TODO: this doesn't need to be a Result, just unwrap try_intos they are guaranteed to succeed pub fn create_stacked_item(area_client: AreaClient, item_id: ClientItemId, tool: &item::tool::Tool, amount: usize) -> Result { let bytes = tool.as_stacked_bytes(amount); Ok(CreateItem { diff --git a/src/ship/packet/handler/trade.rs b/src/ship/packet/handler/trade.rs index 618dfa0..d67d28c 100644 --- a/src/ship/packet/handler/trade.rs +++ b/src/ship/packet/handler/trade.rs @@ -9,7 +9,7 @@ use crate::common::serverstate::ClientId; use crate::ship::ship::{SendShipPacket, ShipError, Clients, Rooms, ItemShops, TradeItem, TradeState, TradeStatus}; use crate::ship::location::{ClientLocation, ClientLocationError}; use crate::ship::drops::ItemDrop; -use crate::ship::items::{ItemManager, ItemManagerError, ClientItemId, TriggerCreateItem, FloorItem, FloorType}; +use crate::ship::items::{ItemManager, ItemManagerError, ClientItemId, TriggerCreateItem, FloorItem, FloorType, ItemToTradeDetail}; use crate::ship::items::inventory::InventoryItem; use crate::entity::gateway::EntityGateway; use crate::entity::item; @@ -125,7 +125,6 @@ where } } -// this function is a shitshow due to not thinking of what would happen if I needed more than 1 client at a time pub async fn trade_confirmed(id: ClientId, entity_gateway: &mut EG, client_location: &ClientLocation, @@ -135,47 +134,76 @@ pub async fn trade_confirmed(id: ClientId, where EG: EntityGateway { - let (this_client_confirmed, other_client_id) = { - let client = clients.get(&id).ok_or(ShipError::ClientNotFound(id))?; - (client.confirmed_trade, client.trade.as_ref().ok_or(TradeError::NotInTradeMenu)?.0) - }; - let other_client_confirmed = { - let client = clients.get(&other_client_id).ok_or(ShipError::ClientNotFound(id))?; - client.confirmed_trade - }; - { let this_client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; - this_client.confirmed_trade = true; + this_client.trade.as_mut().ok_or(TradeError::NotInTradeMenu)?.status = TradeStatus::Confirmed; } let both_confirmed = { let this_client = clients.get(&id).ok_or(ShipError::ClientNotFound(id))?; - let other_client = clients.get(&other_client_id).ok_or(ShipError::ClientNotFound(id))?; - this_client.confirmed_trade && other_client.confirmed_trade + let other_client_id = this_client.trade.as_ref().ok_or(TradeError::NotInTradeMenu)?.other_client; + let other_client = clients.get(&other_client_id).ok_or(ShipError::ClientNotFound(other_client_id))?; + + let this_client_trade = this_client.trade.as_ref().ok_or(TradeError::NotInTradeMenu)?; + let other_client_trade = other_client.trade.as_ref().ok_or(TradeError::NotInTradeMenu)?; + this_client_trade.status == TradeStatus::Confirmed && other_client_trade.status == TradeStatus::Confirmed }; - + if both_confirmed { - { - let this_client = clients.get(&id).ok_or(ShipError::ClientNotFound(id))?; - let other_client = clients.get(&other_client_id).ok_or(ShipError::ClientNotFound(id))?; - - let this_character_items = &this_client.trade.as_ref().ok_or(TradeError::NotInTradeMenu)?.1; - item_manager.send_items_to_other_player(entity_gateway, &this_client.character, &other_client.character, this_character_items).await?; - - let other_character_items = &other_client.trade.as_ref().ok_or(TradeError::NotInTradeMenu)?.1; - item_manager.send_items_to_other_player(entity_gateway, &other_client.character, &this_client.character, other_character_items).await?; - } - { + let this_client_trade = { let this_client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; + let this_client_trade = this_client.trade.as_ref().ok_or(TradeError::NotInTradeMenu)?.clone(); this_client.trade = None; - } - { - let other_client = clients.get_mut(&other_client_id).ok_or(ShipError::ClientNotFound(id))?; + this_client_trade + }; + let other_client_trade = { + let other_client = clients.get_mut(&this_client_trade.other_client).ok_or(ShipError::ClientNotFound(this_client_trade.other_client))?; + let other_client_trade = other_client.trade.as_ref().ok_or(TradeError::NotInTradeMenu)?.clone(); other_client.trade = None; - } + other_client_trade + }; + let this_client = clients.get(&id).ok_or(ShipError::ClientNotFound(id))?; + let other_client = clients.get(&this_client_trade.other_client).ok_or(ShipError::ClientNotFound(this_client_trade.other_client))?; - Ok(Box::new(None.into_iter())) + let this_local_client = client_location.get_local_client(id)?; + let other_local_client = client_location.get_local_client(this_client_trade.other_client)?; + + let traded_items = item_manager.trade_items( + entity_gateway, + (&this_local_client, &this_client.character, &this_client_trade.items), + (&other_local_client, &other_client.character, &other_client_trade.items) + ).await?; + + let clients_in_room = client_location.get_all_clients_by_client(id)?; + let traded_item_packets = traded_items + .into_iter() + .map(|item| { + match item.item_detail { + ItemToTradeDetail::Individual(item_detail) => { + [ + GameMessage::CreateItem(builder::message::create_individual_item(item.add_to, item.item_id, &item_detail).unwrap()), + GameMessage::PlayerNoLongerHasItem(builder::message::player_no_longer_has_item(item.remove_from, item.item_id, 1)) // TODO: amount = ? + ] + }, + ItemToTradeDetail::Stacked(tool, amount) => { + [ + GameMessage::CreateItem(builder::message::create_stacked_item(item.add_to, item.item_id, &tool, amount).unwrap()), + GameMessage::PlayerNoLongerHasItem(builder::message::player_no_longer_has_item(item.remove_from, item.item_id, amount as u32)) + ] + }, + } + }) + .flatten() + .map(move |packet| { + clients_in_room + .clone() + .into_iter() + .map(move |client| { + (client.client, SendShipPacket::Message(Message::new(packet.clone()))) + }) + }) + .flatten(); + Ok(Box::new(traded_item_packets)) } else { Ok(Box::new(None.into_iter())) From 82ef5ba2ea299fa79043bd98c3911cea2569ed93 Mon Sep 17 00:00:00 2001 From: jake Date: Fri, 12 Nov 2021 10:42:33 -0700 Subject: [PATCH 21/41] RIP ItemLocation fun while it lasted ItemLocation ceased to be the canonical place to store an item's location. replaced with ItemNote which basically covers the actual use case but without the enforcing of a location. --- src/bin/main.rs | 60 +----------- src/entity/gateway/entitygateway.rs | 2 +- src/entity/gateway/inmemory.rs | 6 +- src/entity/gateway/postgres/models.rs | 115 ++++++++++++---------- src/entity/gateway/postgres/postgres.rs | 63 ++---------- src/entity/item/mod.rs | 36 +++---- src/login/character.rs | 83 +++++++++------- src/ship/items/bank.rs | 24 ++--- src/ship/items/inventory.rs | 8 +- src/ship/items/manager.rs | 121 ++++++++--------------- tests/test_bank.rs | 125 ------------------------ tests/test_item_actions.rs | 27 ----- tests/test_item_pickup.rs | 39 -------- tests/test_item_use.rs | 15 --- tests/test_mags.rs | 19 ---- tests/test_rooms.rs | 6 -- tests/test_shops.rs | 3 - 17 files changed, 182 insertions(+), 570 deletions(-) diff --git a/src/bin/main.rs b/src/bin/main.rs index a9ed329..8d8f549 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -9,7 +9,7 @@ use elseware::entity::account::{NewUserAccountEntity, NewUserSettingsEntity}; #[allow(unused_imports)] use elseware::entity::gateway::{EntityGateway, InMemoryGateway, PostgresGateway}; use elseware::entity::character::NewCharacterEntity; -use elseware::entity::item::{NewItemEntity, ItemDetail, ItemLocation}; +use elseware::entity::item::{NewItemEntity, ItemDetail}; use elseware::common::interserver::AuthToken; use elseware::entity::item; @@ -87,10 +87,6 @@ fn main() { tekked: true, } ), - location: item::ItemLocation::Bank { - character_id: character.id, - name: item::BankName("".to_string()) - } }).await.unwrap(); } @@ -102,10 +98,6 @@ fn main() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Bank { - character_id: character.id, - name: item::BankName("".to_string()) - } }).await.unwrap(); } @@ -122,9 +114,6 @@ fn main() { tekked: false, } ), - location: ItemLocation::Inventory { - character_id: character.id, - } }).await.unwrap(); let item1 = entity_gateway.create_item( NewItemEntity { @@ -139,9 +128,6 @@ fn main() { tekked: true, } ), - location: ItemLocation::Inventory { - character_id: character.id, - } }).await.unwrap(); let item2_w = entity_gateway.create_item( NewItemEntity { @@ -156,9 +142,6 @@ fn main() { tekked: true, } ), - location: ItemLocation::Inventory { - character_id: character.id, - } }).await.unwrap(); let item3 = entity_gateway.create_item( NewItemEntity { @@ -173,9 +156,6 @@ fn main() { tekked: true, } ), - location: ItemLocation::Inventory { - character_id: character.id, - } }).await.unwrap(); let item4 = entity_gateway.create_item( NewItemEntity { @@ -190,17 +170,11 @@ fn main() { tekked: true, } ), - location: ItemLocation::Inventory { - character_id: character.id, - } }).await.unwrap(); let item5_m = entity_gateway.create_item( item::NewItemEntity { item: item::ItemDetail::Mag(item::mag::Mag::baby_mag(0)), - location: item::ItemLocation::Inventory { - character_id: character.id, - } }).await.unwrap(); for _ in 0..10usize { @@ -211,9 +185,6 @@ fn main() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::FedToMag { - mag: item5_m.id, - } }).await.unwrap(); entity_gateway.feed_mag(&item5_m.id, &fed_tool.id).await.unwrap(); } @@ -226,9 +197,6 @@ fn main() { tool: item::tool::ToolType::CellOfMag502, } ), - location: item::ItemLocation::Inventory { - character_id: character.id, - } }).await.unwrap(); let cell = entity_gateway.create_item( item::NewItemEntity { @@ -237,7 +205,6 @@ fn main() { tool: item::tool::ToolType::CellOfMag502, } ), - location: item::ItemLocation::Consumed, }).await.unwrap(); entity_gateway.use_mag_cell(&item5_m.id, &cell.id).await.unwrap(); @@ -254,10 +221,6 @@ fn main() { tekked: false, } ), - location: ItemLocation::Bank { - character_id: character.id, - name: item::BankName("".to_string()), - } }).await.unwrap(); let item7_a = entity_gateway.create_item( NewItemEntity { @@ -269,9 +232,6 @@ fn main() { slots: 4, } ), - location: ItemLocation::Inventory { - character_id: character.id, - } } ).await.unwrap(); let item8_s = entity_gateway.create_item( @@ -283,9 +243,6 @@ fn main() { evp: 0, } ), - location: ItemLocation::Inventory { - character_id: character.id, - } } ).await.unwrap(); let item9_u0 = entity_gateway.create_item( @@ -296,9 +253,6 @@ fn main() { modifier: Some(item::unit::UnitModifier::Minus), } ), - location: ItemLocation::Inventory { - character_id: character.id, - } } ).await.unwrap(); let item10_u1 = entity_gateway.create_item( @@ -309,9 +263,6 @@ fn main() { modifier: Some(item::unit::UnitModifier::Minus), } ), - location: ItemLocation::Inventory { - character_id: character.id, - } } ).await.unwrap(); let item11_u2 = entity_gateway.create_item( @@ -322,9 +273,6 @@ fn main() { modifier: Some(item::unit::UnitModifier::Minus), } ), - location: ItemLocation::Inventory { - character_id: character.id, - } } ).await.unwrap(); let item12_u3 = entity_gateway.create_item( @@ -335,9 +283,6 @@ fn main() { modifier: Some(item::unit::UnitModifier::Minus), } ), - location: ItemLocation::Inventory { - character_id: character.id, - } } ).await.unwrap(); let item13 = entity_gateway.create_item( @@ -345,9 +290,6 @@ fn main() { item: ItemDetail::Mag( item::mag::Mag::baby_mag(5) ), - location: ItemLocation::Inventory { - character_id: character.id, - } } ).await.unwrap(); diff --git a/src/entity/gateway/entitygateway.rs b/src/entity/gateway/entitygateway.rs index 27cc5d5..46e13ed 100644 --- a/src/entity/gateway/entitygateway.rs +++ b/src/entity/gateway/entitygateway.rs @@ -65,7 +65,7 @@ pub trait EntityGateway: Send + Sync + Clone { unimplemented!(); } - async fn change_item_location(&mut self, _item_id: &ItemEntityId, _item_location: ItemLocation) -> Result<(), GatewayError> { + async fn add_item_note(&mut self, _item_id: &ItemEntityId, _item_note: ItemNote) -> Result<(), GatewayError> { unimplemented!(); } diff --git a/src/entity/gateway/inmemory.rs b/src/entity/gateway/inmemory.rs index 4a95b52..24b6500 100644 --- a/src/entity/gateway/inmemory.rs +++ b/src/entity/gateway/inmemory.rs @@ -223,17 +223,13 @@ impl EntityGateway for InMemoryGateway { + 1; let new_item = ItemEntity { id: ItemEntityId(id), - location: item.location, item: item.item, }; items.insert(ItemEntityId(id), new_item.clone()); Ok(new_item) } - async fn change_item_location(&mut self, item_id: &ItemEntityId, item_location: ItemLocation) -> Result<(), GatewayError> { - if let Some(item_entity) = self.items.lock().unwrap().get_mut(item_id) { - item_entity.location = item_location - } + async fn add_item_note(&mut self, item_id: &ItemEntityId, item_note: ItemNote) -> Result<(), GatewayError> { Ok(()) } diff --git a/src/entity/gateway/postgres/models.rs b/src/entity/gateway/postgres/models.rs index 917aca8..f57e39d 100644 --- a/src/entity/gateway/postgres/models.rs +++ b/src/entity/gateway/postgres/models.rs @@ -571,22 +571,21 @@ pub struct PgItem { #[derive(Debug, Serialize, Deserialize)] -pub enum PgItemLocationDetail { - Inventory { +pub enum PgItemNoteDetail { + CharacterCreation { character_id: u32, }, - Bank { - character_id: u32, - name: String, - }, - LocalFloor { + EnemyDrop { character_id: u32, map_area: MapArea, x: f32, y: f32, z: f32, }, - SharedFloor { + Pickup { + character_id: u32, + }, + PlayerDrop { map_area: MapArea, x: f32, y: f32, @@ -596,73 +595,80 @@ pub enum PgItemLocationDetail { FedToMag { mag: u32, }, - Shop, + BoughtAtShop { + character_id: u32, + }, + SoldToShop, Trade { - id: i32, - character_to: i32, - character_from: i32, - } + id: u32, + character_to: u32, + character_from: u32, + }, } -impl From for PgItemLocationDetail { - fn from(other: ItemLocation) -> PgItemLocationDetail { +impl From for PgItemNoteDetail { + fn from(other: ItemNote) -> PgItemNoteDetail { match other { - ItemLocation::Inventory{character_id} => PgItemLocationDetail::Inventory{ - character_id: character_id.0 - }, - ItemLocation::Bank{character_id, name} => PgItemLocationDetail::Bank{ + ItemNote::CharacterCreation{character_id} => PgItemNoteDetail::CharacterCreation { character_id: character_id.0, - name: name.0 }, - ItemLocation::LocalFloor{character_id, map_area, x,y,z} => PgItemLocationDetail::LocalFloor{ + ItemNote::EnemyDrop{character_id, map_area, x, y, z} => PgItemNoteDetail::EnemyDrop { character_id: character_id.0, map_area, - x,y,z + x,y,z, + }, + ItemNote::Pickup{character_id} => PgItemNoteDetail::Pickup { + character_id: character_id.0, }, - ItemLocation::SharedFloor{map_area, x,y,z} => PgItemLocationDetail::SharedFloor{ + ItemNote::PlayerDrop{map_area, x, y, z} => PgItemNoteDetail::PlayerDrop { map_area, - x,y,z + x,y,z, }, - ItemLocation::Consumed => PgItemLocationDetail::Consumed, - ItemLocation::FedToMag{mag} => PgItemLocationDetail::FedToMag{ + ItemNote::Consumed => PgItemNoteDetail::Consumed, + ItemNote::FedToMag{mag} => PgItemNoteDetail::FedToMag{ mag: mag.0 }, - ItemLocation::Shop => PgItemLocationDetail::Shop, - ItemLocation::Trade{id, character_to, character_from} => PgItemLocationDetail::Trade { - id: id.0 as i32, - character_to: character_to.0 as i32, - character_from: character_from.0 as i32, + ItemNote::BoughtAtShop{character_id} => PgItemNoteDetail::BoughtAtShop { + character_id: character_id.0, + }, + ItemNote::SoldToShop => PgItemNoteDetail::SoldToShop, + ItemNote::Trade{id, character_to, character_from} => PgItemNoteDetail::Trade { + id: id.0, + character_to: character_to.0, + character_from: character_from.0, } } } } -impl From for ItemLocation { - fn from(other: PgItemLocationDetail) -> ItemLocation { +impl From for ItemNote { + fn from(other: PgItemNoteDetail) -> ItemNote { match other { - PgItemLocationDetail::Inventory{character_id} => ItemLocation::Inventory{ - character_id: CharacterEntityId(character_id) - }, - PgItemLocationDetail::Bank{character_id, name} => ItemLocation::Bank{ - character_id: CharacterEntityId(character_id), - name: BankName(name) + PgItemNoteDetail::CharacterCreation{character_id} => ItemNote::CharacterCreation { + character_id: CharacterEntityId(character_id as u32), }, - PgItemLocationDetail::LocalFloor{character_id, map_area, x,y,z} => ItemLocation::LocalFloor{ - character_id: CharacterEntityId(character_id), + PgItemNoteDetail::EnemyDrop{character_id, map_area, x, y, z} => ItemNote::EnemyDrop { + character_id: CharacterEntityId(character_id as u32), map_area, - x,y,z + x,y,z, + }, + PgItemNoteDetail::Pickup{character_id} => ItemNote::Pickup { + character_id: CharacterEntityId(character_id as u32), }, - PgItemLocationDetail::SharedFloor{map_area, x,y,z} => ItemLocation::SharedFloor{ + PgItemNoteDetail::PlayerDrop{map_area, x, y, z} => ItemNote::PlayerDrop { map_area, - x,y,z + x,y,z, }, - PgItemLocationDetail::Consumed => ItemLocation::Consumed, - PgItemLocationDetail::FedToMag{mag} => ItemLocation::FedToMag{ + PgItemNoteDetail::Consumed => ItemNote::Consumed, + PgItemNoteDetail::FedToMag{mag} => ItemNote::FedToMag{ mag: ItemEntityId(mag) }, - PgItemLocationDetail::Shop => ItemLocation::Shop, - PgItemLocationDetail::Trade {id, character_to, character_from} => ItemLocation::Trade { - id: TradeId(id as usize), + PgItemNoteDetail::BoughtAtShop{character_id} => ItemNote::BoughtAtShop { + character_id: CharacterEntityId(character_id), + }, + PgItemNoteDetail::SoldToShop => ItemNote::SoldToShop, + PgItemNoteDetail::Trade {id, character_to, character_from} => ItemNote::Trade { + id: TradeId(id as u32), character_to: CharacterEntityId(character_to as u32), character_from: CharacterEntityId(character_from as u32), } @@ -672,9 +678,9 @@ impl From for ItemLocation { #[derive(Debug, sqlx::FromRow)] -pub struct PgItemLocation { +pub struct PgItemNote { //pub id: i32, - pub location: sqlx::types::Json, + pub note: sqlx::types::Json, created_at: chrono::DateTime, } @@ -723,19 +729,20 @@ pub struct PgItemEntity { pub item: sqlx::types::Json, } +/* #[derive(Debug, sqlx::FromRow)] pub struct PgItemWithLocation { pub id: i32, pub item: sqlx::types::Json, pub location: sqlx::types::Json, } +*/ -impl From for ItemEntity { - fn from(other: PgItemWithLocation) -> ItemEntity { +impl From for ItemEntity { + fn from(other: PgItemEntity) -> ItemEntity { ItemEntity { id: ItemEntityId(other.id as u32), item: other.item.0.into(), - location: other.location.0.into(), } } } diff --git a/src/entity/gateway/postgres/postgres.rs b/src/entity/gateway/postgres/postgres.rs index 4f872c4..3c17246 100644 --- a/src/entity/gateway/postgres/postgres.rs +++ b/src/entity/gateway/postgres/postgres.rs @@ -44,7 +44,7 @@ impl PostgresGateway { } async fn apply_item_modifications(&self, item: ItemEntity) -> ItemEntity { - let ItemEntity {id, item, location} = item; + let ItemEntity {id, item} = item; let item = match item { ItemDetail::Weapon(mut weapon) => { @@ -101,7 +101,6 @@ impl PostgresGateway { ItemEntity { id, item, - location } } } @@ -294,64 +293,18 @@ impl EntityGateway for PostgresGateway { let new_item = sqlx::query_as::<_, PgItem>("insert into item (item) values ($1) returning *;") .bind(sqlx::types::Json(PgItemDetail::from(item.item))) .fetch_one(&mut tx).await?; - let location = sqlx::query_as::<_, PgItemLocation>("insert into item_location (item, location) values ($1, $2) returning *") - .bind(new_item.id) - .bind(sqlx::types::Json(PgItemLocationDetail::from(item.location))) - .fetch_one(&mut tx).await?; tx.commit().await?; Ok(ItemEntity { id: ItemEntityId(new_item.id as u32), item: new_item.item.0.into(), - location: location.location.0.into(), }) - - /* - let mut tx = self.pool.begin().await?; - let new_item = sqlx::query_as::<_, PgItem>("insert into item (item) values ($1) returning *;") - .bind(sqlx::types::Json(PgItemDetail::from(item.item))) - .fetch_one(&mut tx).await?; - let location = if let ItemLocation::Inventory{slot, ..} = &item.location { - sqlx::query("insert into item_location (item, location) values ($1, $2)") - .bind(new_item.id) - .bind(sqlx::types::Json(PgItemLocationDetail::from(item.location.clone()))) - .execute(&mut tx).await?; - sqlx::query("insert into inventory_slot (item, slot) values ($1, $2)") - .bind(new_item.id) - .bind(*slot as i32) - .execute(&mut tx).await?; - sqlx::query_as::<_, PgItemLocation>(r#"select - item_location.item, - jsonb_set(item_location.location, '{Inventory,slot}', inventory_slot.slot::text::jsonb) as location, - item_location.created_at - from item_location - join item on item.id = item_location.item - join inventory_slot on inventory_slot.item = item.id - where item.id = $1 - order by item_location.created_at - limit 1"#) - .bind(new_item.id) - .fetch_one(&mut tx).await? - } - else { - sqlx::query_as::<_, PgItemLocation>("insert into item_location (item, location) values ($1, $2) returning *") - .bind(new_item.id) - .bind(sqlx::types::Json(PgItemLocationDetail::from(item.location))) - .fetch_one(&mut tx).await? - }; - tx.commit().await?; - Ok(ItemEntity { - id: ItemEntityId(new_item.id as u32), - item: new_item.item.0.into(), - location: location.location.0.into(), - }) - */ } - async fn change_item_location(&mut self, item_id: &ItemEntityId, item_location: ItemLocation) -> Result<(), GatewayError> { - sqlx::query("insert into item_location (item, location) values ($1, $2)") + async fn add_item_note(&mut self, item_id: &ItemEntityId, item_note: ItemNote) -> Result<(), GatewayError> { + sqlx::query("insert into item_note(item, note) values ($1, $2)") .bind(item_id.0) - .bind(sqlx::types::Json(PgItemLocationDetail::from(item_location))) + .bind(sqlx::types::Json(PgItemNoteDetail::from(item_note))) .execute(&self.pool).await?; Ok(()) @@ -465,7 +418,7 @@ impl EntityGateway for PostgresGateway { for inv_item in inventory.items.0.into_iter() { match inv_item { PgInventoryItemEntity::Individual(item) => { - let entity = sqlx::query_as::<_, PgItemWithLocation>("select item.id, item.item, item_location.location from item join item_location on item.id = item_location.item where id = $1") + let entity = sqlx::query_as::<_, PgItemEntity>("select item.id, item.item from item where id = $1") .bind(item) .fetch_one(&self.pool).await .map(|item| item.into()) @@ -476,7 +429,7 @@ impl EntityGateway for PostgresGateway { PgInventoryItemEntity::Stacked(items) => { let mut stacked_item = Vec::new(); for s_item in items { - stacked_item.push(sqlx::query_as::<_, PgItemWithLocation>("select item.id, item.item, item_location.location from item join item_location on item.id = item_location.item where id = $1") + stacked_item.push(sqlx::query_as::<_, PgItemEntity>("select item.id, item.item from item where id = $1") .bind(s_item) .fetch_one(&self.pool).await .map(|item| item.into()) @@ -501,7 +454,7 @@ impl EntityGateway for PostgresGateway { for bank_item in bank.items.0.into_iter() { match bank_item { PgInventoryItemEntity::Individual(item) => { - let entity = sqlx::query_as::<_, PgItemWithLocation>("select item.id, item.item, item_location.location from item join item_location on item.id = item_location.item where id = $1") + let entity = sqlx::query_as::<_, PgItemEntity>("select item.id, item.item from item where id = $1") .bind(item) .fetch_one(&self.pool).await .map(|item| item.into()) @@ -512,7 +465,7 @@ impl EntityGateway for PostgresGateway { PgInventoryItemEntity::Stacked(items) => { let mut stacked_item = Vec::new(); for s_item in items { - stacked_item.push(sqlx::query_as::<_, PgItemWithLocation>("select item.id, item.item, item_location.location from item join item_location on item.id = item_location.item where id = $1") + stacked_item.push(sqlx::query_as::<_, PgItemEntity>("select item.id, item.item from item where id = $1") .bind(s_item) .fetch_one(&self.pool).await .map(|item| item.into()) diff --git a/src/entity/item/mod.rs b/src/entity/item/mod.rs index b4260a3..5c320b6 100644 --- a/src/entity/item/mod.rs +++ b/src/entity/item/mod.rs @@ -19,24 +19,27 @@ pub struct ItemEntityId(pub u32); pub struct ItemId(u32); #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub struct BankName(pub String); +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] +pub struct TradeId(pub u32); #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub enum ItemLocation { - Inventory { - character_id: CharacterEntityId, - }, - Bank { +pub enum ItemNote { + CharacterCreation { character_id: CharacterEntityId, - name: BankName, }, - LocalFloor { + EnemyDrop { character_id: CharacterEntityId, + //monster_type: MonsterType, + //droprate: f32, map_area: MapArea, x: f32, y: f32, z: f32, }, - SharedFloor { + Pickup { + character_id: CharacterEntityId, + }, + PlayerDrop { map_area: MapArea, x: f32, y: f32, @@ -46,20 +49,15 @@ pub enum ItemLocation { FedToMag { mag: ItemEntityId, }, - Shop, + BoughtAtShop { + character_id: CharacterEntityId, + }, + SoldToShop, Trade { - //id: TradeId, + id: TradeId, character_to: CharacterEntityId, character_from: CharacterEntityId, }, - /*Destroyed { - // marks an item that has been consumed in some way - }, - Transformed { - item_id, - change_event - } -*/ } #[derive(Debug, Clone, PartialEq)] @@ -168,14 +166,12 @@ impl ItemDetail { #[derive(Clone, Debug)] pub struct NewItemEntity { - pub location: ItemLocation, pub item: ItemDetail, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ItemEntity { pub id: ItemEntityId, - pub location: ItemLocation, pub item: ItemDetail, } diff --git a/src/login/character.rs b/src/login/character.rs index 12e874a..b3afa67 100644 --- a/src/login/character.rs +++ b/src/login/character.rs @@ -20,7 +20,7 @@ use libpso::{utf8_to_array, utf8_to_utf16_array}; use crate::entity::gateway::{EntityGateway, GatewayError}; use crate::entity::account::{UserAccountId, UserAccountEntity, NewUserSettingsEntity, USERFLAG_NEWCHAR, USERFLAG_DRESSINGROOM}; -use crate::entity::item::{NewItemEntity, ItemDetail, ItemLocation, InventoryItemEntity, InventoryEntity, BankEntity, BankName, EquippedEntity}; +use crate::entity::item::{NewItemEntity, ItemDetail, ItemNote, InventoryItemEntity, InventoryEntity, BankEntity, BankName, EquippedEntity}; use crate::entity::item::weapon::Weapon; use crate::entity::item::armor::Armor; use crate::entity::item::tech::Technique; @@ -220,10 +220,11 @@ async fn new_character(entity_gateway: &mut EG, user: &UserAc special: None, attrs: [None; 3], tekked: true, - }), - location: ItemLocation::Inventory { - character_id: character.id, - }}).await?; + })}).await?; + + entity_gateway.add_item_note(&weapon.id, ItemNote::CharacterCreation { + character_id: character.id, + }).await?; let armor = entity_gateway.create_item( NewItemEntity { @@ -233,10 +234,11 @@ async fn new_character(entity_gateway: &mut EG, user: &UserAc dfp: 0, evp: 0, slots: 0, - }), - location: ItemLocation::Inventory { - character_id: character.id, - }}).await?; + })}).await?; + + entity_gateway.add_item_note(&armor.id, ItemNote::CharacterCreation { + character_id: character.id, + }).await?; let mut mag = { if character.char_class.is_android() { @@ -249,35 +251,40 @@ async fn new_character(entity_gateway: &mut EG, user: &UserAc let mag = entity_gateway.create_item( NewItemEntity { item: ItemDetail::Mag(mag), - location: ItemLocation::Inventory { - character_id: character.id, - }}).await?; - - let mut monomates = Vec::new(); - for _ in 0..4usize { - monomates.push(entity_gateway.create_item( - NewItemEntity { - item: ItemDetail::Tool ( - Tool { - tool: item::tool::ToolType::Monomate, - }), - location: ItemLocation::Inventory { - character_id: character.id, - }}).await?) - } - - let mut monofluids = Vec::new(); - for _ in 0..4usize { - monofluids.push(entity_gateway.create_item( - NewItemEntity { - item: ItemDetail::Tool ( - Tool { - tool: item::tool::ToolType::Monofluid, - }), - location: ItemLocation::Inventory { - character_id: character.id, - }}).await?) - } + }).await?; + + entity_gateway.add_item_note(&mag.id, ItemNote::CharacterCreation { + character_id: character.id, + }).await?; + + let (monomates, monofluids) = futures::future::join_all((0..4usize).map(|_| { + let mut eg = entity_gateway.clone(); + let character_id = character.id; + return async move { + let monomate = eg.create_item( + NewItemEntity { + item: ItemDetail::Tool ( + Tool { + tool: item::tool::ToolType::Monomate, + })}).await?; + + eg.add_item_note(&monomate.id, ItemNote::CharacterCreation { + character_id + }).await?; + + let monofluid = eg.create_item( + NewItemEntity { + item: ItemDetail::Tool ( + Tool { + tool: item::tool::ToolType::Monofluid, + })}).await?; + + eg.add_item_note(&monofluid.id, ItemNote::CharacterCreation { + character_id + }).await?; + + Ok((monomate, monofluid)) + }})).await.into_iter().collect::, GatewayError>>()?.into_iter().unzip(); let inventory = InventoryEntity { items: vec![InventoryItemEntity::Individual(weapon.clone()), InventoryItemEntity::Individual(armor.clone()), InventoryItemEntity::Individual(mag.clone()), diff --git a/src/ship/items/bank.rs b/src/ship/items/bank.rs index f3d5d10..b35a758 100644 --- a/src/ship/items/bank.rs +++ b/src/ship/items/bank.rs @@ -1,6 +1,6 @@ use crate::ship::items::ClientItemId; use libpso::character::character;//::InventoryItem; -use crate::entity::item::{ItemEntityId, ItemEntity, ItemDetail, ItemLocation, BankEntity, BankItemEntity, BankName}; +use crate::entity::item::{ItemEntityId, ItemEntity, ItemDetail, BankEntity, BankItemEntity, BankName}; use crate::entity::character::CharacterEntityId; use crate::entity::item::tool::Tool; use crate::ship::items::inventory::{InventoryItemHandle, InventoryItem}; @@ -301,26 +301,18 @@ impl CharacterBank { BankItem::Individual(item) => { BankItemEntity::Individual(ItemEntity { id: item.entity_id, - location: ItemLocation::Bank { - character_id: *character_id, - name: bank_name.clone(), - }, item: item.item.clone(), }) }, BankItem::Stacked(items) => { BankItemEntity::Stacked(items.entity_ids.iter() - .map(|id| { - ItemEntity { - id: *id, - location: ItemLocation::Bank { - character_id: *character_id, - name: bank_name.clone(), - }, - item: ItemDetail::Tool(items.tool) - } - }) - .collect()) + .map(|id| { + ItemEntity { + id: *id, + item: ItemDetail::Tool(items.tool) + } + }) + .collect()) }, } }) diff --git a/src/ship/items/inventory.rs b/src/ship/items/inventory.rs index 314eeab..c949d74 100644 --- a/src/ship/items/inventory.rs +++ b/src/ship/items/inventory.rs @@ -2,7 +2,7 @@ use std::cmp::Ordering; use thiserror::Error; use libpso::character::character;//::InventoryItem; use crate::entity::character::CharacterEntityId; -use crate::entity::item::{ItemEntityId, ItemDetail, ItemEntity, ItemType, ItemLocation, InventoryEntity, InventoryItemEntity, EquippedEntity}; +use crate::entity::item::{ItemEntityId, ItemDetail, ItemEntity, ItemType, InventoryEntity, InventoryItemEntity, EquippedEntity}; use crate::entity::item::tool::Tool; use crate::entity::item::mag::Mag; use crate::entity::item::weapon::Weapon; @@ -815,9 +815,6 @@ impl CharacterInventory { InventoryItem::Individual(item) => { InventoryItemEntity::Individual(ItemEntity { id: item.entity_id, - location: ItemLocation::Inventory { - character_id: *character_id, - }, item: item.item.clone(), }) }, @@ -826,9 +823,6 @@ impl CharacterInventory { .map(|id| { ItemEntity { id: *id, - location: ItemLocation::Inventory { - character_id: *character_id, - }, item: ItemDetail::Tool(items.tool) } }) diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs index 773819c..701a4aa 100644 --- a/src/ship/items/manager.rs +++ b/src/ship/items/manager.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use thiserror::Error; use crate::entity::gateway::{EntityGateway, GatewayError}; use crate::entity::character::{CharacterEntity, CharacterEntityId, TechLevel}; -use crate::entity::item::{ItemDetail, ItemLocation, BankName}; +use crate::entity::item::{ItemDetail, ItemNote, BankName}; use crate::entity::item::{Meseta, NewItemEntity, ItemEntity, ItemEntityId, InventoryItemEntity, BankItemEntity}; use crate::entity::item::tool::{Tool, ToolType}; use crate::entity::item::weapon; @@ -366,13 +366,13 @@ impl ItemManager { 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?; + entity_gateway.add_item_note(&entity.id, ItemNote::EnemyDrop { + character_id: character.id, + map_area: item_drop.map_area, + x: item_drop.x, + y: item_drop.y, + z: item_drop.z, }).await?; FloorItem::Individual(IndividualFloorItem { entity_id: entity.id, @@ -387,13 +387,13 @@ impl ItemManager { 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?; + entity_gateway.add_item_note(&entity.id, ItemNote::EnemyDrop { + character_id: character.id, + map_area: item_drop.map_area, + x: item_drop.x, + y: item_drop.y, + z: item_drop.z, }).await?; FloorItem::Stacked(StackedFloorItem { entity_ids: vec![entity.id], @@ -438,9 +438,9 @@ impl ItemManager { 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( + entity_gateway.add_item_note( &individual_floor_item.entity_id, - ItemLocation::SharedFloor { + ItemNote::PlayerDrop { map_area: item_drop_location.0, x: item_drop_location.1, y: item_drop_location.2, @@ -451,9 +451,9 @@ impl ItemManager { 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_gateway.add_item_note( entity_id, - ItemLocation::SharedFloor { + ItemNote::PlayerDrop { map_area: item_drop_location.0, x: item_drop_location.1, y: item_drop_location.2, @@ -515,9 +515,9 @@ impl ItemManager { .ok_or(ItemManagerError::CouldNotSplitItem(item_id))?; for entity_id in &stacked_floor_item.entity_ids { - entity_gateway.change_item_location( + entity_gateway.add_item_note( entity_id, - ItemLocation::SharedFloor { + ItemNote::PlayerDrop { map_area: drop_location.map_area, x: drop_location.x, y: 0.0, @@ -547,8 +547,8 @@ impl ItemManager { }; for entity_id in consumed_item.entity_ids() { - entity_gateway.change_item_location(&entity_id, - ItemLocation::Consumed).await?; + entity_gateway.add_item_note(&entity_id, + ItemNote::Consumed).await?; } entity_gateway.set_character_inventory(&character.id, &inventory.as_inventory_entity(&character.id)).await?; @@ -569,25 +569,6 @@ impl ItemManager { 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?; - } - } - } - entity_gateway.set_character_inventory(&character.id, &inventory.as_inventory_entity(&character.id)).await?; entity_gateway.set_character_bank(&character.id, &bank.as_bank_entity(&character.id, &BankName("".into())), BankName("".into())).await?; Ok(()) @@ -608,23 +589,6 @@ impl ItemManager { let item_to_withdraw = bank.get_item_handle_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?; let inventory_item_slot = { 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, - }).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, - }).await?; - } - } - } inventory_item.1 }; @@ -662,7 +626,7 @@ impl ItemManager { for entity_id in consumed_tool.entity_ids() { entity_gateway.feed_mag(&individual_item.entity_id, &entity_id).await?; - entity_gateway.change_item_location(&entity_id, ItemLocation::FedToMag { + entity_gateway.add_item_note(&entity_id, ItemNote::FedToMag { mag: individual_item.entity_id, }).await?; } @@ -800,10 +764,13 @@ impl ItemManager { if tool.is_stackable() { let mut item_entities = Vec::new(); for _ in 0..amount { - item_entities.push(entity_gateway.create_item(NewItemEntity { - location: ItemLocation::Shop, + let item_entity = entity_gateway.create_item(NewItemEntity { item: ItemDetail::Tool(tool), - }).await?); + }).await?; + entity_gateway.add_item_note(&item_entity.id, ItemNote::BoughtAtShop { + character_id: character.id, + }).await?; + item_entities.push(item_entity); } let floor_item = StackedFloorItem { entity_ids: item_entities.into_iter().map(|i| i.id).collect(), @@ -817,21 +784,18 @@ impl ItemManager { }; let item_id = { let (picked_up_item, _slot) = inventory.pick_up_stacked_floor_item(&floor_item).ok_or(ItemManagerError::CouldNotAddBoughtItemToInventory)?; - for entity_id in &picked_up_item.entity_ids { - entity_gateway.change_item_location(entity_id, - ItemLocation::Inventory { - character_id: character.id, - }).await?; - } picked_up_item.item_id }; inventory.get_item_by_id(item_id).ok_or(ItemManagerError::ItemIdNotInInventory(item_id))? } else { let item_entity = entity_gateway.create_item(NewItemEntity { - location: ItemLocation::Shop, item: ItemDetail::Tool(tool), }).await?; + entity_gateway.add_item_note(&item_entity.id, ItemNote::BoughtAtShop { + character_id: character.id, + }).await?; + let floor_item = IndividualFloorItem { entity_id: item_entity.id, item_id, @@ -844,10 +808,6 @@ impl ItemManager { }; let item_id = { let (picked_up_item, _slot) = inventory.pick_up_individual_floor_item(&floor_item).ok_or(ItemManagerError::CouldNotAddBoughtItemToInventory)?; - entity_gateway.change_item_location(&picked_up_item.entity_id, - ItemLocation::Inventory { - character_id: character.id, - }).await?; picked_up_item.item_id }; inventory.get_item_by_id(item_id).ok_or(ItemManagerError::ItemIdNotInInventory(item_id))? @@ -855,9 +815,11 @@ impl ItemManager { }, item_detail => { let item_entity = entity_gateway.create_item(NewItemEntity { - location: ItemLocation::Shop, item: item_detail.clone(), }).await?; + entity_gateway.add_item_note(&item_entity.id, ItemNote::BoughtAtShop { + character_id: character.id, + }).await?; let floor_item = IndividualFloorItem { entity_id: item_entity.id, item_id, @@ -870,10 +832,6 @@ impl ItemManager { }; let item_id = { let (picked_up_item, _slot) = inventory.pick_up_individual_floor_item(&floor_item).ok_or(ItemManagerError::CouldNotAddBoughtItemToInventory)?; - entity_gateway.change_item_location(&picked_up_item.entity_id, - ItemLocation::Inventory { - character_id: character.id, - }).await?; picked_up_item.item_id }; inventory.get_item_by_id(item_id).ok_or(ItemManagerError::ItemIdNotInInventory(item_id))? @@ -1121,9 +1079,10 @@ impl ItemAction for AddIndividualFloorItemToInventory { let inventory = item_manager.character_inventory.get_mut(&self.character.id).ok_or(ItemManagerError::NoCharacter(self.character.id))?; let inv_item = inventory.add_individual_floor_item(&self.item); - entity_gateway.change_item_location( + + entity_gateway.add_item_note( &self.item.entity_id, - ItemLocation::Inventory { + ItemNote::Pickup { character_id: self.character.id, } ).await?; diff --git a/tests/test_bank.rs b/tests/test_bank.rs index c0a1d64..e55d153 100644 --- a/tests/test_bank.rs +++ b/tests/test_bank.rs @@ -29,10 +29,6 @@ async fn test_bank_items_sent_in_character_login() { tekked: true, } ), - location: item::ItemLocation::Bank { - character_id: char1.id, - name: item::BankName("".to_string()) - } }).await.unwrap(); entity_gateway.set_character_bank(&char1.id, &item::BankEntity::new(vec![item]), item::BankName("".into())).await.unwrap(); @@ -70,10 +66,6 @@ async fn test_request_bank_items() { tekked: true, } ), - location: item::ItemLocation::Bank { - character_id: char1.id, - name: item::BankName("".to_string()) - } }).await.unwrap()); } @@ -118,10 +110,6 @@ async fn test_request_stacked_bank_items() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Bank { - character_id: char1.id, - name: item::BankName("".to_string()) - } }).await.unwrap()); } @@ -167,10 +155,6 @@ async fn test_request_bank_items_sorted() { tekked: true, } ), - location: item::ItemLocation::Bank { - character_id: char1.id, - name: item::BankName("".to_string()) - } }).await.unwrap(); let monomate = entity_gateway.create_item( item::NewItemEntity { @@ -179,10 +163,6 @@ async fn test_request_bank_items_sorted() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Bank { - character_id: char1.id, - name: item::BankName("".to_string()) - } }).await.unwrap(); let item2 = entity_gateway.create_item( item::NewItemEntity { @@ -195,10 +175,6 @@ async fn test_request_bank_items_sorted() { tekked: true, } ), - location: item::ItemLocation::Bank { - character_id: char1.id, - name: item::BankName("".to_string()) - } }).await.unwrap(); let bank = vec![item::BankItemEntity::Individual(item1), vec![monomate].into(), item2.into()]; @@ -245,9 +221,6 @@ async fn test_deposit_individual_item() { tekked: true, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap(); let item1 = entity_gateway.create_item( item::NewItemEntity { @@ -260,9 +233,6 @@ async fn test_deposit_individual_item() { tekked: true, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap(); entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![item0, item1])).await.unwrap(); @@ -322,9 +292,6 @@ async fn test_deposit_stacked_item() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } @@ -386,9 +353,6 @@ async fn test_deposit_partial_stacked_item() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } @@ -460,9 +424,6 @@ async fn test_deposit_stacked_item_with_stack_already_in_bank() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); bank_monomates.push(entity_gateway.create_item( @@ -472,10 +433,6 @@ async fn test_deposit_stacked_item_with_stack_already_in_bank() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Bank { - character_id: char1.id, - name: item::BankName("".into()), - } }).await.unwrap()); } @@ -537,9 +494,6 @@ async fn test_deposit_stacked_item_with_full_stack_in_bank() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } @@ -552,10 +506,6 @@ async fn test_deposit_stacked_item_with_full_stack_in_bank() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Bank { - character_id: char1.id, - name: item::BankName("".into()), - } }).await.unwrap()); } @@ -619,9 +569,6 @@ async fn test_deposit_individual_item_in_full_bank() { tekked: true, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); let mut bank = Vec::new(); @@ -637,10 +584,6 @@ async fn test_deposit_individual_item_in_full_bank() { tekked: true, } ), - location: item::ItemLocation::Bank { - character_id: char1.id, - name: item::BankName("".to_string()) - } }).await.unwrap()); } @@ -697,9 +640,6 @@ async fn test_deposit_stacked_item_in_full_bank() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } @@ -716,10 +656,6 @@ async fn test_deposit_stacked_item_in_full_bank() { tekked: true, } ), - location: item::ItemLocation::Bank { - character_id: char1.id, - name: item::BankName("".to_string()) - } }).await.unwrap()); } @@ -777,9 +713,6 @@ async fn test_deposit_stacked_item_in_full_bank_with_partial_stack() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } @@ -792,10 +725,6 @@ async fn test_deposit_stacked_item_in_full_bank_with_partial_stack() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Bank { - character_id: char1.id, - name: item::BankName("".to_string()) - } }).await.unwrap()); } @@ -812,10 +741,6 @@ async fn test_deposit_stacked_item_in_full_bank_with_partial_stack() { tekked: true, } ), - location: item::ItemLocation::Bank { - character_id: char1.id, - name: item::BankName("".to_string()) - } }).await.unwrap().into()); } almost_full_bank.push(bank_monomates.into()); @@ -990,10 +915,6 @@ async fn test_withdraw_individual_item() { tekked: true, } ), - location: item::ItemLocation::Bank { - character_id: char1.id, - name: item::BankName("".to_string()) - } }).await.unwrap()); entity_gateway.set_character_bank(&char1.id, &item::BankEntity::new(bank), item::BankName("".into())).await.unwrap(); @@ -1053,10 +974,6 @@ async fn test_withdraw_stacked_item() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Bank { - character_id: char1.id, - name: item::BankName("".to_string()) - } }).await.unwrap()); } @@ -1117,10 +1034,6 @@ async fn test_withdraw_partial_stacked_item() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Bank { - character_id: char1.id, - name: item::BankName("".into()) - } }).await.unwrap()); } entity_gateway.set_character_bank(&char1.id, &item::BankEntity::new(vec![monomates]), item::BankName("".into())).await.unwrap(); @@ -1188,9 +1101,6 @@ async fn test_withdraw_stacked_item_with_stack_already_in_inventory() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); bank_monomates.push(entity_gateway.create_item( @@ -1200,10 +1110,6 @@ async fn test_withdraw_stacked_item_with_stack_already_in_inventory() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Bank { - character_id: char1.id, - name: item::BankName("".into()), - } }).await.unwrap()); } @@ -1267,10 +1173,6 @@ async fn test_withdraw_stacked_item_with_full_stack_in_inventory() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Bank { - character_id: char1.id, - name: item::BankName("".into()), - } }).await.unwrap()); } @@ -1283,9 +1185,6 @@ async fn test_withdraw_stacked_item_with_full_stack_in_inventory() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } @@ -1349,10 +1248,6 @@ async fn test_withdraw_individual_item_in_full_inventory() { tekked: true, } ), - location: item::ItemLocation::Bank { - character_id: char1.id, - name: item::BankName("".to_string()) - } }).await.unwrap()); let mut inventory = Vec::new(); @@ -1368,9 +1263,6 @@ async fn test_withdraw_individual_item_in_full_inventory() { tekked: true, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } @@ -1423,10 +1315,6 @@ async fn test_withdraw_stacked_item_in_full_inventory() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Bank { - character_id: char1.id, - name: item::BankName("".to_string()) - } }).await.unwrap()); } @@ -1443,9 +1331,6 @@ async fn test_withdraw_stacked_item_in_full_inventory() { tekked: true, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } @@ -1504,10 +1389,6 @@ async fn test_withdraw_stacked_item_in_full_inventory_with_partial_stack() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Bank { - character_id: char1.id, - name: item::BankName("".to_string()) - } }).await.unwrap()); } entity_gateway.set_character_bank(&char1.id, &item::BankEntity::new(vec![bank_item]), item::BankName("".into())).await.unwrap(); @@ -1525,9 +1406,6 @@ async fn test_withdraw_stacked_item_in_full_inventory_with_partial_stack() { tekked: true, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap().into()); } @@ -1540,9 +1418,6 @@ async fn test_withdraw_stacked_item_in_full_inventory_with_partial_stack() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } items.push(item::InventoryItemEntity::Stacked(item29)); diff --git a/tests/test_item_actions.rs b/tests/test_item_actions.rs index fa27041..456a505 100644 --- a/tests/test_item_actions.rs +++ b/tests/test_item_actions.rs @@ -26,9 +26,6 @@ async fn test_equip_unit_from_equip_menu() { evp: 0, slots: 4, }), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); p1_inv.push(entity_gateway.create_item( @@ -38,9 +35,6 @@ async fn test_equip_unit_from_equip_menu() { unit: item::unit::UnitType::KnightPower, modifier: None, }), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); p1_inv.push(entity_gateway.create_item( @@ -50,9 +44,6 @@ async fn test_equip_unit_from_equip_menu() { unit: item::unit::UnitType::KnightPower, modifier: Some(item::unit::UnitModifier::Plus), }), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); let equipped = item::EquippedEntity { @@ -112,9 +103,6 @@ async fn test_unequip_armor_with_units() { evp: 0, slots: 4, }), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); p1_inv.push(entity_gateway.create_item( @@ -124,9 +112,6 @@ async fn test_unequip_armor_with_units() { unit: item::unit::UnitType::KnightPower, modifier: None, }), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); p1_inv.push(entity_gateway.create_item( @@ -136,9 +121,6 @@ async fn test_unequip_armor_with_units() { unit: item::unit::UnitType::KnightPower, modifier: Some(item::unit::UnitModifier::Plus), }), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); let equipped = item::EquippedEntity { @@ -189,9 +171,6 @@ async fn test_sort_items() { evp: 0, slots: 4, }), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); p1_inv.push(entity_gateway.create_item( @@ -201,9 +180,6 @@ async fn test_sort_items() { unit: item::unit::UnitType::KnightPower, modifier: None, }), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); p1_inv.push(entity_gateway.create_item( @@ -213,9 +189,6 @@ async fn test_sort_items() { unit: item::unit::UnitType::KnightPower, modifier: Some(item::unit::UnitModifier::Plus), }), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); diff --git a/tests/test_item_pickup.rs b/tests/test_item_pickup.rs index e1600d8..d5287d0 100644 --- a/tests/test_item_pickup.rs +++ b/tests/test_item_pickup.rs @@ -29,9 +29,6 @@ async fn test_pick_up_individual_item() { tekked: true, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); @@ -99,9 +96,6 @@ async fn test_pick_up_item_stack_of_items_already_in_inventory() { tool: item::tool::ToolType::Monomate } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); let mut p2_items = Vec::new(); @@ -115,9 +109,6 @@ async fn test_pick_up_item_stack_of_items_already_in_inventory() { tool: tool } ), - location: item::ItemLocation::Inventory { - character_id: char2.id, - } }).await.unwrap()); } p2_items.push(item); @@ -181,9 +172,6 @@ async fn test_pick_up_item_stack_of_items_not_already_held() { tool: item::tool::ToolType::Monomate } ), - location: item::ItemLocation::Inventory { - character_id: char2.id, - } }).await.unwrap()); entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(vec![p2_monomate])).await.unwrap(); @@ -248,9 +236,6 @@ async fn test_pick_up_meseta_when_inventory_full() { tekked: true, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } @@ -327,9 +312,6 @@ async fn test_pick_up_partial_stacked_item_when_inventory_is_otherwise_full() { tekked: true, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap().into()); } @@ -340,9 +322,6 @@ async fn test_pick_up_partial_stacked_item_when_inventory_is_otherwise_full() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()])); let mut p2_monomates = Vec::new(); @@ -353,9 +332,6 @@ async fn test_pick_up_partial_stacked_item_when_inventory_is_otherwise_full() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Inventory { - character_id: char2.id, - } }).await.unwrap()); entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); @@ -419,9 +395,6 @@ async fn test_can_not_pick_up_item_when_inventory_full() { tekked: true, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } @@ -437,9 +410,6 @@ async fn test_can_not_pick_up_item_when_inventory_full() { tekked: true, } ), - location: item::ItemLocation::Inventory { - character_id: char2.id, - } }).await.unwrap()); entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); @@ -552,9 +522,6 @@ async fn test_pick_up_stack_that_would_exceed_stack_limit() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } @@ -567,9 +534,6 @@ async fn test_pick_up_stack_that_would_exceed_stack_limit() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Inventory { - character_id: char2.id, - } }).await.unwrap()); } entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![p1_monomates])).await.unwrap(); @@ -747,9 +711,6 @@ async fn test_player_drops_partial_stack_and_other_player_picks_it_up() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } diff --git a/tests/test_item_use.rs b/tests/test_item_use.rs index ba28e19..45c9f3e 100644 --- a/tests/test_item_use.rs +++ b/tests/test_item_use.rs @@ -29,9 +29,6 @@ async fn test_use_monomate() { tool: tool } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } p1_items.push(item::InventoryItemEntity::Stacked(item)); @@ -79,9 +76,6 @@ async fn test_use_monomate_twice() { tool: tool } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } p1_items.push(item::InventoryItemEntity::Stacked(item)); @@ -132,9 +126,6 @@ async fn test_use_last_monomate() { tool: tool } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()])); } @@ -176,9 +167,6 @@ async fn test_use_nonstackable_tool() { tool: item::tool::ToolType::MagicStoneIritista, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_items)).await.unwrap(); @@ -217,9 +205,6 @@ async fn test_use_materials() { tool: tool } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } p1_inv.push(item::InventoryItemEntity::Stacked(item)); diff --git a/tests/test_mags.rs b/tests/test_mags.rs index 268833d..9886215 100644 --- a/tests/test_mags.rs +++ b/tests/test_mags.rs @@ -22,10 +22,6 @@ async fn test_mag_feed() { item: item::ItemDetail::Mag( item::mag::Mag::baby_mag(0) ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - //equipped: true, - } }).await.unwrap(); let mut monomates = Vec::new(); @@ -37,9 +33,6 @@ async fn test_mag_feed() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } @@ -108,9 +101,6 @@ async fn test_mag_change_owner() { item: item::ItemDetail::Mag( item::mag::Mag::baby_mag(0) ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap(); entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![mag])).await.unwrap(); @@ -169,9 +159,6 @@ async fn test_mag_cell() { item: item::ItemDetail::Mag( item::mag::Mag::baby_mag(0) ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap(); for _ in 0..1000usize { @@ -182,9 +169,6 @@ async fn test_mag_cell() { tool: item::tool::ToolType::Monomate, } ), - location: item::ItemLocation::FedToMag { - mag: mag.id, - } }).await.unwrap(); entity_gateway.feed_mag(&mag.id, &fed_tool.id).await.unwrap(); } @@ -195,9 +179,6 @@ async fn test_mag_cell() { tool: item::tool::ToolType::CellOfMag502, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap(); let equipped = item::EquippedEntity { diff --git a/tests/test_rooms.rs b/tests/test_rooms.rs index 45f8ace..91e8963 100644 --- a/tests/test_rooms.rs +++ b/tests/test_rooms.rs @@ -31,9 +31,6 @@ async fn test_item_ids_reset_when_rejoining_rooms() { tekked: true, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } @@ -50,9 +47,6 @@ async fn test_item_ids_reset_when_rejoining_rooms() { tekked: true, } ), - location: item::ItemLocation::Inventory { - character_id: char2.id, - } }).await.unwrap()); } diff --git a/tests/test_shops.rs b/tests/test_shops.rs index ade8ccf..78e2e38 100644 --- a/tests/test_shops.rs +++ b/tests/test_shops.rs @@ -321,9 +321,6 @@ async fn test_other_clients_see_stacked_purchase() { tool: item::tool::ToolType::Monomate } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap(); let mut ship = Box::new(ShipServerState::builder() From 8b87bd8d7b18b479273c5785e1b611a187111cff Mon Sep 17 00:00:00 2001 From: jake Date: Thu, 2 Dec 2021 01:57:30 -0700 Subject: [PATCH 22/41] fix mag item data --- src/entity/item/mag.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/entity/item/mag.rs b/src/entity/item/mag.rs index 4f36983..e75656e 100644 --- a/src/entity/item/mag.rs +++ b/src/entity/item/mag.rs @@ -567,6 +567,7 @@ impl Mag { pub fn as_bytes(&self) -> [u8; 16] { let mut result = [0; 16]; result[0..3].copy_from_slice(&self.mag.value()); + result[2] = self.level() as u8; result[3] = self.photon_blast_value(); result[4..6].copy_from_slice(&self.def.to_le_bytes()); result[6..8].copy_from_slice(&self.pow.to_le_bytes()); From c90c7c1fd35add7b77e6e52e158bf00f9cfff8c2 Mon Sep 17 00:00:00 2001 From: jake Date: Fri, 10 Dec 2021 13:07:39 -0700 Subject: [PATCH 23/41] update anyhow --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2feadfe..582634f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,5 +31,5 @@ refinery = { version = "0.5.0", features = ["postgres"] } sqlx = { version = "0.4.0", features = ["runtime-async-std-native-tls", "postgres", "json", "chrono"] } strum = "0.19.5" strum_macros = "0.19" -anyhow = "1.0.33" +anyhow = { version = "1.0.47", features = ["backtrace"] } From d4fc9151648bbd5463928c904d73da5180ca3591 Mon Sep 17 00:00:00 2001 From: jake Date: Fri, 10 Dec 2021 13:14:50 -0700 Subject: [PATCH 24/41] add debug to characterentity --- src/entity/character.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/entity/character.rs b/src/entity/character.rs index 117fddb..7c1572e 100644 --- a/src/entity/character.rs +++ b/src/entity/character.rs @@ -191,7 +191,7 @@ impl CharacterTechniques { } -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct CharacterConfig { pub raw_data: [u8; 0xE8], } @@ -214,7 +214,7 @@ impl CharacterConfig { } } -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct CharacterInfoboard { pub board: [u16; 172], } @@ -237,12 +237,12 @@ impl CharacterInfoboard { } } -#[derive(Clone, Default)] +#[derive(Debug, Clone, Default)] pub struct CharacterGuildCard { pub description: String, } -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct CharacterTechMenu { pub tech_menu: [u8; 40], } @@ -261,7 +261,7 @@ impl CharacterTechMenu { } } -#[derive(Clone, Default)] +#[derive(Clone, Default, Debug)] pub struct CharacterMaterials { pub power: u32, pub mind: u32, @@ -322,7 +322,7 @@ impl NewCharacterEntity { } } -#[derive(Clone, Default)] +#[derive(Clone, Default, Debug)] pub struct CharacterEntity { pub id: CharacterEntityId, pub user_id: UserAccountId, From 070735edbf26f3c94347b3d1058c3f59df9c71ee Mon Sep 17 00:00:00 2001 From: jake Date: Fri, 10 Dec 2021 13:15:33 -0700 Subject: [PATCH 25/41] ItemDetail::tool --- src/entity/item/mod.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/entity/item/mod.rs b/src/entity/item/mod.rs index 5c320b6..611c579 100644 --- a/src/entity/item/mod.rs +++ b/src/entity/item/mod.rs @@ -162,6 +162,13 @@ impl ItemDetail { _ => None, } } + + pub fn tool(&self) -> Option<&tool::Tool> { + match self { + ItemDetail::Tool(tool) => Some(tool), + _ => None, + } + } } #[derive(Clone, Debug)] From d85ada945eebb5f4956dbbd6c8b9274499ca1689 Mon Sep 17 00:00:00 2001 From: jake Date: Fri, 10 Dec 2021 13:16:18 -0700 Subject: [PATCH 26/41] add some functions to Inventory --- src/ship/items/inventory.rs | 77 +++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/src/ship/items/inventory.rs b/src/ship/items/inventory.rs index c949d74..a44c97d 100644 --- a/src/ship/items/inventory.rs +++ b/src/ship/items/inventory.rs @@ -240,6 +240,13 @@ impl InventoryItem { } } + pub fn stacked_mut(&mut self) -> Option<&mut StackedInventoryItem> { + match self { + InventoryItem::Stacked(ref mut stacked_inventory_item) => Some(stacked_inventory_item), + _ => None + } + } + pub fn mag(&self) -> Option<&Mag> { match self { InventoryItem::Individual(individual_inventory_item) => individual_inventory_item.mag(), @@ -477,6 +484,23 @@ impl CharacterInventory { } } + pub fn stack_item_id(&self, tool: &Tool) -> Option { + self.items.iter() + .filter_map(|item| { + match item { + InventoryItem::Stacked(s_item) => { + Some(s_item) + }, + _ => None + } + }) + .find(|s_item| { + s_item.tool == *tool + }) + .map(|item| { + item.item_id + }) + } pub fn get_item_handle_by_id(&mut self, item_id: ClientItemId) -> Option { let (slot, _) = self.items.iter() @@ -571,10 +595,57 @@ impl CharacterInventory { .next() } - pub fn add_item(&mut self, item: InventoryItem) -> Result<(), InventoryAddError> { // TODO: errors - // TODO: check slot conflict? + pub fn take_stacked_item_by_id(&mut self, item_id: ClientItemId, amount: usize) -> Option { + let idx = self.items + .iter_mut() + .position(|i| i.item_id() == item_id)?; + let item: &mut StackedInventoryItem = self.items.get_mut(idx)?.stacked_mut()?; + match item.entity_ids.len().cmp(&amount) { + Ordering::Equal => { + let item = self.items.remove(idx); + item.stacked().cloned() + }, + Ordering::Greater => { + let entity_ids = item.entity_ids.drain(..amount).collect(); + Some(StackedInventoryItem { + entity_ids, + tool: item.tool, + item_id: item.item_id, + }) + }, + Ordering::Less => { + None + } + } + } + + pub fn add_item(&mut self, item: InventoryItem) { self.items.push(item); - Ok(()) + } + + pub fn add_stacked_item(&mut self, mut item: StackedInventoryItem) { + let existing_item = self.items + .iter_mut() + .filter_map(|i| { + match i { + InventoryItem::Stacked(stacked) => { + Some(stacked) + }, + _ => None + } + }) + .find(|i| { + i.tool == item.tool + }); + + match existing_item { + Some(existing_item) => { + existing_item.entity_ids.append(&mut item.entity_ids) + }, + None => { + self.items.push(InventoryItem::Stacked(item)) + } + } } pub fn add_individual_floor_item(&mut self, floor_item: &IndividualFloorItem) -> &InventoryItem { From 3865caba8d1454de9c8c9a61bff3d2ff65d07588 Mon Sep 17 00:00:00 2001 From: jake Date: Fri, 10 Dec 2021 13:19:22 -0700 Subject: [PATCH 27/41] refcell room id counter --- src/ship/items/manager.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs index 701a4aa..0f0b1b2 100644 --- a/src/ship/items/manager.rs +++ b/src/ship/items/manager.rs @@ -1,5 +1,6 @@ use crate::ship::items::ClientItemId; use std::collections::HashMap; +use std::cell::RefCell; use thiserror::Error; use crate::entity::gateway::{EntityGateway, GatewayError}; use crate::entity::character::{CharacterEntity, CharacterEntityId, TechLevel}; @@ -95,7 +96,7 @@ pub struct ItemManager { pub(self) character_room: HashMap, pub(self) room_floor: HashMap, - pub(self) room_item_id_counter: HashMap ClientItemId + Send>>, + pub(self) room_item_id_counter: RefCell ClientItemId + Send>>>, } impl Default for ItemManager { @@ -107,7 +108,7 @@ impl Default for ItemManager { character_floor: HashMap::new(), character_room: HashMap::new(), room_floor: HashMap::new(), - room_item_id_counter: HashMap::new(), + room_item_id_counter: RefCell::new(HashMap::new()), } } } @@ -196,7 +197,7 @@ impl ItemManager { self.room_floor.entry(room_id).or_insert_with(RoomFloorItems::default); let mut inc = 0x00810000; - self.room_item_id_counter.entry(room_id).or_insert_with(|| Box::new(move || { + self.room_item_id_counter.borrow_mut().entry(room_id).or_insert_with(|| Box::new(move || { inc += 1; ClientItemId(inc) })); @@ -361,7 +362,7 @@ impl ItemManager { _ => 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 item_id = self.room_item_id_counter.borrow_mut().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 { @@ -482,7 +483,7 @@ impl ItemManager { 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 item_id = self.room_item_id_counter.borrow_mut().get_mut(room_id).ok_or(ItemManagerError::NoCharacter(character.id))?(); let floor_item = FloorItem::Meseta(MesetaFloorItem { item_id, meseta: Meseta(amount), @@ -510,7 +511,7 @@ impl ItemManager { 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 new_item_id = self.room_item_id_counter.borrow_mut().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))?; @@ -911,7 +912,7 @@ impl ItemManager { entity_id, item_id, item: ItemDetail::Weapon(weapon.clone()), - }))?; + })); entity_gateway.set_character_inventory(&character.id, &inventory.as_inventory_entity(&character.id)).await?; From 5d410f88f3258e9c61287d199279e6e53613af7d Mon Sep 17 00:00:00 2001 From: jake Date: Fri, 10 Dec 2021 13:20:20 -0700 Subject: [PATCH 28/41] PartialEq LocalClientId u8 --- src/ship/location.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ship/location.rs b/src/ship/location.rs index a0c05f5..a40f858 100644 --- a/src/ship/location.rs +++ b/src/ship/location.rs @@ -109,6 +109,12 @@ impl LocalClientId { } } +impl PartialEq for LocalClientId { + fn eq(&self, other: &u8) -> bool { + self.0 == *other as usize + } +} + #[derive(Debug, Copy, Clone, PartialEq)] pub struct AreaClient { pub client: ClientId, From b3b6dad6ad0162a08ee17872d5a8949c5c5a6d15 Mon Sep 17 00:00:00 2001 From: jake Date: Fri, 10 Dec 2021 13:24:59 -0700 Subject: [PATCH 29/41] trades! --- src/entity/gateway/inmemory.rs | 1 - src/ship/items/manager.rs | 182 +++++----- src/ship/items/transaction.rs | 2 +- src/ship/mod.rs | 1 + src/ship/packet/handler/trade.rs | 553 +++++++++++++++++++++++-------- src/ship/ship.rs | 37 +-- src/ship/trade.rs | 172 ++++++++++ 7 files changed, 687 insertions(+), 261 deletions(-) create mode 100644 src/ship/trade.rs diff --git a/src/entity/gateway/inmemory.rs b/src/entity/gateway/inmemory.rs index 24b6500..deb737b 100644 --- a/src/entity/gateway/inmemory.rs +++ b/src/entity/gateway/inmemory.rs @@ -268,7 +268,6 @@ impl EntityGateway for InMemoryGateway { } async fn get_character_inventory(&mut self, char_id: &CharacterEntityId) -> Result { - println!("getting inv"); let inventories = self.inventories.lock().unwrap(); Ok(inventories .iter() diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs index 0f0b1b2..f669309 100644 --- a/src/ship/items/manager.rs +++ b/src/ship/items/manager.rs @@ -1,3 +1,4 @@ +use log::warn; use crate::ship::items::ClientItemId; use std::collections::HashMap; use std::cell::RefCell; @@ -9,7 +10,8 @@ use crate::entity::item::{Meseta, NewItemEntity, ItemEntity, ItemEntityId, Inven use crate::entity::item::tool::{Tool, ToolType}; use crate::entity::item::weapon; use crate::ship::map::MapArea; -use crate::ship::ship::{TradeItem, ItemDropLocation}; +use crate::ship::ship::ItemDropLocation; +use crate::ship::trade::TradeItem; use crate::ship::drops::{ItemDrop, ItemDropType}; use crate::ship::location::{AreaClient, RoomId}; use crate::ship::shops::ShopItem; @@ -921,41 +923,41 @@ impl ItemManager { pub async fn trade_items(&mut self, entity_gateway: &mut EG, + room_id: RoomId, p1: (&AreaClient, &CharacterEntity, &Vec), p2: (&AreaClient, &CharacterEntity, &Vec)) -> Result, anyhow::Error> { - let it = ItemTransaction::new(&self, (p1, p2)) - .act(|it, (p1, p2)| -> Result<_, anyhow::Error> { + let it = ItemTransaction::new(&self, (p1, p2, room_id)) + .act(|it, (p1, p2, room_id)| -> Result<_, anyhow::Error> { let p1_inventory = it.manager.get_character_inventory(p1.1)?; let p2_inventory = it.manager.get_character_inventory(p2.1)?; //TODO: inv-selftrade+othertrade <= 30 //if p1_inventory - let trade_items = [(p1, p2, p1_inventory), (p2, p1, p2_inventory)] - .map(|((src_client, dest_client, src_inventory))| { + + let trade_items = [(p1, p2, p1_inventory, p2_inventory), (p2, p1, p2_inventory, p1_inventory)] + .map(|(src_client, dest_client, src_inventory, dest_inventory)| { src_client.2.iter() - .map(|item| -> Option<(Option, Vec>>)> { + .map(|item| -> Option<(Option, Box>)> { match item { TradeItem::Individual(item_id) => { let item = src_inventory.get_item_by_id(*item_id)?.individual()?; + let new_item_id = it.manager.room_item_id_counter.borrow_mut().get_mut(&room_id)?(); Some(( Some(ItemToTrade { add_to: *dest_client.0, remove_from: *src_client.0, - item_id: *item_id, + current_item_id: *item_id, + new_item_id: new_item_id, item_detail: ItemToTradeDetail::Individual(item.item.clone()) }), - vec![ - Box::new(AddIndividualItemToInventory { - character_id: dest_client.1.id, - item_id: item.entity_id, - }), - Box::new(RemoveIndividualItemFromInventory { - character_id: src_client.1.id, - item_id: item.entity_id, - }) - ] + Box::new(TradeIndividualItem { + src_character_id: src_client.1.id, + dest_character_id: dest_client.1.id, + current_item_id: *item_id, + new_item_id: new_item_id, + }), )) }, TradeItem::Stacked(item_id, amount) => { @@ -964,38 +966,33 @@ impl ItemManager { None } else { + let new_item_id = it.manager.room_item_id_counter.borrow_mut().get_mut(&room_id)?(); Some(( Some(ItemToTrade { add_to: *dest_client.0, remove_from: *src_client.0, - item_id: *item_id, + current_item_id: *item_id, + new_item_id: new_item_id, item_detail: ItemToTradeDetail::Stacked(item.tool, *amount) }), - vec![ - Box::new(AddStackedItemToInventory { - character_id: dest_client.1.id, - item_ids: item.entity_ids.iter().cloned().take(*amount).collect(), - }), - Box::new(RemoveStackedItemFromInventory { - character_id: src_client.1.id, - item_ids: item.entity_ids.iter().cloned().take(*amount).collect(), - }), - ] + Box::new(TradeStackedItem { + src_character_id: src_client.1.id, + dest_character_id: dest_client.1.id, + //item_ids: item.entity_ids.iter().cloned().take(*amount).collect(), + current_item_id: *item_id, + new_item_id: new_item_id, + amount: *amount, + }), )) } }, TradeItem::Meseta(amount) => { - Some((None, - vec![ - Box::new(AddMesetaToInventory { - character_id: dest_client.1.id, - amount: *amount, - }), - Box::new(RemoveMesetaFromInventory { - character_id: src_client.1.id, - amount: *amount, - }), - ] + Some((None, // is there a packet that informs other clients about meseta changes? + Box::new(TradeMeseta { + src_character_id: src_client.1.id, + dest_character_id: dest_client.1.id, + amount: *amount, + }), )) } } @@ -1003,11 +1000,13 @@ impl ItemManager { .collect::>>() }); + if let [Some(p1_trades), Some(p2_trades)] = trade_items { - let (p1_item_trades, p1_item_actions): (Vec>, Vec>>>) = p1_trades.into_iter().unzip(); - let (p2_item_trades, p2_item_actions): (Vec>, Vec>>>) = p2_trades.into_iter().unzip(); + let (p1_item_trades, p1_item_actions): (Vec>, Vec>>) = p1_trades.into_iter().unzip(); + let (p2_item_trades, p2_item_actions): (Vec>, Vec>>) = p2_trades.into_iter().unzip(); + let item_trades = p1_item_trades.into_iter().flatten().chain(p2_item_trades.into_iter().flatten()); - let item_actions = p1_item_actions.into_iter().flatten().chain(p2_item_actions.into_iter().flatten()); + let item_actions = p1_item_actions.into_iter().chain(p2_item_actions.into_iter()); for action in item_actions { it.action(action); @@ -1020,25 +1019,30 @@ impl ItemManager { } }); - it.commit(self, entity_gateway) + Ok(it.commit(self, entity_gateway) .await - .map_err(|err| err.into()) + //.map_err(|err| err.into()) + .unwrap()) } } +#[derive(Debug)] pub enum ItemToTradeDetail { Individual(ItemDetail), Stacked(Tool, usize), } +#[derive(Debug)] pub struct ItemToTrade { pub add_to: AreaClient, pub remove_from: AreaClient, - pub item_id: ClientItemId, + pub current_item_id: ClientItemId, + pub new_item_id: ClientItemId, pub item_detail: ItemToTradeDetail, } +#[derive(Debug)] struct RemoveFromLocalFloor { character_id: CharacterEntityId, item_id: ClientItemId, @@ -1046,7 +1050,7 @@ struct RemoveFromLocalFloor { #[async_trait::async_trait] impl ItemAction for RemoveFromLocalFloor { - async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + async fn commit(&self, item_manager: &mut ItemManager, _entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { let local_floor = item_manager.character_floor.get_mut(&self.character_id).ok_or(ItemManagerError::NoCharacter(self.character_id))?; local_floor.remove_item(&self.item_id); Ok(()) @@ -1054,6 +1058,7 @@ impl ItemAction for RemoveFromLocalFloor { } +#[derive(Debug)] struct RemoveFromSharedFloor { room_id: RoomId, item_id: ClientItemId, @@ -1061,7 +1066,7 @@ struct RemoveFromSharedFloor { #[async_trait::async_trait] impl ItemAction for RemoveFromSharedFloor { - async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + async fn commit(&self, item_manager: &mut ItemManager, _entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { let shared_floor = item_manager.room_floor.get_mut(&self.room_id).ok_or(ItemManagerError::NoRoom(self.room_id))?; shared_floor.remove_item(&self.item_id); Ok(()) @@ -1069,6 +1074,7 @@ impl ItemAction for RemoveFromSharedFloor { } +#[derive(Debug)] struct AddIndividualFloorItemToInventory{ character: CharacterEntity, item: IndividualFloorItem, @@ -1080,7 +1086,6 @@ impl ItemAction for AddIndividualFloorItemToInventory { let inventory = item_manager.character_inventory.get_mut(&self.character.id).ok_or(ItemManagerError::NoCharacter(self.character.id))?; let inv_item = inventory.add_individual_floor_item(&self.item); - entity_gateway.add_item_note( &self.item.entity_id, ItemNote::Pickup { @@ -1098,6 +1103,7 @@ impl ItemAction for AddIndividualFloorItemToInventory { } +#[derive(Debug)] struct AddStackedFloorItemToInventory{ character_id: CharacterEntityId, item: StackedFloorItem, @@ -1115,6 +1121,7 @@ impl ItemAction for AddStackedFloorItemToInventory { } +#[derive(Debug)] struct AddMesetaFloorItemToInventory{ character: CharacterEntity, item: MesetaFloorItem, @@ -1131,73 +1138,62 @@ impl ItemAction for AddMesetaFloorItemToInventory { } -struct AddIndividualItemToInventory { - character_id: CharacterEntityId, - item_id: ItemEntityId, +#[derive(Debug)] +struct TradeIndividualItem { + src_character_id: CharacterEntityId, + dest_character_id: CharacterEntityId, + current_item_id: ClientItemId, + new_item_id: ClientItemId, } #[async_trait::async_trait] -impl ItemAction for AddIndividualItemToInventory { - async fn commit(&self, _item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { - Ok(()) - } -} - -struct AddStackedItemToInventory { - character_id: CharacterEntityId, - item_ids: Vec, -} - -#[async_trait::async_trait] -impl ItemAction for AddStackedItemToInventory { - async fn commit(&self, _item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { - Ok(()) - } -} +impl ItemAction for TradeIndividualItem { + async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + let src_inventory = item_manager.character_inventory.get_mut(&self.src_character_id).ok_or(ItemManagerError::NoCharacter(self.src_character_id))?; + let inventory_item = src_inventory.take_item_by_id(self.current_item_id).ok_or(ItemManagerError::NoSuchItemId(self.current_item_id))?; + entity_gateway.set_character_inventory(&self.src_character_id, &src_inventory.as_inventory_entity(&self.src_character_id)).await?; -struct RemoveIndividualItemFromInventory { - character_id: CharacterEntityId, - item_id: ItemEntityId, -} + let dest_inventory = item_manager.character_inventory.get_mut(&self.dest_character_id).ok_or(ItemManagerError::NoCharacter(self.dest_character_id))?; + dest_inventory.add_item(inventory_item); + entity_gateway.set_character_inventory(&self.dest_character_id, &dest_inventory.as_inventory_entity(&self.dest_character_id)).await?; -#[async_trait::async_trait] -impl ItemAction for RemoveIndividualItemFromInventory { - async fn commit(&self, _item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { Ok(()) } } -struct RemoveStackedItemFromInventory { - character_id: CharacterEntityId, - item_ids: Vec, +#[derive(Debug)] +struct TradeStackedItem { + src_character_id: CharacterEntityId, + dest_character_id: CharacterEntityId, + current_item_id: ClientItemId, + new_item_id: ClientItemId, + amount: usize, } #[async_trait::async_trait] -impl ItemAction for RemoveStackedItemFromInventory { - async fn commit(&self, _item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { - Ok(()) - } -} +impl ItemAction for TradeStackedItem { + async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + let src_inventory = item_manager.character_inventory.get_mut(&self.src_character_id).ok_or(ItemManagerError::NoCharacter(self.src_character_id))?; + let inventory_item = src_inventory.take_stacked_item_by_id(self.current_item_id, self.amount).ok_or(ItemManagerError::NoSuchItemId(self.current_item_id))?; + entity_gateway.set_character_inventory(&self.src_character_id, &src_inventory.as_inventory_entity(&self.src_character_id)).await?; -struct AddMesetaToInventory { - character_id: CharacterEntityId, - amount: usize, -} + let dest_inventory = item_manager.character_inventory.get_mut(&self.dest_character_id).ok_or(ItemManagerError::NoCharacter(self.dest_character_id))?; + dest_inventory.add_stacked_item(inventory_item); + entity_gateway.set_character_inventory(&self.dest_character_id, &dest_inventory.as_inventory_entity(&self.dest_character_id)).await?; -#[async_trait::async_trait] -impl ItemAction for AddMesetaToInventory { - async fn commit(&self, _item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { Ok(()) } } -struct RemoveMesetaFromInventory { - character_id: CharacterEntityId, +#[derive(Debug)] +struct TradeMeseta { + src_character_id: CharacterEntityId, + dest_character_id: CharacterEntityId, amount: usize, } #[async_trait::async_trait] -impl ItemAction for RemoveMesetaFromInventory { +impl ItemAction for TradeMeseta { async fn commit(&self, _item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { Ok(()) } diff --git a/src/ship/items/transaction.rs b/src/ship/items/transaction.rs index 4c385c5..4c2132a 100644 --- a/src/ship/items/transaction.rs +++ b/src/ship/items/transaction.rs @@ -20,7 +20,7 @@ pub enum TransactionCommitError { } #[async_trait::async_trait] -pub trait ItemAction: std::marker::Send + std::marker::Sync { +pub trait ItemAction: std::marker::Send + std::marker::Sync + std::fmt::Debug { async fn commit(&self, manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError>; } diff --git a/src/ship/mod.rs b/src/ship/mod.rs index f29531b..7ad6a1b 100644 --- a/src/ship/mod.rs +++ b/src/ship/mod.rs @@ -11,3 +11,4 @@ pub mod drops; pub mod packet; pub mod quests; pub mod shops; +pub mod trade; diff --git a/src/ship/packet/handler/trade.rs b/src/ship/packet/handler/trade.rs index d67d28c..bd311d2 100644 --- a/src/ship/packet/handler/trade.rs +++ b/src/ship/packet/handler/trade.rs @@ -6,11 +6,12 @@ use libpso::packet::ship::*; use libpso::packet::messages::*; use crate::common::leveltable::CharacterLevelTable; use crate::common::serverstate::ClientId; -use crate::ship::ship::{SendShipPacket, ShipError, Clients, Rooms, ItemShops, TradeItem, TradeState, TradeStatus}; -use crate::ship::location::{ClientLocation, ClientLocationError}; +use crate::ship::ship::{SendShipPacket, ShipError, Clients, Rooms, ItemShops}; +use crate::ship::location::{ClientLocation, ClientLocationError, LocalClientId}; use crate::ship::drops::ItemDrop; use crate::ship::items::{ItemManager, ItemManagerError, ClientItemId, TriggerCreateItem, FloorItem, FloorType, ItemToTradeDetail}; use crate::ship::items::inventory::InventoryItem; +use crate::ship::trade::{TradeItem, TradeState, TradeStatus}; use crate::entity::gateway::EntityGateway; use crate::entity::item; use libpso::utf8_to_utf16_array; @@ -18,80 +19,320 @@ use crate::ship::packet::builder; use crate::ship::shops::{ShopItem, ToolShopItem, ArmorShopItem}; #[derive(thiserror::Error, Debug)] -#[error("")] pub enum TradeError { + #[error("no partner")] CouldNotFindTradePartner, + #[error("item does not match id")] ClientItemIdDidNotMatchItem(ClientItemId, [u8; 16]), + #[error("invalid stack {1}")] InvalidStackAmount(ClientItemId, usize), + #[error("not in trade menu")] NotInTradeMenu, + #[error("trade menu at an invalid point")] + MismatchedStatus, } - -pub async fn inner_items_to_trade(id: ClientId, - items_to_trade: &ItemsToTrade, - entity_gateway: &mut EG, - client_location: &ClientLocation, - clients: &mut Clients, - item_manager: &mut ItemManager) - -> Result + Send>, anyhow::Error> +// TODO: remove target +pub async fn trade_request(id: ClientId, + trade_request: &TradeRequest, + target: u32, + entity_gateway: &mut EG, + client_location: &ClientLocation, + clients: &mut Clients, + item_manager: &mut ItemManager, + trades: &mut TradeState) + -> Result + Send>, anyhow::Error> where EG: EntityGateway { - let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; - let inventory = item_manager.get_character_inventory_mut(&client.character)?; - let trade_partner = client_location.get_client_neighbors(id)? - .into_iter() - .filter(|ac| { - ac.local_client.id() == items_to_trade.trade_target - }) - .next() - .ok_or(TradeError::CouldNotFindTradePartner)?; - - let item_blobs = items_to_trade.items.iter().take(items_to_trade.count as usize); - let trade_items = item_blobs - .map(|item| { - // TOOD: meseta? - let real_item = inventory.get_item_handle_by_id(ClientItemId(item.item_id)) - .ok_or(ItemManagerError::NoSuchItemId(ClientItemId(item.item_id)))?; - let real_item = real_item - .item() - .ok_or(ItemManagerError::NoSuchItemId(ClientItemId(item.item_id)))?; - let trade_item_bytes: [u8; 16] = item.item_data.iter() - .chain(item.item_data2.iter()) - .cloned().collect::>() - .try_into() - .unwrap(); - if real_item.as_client_bytes() == trade_item_bytes { - match real_item { - InventoryItem::Individual(individual_inventory_item) => { - Ok(TradeItem::Individual(individual_inventory_item.item_id)) - }, - InventoryItem::Stacked(stacked_inventory_item) => { - let amount = trade_item_bytes[5] as usize; - if amount > stacked_inventory_item.entity_ids.len() { - Ok(TradeItem::Stacked(stacked_inventory_item.item_id, amount)) + let trade_request = trade_request.clone(); // TODO: make this function take ownership of packet + match trade_request.trade { + TradeRequestCommand::Initialize(ref act, meseta) => { + match act { + TradeRequestInitializeCommand::Initialize => { + let trade_partner = client_location.get_client_neighbors(id)? + .into_iter() + .filter(|ac| { + ac.local_client.id() == trade_request.client + }) + .next() + .ok_or(TradeError::CouldNotFindTradePartner)?; + trades.new_trade(&id, &trade_partner.client); + Ok(Box::new(client_location.get_all_clients_by_client(id)?.into_iter() + .filter(move |client| client.local_client.id() == target as u8) + .map(move |client| { + (client.client, SendShipPacket::DirectMessage(DirectMessage::new(target, GameMessage::TradeRequest(trade_request.clone())))) + }))) + }, + TradeRequestInitializeCommand::Respond => { + Ok(trades + .with(&id, |this, other| -> Option + Send>> { + if this.status == TradeStatus::ReceivedRequest && other.status == TradeStatus::SentRequest { + this.status = TradeStatus::Trading; + other.status = TradeStatus::Trading; + + let trade_request = trade_request.clone(); + Some(Box::new(client_location.get_all_clients_by_client(id).ok()?.into_iter() + .filter(move |client| client.local_client.id() == target as u8) + .map(move |client| { + (client.client, SendShipPacket::DirectMessage(DirectMessage::new(target, GameMessage::TradeRequest(trade_request.clone())))) + }))) + } + else { + None + } + })? + .unwrap_or_else(|| -> Box + Send> { + trades.remove_trade(&id); + Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() + .filter(move |client| client.local_client.id() == target as u8) + .map(move |client| { + (client.client, SendShipPacket::CancelTrade(CancelTrade {})) + }) + .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {}))))) + })) + } + } + }, + TradeRequestCommand::AddItem(item_id, amount) => { + Ok(trades + .with(&id, |this, other| -> Option + Send>> { + if this.status == TradeStatus::Trading && other.status == TradeStatus::Trading { + let client = clients.get(&this.client())?;//.ok_or(ShipError::ClientNotFound(id)).ok()?; + let inventory = item_manager.get_character_inventory(&client.character).ok()?; + let item = inventory.get_item_by_id(ClientItemId(item_id))?; + + match item { + InventoryItem::Individual(_) => { + this.items.push(TradeItem::Individual(ClientItemId(item_id))); + }, + InventoryItem::Stacked(stacked_item) => { + if stacked_item.count() < amount as usize { + return None; + } + this.items.push(TradeItem::Stacked(ClientItemId(item_id), amount as usize)); + }, } - else { - Err(TradeError::InvalidStackAmount(stacked_inventory_item.item_id, amount).into()) + + let trade_request = trade_request.clone(); + Some(Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() + .filter(move |client| client.local_client.id() == target as u8) + .map(move |client| { + (client.client, SendShipPacket::DirectMessage(DirectMessage::new(target, GameMessage::TradeRequest(trade_request.clone())))) + }))) + } + else { + None + } + })? + .unwrap_or_else(|| { + trades.remove_trade(&id); + Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() + .filter(move |client| client.local_client.id() == target as u8) + .map(move |client| { + (client.client, SendShipPacket::CancelTrade(CancelTrade {})) + }) + .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {}))))) + })) + }, + TradeRequestCommand::RemoveItem(item_id, amount) => { + Ok(trades + .with(&id, |this, other| -> Option + Send>> { + if this.status == TradeStatus::Trading && other.status == TradeStatus::Trading { + let client = clients.get(&this.client())?; //.ok_or(ShipError::ClientNotFound(id)).ok()?; + let inventory = item_manager.get_character_inventory(&client.character).ok()?; + let item = inventory.get_item_by_id(ClientItemId(item_id))?; + + match item { + InventoryItem::Individual(_) => { + this.items.retain(|item| { + item.item_id() != ClientItemId(item_id) + }) + //this.items.push(TradeItem::Individual(ClientItemId(item_id))); + }, + InventoryItem::Stacked(stacked_item) => { + let trade_item_index = this.items.iter() + .position(|item| { + item.item_id() == ClientItemId(item_id) + })?; + + match this.items[trade_item_index].stacked()?.1.cmp(&(amount as usize)) { + std::cmp::Ordering::Greater => { + this.items[trade_item_index].stacked()?.1 -= amount as usize; + }, + std::cmp::Ordering::Equal => { + this.items.remove(trade_item_index); + }, + std::cmp::Ordering::Less => { + return None + } + } + }, } + let trade_request = trade_request.clone(); + Some(Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() + .filter(move |client| client.local_client.id() == target as u8) + .map(move |client| { + (client.client, SendShipPacket::DirectMessage(DirectMessage::new(target, GameMessage::TradeRequest(trade_request.clone())))) + }))) } - } - } - else { - Err(TradeError::ClientItemIdDidNotMatchItem(ClientItemId(item.item_id), trade_item_bytes).into()) + else { + None + } + })? + .unwrap_or_else(|| { + trades.remove_trade(&id); + Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() + .filter(move |client| client.local_client.id() == target as u8) + .map(move |client| { + (client.client, SendShipPacket::CancelTrade(CancelTrade {})) + }) + .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {}))))) + })) + }, + TradeRequestCommand::Confirm => { + Ok(trades + .with(&id, |this, other| -> Option + Send>> { + if this.status == TradeStatus::Trading && (other.status == TradeStatus::Trading || other.status == TradeStatus::Confirmed) { + this.status = TradeStatus::Confirmed; + + let trade_request = trade_request.clone(); + Some(Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() + .filter(move |client| client.local_client.id() == target as u8) + .map(move |client| { + (client.client, SendShipPacket::DirectMessage(DirectMessage::new(target, GameMessage::TradeRequest(trade_request.clone())))) + }))) + } + else { + None + } + })? + .unwrap_or_else(|| { + trades.remove_trade(&id); + Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() + .filter(move |client| client.local_client.id() == target as u8) + .map(move |client| { + (client.client, SendShipPacket::CancelTrade(CancelTrade {})) + }) + .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {}))))) + })) + }, + TradeRequestCommand::FinalConfirm => { + Ok(trades + .with(&id, |this, other| -> Option + Send>> { + if this.status == TradeStatus::Confirmed && (other.status == TradeStatus::Confirmed || other.status == TradeStatus::FinalConfirm) { + this.status = TradeStatus::FinalConfirm; + + let trade_request = trade_request.clone(); + Some(Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() + .filter(move |client| client.local_client.id() == target as u8) + .map(move |client| { + (client.client, SendShipPacket::DirectMessage(DirectMessage::new(target, GameMessage::TradeRequest(trade_request.clone())))) + }))) + } + else { + None + } + })? + .unwrap_or_else(|| { + trades.remove_trade(&id); + Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() + .filter(move |client| client.local_client.id() == target as u8) + .map(move |client| { + (client.client, SendShipPacket::CancelTrade(CancelTrade {})) + }) + .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {}))))) + })) + }, + } +} + + +fn status_is(status: &TradeStatus, statuses: &[TradeStatus; N]) -> bool { + statuses.iter().any(|s| s == status) +} + +fn status_is_not(status: &TradeStatus, statuses: &[TradeStatus; N]) -> bool { + !status_is(status, statuses) +} + +pub async fn inner_items_to_trade(id: ClientId, + items_to_trade: &ItemsToTrade, + entity_gateway: &mut EG, + client_location: &ClientLocation, + clients: &mut Clients, + item_manager: &mut ItemManager, + trades: &mut TradeState) + -> Result + Send>, anyhow::Error> +where + EG: EntityGateway +{ + Ok(trades + .with(&id, |this, other| -> Result + Send>, anyhow::Error> { + println!("statuses {:?} {:?}", this.status, other.status); + if status_is_not(&this.status, &[TradeStatus::FinalConfirm]) || status_is_not(&other.status, &[TradeStatus::FinalConfirm, TradeStatus::ItemsChecked]) { + //if this.status != TradeStatus::FinalConfirm || (other.status != TradeStatus::FinalConfirm || other.status != TradeStatus::ItemsChecked) { + return Err(TradeError::MismatchedStatus.into()) } - }) - .collect::, anyhow::Error>>()?; - // TODO: check room in inventory for items - client.trade = Some(TradeState { - other_client: trade_partner.client, - items: trade_items, - status: TradeStatus::Unconfirmed - }); + let client = clients.get(&this.client()).ok_or(ShipError::ClientNotFound(this.client()))?; + let inventory = item_manager.get_character_inventory(&client.character)?; + + let item_blobs = items_to_trade.items.iter().take(items_to_trade.count as usize); + let trade_items = item_blobs + .map(|item| { + // TOOD: meseta? + let real_item = inventory.get_item_by_id(ClientItemId(item.item_id)) + .ok_or(ItemManagerError::NoSuchItemId(ClientItemId(item.item_id)))?; + let trade_item_bytes: [u8; 16] = item.item_data.iter() + .chain(item.item_data2.iter()) + .cloned().collect::>() + .try_into() + .unwrap(); + match real_item { + InventoryItem::Individual(individual_inventory_item) => { + println!("real indiv item: {:?} {:?} ==? {:?}", real_item, real_item.as_client_bytes(), trade_item_bytes); + if real_item.as_client_bytes() == trade_item_bytes { + Ok(TradeItem::Individual(individual_inventory_item.item_id)) + } + else { + Err(TradeError::ClientItemIdDidNotMatchItem(ClientItemId(item.item_id), trade_item_bytes).into()) + } + }, + InventoryItem::Stacked(stacked_inventory_item) => { + println!("real stack item: {:?} {:?} ==? {:?}", real_item, real_item.as_client_bytes(), trade_item_bytes); + if real_item.as_client_bytes()[0..4] == trade_item_bytes[0..4] { + let amount = trade_item_bytes[5] as usize; + if amount <= stacked_inventory_item.entity_ids.len() { + Ok(TradeItem::Stacked(stacked_inventory_item.item_id, amount)) + } + else { + Err(TradeError::InvalidStackAmount(stacked_inventory_item.item_id, amount).into()) + } + } + else { + Err(TradeError::ClientItemIdDidNotMatchItem(ClientItemId(item.item_id), trade_item_bytes).into()) + } + } + } + }) + .collect::, anyhow::Error>>()?; - Ok(Box::new(vec![(trade_partner.client, SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {}))].into_iter())) + this.status = TradeStatus::ItemsChecked; + Ok(Box::new(std::iter::once((other.client(), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {}))))) + })? + .unwrap_or_else(|err| { + println!("asdf {:?}", err); + log::warn!("trade error: {:?}", err); + let (this, other) = trades.remove_trade(&id); + Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() + .filter(move |client| other.as_ref().map(|other| client.client == other.client() ).unwrap_or_else(|| false)) + .map(move |client| { + (client.client, SendShipPacket::CancelTrade(CancelTrade {})) + }) + .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {}))))) + })) } pub async fn items_to_trade(id: ClientId, @@ -99,28 +340,25 @@ pub async fn items_to_trade(id: ClientId, entity_gateway: &mut EG, client_location: &ClientLocation, clients: &mut Clients, - item_manager: &mut ItemManager) + item_manager: &mut ItemManager, + trades: &mut TradeState) -> Result + Send>, anyhow::Error> where EG: EntityGateway { - let t = inner_items_to_trade(id, items_to_trade_pkt, entity_gateway, client_location, clients, item_manager).await; + let t = inner_items_to_trade(id, items_to_trade_pkt, entity_gateway, client_location, clients, item_manager, trades).await; match t { Ok(p) => Ok(p), Err(err) => { - let client = clients.get(&id).ok_or(ShipError::ClientNotFound(id))?; - let trade_partner = client.trade.as_ref() - .and_then(|trade_state| { - client_location.get_local_client(trade_state.other_client).ok() - }) - .map(|trade_partner| { - (trade_partner.client, SendShipPacket::CancelTrade(CancelTrade {})) - }); - - log::warn!("error in trading: {:?}", err); - Ok(Box::new(vec![(id, SendShipPacket::CancelTrade(CancelTrade {}))] - .into_iter() - .chain(trade_partner.into_iter()))) + log::warn!("atrade error: {:?}", err); + println!("qwer"); + let (this, other) = trades.remove_trade(&id); + Ok(Box::new(client_location.get_all_clients_by_client(id)?.into_iter() + .filter(move |client| other.as_ref().map(|other| client.client == other.client()).unwrap_or_else(|| false)) + .map(move |client| { + (client.client, SendShipPacket::CancelTrade(CancelTrade {})) + }) + .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {})))))) } } } @@ -129,83 +367,112 @@ pub async fn trade_confirmed(id: ClientId, entity_gateway: &mut EG, client_location: &ClientLocation, clients: &mut Clients, - item_manager: &mut ItemManager) + item_manager: &mut ItemManager, + trades: &mut TradeState) -> Result + Send>, anyhow::Error> where EG: EntityGateway { - { - let this_client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; - this_client.trade.as_mut().ok_or(TradeError::NotInTradeMenu)?.status = TradeStatus::Confirmed; + enum TradeReady<'a> { + OnePlayer, + BothPlayers(crate::ship::location::RoomId, + (crate::ship::location::AreaClient, &'a crate::ship::ship::ClientState, crate::ship::trade::ClientTradeState), + (crate::ship::location::AreaClient, &'a crate::ship::ship::ClientState, crate::ship::trade::ClientTradeState)), } - let both_confirmed = { - let this_client = clients.get(&id).ok_or(ShipError::ClientNotFound(id))?; - let other_client_id = this_client.trade.as_ref().ok_or(TradeError::NotInTradeMenu)?.other_client; - let other_client = clients.get(&other_client_id).ok_or(ShipError::ClientNotFound(other_client_id))?; - - let this_client_trade = this_client.trade.as_ref().ok_or(TradeError::NotInTradeMenu)?; - let other_client_trade = other_client.trade.as_ref().ok_or(TradeError::NotInTradeMenu)?; - this_client_trade.status == TradeStatus::Confirmed && other_client_trade.status == TradeStatus::Confirmed - }; - - if both_confirmed { - let this_client_trade = { - let this_client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; - let this_client_trade = this_client.trade.as_ref().ok_or(TradeError::NotInTradeMenu)?.clone(); - this_client.trade = None; - this_client_trade - }; - let other_client_trade = { - let other_client = clients.get_mut(&this_client_trade.other_client).ok_or(ShipError::ClientNotFound(this_client_trade.other_client))?; - let other_client_trade = other_client.trade.as_ref().ok_or(TradeError::NotInTradeMenu)?.clone(); - other_client.trade = None; - other_client_trade - }; - let this_client = clients.get(&id).ok_or(ShipError::ClientNotFound(id))?; - let other_client = clients.get(&this_client_trade.other_client).ok_or(ShipError::ClientNotFound(this_client_trade.other_client))?; - - let this_local_client = client_location.get_local_client(id)?; - let other_local_client = client_location.get_local_client(this_client_trade.other_client)?; - - let traded_items = item_manager.trade_items( - entity_gateway, - (&this_local_client, &this_client.character, &this_client_trade.items), - (&other_local_client, &other_client.character, &other_client_trade.items) - ).await?; - - let clients_in_room = client_location.get_all_clients_by_client(id)?; - let traded_item_packets = traded_items - .into_iter() - .map(|item| { - match item.item_detail { - ItemToTradeDetail::Individual(item_detail) => { - [ - GameMessage::CreateItem(builder::message::create_individual_item(item.add_to, item.item_id, &item_detail).unwrap()), - GameMessage::PlayerNoLongerHasItem(builder::message::player_no_longer_has_item(item.remove_from, item.item_id, 1)) // TODO: amount = ? - ] - }, - ItemToTradeDetail::Stacked(tool, amount) => { - [ - GameMessage::CreateItem(builder::message::create_stacked_item(item.add_to, item.item_id, &tool, amount).unwrap()), - GameMessage::PlayerNoLongerHasItem(builder::message::player_no_longer_has_item(item.remove_from, item.item_id, amount as u32)) - ] - }, + let trade_instructions = trades + .with(&id, |this, other| -> Result<_, anyhow::Error> { + if status_is_not(&this.status, &[TradeStatus::ItemsChecked]) || status_is_not(&other.status, &[TradeStatus::ItemsChecked, TradeStatus::TradeComplete]) { + return Err(TradeError::MismatchedStatus.into()) + } + + // TODO: check for space in inventory! + this.status = TradeStatus::TradeComplete; + + if this.status == TradeStatus::TradeComplete && other.status == TradeStatus::TradeComplete { + let this_client = clients.get(&this.client()).ok_or(ShipError::ClientNotFound(this.client()))?; + let other_client = clients.get(&other.client()).ok_or(ShipError::ClientNotFound(other.client()))?; + let this_local_client = client_location.get_local_client(this.client())?; + let other_local_client = client_location.get_local_client(other.client())?; + let room_id = client_location.get_room(id).map_err(|err| -> ClientLocationError { err.into() })?; + + Ok(TradeReady::BothPlayers(room_id, + (this_local_client, this_client, this.clone()), + (other_local_client, other_client, other.clone()))) + } + else { + Ok(TradeReady::OnePlayer) + } + }); + + // TODO: this match needs to handle errors better + match trade_instructions { + Ok(Ok(trade)) => { + match trade { + TradeReady::OnePlayer => { + Ok(Box::new(None.into_iter()) as Box + Send>) + }, + TradeReady::BothPlayers(room_id, (this_local_client, this_client, this), (other_local_client, other_client, other)) => { + let traded_items = item_manager.trade_items(entity_gateway, + room_id, + (&this_local_client, &this_client.character, &this.items), + (&other_local_client, &other_client.character, &other.items)).await?; + + let clients_in_room = client_location.get_all_clients_by_client(id)?; + let traded_item_packets = traded_items + .into_iter() + .map(|item| { + match item.item_detail { + ItemToTradeDetail::Individual(item_detail) => { + [ + GameMessage::CreateItem(builder::message::create_individual_item(item.add_to, item.new_item_id, &item_detail).unwrap()), + GameMessage::PlayerNoLongerHasItem(builder::message::player_no_longer_has_item(item.remove_from, item.current_item_id, 1)) // TODO: amount = ? + ] + }, + ItemToTradeDetail::Stacked(tool, amount) => { + [ + GameMessage::CreateItem(builder::message::create_stacked_item(item.add_to, item.new_item_id, &tool, amount).unwrap()), + GameMessage::PlayerNoLongerHasItem(builder::message::player_no_longer_has_item(item.remove_from, item.current_item_id, amount as u32)) + ] + }, + } + }) + .flatten() + .map(move |packet| { + clients_in_room + .clone() + .into_iter() + .filter_map(move |client| { + match packet { + GameMessage::PlayerNoLongerHasItem(ref no_longer) => { + if client.local_client == no_longer.client { + None + } + else { + Some((client.client, SendShipPacket::Message(Message::new(packet.clone())))) + } + } + _ => Some((client.client, SendShipPacket::Message(Message::new(packet.clone())))) + } + }) + }) + .flatten(); + let close_trade = vec![ + (this.client(), SendShipPacket::TradeSuccessful(TradeSuccessful::default())), + (other.client(), SendShipPacket::TradeSuccessful(TradeSuccessful::default())) + ].into_iter(); + Ok(Box::new(traded_item_packets.chain(close_trade))) } - }) - .flatten() - .map(move |packet| { - clients_in_room - .clone() - .into_iter() + } + }, + err @ _ => { + let (this, other) = trades.remove_trade(&id); + Ok(Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() + .filter(move |client| other.as_ref().map(|other| client.client == other.client()).unwrap_or_else(|| false)) .map(move |client| { - (client.client, SendShipPacket::Message(Message::new(packet.clone()))) + (client.client, SendShipPacket::CancelTrade(CancelTrade {})) }) - }) - .flatten(); - Ok(Box::new(traded_item_packets)) - } - else { - Ok(Box::new(None.into_iter())) + .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {})))))) + } } } diff --git a/src/ship/ship.rs b/src/ship/ship.rs index 8166936..0b97463 100644 --- a/src/ship/ship.rs +++ b/src/ship/ship.rs @@ -33,6 +33,7 @@ use crate::ship::quests; use crate::ship::map::{MapsError, MapAreaError, MapArea}; use crate::ship::packet::handler; use crate::ship::shops::{WeaponShop, ToolShop, ArmorShop, WeaponShopItem, ToolShopItem, ArmorShopItem}; +use crate::ship::trade::TradeState; pub const SHIP_PORT: u16 = 23423; pub const QUEST_CATEGORY_MENU_ID: u32 = 0xA2; @@ -41,7 +42,7 @@ pub type Rooms = [Option; MAX_ROOMS]; pub type Clients = HashMap; #[derive(Error, Debug)] -#[error("")] +#[error("shiperror")] pub enum ShipError { ClientNotFound(ClientId), NoCharacterInSlot(ClientId, u32), @@ -73,6 +74,7 @@ pub enum ShipError { InvalidShip(usize), InvalidBlock(usize), InvalidItem(items::ClientItemId), + TradeError(#[from] crate::ship::packet::handler::trade::TradeError), } #[derive(Debug)] @@ -190,6 +192,7 @@ pub enum SendShipPacket { RedirectClient(RedirectClient), AcknowledgeTrade(AcknowledgeTrade), CancelTrade(CancelTrade), + TradeSuccessful(TradeSuccessful), } impl SendServerPacket for SendShipPacket { @@ -229,6 +232,7 @@ impl SendServerPacket for SendShipPacket { SendShipPacket::RedirectClient(pkt) => pkt.as_bytes(), SendShipPacket::AcknowledgeTrade(pkt) => pkt.as_bytes(), SendShipPacket::CancelTrade(pkt) => pkt.as_bytes(), + SendShipPacket::TradeSuccessful(pkt) => pkt.as_bytes(), } } } @@ -248,25 +252,10 @@ pub struct LoadingQuest { } -#[derive(Clone)] -pub enum TradeItem { - Individual(items::ClientItemId), - Stacked(items::ClientItemId, usize), - Meseta(usize), -} -#[derive(Clone, Eq, PartialEq)] -pub enum TradeStatus { - Confirmed, - Unconfirmed, -} -#[derive(Clone)] -pub struct TradeState { - pub other_client: ClientId, - pub items: Vec, - pub status: TradeStatus, -} + + pub struct ClientState { pub user: UserAccountEntity, @@ -286,7 +275,6 @@ pub struct ClientState { pub tool_shop: Vec, pub armor_shop: Vec, pub tek: Option<(items::ClientItemId, item::weapon::TekSpecialModifier, item::weapon::TekPercentModifier, i32)>, - pub trade: Option, } impl ClientState { @@ -307,7 +295,6 @@ impl ClientState { tool_shop: Vec::new(), armor_shop: Vec::new(), tek: None, - trade: None, } } } @@ -411,6 +398,7 @@ impl ShipServerStateBuilder { auth_token: self.auth_token.unwrap_or_else(|| AuthToken("".into())), ship_list: Vec::new(), shipgate_sender: None, + trades: Default::default(), } } } @@ -457,6 +445,7 @@ pub struct ShipServerState { auth_token: AuthToken, ship_list: Vec, shipgate_sender: Option>, + trades: TradeState, } impl ShipServerState { @@ -561,6 +550,9 @@ impl ShipServerState { GameMessage::TekAccept(tek_accept) => { handler::direct_message::accept_tek_item(id, tek_accept, &mut self.entity_gateway, &block.client_location, &mut self.clients, &mut self.item_manager).await? }, + GameMessage::TradeRequest(trade_request) => { + handler::trade::trade_request(id, trade_request, target, &mut self.entity_gateway, &block.client_location, &mut self.clients, &mut self.item_manager, &mut self.trades).await? + }, _ => { let cmsg = msg.clone(); Box::new(block.client_location.get_all_clients_by_client(id).unwrap().into_iter() @@ -729,13 +721,12 @@ impl ServerState for ShipServerState { handler::ship::block_list(id, &self.name, self.blocks.0.len()) }, RecvShipPacket::ItemsToTrade(items_to_trade) => { - log::warn!("trade! {:?} {:?}", id, items_to_trade); let block = self.blocks.with_client(id, &self.clients)?; - handler::trade::items_to_trade(id, items_to_trade, &mut self.entity_gateway, &block.client_location, &mut self.clients, &mut self.item_manager).await? + handler::trade::items_to_trade(id, items_to_trade, &mut self.entity_gateway, &block.client_location, &mut self.clients, &mut self.item_manager, &mut self.trades).await? }, RecvShipPacket::TradeConfirmed(_) => { let block = self.blocks.with_client(id, &self.clients)?; - handler::trade::trade_confirmed(id, &mut self.entity_gateway, &block.client_location, &mut self.clients, &mut self.item_manager).await? + handler::trade::trade_confirmed(id, &mut self.entity_gateway, &block.client_location, &mut self.clients, &mut self.item_manager, &mut self.trades).await? }, }) } diff --git a/src/ship/trade.rs b/src/ship/trade.rs new file mode 100644 index 0000000..eb88edc --- /dev/null +++ b/src/ship/trade.rs @@ -0,0 +1,172 @@ +use std::collections::HashMap; +use std::cell::RefCell; +use async_std::sync::{Arc, Mutex}; + +use crate::common::serverstate::ClientId; +use crate::ship::items; + +pub const MESETA_ITEM_ID: items::ClientItemId = items::ClientItemId(0xFFFFFF01); + +#[derive(Debug, Clone)] +pub enum TradeItem { + Individual(items::ClientItemId), + Stacked(items::ClientItemId, usize), + Meseta(usize), +} + +impl TradeItem { + pub fn stacked(&self) -> Option<(items::ClientItemId, usize)> { + match self { + TradeItem::Stacked(item_id, amount) => Some((*item_id, *amount as usize)), + _ => None + } + } + + pub fn item_id(&self) -> items::ClientItemId { + match self { + TradeItem::Individual(item_id) => *item_id, + TradeItem::Stacked(item_id, _) => *item_id, + TradeItem::Meseta(_) => MESETA_ITEM_ID, + } + } +} + + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum TradeStatus { + SentRequest, + ReceivedRequest, + Trading, + Confirmed, + FinalConfirm, + ItemsChecked, + TradeComplete, +} + + +#[derive(Debug, Clone)] +pub struct ClientTradeState { + client: ClientId, + other_client: ClientId, + pub items: Vec, + pub status: TradeStatus, +} + + +impl ClientTradeState { + pub fn client(&self) -> ClientId { + self.client + } + + pub fn other_client(&self) -> ClientId { + self.other_client + } +} + +/* +pub struct ClientTradeHandle<'a> { + handle: std::cell::Ref<'a, ClientTradeState>, +} + +impl<'a> ClientTradeHandle<'a> { + fn new(handle: std::cell::Ref<'a, ClientTradeState>) -> ClientTradeHandle<'a> { + ClientTradeHandle { + handle: handle, + } + } +} + +impl<'a> std::ops::Deref for ClientTradeHandle<'a> { + type Target = ClientTradeState; + fn deref(&self) -> &ClientTradeState { + &self.handle + } +} + +impl<'a> std::ops::DerefMut for ClientTradeHandle<'a> { + fn deref_mut(&mut self) -> &mut ClientTradeState { + &mut self.handle + } +} + +impl<'a> Clone for ClientTradeHandle<'a> { + +} +*/ + + +#[derive(thiserror::Error, Debug)] +#[error("")] +pub enum TradeStateError { + ClientNotInTrade(ClientId), + MismatchedTrade(ClientId, ClientId), +} + +#[derive(Default)] +pub struct TradeState { + trades: HashMap>, +} + +impl TradeState { + pub fn new_trade(&mut self, sender: &ClientId, receiver: &ClientId) { + let state = ClientTradeState { + client: *sender, + other_client: *receiver, + items: Default::default(), + status: TradeStatus::SentRequest, + }; + self.trades.insert(*sender, RefCell::new(state)); + + let state = ClientTradeState { + client: *receiver, + other_client: *sender, + items: Default::default(), + status: TradeStatus::ReceivedRequest, + }; + self.trades.insert(*receiver, RefCell::new(state)); + } + + pub fn with (&self, client: &ClientId, func: F) -> Result + where + F: Fn(&mut ClientTradeState, &mut ClientTradeState) -> T + { + let mut c1 = self.trades.get(client).ok_or_else(|| TradeStateError::ClientNotInTrade(*client))?.borrow_mut(); + let mut c2 = self.trades.get(&c1.other_client).ok_or_else(|| TradeStateError::ClientNotInTrade(c1.other_client))?.borrow_mut(); + + // sanity check + if c1.client != c2.other_client { + return Err(TradeStateError::MismatchedTrade(c1.client, c2.client)); + } + + Ok(func(&mut *c1, &mut *c2)) + } + /* + pub fn with (&self, client: &ClientId, func: F) -> Result + where + F: Fn(&mut ClientTradeHandle, &mut ClientTradeHandle) -> T + { + let c1 = ClientTradeHandle::new(self.trades.get(client).ok_or_else(|| TradeStateError::ClientNotInTrade(*client))?.borrow_mut()); + let c2 = ClientTradeHandle::new(self.trades.get(&c1.other_client).ok_or_else(|| TradeStateError::ClientNotInTrade(c1.other_client))?.borrow_mut()); + + // sanity check + if c1.client != c2.other_client { + return Err(TradeStateError::MismatchedTrade(c1.client, c2.client)); + } + + Ok(func(&mut c1, &mut c2)) + } + */ + + // TODO: is it possible for this to not return Options? + pub fn remove_trade(&mut self, client: &ClientId) -> (Option, Option) { + let c1 = self.trades.remove(client).map(|c| c.into_inner()); + let c2 = if let Some(ref state) = c1 { + self.trades.remove(&state.other_client).map(|c| c.into_inner()) + } + else { + None + }; + + (c1, c2) + } +} From 71ba3c3c3d9f2a333e16a14799c0aa4fd54f5212 Mon Sep 17 00:00:00 2001 From: jake Date: Fri, 10 Dec 2021 13:25:27 -0700 Subject: [PATCH 30/41] forgot to add this migration --- .../gateway/postgres/migrations/V0003__item_notes.sql | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/entity/gateway/postgres/migrations/V0003__item_notes.sql diff --git a/src/entity/gateway/postgres/migrations/V0003__item_notes.sql b/src/entity/gateway/postgres/migrations/V0003__item_notes.sql new file mode 100644 index 0000000..3a2f250 --- /dev/null +++ b/src/entity/gateway/postgres/migrations/V0003__item_notes.sql @@ -0,0 +1,7 @@ +drop table item_location; + +create table item_note ( + item integer references item (id) not null, + note jsonb not null, + created_at timestamptz default current_timestamp not null +); From 3b7dcd02957ae1653a060713a68ea51d7c88b8b7 Mon Sep 17 00:00:00 2001 From: jake Date: Fri, 10 Dec 2021 13:43:11 -0700 Subject: [PATCH 31/41] remove printlns --- src/ship/packet/handler/trade.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/ship/packet/handler/trade.rs b/src/ship/packet/handler/trade.rs index bd311d2..3c74384 100644 --- a/src/ship/packet/handler/trade.rs +++ b/src/ship/packet/handler/trade.rs @@ -270,7 +270,6 @@ where { Ok(trades .with(&id, |this, other| -> Result + Send>, anyhow::Error> { - println!("statuses {:?} {:?}", this.status, other.status); if status_is_not(&this.status, &[TradeStatus::FinalConfirm]) || status_is_not(&other.status, &[TradeStatus::FinalConfirm, TradeStatus::ItemsChecked]) { //if this.status != TradeStatus::FinalConfirm || (other.status != TradeStatus::FinalConfirm || other.status != TradeStatus::ItemsChecked) { return Err(TradeError::MismatchedStatus.into()) @@ -292,7 +291,6 @@ where .unwrap(); match real_item { InventoryItem::Individual(individual_inventory_item) => { - println!("real indiv item: {:?} {:?} ==? {:?}", real_item, real_item.as_client_bytes(), trade_item_bytes); if real_item.as_client_bytes() == trade_item_bytes { Ok(TradeItem::Individual(individual_inventory_item.item_id)) } @@ -301,7 +299,6 @@ where } }, InventoryItem::Stacked(stacked_inventory_item) => { - println!("real stack item: {:?} {:?} ==? {:?}", real_item, real_item.as_client_bytes(), trade_item_bytes); if real_item.as_client_bytes()[0..4] == trade_item_bytes[0..4] { let amount = trade_item_bytes[5] as usize; if amount <= stacked_inventory_item.entity_ids.len() { @@ -323,7 +320,6 @@ where Ok(Box::new(std::iter::once((other.client(), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {}))))) })? .unwrap_or_else(|err| { - println!("asdf {:?}", err); log::warn!("trade error: {:?}", err); let (this, other) = trades.remove_trade(&id); Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() @@ -351,7 +347,6 @@ where Ok(p) => Ok(p), Err(err) => { log::warn!("atrade error: {:?}", err); - println!("qwer"); let (this, other) = trades.remove_trade(&id); Ok(Box::new(client_location.get_all_clients_by_client(id)?.into_iter() .filter(move |client| other.as_ref().map(|other| client.client == other.client()).unwrap_or_else(|| false)) From 6d2753d082cd8f02106745a1c5c20c84b0225fd9 Mon Sep 17 00:00:00 2001 From: jake Date: Fri, 10 Dec 2021 23:41:17 -0700 Subject: [PATCH 32/41] improve error display a bit --- src/common/serverstate.rs | 2 +- src/entity/character.rs | 2 +- src/entity/item/mod.rs | 2 +- src/ship/items/manager.rs | 15 ++++++++++++++- src/ship/items/mod.rs | 2 +- src/ship/location.rs | 20 ++++++++++---------- src/ship/ship.rs | 7 ++++++- src/ship/trade.rs | 2 +- 8 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/common/serverstate.rs b/src/common/serverstate.rs index ba9caa6..4d272ce 100644 --- a/src/common/serverstate.rs +++ b/src/common/serverstate.rs @@ -1,7 +1,7 @@ use libpso::PacketParseError; use libpso::crypto::PSOCipher; -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, derive_more::Display)] pub struct ClientId(pub usize); pub enum OnConnect { diff --git a/src/entity/character.rs b/src/entity/character.rs index 7c1572e..d6d4e3c 100644 --- a/src/entity/character.rs +++ b/src/entity/character.rs @@ -272,7 +272,7 @@ pub struct CharacterMaterials { pub tp: u32, } -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default, derive_more::Display)] pub struct CharacterEntityId(pub u32); #[derive(Clone)] diff --git a/src/entity/item/mod.rs b/src/entity/item/mod.rs index 611c579..ff722dd 100644 --- a/src/entity/item/mod.rs +++ b/src/entity/item/mod.rs @@ -17,7 +17,7 @@ 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, Hash, PartialOrd, Ord, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, derive_more::Display)] pub struct BankName(pub String); #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub struct TradeId(pub u32); diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs index f669309..143fd26 100644 --- a/src/ship/items/manager.rs +++ b/src/ship/items/manager.rs @@ -34,33 +34,46 @@ pub enum TriggerCreateItem { } #[derive(Error, Debug)] -#[error("")] +#[error("itemmanager")] pub enum ItemManagerError { + #[error("gateway")] EntityGatewayError, + #[error("no such item id {0}")] NoSuchItemId(ClientItemId), NoCharacter(CharacterEntityId), NoRoom(RoomId), CouldNotAddToInventory(ClientItemId), //ItemBelongsToOtherPlayer, + #[error("shrug")] Idunnoman, CouldNotSplitItem(ClientItemId), + #[error("could not drop meseta")] CouldNotDropMeseta, InvalidBankName(BankName), + #[error("not enough tools")] NotEnoughTools(Tool, usize, usize), // have, expected InventoryItemConsumeError(#[from] InventoryItemConsumeError), + #[error("bank full")] BankFull, WrongItemType(ClientItemId), UseItemError(#[from] use_tool::UseItemError), + #[error("could not buy item")] CouldNotBuyItem, + #[error("could not add bought item to inventory")] CouldNotAddBoughtItemToInventory, ItemIdNotInInventory(ClientItemId), + #[error("cannot get mut item")] CannotGetMutItem, + #[error("cannot get individual item")] CannotGetIndividualItem, InvalidSlot(u8, u8), // slots available, slot attempted + #[error("no armor equipped")] NoArmorEquipped, GatewayError(#[from] GatewayError), + #[error("stacked item")] StackedItemError(Vec), ItemTransactionAction(Box), + #[error("invalid trade")] InvalidTrade, } diff --git a/src/ship/items/mod.rs b/src/ship/items/mod.rs index 7a259ae..37de948 100644 --- a/src/ship/items/mod.rs +++ b/src/ship/items/mod.rs @@ -6,7 +6,7 @@ mod transaction; pub mod use_tool; use serde::{Serialize, Deserialize}; -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, derive_more::Display)] pub struct ClientItemId(pub u32); // TODO: remove these and fix use statements in the rest of the codebase diff --git a/src/ship/location.rs b/src/ship/location.rs index a40f858..efba55f 100644 --- a/src/ship/location.rs +++ b/src/ship/location.rs @@ -15,7 +15,7 @@ pub enum AreaType { #[derive(Debug, Copy, Clone, PartialEq)] pub struct LobbyId(pub usize); -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, derive_more::Display)] pub struct RoomId(pub usize); impl LobbyId { @@ -26,7 +26,7 @@ impl LobbyId { #[derive(Error, Debug, PartialEq)] -#[error("")] +#[error("create room")] pub enum CreateRoomError { NoOpenSlots, ClientInAreaAlready, @@ -34,7 +34,7 @@ pub enum CreateRoomError { } #[derive(Error, Debug, PartialEq)] -#[error("")] +#[error("join room")] pub enum JoinRoomError { RoomDoesNotExist, RoomFull, @@ -42,7 +42,7 @@ pub enum JoinRoomError { } #[derive(Error, Debug, PartialEq)] -#[error("")] +#[error("join lobby")] pub enum JoinLobbyError { LobbyDoesNotExist, LobbyFull, @@ -50,7 +50,7 @@ pub enum JoinLobbyError { } #[derive(Error, Debug, PartialEq)] -#[error("")] +#[error("get area")] pub enum GetAreaError { NotInRoom, NotInLobby, @@ -58,28 +58,28 @@ pub enum GetAreaError { } #[derive(Error, Debug, PartialEq)] -#[error("")] +#[error("client removal")] pub enum ClientRemovalError { ClientNotInArea, InvalidArea, } #[derive(Error, Debug, PartialEq)] -#[error("")] +#[error("get clients")] pub enum GetClientsError { InvalidClient, InvalidArea, } #[derive(Error, Debug, PartialEq)] -#[error("")] +#[error("get neighbor")] pub enum GetNeighborError { InvalidClient, InvalidArea, } #[derive(Error, Debug, PartialEq)] -#[error("")] +#[error("get leader")] pub enum GetLeaderError { InvalidClient, InvalidArea, @@ -87,7 +87,7 @@ pub enum GetLeaderError { } #[derive(Error, Debug, PartialEq)] -#[error("")] +#[error("clientlocation")] pub enum ClientLocationError { CreateRoomError(#[from] CreateRoomError), JoinRoomError(#[from] JoinRoomError), diff --git a/src/ship/ship.rs b/src/ship/ship.rs index 0b97463..6ee71fe 100644 --- a/src/ship/ship.rs +++ b/src/ship/ship.rs @@ -42,11 +42,12 @@ pub type Rooms = [Option; MAX_ROOMS]; pub type Clients = HashMap; #[derive(Error, Debug)] -#[error("shiperror")] +#[error("shiperror {0:?}")] pub enum ShipError { ClientNotFound(ClientId), NoCharacterInSlot(ClientId, u32), InvalidSlot(ClientId, u32), + #[error("")] TooManyClients, ClientLocationError(#[from] ClientLocationError), GetNeighborError(#[from] GetNeighborError), @@ -57,10 +58,12 @@ pub enum ShipError { InvalidRoom(u32), MonsterAlreadyDroppedItem(ClientId, u16), SliceError(#[from] std::array::TryFromSliceError), + #[error("")] ItemError, // TODO: refine this PickUpInvalidItemId(u32), DropInvalidItemId(u32), ItemManagerError(#[from] items::ItemManagerError), + #[error("")] ItemDropLocationNotSet, BoxAlreadyDroppedItem(ClientId, u16), InvalidQuestCategory(u32), @@ -68,12 +71,14 @@ pub enum ShipError { InvalidQuestFilename(String), IoError(#[from] std::io::Error), NotEnoughMeseta(ClientId, u32), + #[error("")] ShopError, GatewayError(#[from] GatewayError), UnknownMonster(crate::ship::monster::MonsterType), InvalidShip(usize), InvalidBlock(usize), InvalidItem(items::ClientItemId), + #[error("tradeerror {0}")] TradeError(#[from] crate::ship::packet::handler::trade::TradeError), } diff --git a/src/ship/trade.rs b/src/ship/trade.rs index eb88edc..a3a3b0a 100644 --- a/src/ship/trade.rs +++ b/src/ship/trade.rs @@ -102,7 +102,7 @@ pub enum TradeStateError { MismatchedTrade(ClientId, ClientId), } -#[derive(Default)] +#[derive(Default, Debug)] pub struct TradeState { trades: HashMap>, } From 6bd341cd010b250b22b7dbe91d88b922c313d504 Mon Sep 17 00:00:00 2001 From: jake Date: Sun, 12 Dec 2021 15:55:24 -0700 Subject: [PATCH 33/41] check inventory has enough space to accept traded items --- src/ship/items/inventory.rs | 12 +++++++--- src/ship/items/manager.rs | 48 +++++++++++++++++++++++++++++++++---- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/ship/items/inventory.rs b/src/ship/items/inventory.rs index a44c97d..5bb5c19 100644 --- a/src/ship/items/inventory.rs +++ b/src/ship/items/inventory.rs @@ -93,10 +93,16 @@ pub enum YesThereIsSpace { ExistingStack, } +#[derive(Debug, Clone)] +pub enum NoThereIsNotSpace { + FullStack, + FullInventory, +} + #[derive(Debug, Clone)] pub enum SpaceForStack { Yes(YesThereIsSpace), - No, + No(NoThereIsNotSpace), } impl InventoryItem { @@ -470,7 +476,7 @@ impl CharacterInventory { SpaceForStack::Yes(YesThereIsSpace::ExistingStack) } else { - SpaceForStack::No + SpaceForStack::No(NoThereIsNotSpace::FullStack) } } None => { @@ -478,7 +484,7 @@ impl CharacterInventory { SpaceForStack::Yes(YesThereIsSpace::NewStack) } else { - SpaceForStack::No + SpaceForStack::No(NoThereIsNotSpace::FullInventory) } } } diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs index 143fd26..3050432 100644 --- a/src/ship/items/manager.rs +++ b/src/ship/items/manager.rs @@ -15,6 +15,7 @@ use crate::ship::trade::TradeItem; use crate::ship::drops::{ItemDrop, ItemDropType}; use crate::ship::location::{AreaClient, RoomId}; use crate::ship::shops::ShopItem; +use crate::ship::packet::handler::trade::TradeError; use crate::ship::items::bank::*; use crate::ship::items::floor::*; @@ -329,7 +330,7 @@ impl ItemManager { })); TriggerCreateItem::No }, - SpaceForStack::No => { + SpaceForStack::No(_) => { return Err(ItemManagerError::CouldNotAddToInventory(*item_id).into()); }, } @@ -945,9 +946,48 @@ impl ItemManager { let p1_inventory = it.manager.get_character_inventory(p1.1)?; let p2_inventory = it.manager.get_character_inventory(p2.1)?; - //TODO: inv-selftrade+othertrade <= 30 - //if p1_inventory - + [(p2_inventory, p1_inventory, p2.2), (p1_inventory, p2_inventory, p1.2)].iter() + .map(|(src_inventory, dest_inventory, to_trade)| { + to_trade + .iter() + .try_fold(dest_inventory.count(), |acc, item| { + match item { + TradeItem::Individual(..) => { + if acc >= 30 { + Err(TradeError::NoInventorySpace) + } + else { + Ok(acc + 1) + } + }, + TradeItem::Stacked(item_id, amount) => { + let stacked_inventory_item = src_inventory + .get_item_by_id(*item_id) + .ok_or_else(|| TradeError::InvalidItemId(*item_id))? + .stacked() + .ok_or_else(|| TradeError::InvalidItemId(*item_id))?; + match dest_inventory.space_for_stacked_item(&stacked_inventory_item.tool, *amount) { + SpaceForStack::Yes(YesThereIsSpace::ExistingStack) => { + Ok(acc) + }, + SpaceForStack::Yes(YesThereIsSpace::NewStack) => { + Ok(acc + 1) + }, + SpaceForStack::No(NoThereIsNotSpace::FullStack) => { + Err(TradeError::NoStackSpace) + }, + SpaceForStack::No(NoThereIsNotSpace::FullInventory) => { + Err(TradeError::NoInventorySpace) + }, + } + }, + TradeItem::Meseta(..) => { + Ok(acc) + } + } + }) + }) + .collect::, _>>()?; let trade_items = [(p1, p2, p1_inventory, p2_inventory), (p2, p1, p2_inventory, p1_inventory)] .map(|(src_client, dest_client, src_inventory, dest_inventory)| { From 872794e45fe4904a44ca8896b67cc7aa753b041b Mon Sep 17 00:00:00 2001 From: jake Date: Sun, 12 Dec 2021 15:55:59 -0700 Subject: [PATCH 34/41] improve error messages --- src/ship/items/manager.rs | 5 ++--- src/ship/items/mod.rs | 2 +- src/ship/items/transaction.rs | 6 ++++-- src/ship/packet/handler/trade.rs | 28 ++++++++++++++++------------ 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs index 3050432..37808a4 100644 --- a/src/ship/items/manager.rs +++ b/src/ship/items/manager.rs @@ -1072,10 +1072,9 @@ impl ItemManager { } }); - Ok(it.commit(self, entity_gateway) + it.commit(self, entity_gateway) .await - //.map_err(|err| err.into()) - .unwrap()) + .map_err(|err| err.into()) } } diff --git a/src/ship/items/mod.rs b/src/ship/items/mod.rs index 37de948..0d76eb3 100644 --- a/src/ship/items/mod.rs +++ b/src/ship/items/mod.rs @@ -2,7 +2,7 @@ mod bank; mod floor; pub mod inventory; mod manager; -mod transaction; +pub mod transaction; pub mod use_tool; use serde::{Serialize, Deserialize}; diff --git a/src/ship/items/transaction.rs b/src/ship/items/transaction.rs index 4c2132a..c21c868 100644 --- a/src/ship/items/transaction.rs +++ b/src/ship/items/transaction.rs @@ -13,9 +13,10 @@ use crate::entity::gateway::GatewayError; use crate::ship::location::{AreaClient, RoomId}; #[derive(Error, Debug)] -#[error("")] pub enum TransactionCommitError { + #[error("transaction commit gateway error {0}")] Gateway(#[from] GatewayError), + #[error("transaction commit itemmanager error {0}")] ItemManager(#[from] ItemManagerError), } @@ -77,9 +78,10 @@ impl<'a, T, EG: EntityGateway> ItemTransaction<'a, T, EG> { #[derive(Error, Debug)] -#[error("")] pub enum TransactionError { + #[error("transaction action error {0:?}")] Action(E), + #[error("transaction commit error {0}")] Commit(#[from] TransactionCommitError), } diff --git a/src/ship/packet/handler/trade.rs b/src/ship/packet/handler/trade.rs index 3c74384..bd4134a 100644 --- a/src/ship/packet/handler/trade.rs +++ b/src/ship/packet/handler/trade.rs @@ -18,10 +18,12 @@ use libpso::utf8_to_utf16_array; use crate::ship::packet::builder; use crate::ship::shops::{ShopItem, ToolShopItem, ArmorShopItem}; -#[derive(thiserror::Error, Debug)] +#[derive(thiserror::Error, Debug, PartialEq, Eq)] pub enum TradeError { #[error("no partner")] CouldNotFindTradePartner, + #[error("invalid item id")] + InvalidItemId(ClientItemId), #[error("item does not match id")] ClientItemIdDidNotMatchItem(ClientItemId, [u8; 16]), #[error("invalid stack {1}")] @@ -30,6 +32,10 @@ pub enum TradeError { NotInTradeMenu, #[error("trade menu at an invalid point")] MismatchedStatus, + #[error("no space in inventory")] + NoInventorySpace, + #[error("no space in stack")] + NoStackSpace, } @@ -97,11 +103,11 @@ where }, TradeRequestCommand::AddItem(item_id, amount) => { Ok(trades - .with(&id, |this, other| -> Option + Send>> { + .with(&id, |this, other| -> Result + Send>, anyhow::Error> { if this.status == TradeStatus::Trading && other.status == TradeStatus::Trading { - let client = clients.get(&this.client())?;//.ok_or(ShipError::ClientNotFound(id)).ok()?; - let inventory = item_manager.get_character_inventory(&client.character).ok()?; - let item = inventory.get_item_by_id(ClientItemId(item_id))?; + let client = clients.get(&this.client()).ok_or(ShipError::ClientNotFound(this.client()))?; + let inventory = item_manager.get_character_inventory(&client.character)?; + let item = inventory.get_item_by_id(ClientItemId(item_id)).ok_or(ItemManagerError::NoSuchItemId(ClientItemId(item_id)))?; match item { InventoryItem::Individual(_) => { @@ -109,24 +115,24 @@ where }, InventoryItem::Stacked(stacked_item) => { if stacked_item.count() < amount as usize { - return None; + return Err(TradeError::InvalidStackAmount(ClientItemId(item_id), amount as usize).into()); } this.items.push(TradeItem::Stacked(ClientItemId(item_id), amount as usize)); }, } let trade_request = trade_request.clone(); - Some(Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() + Ok(Box::new(client_location.get_all_clients_by_client(id)?.into_iter() .filter(move |client| client.local_client.id() == target as u8) .map(move |client| { (client.client, SendShipPacket::DirectMessage(DirectMessage::new(target, GameMessage::TradeRequest(trade_request.clone())))) }))) } else { - None + Err(TradeError::MismatchedStatus.into()) } })? - .unwrap_or_else(|| { + .unwrap_or_else(|err| { trades.remove_trade(&id); Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() .filter(move |client| client.local_client.id() == target as u8) @@ -194,7 +200,7 @@ where TradeRequestCommand::Confirm => { Ok(trades .with(&id, |this, other| -> Option + Send>> { - if this.status == TradeStatus::Trading && (other.status == TradeStatus::Trading || other.status == TradeStatus::Confirmed) { + if status_is(&this.status, &[TradeStatus::Trading]) && status_is(&other.status, &[TradeStatus::Trading, TradeStatus::Confirmed]) { this.status = TradeStatus::Confirmed; let trade_request = trade_request.clone(); @@ -380,8 +386,6 @@ where if status_is_not(&this.status, &[TradeStatus::ItemsChecked]) || status_is_not(&other.status, &[TradeStatus::ItemsChecked, TradeStatus::TradeComplete]) { return Err(TradeError::MismatchedStatus.into()) } - - // TODO: check for space in inventory! this.status = TradeStatus::TradeComplete; if this.status == TradeStatus::TradeComplete && other.status == TradeStatus::TradeComplete { From 3a10de502ae0019ef73758e4066dec8ca4bac0b7 Mon Sep 17 00:00:00 2001 From: jake Date: Sun, 12 Dec 2021 22:33:58 -0700 Subject: [PATCH 35/41] tell other players you don't have meseta when you drop it --- src/ship/packet/builder/message.rs | 9 +++++++++ src/ship/packet/handler/message.rs | 17 +++++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/ship/packet/builder/message.rs b/src/ship/packet/builder/message.rs index 01c434c..1361b1d 100644 --- a/src/ship/packet/builder/message.rs +++ b/src/ship/packet/builder/message.rs @@ -156,6 +156,15 @@ pub fn player_no_longer_has_item(area_client: AreaClient, item_id: ClientItemId, } } +pub fn player_no_longer_has_meseta(area_client: AreaClient, amount: u32) -> PlayerNoLongerHasItem { + PlayerNoLongerHasItem { + client: area_client.local_client.id(), + target: 0, + item_id: 0xFFFFFFFF, + amount, + } +} + pub fn shop_list(shop_type: u8, items: &[I]) -> ShopList { let items = items.iter() .enumerate() diff --git a/src/ship/packet/handler/message.rs b/src/ship/packet/handler/message.rs index 39d3717..882c3e0 100644 --- a/src/ship/packet/handler/message.rs +++ b/src/ship/packet/handler/message.rs @@ -135,13 +135,26 @@ where 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)?; + let no_longer_has_meseta_pkt = builder::message::player_no_longer_has_meseta(area_client, no_longer_has_item.amount as u32); client.item_drop_location = None; let clients_in_area = client_location.get_clients_in_room(room_id).map_err(|err| -> ClientLocationError { err.into() })?; Ok(Box::new(clients_in_area.into_iter() .map(move |c| { - (c.client, SendShipPacket::Message(Message::new(GameMessage::DropSplitStack(dropped_meseta_pkt.clone())))) - }))) + std::iter::once((c.client, SendShipPacket::Message(Message::new(GameMessage::DropSplitStack(dropped_meseta_pkt.clone()))))) + .chain( + if c.client != id { + Box::new(std::iter::once( + (c.client, SendShipPacket::Message(Message::new(GameMessage::PlayerNoLongerHasItem(no_longer_has_meseta_pkt.clone())))) + )) as Box + Send> + } + else { + Box::new(std::iter::empty()) as Box + Send> + } + ) + }) + .flatten() + )) } else { 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?; From 72d72801e1199974335c2098d22e637ebf237489 Mon Sep 17 00:00:00 2001 From: jake Date: Sun, 12 Dec 2021 22:55:08 -0700 Subject: [PATCH 36/41] trade meseta --- src/ship/items/manager.rs | 38 ++++--- src/ship/packet/builder/message.rs | 12 ++ src/ship/packet/handler/trade.rs | 174 ++++++++++++++++++----------- src/ship/trade.rs | 7 +- 4 files changed, 145 insertions(+), 86 deletions(-) diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs index 37808a4..efd9a64 100644 --- a/src/ship/items/manager.rs +++ b/src/ship/items/manager.rs @@ -15,7 +15,7 @@ use crate::ship::trade::TradeItem; use crate::ship::drops::{ItemDrop, ItemDropType}; use crate::ship::location::{AreaClient, RoomId}; use crate::ship::shops::ShopItem; -use crate::ship::packet::handler::trade::TradeError; +use crate::ship::packet::handler::trade::{TradeError, OTHER_MESETA_ITEM_ID}; use crate::ship::items::bank::*; use crate::ship::items::floor::*; @@ -938,8 +938,8 @@ impl ItemManager { pub async fn trade_items(&mut self, entity_gateway: &mut EG, room_id: RoomId, - p1: (&AreaClient, &CharacterEntity, &Vec), - p2: (&AreaClient, &CharacterEntity, &Vec)) + p1: (&AreaClient, &CharacterEntity, &Vec, usize), + p2: (&AreaClient, &CharacterEntity, &Vec, usize)) -> Result, anyhow::Error> { let it = ItemTransaction::new(&self, (p1, p2, room_id)) .act(|it, (p1, p2, room_id)| -> Result<_, anyhow::Error> { @@ -980,9 +980,6 @@ impl ItemManager { Err(TradeError::NoInventorySpace) }, } - }, - TradeItem::Meseta(..) => { - Ok(acc) } } }) @@ -1038,18 +1035,28 @@ impl ItemManager { }), )) } - }, - TradeItem::Meseta(amount) => { - Some((None, // is there a packet that informs other clients about meseta changes? - Box::new(TradeMeseta { - src_character_id: src_client.1.id, - dest_character_id: dest_client.1.id, - amount: *amount, - }), - )) } } }) + .chain( + if src_client.3 > 0 { + Box::new(std::iter::once(Some( + (Some(ItemToTrade { + add_to: *dest_client.0, + remove_from: *src_client.0, + current_item_id: OTHER_MESETA_ITEM_ID, + new_item_id: OTHER_MESETA_ITEM_ID, + item_detail: ItemToTradeDetail::Meseta(src_client.3) + }), + Box::new(TradeMeseta { + src_character_id: src_client.1.id, + dest_character_id: dest_client.1.id, + amount: src_client.3, + }) as Box>)))) as Box> + } + else { + Box::new(std::iter::empty()) as Box> + }) .collect::>>() }); @@ -1082,6 +1089,7 @@ impl ItemManager { pub enum ItemToTradeDetail { Individual(ItemDetail), Stacked(Tool, usize), + Meseta(usize), } #[derive(Debug)] diff --git a/src/ship/packet/builder/message.rs b/src/ship/packet/builder/message.rs index 1361b1d..186e637 100644 --- a/src/ship/packet/builder/message.rs +++ b/src/ship/packet/builder/message.rs @@ -53,6 +53,18 @@ pub fn create_stacked_item(area_client: AreaClient, item_id: ClientItemId, tool: }) } +pub fn create_meseta(area_client: AreaClient, amount: usize) -> CreateItem { + let bytes: [u8; 12] = [4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + CreateItem { + client: area_client.local_client.id(), + target: 0, + item_data: bytes, + item_id: 0xFFFFFFFF, + item_data2: u32::to_le_bytes(amount as u32), + unknown: 0, + } +} + pub fn create_withdrawn_inventory_item(area_client: AreaClient, item: &InventoryItem) -> Result { let bytes = item.as_client_bytes(); Ok(CreateItem { diff --git a/src/ship/packet/handler/trade.rs b/src/ship/packet/handler/trade.rs index bd4134a..8300bb9 100644 --- a/src/ship/packet/handler/trade.rs +++ b/src/ship/packet/handler/trade.rs @@ -18,6 +18,10 @@ use libpso::utf8_to_utf16_array; use crate::ship::packet::builder; use crate::ship::shops::{ShopItem, ToolShopItem, ArmorShopItem}; +pub const MESETA_ITEM_ID: ClientItemId = ClientItemId(0xFFFFFF01); +pub const OTHER_MESETA_ITEM_ID: ClientItemId = ClientItemId(0xFFFFFFFF); + + #[derive(thiserror::Error, Debug, PartialEq, Eq)] pub enum TradeError { #[error("no partner")] @@ -36,6 +40,8 @@ pub enum TradeError { NoInventorySpace, #[error("no space in stack")] NoStackSpace, + #[error("invalid meseta amount")] + InvalidMeseta, } @@ -60,7 +66,7 @@ where let trade_partner = client_location.get_client_neighbors(id)? .into_iter() .filter(|ac| { - ac.local_client.id() == trade_request.client + ac.local_client.id() == target as u8 //trade_request.client }) .next() .ok_or(TradeError::CouldNotFindTradePartner)?; @@ -107,18 +113,23 @@ where if this.status == TradeStatus::Trading && other.status == TradeStatus::Trading { let client = clients.get(&this.client()).ok_or(ShipError::ClientNotFound(this.client()))?; let inventory = item_manager.get_character_inventory(&client.character)?; - let item = inventory.get_item_by_id(ClientItemId(item_id)).ok_or(ItemManagerError::NoSuchItemId(ClientItemId(item_id)))?; + if ClientItemId(item_id) == MESETA_ITEM_ID { + this.meseta += amount as usize; + } + else { + let item = inventory.get_item_by_id(ClientItemId(item_id)).ok_or(ItemManagerError::NoSuchItemId(ClientItemId(item_id)))?; - match item { - InventoryItem::Individual(_) => { - this.items.push(TradeItem::Individual(ClientItemId(item_id))); - }, - InventoryItem::Stacked(stacked_item) => { - if stacked_item.count() < amount as usize { - return Err(TradeError::InvalidStackAmount(ClientItemId(item_id), amount as usize).into()); - } - this.items.push(TradeItem::Stacked(ClientItemId(item_id), amount as usize)); - }, + match item { + InventoryItem::Individual(_) => { + this.items.push(TradeItem::Individual(ClientItemId(item_id))); + }, + InventoryItem::Stacked(stacked_item) => { + if stacked_item.count() < amount as usize { + return Err(TradeError::InvalidStackAmount(ClientItemId(item_id), amount as usize).into()); + } + this.items.push(TradeItem::Stacked(ClientItemId(item_id), amount as usize)); + }, + } } let trade_request = trade_request.clone(); @@ -148,33 +159,38 @@ where if this.status == TradeStatus::Trading && other.status == TradeStatus::Trading { let client = clients.get(&this.client())?; //.ok_or(ShipError::ClientNotFound(id)).ok()?; let inventory = item_manager.get_character_inventory(&client.character).ok()?; - let item = inventory.get_item_by_id(ClientItemId(item_id))?; + if ClientItemId(item_id) == MESETA_ITEM_ID { + this.meseta -= amount as usize; + } + else { + let item = inventory.get_item_by_id(ClientItemId(item_id))?; - match item { - InventoryItem::Individual(_) => { - this.items.retain(|item| { - item.item_id() != ClientItemId(item_id) - }) - //this.items.push(TradeItem::Individual(ClientItemId(item_id))); - }, - InventoryItem::Stacked(stacked_item) => { - let trade_item_index = this.items.iter() - .position(|item| { - item.item_id() == ClientItemId(item_id) - })?; + match item { + InventoryItem::Individual(_) => { + this.items.retain(|item| { + item.item_id() != ClientItemId(item_id) + }) + //this.items.push(TradeItem::Individual(ClientItemId(item_id))); + }, + InventoryItem::Stacked(stacked_item) => { + let trade_item_index = this.items.iter() + .position(|item| { + item.item_id() == ClientItemId(item_id) + })?; - match this.items[trade_item_index].stacked()?.1.cmp(&(amount as usize)) { - std::cmp::Ordering::Greater => { - this.items[trade_item_index].stacked()?.1 -= amount as usize; - }, - std::cmp::Ordering::Equal => { - this.items.remove(trade_item_index); - }, - std::cmp::Ordering::Less => { - return None + match this.items[trade_item_index].stacked()?.1.cmp(&(amount as usize)) { + std::cmp::Ordering::Greater => { + this.items[trade_item_index].stacked()?.1 -= amount as usize; + }, + std::cmp::Ordering::Equal => { + this.items.remove(trade_item_index); + }, + std::cmp::Ordering::Less => { + return None + } } - } - }, + }, + } } let trade_request = trade_request.clone(); Some(Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() @@ -277,7 +293,6 @@ where Ok(trades .with(&id, |this, other| -> Result + Send>, anyhow::Error> { if status_is_not(&this.status, &[TradeStatus::FinalConfirm]) || status_is_not(&other.status, &[TradeStatus::FinalConfirm, TradeStatus::ItemsChecked]) { - //if this.status != TradeStatus::FinalConfirm || (other.status != TradeStatus::FinalConfirm || other.status != TradeStatus::ItemsChecked) { return Err(TradeError::MismatchedStatus.into()) } @@ -285,37 +300,48 @@ where let inventory = item_manager.get_character_inventory(&client.character)?; let item_blobs = items_to_trade.items.iter().take(items_to_trade.count as usize); - let trade_items = item_blobs + item_blobs .map(|item| { - // TOOD: meseta? - let real_item = inventory.get_item_by_id(ClientItemId(item.item_id)) - .ok_or(ItemManagerError::NoSuchItemId(ClientItemId(item.item_id)))?; - let trade_item_bytes: [u8; 16] = item.item_data.iter() - .chain(item.item_data2.iter()) - .cloned().collect::>() - .try_into() - .unwrap(); - match real_item { - InventoryItem::Individual(individual_inventory_item) => { - if real_item.as_client_bytes() == trade_item_bytes { - Ok(TradeItem::Individual(individual_inventory_item.item_id)) - } - else { - Err(TradeError::ClientItemIdDidNotMatchItem(ClientItemId(item.item_id), trade_item_bytes).into()) - } - }, - InventoryItem::Stacked(stacked_inventory_item) => { - if real_item.as_client_bytes()[0..4] == trade_item_bytes[0..4] { - let amount = trade_item_bytes[5] as usize; - if amount <= stacked_inventory_item.entity_ids.len() { - Ok(TradeItem::Stacked(stacked_inventory_item.item_id, amount)) + if ClientItemId(item.item_id) == OTHER_MESETA_ITEM_ID { + if item.item_data[0] != 4 { + return Err(TradeError::InvalidItemId(ClientItemId(item.item_id)).into()) + } + let amount = u32::from_le_bytes(item.item_data2); + if amount > client.character.meseta { + return Err(TradeError::InvalidMeseta.into()) + } + Ok(()) + } + else { + let real_item = inventory.get_item_by_id(ClientItemId(item.item_id)) + .ok_or(ItemManagerError::NoSuchItemId(ClientItemId(item.item_id)))?; + let trade_item_bytes: [u8; 16] = item.item_data.iter() + .chain(item.item_data2.iter()) + .cloned().collect::>() + .try_into() + .unwrap(); + match real_item { + InventoryItem::Individual(individual_inventory_item) => { + if real_item.as_client_bytes() == trade_item_bytes { + Ok(()) } else { - Err(TradeError::InvalidStackAmount(stacked_inventory_item.item_id, amount).into()) + Err(TradeError::ClientItemIdDidNotMatchItem(ClientItemId(item.item_id), trade_item_bytes).into()) + } + }, + InventoryItem::Stacked(stacked_inventory_item) => { + if real_item.as_client_bytes()[0..4] == trade_item_bytes[0..4] { + let amount = trade_item_bytes[5] as usize; + if amount <= stacked_inventory_item.entity_ids.len() { + Ok(()) + } + else { + Err(TradeError::InvalidStackAmount(stacked_inventory_item.item_id, amount).into()) + } + } + else { + Err(TradeError::ClientItemIdDidNotMatchItem(ClientItemId(item.item_id), trade_item_bytes).into()) } - } - else { - Err(TradeError::ClientItemIdDidNotMatchItem(ClientItemId(item.item_id), trade_item_bytes).into()) } } } @@ -323,7 +349,15 @@ where .collect::, anyhow::Error>>()?; this.status = TradeStatus::ItemsChecked; - Ok(Box::new(std::iter::once((other.client(), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {}))))) + if this.status == TradeStatus::ItemsChecked && other.status == TradeStatus::ItemsChecked { + Ok(Box::new(vec![ + (this.client(), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})), + (other.client(), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})), + ].into_iter())) + } + else { + Ok(Box::new(None.into_iter())) + } })? .unwrap_or_else(|err| { log::warn!("trade error: {:?}", err); @@ -414,8 +448,8 @@ where TradeReady::BothPlayers(room_id, (this_local_client, this_client, this), (other_local_client, other_client, other)) => { let traded_items = item_manager.trade_items(entity_gateway, room_id, - (&this_local_client, &this_client.character, &this.items), - (&other_local_client, &other_client.character, &other.items)).await?; + (&this_local_client, &this_client.character, &this.items, this.meseta), + (&other_local_client, &other_client.character, &other.items, other.meseta)).await?; let clients_in_room = client_location.get_all_clients_by_client(id)?; let traded_item_packets = traded_items @@ -434,6 +468,12 @@ where GameMessage::PlayerNoLongerHasItem(builder::message::player_no_longer_has_item(item.remove_from, item.current_item_id, amount as u32)) ] }, + ItemToTradeDetail::Meseta(amount) => { + [ + GameMessage::CreateItem(builder::message::create_meseta(item.add_to, amount)), + GameMessage::PlayerNoLongerHasItem(builder::message::player_no_longer_has_item(item.remove_from, item.current_item_id, amount as u32)) + ] + }, } }) .flatten() diff --git a/src/ship/trade.rs b/src/ship/trade.rs index a3a3b0a..e6e1ae3 100644 --- a/src/ship/trade.rs +++ b/src/ship/trade.rs @@ -5,13 +5,10 @@ use async_std::sync::{Arc, Mutex}; use crate::common::serverstate::ClientId; use crate::ship::items; -pub const MESETA_ITEM_ID: items::ClientItemId = items::ClientItemId(0xFFFFFF01); - #[derive(Debug, Clone)] pub enum TradeItem { Individual(items::ClientItemId), Stacked(items::ClientItemId, usize), - Meseta(usize), } impl TradeItem { @@ -26,7 +23,6 @@ impl TradeItem { match self { TradeItem::Individual(item_id) => *item_id, TradeItem::Stacked(item_id, _) => *item_id, - TradeItem::Meseta(_) => MESETA_ITEM_ID, } } } @@ -49,6 +45,7 @@ pub struct ClientTradeState { client: ClientId, other_client: ClientId, pub items: Vec, + pub meseta: usize, pub status: TradeStatus, } @@ -113,6 +110,7 @@ impl TradeState { client: *sender, other_client: *receiver, items: Default::default(), + meseta: 0, status: TradeStatus::SentRequest, }; self.trades.insert(*sender, RefCell::new(state)); @@ -121,6 +119,7 @@ impl TradeState { client: *receiver, other_client: *sender, items: Default::default(), + meseta: 0, status: TradeStatus::ReceivedRequest, }; self.trades.insert(*receiver, RefCell::new(state)); From 9c34baeb269f44619af26414245c6a06f0ca2068 Mon Sep 17 00:00:00 2001 From: jake Date: Sun, 26 Dec 2021 23:31:12 -0700 Subject: [PATCH 37/41] meseta refactor --- src/bin/main.rs | 29 ++++++-- src/entity/character.rs | 7 -- src/entity/gateway/entitygateway.rs | 14 +++- src/entity/gateway/inmemory.rs | 38 ++++++++-- .../postgres/migrations/V0004__meseta.sql | 15 ++++ src/entity/gateway/postgres/models.rs | 4 -- src/entity/gateway/postgres/postgres.rs | 45 +++++++++--- src/entity/item/mod.rs | 2 +- src/login/character.rs | 4 +- src/ship/character.rs | 24 ++++++- src/ship/items/manager.rs | 72 ++++++++++++++++--- src/ship/packet/builder/mod.rs | 2 + src/ship/packet/handler/direct_message.rs | 42 +++++++---- src/ship/packet/handler/lobby.rs | 2 + src/ship/packet/handler/message.rs | 29 +++++--- src/ship/ship.rs | 4 +- tests/common.rs | 3 + 17 files changed, 260 insertions(+), 76 deletions(-) create mode 100644 src/entity/gateway/postgres/migrations/V0004__meseta.sql diff --git a/src/bin/main.rs b/src/bin/main.rs index 8d8f549..e4d7ee9 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -9,7 +9,7 @@ use elseware::entity::account::{NewUserAccountEntity, NewUserSettingsEntity}; #[allow(unused_imports)] use elseware::entity::gateway::{EntityGateway, InMemoryGateway, PostgresGateway}; use elseware::entity::character::NewCharacterEntity; -use elseware::entity::item::{NewItemEntity, ItemDetail}; +use elseware::entity::item::{NewItemEntity, ItemDetail, InventoryItemEntity}; use elseware::common::interserver::AuthToken; use elseware::entity::item; @@ -67,13 +67,16 @@ fn main() { entity_gateway.create_user_settings(NewUserSettingsEntity::new(fake_user.id)).await.unwrap(); let mut character = NewCharacterEntity::new(fake_user.id); character.name = format!("Test Char {}", i*2); - entity_gateway.create_character(character).await.unwrap(); + let character = entity_gateway.create_character(character).await.unwrap(); + entity_gateway.set_character_meseta(&character.id, item::Meseta(999999)).await.unwrap(); + entity_gateway.set_bank_meseta(&character.id, item::BankName("".into()), item::Meseta(999999)).await.unwrap(); let mut character = NewCharacterEntity::new(fake_user.id); character.slot = 2; character.name = "ItemRefactor".into(); character.exp = 80000000; - character.meseta = 999999; let character = entity_gateway.create_character(character).await.unwrap(); + entity_gateway.set_character_meseta(&character.id, item::Meseta(999999)).await.unwrap(); + entity_gateway.set_bank_meseta(&character.id, item::BankName("".into()), item::Meseta(999999)).await.unwrap(); for _ in 0..3 { entity_gateway.create_item( @@ -162,8 +165,8 @@ fn main() { item: ItemDetail::Weapon( item::weapon::Weapon { weapon: item::weapon::WeaponType::DarkFlow, - grind: 5, - special: Some(item::weapon::WeaponSpecial::Charge), + grind: 0, + special: None, attrs: [Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Hit, value: 100}), Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Dark, value: 100}), Some(item::weapon::WeaponAttribute{attr: item::weapon::Attribute::Native, value: 100}),], @@ -293,6 +296,20 @@ fn main() { } ).await.unwrap(); + let monomates = futures::future::join_all((0..6).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + NewItemEntity { + item: ItemDetail::Tool ( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ), + }).await.unwrap() + } + })).await; + let equipped = item::EquippedEntity { weapon: Some(item2_w.id), armor: Some(item7_a.id), @@ -302,7 +319,7 @@ fn main() { }; entity_gateway.set_character_equips(&character.id, &equipped).await.unwrap(); - let inventory = item::InventoryEntity::new(vec![item0, item1, item2_w, item3, item4, item5_m, item6, item7_a, item8_s, item9_u0, item10_u1, item11_u2, item12_u3, item13]); + let inventory = item::InventoryEntity::new(vec![InventoryItemEntity::from(item0), item1.into(), item2_w.into(), item3.into(), item4.into(), item5_m.into(), item6.into(), item7_a.into(), item8_s.into(), item9_u0.into(), item10_u1.into(), item11_u2.into(), item12_u3.into(), item13.into(), monomates.into()]); entity_gateway.set_character_inventory(&character.id, &inventory).await.unwrap(); entity_gateway.set_character_bank(&character.id, &item::BankEntity::default(), item::BankName("".into())).await.unwrap(); } diff --git a/src/entity/character.rs b/src/entity/character.rs index d6d4e3c..76346a9 100644 --- a/src/entity/character.rs +++ b/src/entity/character.rs @@ -294,8 +294,6 @@ pub struct NewCharacterEntity { pub materials: CharacterMaterials, pub tech_menu: CharacterTechMenu, - pub meseta: u32, - pub bank_meseta: u32, pub option_flags: u32, } @@ -315,8 +313,6 @@ impl NewCharacterEntity { guildcard: CharacterGuildCard::default(), materials: CharacterMaterials::default(), tech_menu: CharacterTechMenu::default(), - meseta: 0, - bank_meseta: 0, option_flags: 0, } } @@ -342,8 +338,5 @@ pub struct CharacterEntity { pub materials: CharacterMaterials, pub tech_menu: CharacterTechMenu, - pub meseta: u32, - // TODO: this should not be tied to the character - pub bank_meseta: u32, pub option_flags: u32, } diff --git a/src/entity/gateway/entitygateway.rs b/src/entity/gateway/entitygateway.rs index 46e13ed..fd8b1aa 100644 --- a/src/entity/gateway/entitygateway.rs +++ b/src/entity/gateway/entitygateway.rs @@ -116,7 +116,19 @@ pub trait EntityGateway: Send + Sync + Clone { unimplemented!(); } - async fn set_character_meseta(&mut self, _char_id: &CharacterEntityId, amount: usize) -> Result<(), GatewayError> { + async fn get_character_meseta(&mut self, _char_id: &CharacterEntityId) -> Result { + unimplemented!(); + } + + async fn set_character_meseta(&mut self, _char_id: &CharacterEntityId, _amount: Meseta) -> Result<(), GatewayError> { + unimplemented!(); + } + + async fn get_bank_meseta(&mut self, _char_id: &CharacterEntityId, _bank: BankName) -> Result { + unimplemented!(); + } + + async fn set_bank_meseta(&mut self, _char_id: &CharacterEntityId, _bank: BankName, _amount: Meseta) -> Result<(), GatewayError> { unimplemented!(); } } diff --git a/src/entity/gateway/inmemory.rs b/src/entity/gateway/inmemory.rs index deb737b..57ad039 100644 --- a/src/entity/gateway/inmemory.rs +++ b/src/entity/gateway/inmemory.rs @@ -13,6 +13,8 @@ pub struct InMemoryGateway { users: Arc>>, user_settings: Arc>>, characters: Arc>>, + character_meseta: Arc>>, + bank_meseta: Arc>>, items: Arc>>, inventories: Arc>>, banks: Arc>>, @@ -27,6 +29,8 @@ impl Default for InMemoryGateway { users: Arc::new(Mutex::new(BTreeMap::new())), user_settings: Arc::new(Mutex::new(BTreeMap::new())), characters: Arc::new(Mutex::new(BTreeMap::new())), + character_meseta: Arc::new(Mutex::new(BTreeMap::new())), + bank_meseta: Arc::new(Mutex::new(BTreeMap::new())), items: Arc::new(Mutex::new(BTreeMap::new())), inventories: Arc::new(Mutex::new(BTreeMap::new())), banks: Arc::new(Mutex::new(BTreeMap::new())), @@ -197,8 +201,6 @@ impl EntityGateway for InMemoryGateway { guildcard: character.guildcard, materials: character.materials, tech_menu: character.tech_menu, - meseta: character.meseta, - bank_meseta: character.bank_meseta, option_flags: character.option_flags, }; characters.insert(new_character.id, new_character.clone()); @@ -314,11 +316,35 @@ impl EntityGateway for InMemoryGateway { Ok(()) } - async fn set_character_meseta(&mut self, char_id: &CharacterEntityId, amount: usize) -> Result<(), GatewayError> { - let mut characters = self.characters.lock().unwrap(); - if let Some(char) = characters.get_mut(&char_id) { - char.meseta = amount as u32; + async fn set_character_meseta(&mut self, char_id: &CharacterEntityId, meseta: Meseta) -> Result<(), GatewayError> { + let mut character_meseta = self.character_meseta.lock().unwrap(); + character_meseta.insert(*char_id, meseta); + Ok(()) + } + + async fn get_character_meseta(&mut self, char_id: &CharacterEntityId) -> Result { + let mut character_meseta = self.character_meseta.lock().unwrap(); + if let Some(meseta) = character_meseta.get_mut(&char_id) { + Ok(*meseta) + } + else { + Err(GatewayError::Error) } + } + + async fn set_bank_meseta(&mut self, char_id: &CharacterEntityId, bank: BankName, meseta: Meseta) -> Result<(), GatewayError> { + let mut bank_meseta = self.bank_meseta.lock().unwrap(); + bank_meseta.insert((*char_id, bank), meseta); Ok(()) } + + async fn get_bank_meseta(&mut self, char_id: &CharacterEntityId, bank: BankName) -> Result { + let mut bank_meseta = self.bank_meseta.lock().unwrap(); + if let Some(meseta) = bank_meseta.get_mut(&(*char_id, bank)) { + Ok(*meseta) + } + else { + Err(GatewayError::Error) + } + } } diff --git a/src/entity/gateway/postgres/migrations/V0004__meseta.sql b/src/entity/gateway/postgres/migrations/V0004__meseta.sql new file mode 100644 index 0000000..97f6b2e --- /dev/null +++ b/src/entity/gateway/postgres/migrations/V0004__meseta.sql @@ -0,0 +1,15 @@ +create table character_meseta ( + pchar integer references character (id) not null unique, + meseta integer not null, +); + +create table bank_meseta ( + pchar integer references character (id) not null, + bank varchar(128) not null, + meseta integer not null, + unique (pchar, bank) +); + + +alter table player_character + drop column meseta, bank_meseta; diff --git a/src/entity/gateway/postgres/models.rs b/src/entity/gateway/postgres/models.rs index f57e39d..2e5404e 100644 --- a/src/entity/gateway/postgres/models.rs +++ b/src/entity/gateway/postgres/models.rs @@ -216,8 +216,6 @@ pub struct PgCharacter { tp: i16, tech_menu: Vec, - meseta: i32, - bank_meseta: i32, } impl From for CharacterEntity { @@ -267,8 +265,6 @@ impl From for CharacterEntity { tech_menu: CharacterTechMenu { tech_menu: vec_to_array(other.tech_menu) }, - meseta: other.meseta as u32, - bank_meseta: other.bank_meseta as u32, } } } diff --git a/src/entity/gateway/postgres/postgres.rs b/src/entity/gateway/postgres/postgres.rs index 3c17246..2fefb03 100644 --- a/src/entity/gateway/postgres/postgres.rs +++ b/src/entity/gateway/postgres/postgres.rs @@ -182,7 +182,7 @@ impl EntityGateway for PostgresGateway { async fn create_character(&mut self, char: NewCharacterEntity) -> Result { let q = r#"insert into player_character (user_account, slot, name, exp, class, section_id, costume, skin, face, head, hair, hair_r, hair_g, hair_b, prop_x, prop_y, techs, - config, infoboard, guildcard, power, mind, def, evade, luck, hp, tp, tech_menu, meseta, bank_meseta, option_flags) + config, infoboard, guildcard, power, mind, def, evade, luck, hp, tp, tech_menu, option_flags) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31) returning *;"#; @@ -215,8 +215,6 @@ impl EntityGateway for PostgresGateway { .bind(char.materials.hp as i16) .bind(char.materials.tp as i16) .bind(char.tech_menu.tech_menu.to_vec()) - .bind(char.meseta as i32) - .bind(char.bank_meseta as i32) .bind(char.option_flags as i32) .fetch_one(&self.pool).await?; @@ -241,7 +239,7 @@ impl EntityGateway for PostgresGateway { let q = r#"update player_character set user_account=$1, slot=$2, name=$3, exp=$4, class=$5, section_id=$6, costume=$7, skin=$8, face=$9, head=$10, hair=$11, hair_r=$12, hair_g=$13, hair_b=$14, prop_x=$15, prop_y=$16, techs=$17, config=$18, infoboard=$19, guildcard=$20, power=$21, mind=$22, def=$23, - evade=$24, luck=$25, hp=$26, tp=$27, tech_menu=$28, meseta=$29, bank_meseta=$30, option_flags=$31 + evade=$24, luck=$25, hp=$26, tp=$27, tech_menu=$28, option_flags=$29 where id=$32;"#; sqlx::query(q) .bind(char.user_id.0) @@ -272,8 +270,6 @@ impl EntityGateway for PostgresGateway { .bind(char.materials.hp as i16) .bind(char.materials.tp as i16) .bind(char.tech_menu.tech_menu.to_vec()) - .bind(char.meseta as i32) - .bind(char.bank_meseta as i32) .bind(char.option_flags as i32) .bind(char.id.0 as i32) .execute(&self.pool).await?; @@ -551,12 +547,43 @@ impl EntityGateway for PostgresGateway { Ok(()) } - async fn set_character_meseta(&mut self, char_id: &CharacterEntityId, amount: usize) -> Result<(), GatewayError> { - sqlx::query(r#"update player_character set meseta=$2 where id = $1"#) + async fn set_character_meseta(&mut self, char_id: &CharacterEntityId, meseta: Meseta) -> Result<(), GatewayError> { + sqlx::query("insert into character_meseta values ($1, $2) on conflict (pchar) do update set items = $2") .bind(char_id.0) - .bind(amount as i32) + .bind(meseta.0 as i32) .execute(&self.pool) .await?; Ok(()) } + + async fn get_character_meseta(&mut self, char_id: &CharacterEntityId) -> Result { + #[derive(sqlx::FromRow)] + struct PgMeseta(i32); + let meseta = sqlx::query_as::<_, PgMeseta>(r#"select meseta from character_meseta where id = $1"#) + .bind(char_id.0) + .fetch_one(&self.pool) + .await?; + Ok(Meseta(meseta.0 as u32)) + } + + async fn set_bank_meseta(&mut self, char_id: &CharacterEntityId, bank: BankName, meseta: Meseta) -> Result<(), GatewayError> { + sqlx::query("insert into bank_meseta values ($1, $2, $3) on conflict (pchar, bank) do update set items = $2") + .bind(char_id.0) + .bind(meseta.0 as i32) + .bind(bank.0) + .execute(&self.pool) + .await?; + Ok(()) + } + + async fn get_bank_meseta(&mut self, char_id: &CharacterEntityId, bank: BankName) -> Result { + #[derive(sqlx::FromRow)] + struct PgMeseta(i32); + let meseta = sqlx::query_as::<_, PgMeseta>(r#"select meseta from character_meseta where id = $1 and bank = $2"#) + .bind(char_id.0) + .bind(bank.0) + .fetch_one(&self.pool) + .await?; + Ok(Meseta(meseta.0 as u32)) + } } diff --git a/src/entity/item/mod.rs b/src/entity/item/mod.rs index ff722dd..5d75798 100644 --- a/src/entity/item/mod.rs +++ b/src/entity/item/mod.rs @@ -60,7 +60,7 @@ pub enum ItemNote { }, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq)] pub struct Meseta(pub u32); impl Meseta { diff --git a/src/login/character.rs b/src/login/character.rs index b3afa67..9b3cf1f 100644 --- a/src/login/character.rs +++ b/src/login/character.rs @@ -20,7 +20,7 @@ use libpso::{utf8_to_array, utf8_to_utf16_array}; use crate::entity::gateway::{EntityGateway, GatewayError}; use crate::entity::account::{UserAccountId, UserAccountEntity, NewUserSettingsEntity, USERFLAG_NEWCHAR, USERFLAG_DRESSINGROOM}; -use crate::entity::item::{NewItemEntity, ItemDetail, ItemNote, InventoryItemEntity, InventoryEntity, BankEntity, BankName, EquippedEntity}; +use crate::entity::item::{NewItemEntity, ItemDetail, ItemNote, InventoryItemEntity, InventoryEntity, BankEntity, BankName, EquippedEntity, Meseta}; use crate::entity::item::weapon::Weapon; use crate::entity::item::armor::Armor; use crate::entity::item::tech::Technique; @@ -201,8 +201,8 @@ async fn new_character(entity_gateway: &mut EG, user: &UserAc _ => {} } - character.meseta = 300; let character = entity_gateway.create_character(character).await?; + entity_gateway.set_character_meseta(&character.id, Meseta(300)).await?; let new_weapon = match character.char_class { CharacterClass::HUmar | CharacterClass::HUnewearl | CharacterClass::HUcast | CharacterClass::HUcaseal => item::weapon::WeaponType::Saber, diff --git a/src/ship/character.rs b/src/ship/character.rs index bbd2ddc..2fdeb35 100644 --- a/src/ship/character.rs +++ b/src/ship/character.rs @@ -2,11 +2,13 @@ use libpso::character::character; use crate::common::leveltable::CharacterStats; use crate::entity::character::CharacterEntity; use crate::ship::items::{CharacterInventory, CharacterBank}; +use crate::entity::item::Meseta; pub struct CharacterBytesBuilder<'a> { character: Option<&'a CharacterEntity>, stats: Option<&'a CharacterStats>, level: Option, + meseta: Option, } impl<'a> Default for CharacterBytesBuilder<'a> { @@ -15,6 +17,7 @@ impl<'a> Default for CharacterBytesBuilder<'a> { character: None, stats: None, level: None, + meseta: None, } } } @@ -42,10 +45,18 @@ impl<'a> CharacterBytesBuilder<'a> { } } + pub fn meseta(self, meseta: Meseta) -> CharacterBytesBuilder<'a> { + CharacterBytesBuilder { + meseta: Some(meseta), + ..self + } + } + pub fn build(self) -> character::Character { let character = self.character.unwrap(); let stats = self.stats.unwrap(); let level = self.level.unwrap(); + let meseta = self.meseta.unwrap(); character::Character { name: libpso::utf8_to_utf16_array!(character.name, 16), hp: stats.hp, @@ -70,7 +81,7 @@ impl<'a> CharacterBytesBuilder<'a> { prop_y: character.appearance.prop_y, config: character.config.as_bytes(), techniques: character.techs.as_bytes(), - meseta: character.meseta, + meseta: meseta.0 as u32, exp: character.exp, ..character::Character::default() } @@ -82,6 +93,7 @@ pub struct FullCharacterBytesBuilder<'a> { character: Option<&'a CharacterEntity>, stats: Option<&'a CharacterStats>, level: Option, + meseta: Option, inventory: Option<&'a CharacterInventory>, bank: Option<&'a CharacterBank>, key_config: Option<&'a [u8; 0x16C]>, @@ -97,6 +109,7 @@ impl<'a> Default for FullCharacterBytesBuilder<'a> { character: None, stats: None, level: None, + meseta: None, inventory: None, bank: None, key_config: None, @@ -131,6 +144,13 @@ impl<'a> FullCharacterBytesBuilder<'a> { } } + pub fn meseta(self, meseta: Meseta) -> FullCharacterBytesBuilder<'a> { + FullCharacterBytesBuilder { + meseta: Some(meseta), + ..self + } + } + pub fn inventory(self, inventory: &'a CharacterInventory) -> FullCharacterBytesBuilder<'a> { FullCharacterBytesBuilder { inventory: Some(inventory), @@ -184,6 +204,7 @@ impl<'a> FullCharacterBytesBuilder<'a> { let character = self.character.unwrap(); let stats = self.stats.unwrap(); let level = self.level.unwrap(); + let meseta = self.meseta.unwrap(); let inventory = self.inventory.unwrap(); let bank = self.bank.unwrap(); let key_config = self.key_config.unwrap(); @@ -204,6 +225,7 @@ impl<'a> FullCharacterBytesBuilder<'a> { .character(character) .stats(stats) .level(level - 1) + .meseta(meseta) .build(), inventory: character::Inventory { item_count: inventory.count() as u8, diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs index efd9a64..044aabd 100644 --- a/src/ship/items/manager.rs +++ b/src/ship/items/manager.rs @@ -106,6 +106,8 @@ pub struct ItemManager { pub(super) id_counter: u32, pub(self) character_inventory: HashMap, + pub(self) character_meseta: HashMap, + pub(self) bank_meseta: HashMap, //character_bank: HashMap>, pub(self) character_bank: HashMap, pub(self) character_floor: HashMap, @@ -120,6 +122,8 @@ impl Default for ItemManager { ItemManager { id_counter: 0, character_inventory: HashMap::new(), + character_meseta: HashMap::new(), + bank_meseta: HashMap::new(), character_bank: HashMap::new(), character_floor: HashMap::new(), character_room: HashMap::new(), @@ -194,8 +198,13 @@ impl ItemManager { .collect::, _>>()?; let character_bank = CharacterBank::new(bank_items); + let character_meseta = entity_gateway.get_character_meseta(&character.id).await?; + let bank_meseta = entity_gateway.get_bank_meseta(&character.id, BankName("".into())).await?; + self.character_inventory.insert(character.id, character_inventory); self.character_bank.insert(character.id, character_bank); + self.character_meseta.insert(character.id, character_meseta); + self.bank_meseta.insert(character.id, bank_meseta); Ok(()) } @@ -237,6 +246,35 @@ impl ItemManager { //.ok_or(ItemManagerError::InvalidBankName(BankName("".to_string())))?) } + pub fn get_character_meseta(&self, character_id: &CharacterEntityId) -> Result<&Meseta, ItemManagerError> { + Ok(self.character_meseta.get(&character_id) + .ok_or(ItemManagerError::NoCharacter(*character_id))?) + } + + pub fn get_character_meseta_mut<'a>(&'a mut self, character_id: &CharacterEntityId) -> Result<&'a mut Meseta, ItemManagerError> { + Ok(self.character_meseta.get_mut(&character_id) + .ok_or(ItemManagerError::NoCharacter(*character_id))?) + } + + pub fn get_bank_meseta(&self, character_id: &CharacterEntityId) -> Result<&Meseta, ItemManagerError> { + Ok(self.bank_meseta.get(&character_id) + .ok_or(ItemManagerError::NoCharacter(*character_id))?) + } + + pub fn get_bank_meseta_mut<'a>(&'a mut self, character_id: &CharacterEntityId) -> Result<&'a mut Meseta, ItemManagerError> { + Ok(self.bank_meseta.get_mut(&character_id) + .ok_or(ItemManagerError::NoCharacter(*character_id))?) + } + + pub fn get_character_and_bank_meseta_mut<'a>(&'a mut self, character_id: &CharacterEntityId) -> Result<(&'a mut Meseta, &'a mut Meseta), ItemManagerError> { + Ok(( + self.character_meseta.get_mut(&character_id) + .ok_or(ItemManagerError::NoCharacter(*character_id))?, + self.bank_meseta.get_mut(&character_id) + .ok_or(ItemManagerError::NoCharacter(*character_id))? + )) + } + /*pub fn get_character_bank_mut(&mut self, character: &CharacterEntity) -> Result<&CharacterBank, ItemManagerError> { Ok(self.character_bank .get_mut(&character.id) @@ -336,11 +374,12 @@ impl ItemManager { } }, FloorItem::Meseta(meseta_floor_item) => { - if character.meseta >= 999999 { + let character_meseta = it.manager.character_meseta.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; + if character_meseta.0 >= 999999 { return Err(ItemManagerError::CouldNotAddToInventory(*item_id).into()); } it.action(Box::new(AddMesetaFloorItemToInventory { - character: (**character).clone(), + character_id: character.id, item: meseta_floor_item.clone() })); @@ -493,11 +532,12 @@ impl ItemManager { -> 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 { + let character_meseta = self.character_meseta.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; + if character_meseta.0 < amount { return Err(ItemManagerError::CouldNotDropMeseta.into()) } - character.meseta -= amount; - entity_gateway.save_character(character).await?; + character_meseta.0 -= amount; + entity_gateway.set_character_meseta(&character.id, *character_meseta).await?; let item_id = self.room_item_id_counter.borrow_mut().get_mut(room_id).ok_or(ItemManagerError::NoCharacter(character.id))?(); let floor_item = FloorItem::Meseta(MesetaFloorItem { @@ -1183,16 +1223,16 @@ impl ItemAction for AddStackedFloorItemToInventory { #[derive(Debug)] struct AddMesetaFloorItemToInventory{ - character: CharacterEntity, + character_id: CharacterEntityId, item: MesetaFloorItem, } #[async_trait::async_trait] impl ItemAction for AddMesetaFloorItemToInventory { - async fn commit(&self, _item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { - let mut nchar = self.character.clone(); - nchar.meseta = std::cmp::min(self.character.meseta + self.item.meseta.0, 999999); - entity_gateway.save_character(&nchar).await?; + async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + let character_meseta = item_manager.character_meseta.get_mut(&self.character_id).ok_or(ItemManagerError::NoCharacter(self.character_id))?; + character_meseta.0 = std::cmp::min(character_meseta.0 + self.item.meseta.0, 999999); + entity_gateway.set_character_meseta(&self.character_id, *character_meseta).await?; Ok(()) } } @@ -1254,7 +1294,17 @@ struct TradeMeseta { #[async_trait::async_trait] impl ItemAction for TradeMeseta { - async fn commit(&self, _item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + async fn commit(&self, item_manager: &mut ItemManager, entity_gateway: &mut EG) -> Result<(), TransactionCommitError> { + { + let src_meseta = item_manager.get_character_meseta_mut(&self.src_character_id)?; + src_meseta.0 -= self.amount as u32; + entity_gateway.set_character_meseta(&self.src_character_id, *src_meseta).await?; + } + { + let dest_meseta = item_manager.get_character_meseta_mut(&self.dest_character_id)?; + dest_meseta.0 += self.amount as u32; + entity_gateway.set_character_meseta(&self.dest_character_id, *dest_meseta).await?; + } Ok(()) } } diff --git a/src/ship/packet/builder/mod.rs b/src/ship/packet/builder/mod.rs index a5c30d7..f5d9f3c 100644 --- a/src/ship/packet/builder/mod.rs +++ b/src/ship/packet/builder/mod.rs @@ -26,10 +26,12 @@ pub fn player_header(tag: u32, client: &ClientState, area_client: &AreaClient) - pub fn player_info(tag: u32, client: &ClientState, area_client: &AreaClient, item_manager: &ItemManager, level_table: &CharacterLevelTable) -> PlayerInfo { 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 meseta = item_manager.get_character_meseta(&client.character.id).unwrap(); let character = CharacterBytesBuilder::default() .character(&client.character) .stats(&stats) .level(level - 1) + .meseta(*meseta) .build(); PlayerInfo { header: player_header(tag, client, area_client), diff --git a/src/ship/packet/handler/direct_message.rs b/src/ship/packet/handler/direct_message.rs index a2f1eda..ea6aff4 100644 --- a/src/ship/packet/handler/direct_message.rs +++ b/src/ship/packet/handler/direct_message.rs @@ -232,8 +232,8 @@ pub async fn send_bank_list(id: ClientId, { 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, client.character.bank_meseta); + let bank_meseta = item_manager.get_bank_meseta(&client.character.id)?; + let bank_items_pkt = builder::message::bank_item_list(bank_items, bank_meseta.0); Ok(Box::new(vec![(id, SendShipPacket::BankItemList(bank_items_pkt))].into_iter())) } @@ -252,11 +252,16 @@ where let other_clients_in_area = client_location.get_all_clients_by_client(id).map_err(|err| -> ClientLocationError { err.into() })?; let bank_action_pkts = match bank_interaction.action { BANK_ACTION_DEPOSIT => { + let character_meseta = item_manager.get_character_meseta(&client.character.id)?; + let bank_meseta = item_manager.get_bank_meseta(&client.character.id)?; if bank_interaction.item_id == 0xFFFFFFFF { - if client.character.meseta >= bank_interaction.meseta_amount && (bank_interaction.meseta_amount + client.character.bank_meseta) <= BANK_MESETA_CAPACITY { - client.character.meseta -= bank_interaction.meseta_amount; - client.character.bank_meseta += bank_interaction.meseta_amount; - entity_gateway.save_character(&client.character).await?; + if character_meseta.0 >= bank_interaction.meseta_amount && (bank_interaction.meseta_amount + bank_meseta.0) <= BANK_MESETA_CAPACITY { + let (character_meseta, bank_meseta) = item_manager.get_character_and_bank_meseta_mut(&client.character.id)?; + character_meseta.0 -= bank_interaction.meseta_amount; + bank_meseta.0 += bank_interaction.meseta_amount; + entity_gateway.set_character_meseta(&client.character.id, *character_meseta).await?; + // TODO: BankName + entity_gateway.set_bank_meseta(&client.character.id, item::BankName("".into()), *bank_meseta).await?; } Vec::new() } @@ -267,11 +272,16 @@ where } }, BANK_ACTION_WITHDRAW => { + let character_meseta = item_manager.get_character_meseta(&client.character.id)?; + let bank_meseta = item_manager.get_bank_meseta(&client.character.id)?; if bank_interaction.item_id == 0xFFFFFFFF { - if client.character.meseta + bank_interaction.meseta_amount <= INVENTORY_MESETA_CAPACITY { - client.character.meseta += bank_interaction.meseta_amount; - client.character.bank_meseta -= bank_interaction.meseta_amount; - entity_gateway.save_character(&client.character).await?; + if (bank_meseta.0 >= bank_interaction.meseta_amount) && (character_meseta.0 + bank_interaction.meseta_amount <= INVENTORY_MESETA_CAPACITY) { + let (character_meseta, bank_meseta) = item_manager.get_character_and_bank_meseta_mut(&client.character.id)?; + character_meseta.0 += bank_interaction.meseta_amount; + bank_meseta.0 -= bank_interaction.meseta_amount; + entity_gateway.set_character_meseta(&client.character.id, *character_meseta).await?; + // TODO: BankName + entity_gateway.set_bank_meseta(&client.character.id, item::BankName("".into()), *bank_meseta).await?; } Vec::new() } @@ -370,12 +380,13 @@ where } }; - if client.character.meseta < item.price() as u32 { + let character_meseta = item_manager.get_character_meseta_mut(&client.character.id)?; + if character_meseta.0 < item.price() as u32 { return Err(ShipError::ShopError.into()) } - client.character.meseta -= item.price() as u32; - entity_gateway.save_character(&client.character).await?; + character_meseta.0 -= item.price() as u32; + entity_gateway.set_character_meseta(&client.character.id, *character_meseta).await?; let inventory_item = item_manager.player_buys_item(entity_gateway, &client.character, item, ClientItemId(buy_item.item_id), buy_item.amount as usize).await?; let create = builder::message::create_withdrawn_inventory_item(area_client, inventory_item)?; @@ -447,8 +458,9 @@ where grind: grind_mod, }); - client.character.meseta -= 100; - entity_gateway.save_character(&client.character).await?; + let character_meseta = item_manager.get_character_meseta_mut(&client.character.id)?; + character_meseta.0 -= 100; + entity_gateway.set_character_meseta(&client.character.id, *character_meseta).await?; let preview_pkt = builder::message::tek_preview(ClientItemId(tek_request.item_id), &weapon)?; diff --git a/src/ship/packet/handler/lobby.rs b/src/ship/packet/handler/lobby.rs index a857ce2..b0004fb 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 meseta = item_manager.get_character_meseta(&client.character.id).unwrap(); let bank = item_manager.get_character_bank(&client.character).unwrap(); let fc = FullCharacterBytesBuilder::default() .character(&client.character) .stats(&stats) .level(level) + .meseta(*meseta) .inventory(inventory) .bank(bank) .key_config(&client.settings.settings.key_config) diff --git a/src/ship/packet/handler/message.rs b/src/ship/packet/handler/message.rs index 882c3e0..19c8281 100644 --- a/src/ship/packet/handler/message.rs +++ b/src/ship/packet/handler/message.rs @@ -194,7 +194,7 @@ pub fn update_player_position(id: ClientId, .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))? .as_ref() .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?; - + match &message.msg { GameMessage::PlayerChangedMap(p) => { client.x = p.x; @@ -258,18 +258,22 @@ pub fn update_player_position(id: ClientId, pub async fn charge_attack(id: ClientId, charge: &ChargeAttack, clients: &mut Clients, - entity_gateway: &mut EG) + entity_gateway: &mut EG, + item_manager: &mut ItemManager) -> Result + Send>, anyhow::Error> where EG: EntityGateway { let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; - if client.character.meseta >= charge.meseta { - client.character.meseta -= charge.meseta; - entity_gateway.save_character(&client.character).await?; + let meseta = item_manager.get_character_meseta_mut(&client.character.id)?; + + if meseta.0 >= charge.meseta { + meseta.0 -= charge.meseta; + entity_gateway.set_character_meseta(&client.character.id, *meseta).await?; + // TODO: this should probably echo the packet Ok(Box::new(None.into_iter())) } else { - Err(ShipError::NotEnoughMeseta(id, client.character.meseta).into()) + Err(ShipError::NotEnoughMeseta(id, meseta.0).into()) } } @@ -293,18 +297,21 @@ where pub async fn player_used_medical_center(id: ClientId, _pumc: &PlayerUsedMedicalCenter, // not needed? entity_gateway: &mut EG, - clients: &mut Clients) + clients: &mut Clients, + item_manager: &mut ItemManager) -> Result + Send>, anyhow::Error> where EG: EntityGateway { let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; - if client.character.meseta >= 10 { - client.character.meseta -= 10; - entity_gateway.save_character(&client.character).await?; + let meseta = item_manager.get_character_meseta_mut(&client.character.id)?; + if meseta.0 >= 10 { + meseta.0 -= 10; + entity_gateway.set_character_meseta(&client.character.id, *meseta).await?; + // TODO: this should probably echo the packet Ok(Box::new(None.into_iter())) } else { - Err(ShipError::NotEnoughMeseta(id, client.character.meseta).into()) + Err(ShipError::NotEnoughMeseta(id, meseta.0).into()) } } diff --git a/src/ship/ship.rs b/src/ship/ship.rs index 6ee71fe..8b0aa17 100644 --- a/src/ship/ship.rs +++ b/src/ship/ship.rs @@ -488,14 +488,14 @@ impl ShipServerState { handler::message::update_player_position(id, msg, &mut self.clients, &block.client_location, &block.rooms)? }, GameMessage::ChargeAttack(charge_attack) => { - handler::message::charge_attack(id, charge_attack, &mut self.clients, &mut self.entity_gateway).await? + handler::message::charge_attack(id, charge_attack, &mut self.clients, &mut self.entity_gateway, &mut self.item_manager).await? }, GameMessage::PlayerUseItem(player_use_item) => { let block = self.blocks.with_client(id, &self.clients)?; handler::message::use_item(id, player_use_item, &mut self.entity_gateway, &block.client_location, &mut self.clients, &mut self.item_manager).await? }, GameMessage::PlayerUsedMedicalCenter(player_used_medical_center) => { - handler::message::player_used_medical_center(id, player_used_medical_center, &mut self.entity_gateway, &mut self.clients).await? + handler::message::player_used_medical_center(id, player_used_medical_center, &mut self.entity_gateway, &mut self.clients, &mut self.item_manager).await? }, GameMessage::PlayerFeedMag(player_feed_mag) => { let block = self.blocks.with_client(id, &self.clients)?; diff --git a/tests/common.rs b/tests/common.rs index 6b97b0e..857a80d 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -4,6 +4,7 @@ use elseware::common::serverstate::{ClientId, ServerState}; use elseware::entity::gateway::EntityGateway; use elseware::entity::account::{UserAccountEntity, NewUserAccountEntity, NewUserSettingsEntity}; use elseware::entity::character::{CharacterEntity, NewCharacterEntity}; +use elseware::entity::item::{Meseta, BankName}; use elseware::ship::ship::{ShipServerState, RecvShipPacket}; use elseware::ship::room::Difficulty; @@ -27,6 +28,8 @@ pub async fn new_user_character(entity_gateway: &mut EG, user let _settings = entity_gateway.create_user_settings(new_settings).await.unwrap(); let new_character = NewCharacterEntity::new(user.id); let character = entity_gateway.create_character(new_character).await.unwrap(); + entity_gateway.set_character_meseta(&character.id, Meseta(0)).await.unwrap(); + entity_gateway.set_bank_meseta(&character.id, BankName("".into()), Meseta(0)).await.unwrap(); (user, character) } From ecf1f23c6c7397ec0a55509c625838c1e2bdc8fe Mon Sep 17 00:00:00 2001 From: jake Date: Mon, 27 Dec 2021 00:43:25 -0700 Subject: [PATCH 38/41] fix tests --- src/entity/item/mod.rs | 7 +++ src/lib.rs | 2 +- src/ship/items/transaction.rs | 6 +++ tests/test_bank.rs | 87 ++++++++++++++++------------------- tests/test_item_pickup.rs | 83 +++++++++++++++++---------------- tests/test_shops.rs | 48 +++++++++---------- 6 files changed, 118 insertions(+), 115 deletions(-) diff --git a/src/entity/item/mod.rs b/src/entity/item/mod.rs index 5d75798..78eb1a3 100644 --- a/src/entity/item/mod.rs +++ b/src/entity/item/mod.rs @@ -223,6 +223,13 @@ impl InventoryItemEntity { _ => None, } } + + pub fn individual<'a>(&'a self) -> Option<&'a ItemEntity> { + match self { + InventoryItemEntity::Individual(i) => Some(i), + _ => None, + } + } } #[derive(Clone, Debug, Default)] diff --git a/src/lib.rs b/src/lib.rs index cb41223..76e1020 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,7 @@ #![feature(inline_const)] #![feature(drain_filter)] #![feature(derive_default_enum)] - +#![feature(try_blocks)] diff --git a/src/ship/items/transaction.rs b/src/ship/items/transaction.rs index c21c868..4d426af 100644 --- a/src/ship/items/transaction.rs +++ b/src/ship/items/transaction.rs @@ -118,9 +118,11 @@ mod test { #[async_std::test] async fn test_item_transaction() { + #[derive(Debug)] struct DummyAction1 { name: String, } + #[derive(Debug)] struct DummyAction2 { value: u32, } @@ -201,8 +203,10 @@ mod test { #[async_std::test] async fn test_item_transaction_with_action_error() { + #[derive(Debug)] struct DummyAction1 { } + #[derive(Debug)] struct DummyAction2 { } @@ -270,8 +274,10 @@ mod test { #[async_std::test] async fn test_item_transaction_with_commit_error() { + #[derive(Debug)] struct DummyAction1 { } + #[derive(Debug)] struct DummyAction2 { } diff --git a/tests/test_bank.rs b/tests/test_bank.rs index e55d153..605a3aa 100644 --- a/tests/test_bank.rs +++ b/tests/test_bank.rs @@ -785,9 +785,8 @@ async fn test_deposit_stacked_item_in_full_bank_with_partial_stack() { async fn test_deposit_meseta() { let mut entity_gateway = InMemoryGateway::default(); - let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; - char1.meseta = 300; - entity_gateway.save_character(&char1).await.unwrap(); + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + entity_gateway.set_character_meseta(&char1.id, item::Meseta(300)).await.unwrap(); let mut ship = Box::new(ShipServerState::builder() .gateway(entity_gateway.clone()) @@ -812,20 +811,19 @@ async fn test_deposit_meseta() { unknown: 0, })))).await.unwrap().for_each(drop); - let characters = entity_gateway.get_characters_by_user(&user1).await.unwrap(); - let char = characters[0].as_ref().unwrap(); - assert!(char.meseta == 277); - assert!(char.bank_meseta == 23); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + let c1_bank_meseta = entity_gateway.get_bank_meseta(&char1.id, item::BankName("".into())).await.unwrap(); + assert!(c1_meseta.0 == 277); + assert!(c1_bank_meseta.0 == 23); } #[async_std::test] async fn test_deposit_too_much_meseta() { let mut entity_gateway = InMemoryGateway::default(); - 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.unwrap(); + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + entity_gateway.set_character_meseta(&char1.id, item::Meseta(300)).await.unwrap(); + entity_gateway.set_bank_meseta(&char1.id, item::BankName("".into()), item::Meseta(999980)).await.unwrap(); let mut ship = Box::new(ShipServerState::builder() .gateway(entity_gateway.clone()) @@ -850,21 +848,19 @@ async fn test_deposit_too_much_meseta() { unknown: 0, })))).await.unwrap().for_each(drop); - let characters = entity_gateway.get_characters_by_user(&user1).await.unwrap(); - let char = characters[0].as_ref().unwrap(); - assert!(char.meseta == 300); - assert!(char.bank_meseta == 999980); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + let c1_bank_meseta = entity_gateway.get_bank_meseta(&char1.id, item::BankName("".into())).await.unwrap(); + assert!(c1_meseta.0 == 300); + assert!(c1_bank_meseta.0 == 999980); } - #[async_std::test] async fn test_deposit_meseta_when_bank_is_maxed() { let mut entity_gateway = InMemoryGateway::default(); - 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.unwrap(); + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + entity_gateway.set_character_meseta(&char1.id, item::Meseta(300)).await.unwrap(); + entity_gateway.set_bank_meseta(&char1.id, item::BankName("".into()), item::Meseta(999999)).await.unwrap(); let mut ship = Box::new(ShipServerState::builder() .gateway(entity_gateway.clone()) @@ -889,10 +885,10 @@ async fn test_deposit_meseta_when_bank_is_maxed() { unknown: 0, })))).await.unwrap().for_each(drop); - let characters = entity_gateway.get_characters_by_user(&user1).await.unwrap(); - let char = characters[0].as_ref().unwrap(); - assert!(char.meseta == 300); - assert!(char.bank_meseta == 999999); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + let c1_bank_meseta = entity_gateway.get_bank_meseta(&char1.id, item::BankName("".into())).await.unwrap(); + assert!(c1_meseta.0 == 300); + assert!(c1_bank_meseta.0 == 999999); } @@ -1464,9 +1460,8 @@ async fn test_withdraw_stacked_item_in_full_inventory_with_partial_stack() { async fn test_withdraw_meseta() { let mut entity_gateway = InMemoryGateway::default(); - let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; - char1.bank_meseta = 300; - entity_gateway.save_character(&char1).await.unwrap(); + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + entity_gateway.set_bank_meseta(&char1.id, item::BankName("".into()), item::Meseta(300)).await.unwrap(); let mut ship = Box::new(ShipServerState::builder() .gateway(entity_gateway.clone()) @@ -1491,20 +1486,19 @@ async fn test_withdraw_meseta() { unknown: 0, })))).await.unwrap().for_each(drop); - let characters = entity_gateway.get_characters_by_user(&user1).await.unwrap(); - let char = characters[0].as_ref().unwrap(); - assert!(char.meseta == 23); - assert!(char.bank_meseta == 277); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + let c1_bank_meseta = entity_gateway.get_bank_meseta(&char1.id, item::BankName("".into())).await.unwrap(); + assert!(c1_meseta.0 == 23); + assert!(c1_bank_meseta.0 == 277); } #[async_std::test] async fn test_withdraw_too_much_meseta() { let mut entity_gateway = InMemoryGateway::default(); - 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.unwrap(); + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + entity_gateway.set_character_meseta(&char1.id, item::Meseta(999980)).await.unwrap(); + entity_gateway.set_bank_meseta(&char1.id, item::BankName("".into()), item::Meseta(300)).await.unwrap(); let mut ship = Box::new(ShipServerState::builder() .gateway(entity_gateway.clone()) @@ -1529,20 +1523,19 @@ async fn test_withdraw_too_much_meseta() { unknown: 0, })))).await.unwrap().for_each(drop); - let characters = entity_gateway.get_characters_by_user(&user1).await.unwrap(); - let char = characters[0].as_ref().unwrap(); - assert!(char.meseta == 999980); - assert!(char.bank_meseta == 300); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + let c1_bank_meseta = entity_gateway.get_bank_meseta(&char1.id, item::BankName("".into())).await.unwrap(); + assert!(c1_meseta.0 == 999980); + assert!(c1_bank_meseta.0 == 300); } #[async_std::test] async fn test_withdraw_meseta_inventory_is_maxed() { let mut entity_gateway = InMemoryGateway::default(); - 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.unwrap(); + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap(); + entity_gateway.set_bank_meseta(&char1.id, item::BankName("".into()), item::Meseta(300)).await.unwrap(); let mut ship = Box::new(ShipServerState::builder() .gateway(entity_gateway.clone()) @@ -1567,8 +1560,8 @@ async fn test_withdraw_meseta_inventory_is_maxed() { unknown: 0, })))).await.unwrap().for_each(drop); - let characters = entity_gateway.get_characters_by_user(&user1).await.unwrap(); - let char = characters[0].as_ref().unwrap(); - assert!(char.meseta == 999999); - assert!(char.bank_meseta == 300); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + let c1_bank_meseta = entity_gateway.get_bank_meseta(&char1.id, item::BankName("".into())).await.unwrap(); + assert!(c1_meseta.0 == 999999); + assert!(c1_bank_meseta.0 == 300); } diff --git a/tests/test_item_pickup.rs b/tests/test_item_pickup.rs index d5287d0..7feb377 100644 --- a/tests/test_item_pickup.rs +++ b/tests/test_item_pickup.rs @@ -221,7 +221,7 @@ async fn test_pick_up_meseta_when_inventory_full() { let mut entity_gateway = InMemoryGateway::default(); let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; - let (user2, mut char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + let (user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; let mut p1_items = Vec::new(); for _ in 0..30usize { @@ -240,9 +240,7 @@ async fn test_pick_up_meseta_when_inventory_full() { } entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_items)).await.unwrap(); - - char2.meseta = 300; - entity_gateway.save_character(&char2).await.unwrap(); + entity_gateway.set_character_meseta(&char2.id, item::Meseta(300)).await.unwrap(); let mut ship = Box::new(ShipServerState::builder() .gateway(entity_gateway.clone()) @@ -284,12 +282,10 @@ async fn test_pick_up_meseta_when_inventory_full() { let inventory_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); assert_eq!(inventory_items.items.len(), 30); - let characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap(); - let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap(); - let characters2 = entity_gateway.get_characters_by_user(&user2).await.unwrap(); - let c2 = characters2.get(0).as_ref().unwrap().as_ref().unwrap(); - assert!(c1.meseta == 23); - assert!(c2.meseta == 277); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + let c2_meseta = entity_gateway.get_character_meseta(&char2.id).await.unwrap(); + assert!(c1_meseta.0 == 23); + assert!(c2_meseta.0 == 277); } #[async_std::test] @@ -469,10 +465,9 @@ async fn test_can_not_pick_up_item_when_inventory_full() { async fn test_can_not_drop_more_meseta_than_is_held() { let mut entity_gateway = InMemoryGateway::default(); - let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; - char1.meseta = 300; - entity_gateway.save_character(&char1).await.unwrap(); + entity_gateway.set_character_meseta(&char1.id, item::Meseta(300)).await.unwrap(); let mut ship = Box::new(ShipServerState::builder() .gateway(entity_gateway.clone()) @@ -501,9 +496,8 @@ async fn test_can_not_drop_more_meseta_than_is_held() { })))).await; assert!(split_attempt.is_err()); - let characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap(); - let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap(); - assert!(c1.meseta == 300); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert!(c1_meseta.0 == 300); } #[async_std::test] @@ -584,13 +578,11 @@ async fn test_pick_up_stack_that_would_exceed_stack_limit() { async fn test_can_not_pick_up_meseta_when_full() { let mut entity_gateway = InMemoryGateway::default(); - let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; - let (user2, mut char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; - char1.meseta = 999999; - entity_gateway.save_character(&char1).await.unwrap(); - char2.meseta = 300; - entity_gateway.save_character(&char2).await.unwrap(); + entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap(); + entity_gateway.set_character_meseta(&char2.id, item::Meseta(300)).await.unwrap(); let mut ship = Box::new(ShipServerState::builder() .gateway(entity_gateway.clone()) @@ -630,25 +622,21 @@ async fn test_can_not_pick_up_meseta_when_full() { })))).await.unwrap().collect::>(); assert!(packets.len() == 0); - let characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap(); - let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap(); - let characters2 = entity_gateway.get_characters_by_user(&user2).await.unwrap(); - let c2 = characters2.get(0).as_ref().unwrap().as_ref().unwrap(); - assert!(c1.meseta == 999999); - assert!(c2.meseta == 277); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + let c2_meseta = entity_gateway.get_character_meseta(&char2.id).await.unwrap(); + assert!(c1_meseta.0 == 999999); + assert!(c2_meseta.0 == 277); } #[async_std::test] async fn test_meseta_caps_at_999999_when_trying_to_pick_up_more() { let mut entity_gateway = InMemoryGateway::default(); - let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; - let (user2, mut char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + let (user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; - char1.meseta = 999998; - entity_gateway.save_character(&char1).await.unwrap(); - char2.meseta = 300; - entity_gateway.save_character(&char2).await.unwrap(); + entity_gateway.set_character_meseta(&char1.id, item::Meseta(999998)).await.unwrap(); + entity_gateway.set_character_meseta(&char2.id, item::Meseta(300)).await.unwrap(); let mut ship = Box::new(ShipServerState::builder() .gateway(entity_gateway.clone()) @@ -687,12 +675,10 @@ async fn test_meseta_caps_at_999999_when_trying_to_pick_up_more() { unknown: [0; 3] })))).await.unwrap().for_each(drop); - let characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap(); - let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap(); - let characters2 = entity_gateway.get_characters_by_user(&user2).await.unwrap(); - let c2 = characters2.get(0).as_ref().unwrap().as_ref().unwrap(); - assert!(c1.meseta == 999999); - assert!(c2.meseta == 277); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + let c2_meseta = entity_gateway.get_character_meseta(&char2.id).await.unwrap(); + assert!(c1_meseta.0 == 999999); + assert!(c2_meseta.0 == 277); } #[async_std::test] @@ -767,3 +753,20 @@ async fn test_player_drops_partial_stack_and_other_player_picks_it_up() { vec![item::ItemEntityId(1), item::ItemEntityId(2)]); }).unwrap(); } + +/* +#[async_std::test] +async fn test_try_and_pick_up_individual_item_twice() { + panic!() +} + +#[async_std::test] +async fn test_try_and_pick_up_stacked_item_twice() { + panic!() +} + +#[async_std::test] +async fn test_try_and_pick_up_meseta_twice() { + panic!() +} +*/ diff --git a/tests/test_shops.rs b/tests/test_shops.rs index 78e2e38..b792325 100644 --- a/tests/test_shops.rs +++ b/tests/test_shops.rs @@ -107,8 +107,8 @@ async fn test_player_buys_from_weapon_shop() { let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; char1.exp = 80000000; - char1.meseta = 999999; entity_gateway.save_character(&char1).await.unwrap(); + entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap(); let mut ship = Box::new(ShipServerState::builder() .gateway(entity_gateway.clone()) @@ -132,9 +132,8 @@ async fn test_player_buys_from_weapon_shop() { unknown1: 0, })))).await.unwrap().for_each(drop); - let characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap(); - let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap(); - assert!(c1.meseta < 999999); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert!(c1_meseta.0 < 999999); //let p1_items = entity_gateway.get_items_by_character(&char1.id).await.unwrap(); let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); assert_eq!(p1_items.items.len(), 1); @@ -146,8 +145,8 @@ async fn test_player_buys_from_tool_shop() { let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; char1.exp = 80000000; - char1.meseta = 999999; entity_gateway.save_character(&char1).await.unwrap(); + entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap(); let mut ship = Box::new(ShipServerState::builder() .gateway(entity_gateway.clone()) @@ -171,9 +170,8 @@ async fn test_player_buys_from_tool_shop() { unknown1: 0, })))).await.unwrap().for_each(drop); - let characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap(); - let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap(); - assert!(c1.meseta < 999999); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert!(c1_meseta.0 < 999999); let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); assert_eq!(p1_items.items.len(), 1); } @@ -184,8 +182,8 @@ async fn test_player_buys_multiple_from_tool_shop() { let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; char1.exp = 80000000; - char1.meseta = 999999; entity_gateway.save_character(&char1).await.unwrap(); + entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap(); let mut ship = Box::new(ShipServerState::builder() .gateway(entity_gateway.clone()) @@ -209,9 +207,8 @@ async fn test_player_buys_multiple_from_tool_shop() { unknown1: 0, })))).await.unwrap().for_each(drop); - let characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap(); - let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap(); - assert!(c1.meseta < 999999); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert!(c1_meseta.0 < 999999); let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); assert_eq!(p1_items.items.len(), 1); p1_items.items[0].with_stacked(|item| { @@ -226,8 +223,8 @@ async fn test_player_buys_from_armor_shop() { let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; char1.exp = 80000000; - char1.meseta = 999999; entity_gateway.save_character(&char1).await.unwrap(); + entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap(); let mut ship = Box::new(ShipServerState::builder() .gateway(entity_gateway.clone()) @@ -251,9 +248,8 @@ async fn test_player_buys_from_armor_shop() { unknown1: 0, })))).await.unwrap().for_each(drop); - let characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap(); - let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap(); - assert!(c1.meseta < 999999); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert!(c1_meseta.0 < 999999); let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); assert_eq!(p1_items.items.len(), 1); } @@ -269,7 +265,7 @@ async fn test_other_clients_see_purchase() { let (_user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; let (_user2, _char2) = new_user_character(&mut entity_gateway, "a2", "a").await; char1.exp = 80000000; - char1.meseta = 999999; + entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap(); entity_gateway.save_character(&char1).await.unwrap(); let mut ship = Box::new(ShipServerState::builder() @@ -312,8 +308,8 @@ async fn test_other_clients_see_stacked_purchase() { let (_user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; let (_user2, _char2) = new_user_character(&mut entity_gateway, "a2", "a").await; char1.exp = 80000000; - char1.meseta = 999999; entity_gateway.save_character(&char1).await.unwrap(); + entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap(); entity_gateway.create_item( item::NewItemEntity { item: item::ItemDetail::Tool( @@ -385,9 +381,8 @@ async fn test_buying_item_without_enough_mseseta() { })))).await; assert!(packets.is_err()); - let characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap(); - let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap(); - assert_eq!(c1.meseta, 0); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 0); let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); assert_eq!(p1_items.items.len(), 0); } @@ -398,8 +393,8 @@ async fn test_player_double_buys_from_tool_shop() { let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; char1.exp = 80000000; - char1.meseta = 999999; entity_gateway.save_character(&char1).await.unwrap(); + entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap(); let mut ship = Box::new(ShipServerState::builder() .gateway(entity_gateway.clone()) @@ -441,9 +436,8 @@ async fn test_player_double_buys_from_tool_shop() { unknown1: 0, })))).await.unwrap().for_each(drop); - let characters1 = entity_gateway.get_characters_by_user(&user1).await.unwrap(); - let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap(); - assert!(c1.meseta < 999999); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert!(c1_meseta.0 < 999999); let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); assert_eq!(p1_items.items.len(), 2); p1_items.items[0].with_stacked(|item| { @@ -464,8 +458,8 @@ async fn test_techs_disappear_from_shop_when_bought() { let (_user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; char1.exp = 80000000; - char1.meseta = 999999; entity_gateway.save_character(&char1).await.unwrap(); + entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap(); let mut ship = Box::new(ShipServerState::builder() .gateway(entity_gateway.clone()) @@ -526,8 +520,8 @@ async fn test_units_disappear_from_shop_when_bought() { let (_user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; char1.exp = 80000000; - char1.meseta = 999999; entity_gateway.save_character(&char1).await.unwrap(); + entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap(); let mut ship = Box::new(ShipServerState::builder() .gateway(entity_gateway.clone()) From 81916d1f572247cafb71169db8f61b3d4cbc7465 Mon Sep 17 00:00:00 2001 From: jake Date: Mon, 27 Dec 2021 00:48:42 -0700 Subject: [PATCH 39/41] TRADING JUST IN TIME TO BARELY MISS XMAS --- src/ship/items/manager.rs | 51 +- src/ship/packet/handler/trade.rs | 56 +- src/ship/trade.rs | 15 +- tests/test_trade.rs | 4271 ++++++++++++++++++++++++++++++ 4 files changed, 4372 insertions(+), 21 deletions(-) create mode 100644 tests/test_trade.rs diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs index 044aabd..3031511 100644 --- a/src/ship/items/manager.rs +++ b/src/ship/items/manager.rs @@ -275,15 +275,6 @@ impl ItemManager { )) } - /*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); @@ -986,14 +977,41 @@ impl ItemManager { let p1_inventory = it.manager.get_character_inventory(p1.1)?; let p2_inventory = it.manager.get_character_inventory(p2.1)?; - [(p2_inventory, p1_inventory, p2.2), (p1_inventory, p2_inventory, p1.2)].iter() - .map(|(src_inventory, dest_inventory, to_trade)| { - to_trade + [(p2_inventory, p1_inventory, p2.2, p1.2), (p1_inventory, p2_inventory, p1.2, p2.2)].iter() + .map(|(src_inventory, dest_inventory, trade_recv, trade_send)| { + let item_slots_lost_to_trade = trade_send + .iter() + .fold(0, |acc, item| { + match item { + TradeItem::Individual(..) => { + acc + 1 + }, + TradeItem::Stacked(item_id, amount) => { + let stacked_inventory_item = try { + src_inventory + .get_item_by_id(*item_id)? + .stacked() + }; + if let Some(Some(item)) = stacked_inventory_item { + if item.count() == *amount { + acc + 1 + } + else { + acc + } + } + else { + acc + } + } + } + }); + trade_recv .iter() .try_fold(dest_inventory.count(), |acc, item| { match item { TradeItem::Individual(..) => { - if acc >= 30 { + if acc >= (30 + item_slots_lost_to_trade) { Err(TradeError::NoInventorySpace) } else { @@ -1017,7 +1035,12 @@ impl ItemManager { Err(TradeError::NoStackSpace) }, SpaceForStack::No(NoThereIsNotSpace::FullInventory) => { - Err(TradeError::NoInventorySpace) + if acc >= (30 + item_slots_lost_to_trade) { + Err(TradeError::NoInventorySpace) + } + else { + Ok(acc + 1) + } }, } } diff --git a/src/ship/packet/handler/trade.rs b/src/ship/packet/handler/trade.rs index 8300bb9..1782546 100644 --- a/src/ship/packet/handler/trade.rs +++ b/src/ship/packet/handler/trade.rs @@ -42,6 +42,14 @@ pub enum TradeError { NoStackSpace, #[error("invalid meseta amount")] InvalidMeseta, + #[error("tried starting a trade while in one already")] + ClientAlreadyInTrade, + #[error("tried starting a trade while with player already in a trade")] + OtherAlreadyInTrade, + #[error("tried to trade item not specified in trade request")] + SketchyTrade, + #[error("items in trade window and items attempted to trade do not match")] + MismatchedTradeItems, } @@ -63,6 +71,9 @@ where TradeRequestCommand::Initialize(ref act, meseta) => { match act { TradeRequestInitializeCommand::Initialize => { + if trades.in_trade(&id) { + return Err(TradeError::ClientAlreadyInTrade.into()) + } let trade_partner = client_location.get_client_neighbors(id)? .into_iter() .filter(|ac| { @@ -70,6 +81,9 @@ where }) .next() .ok_or(TradeError::CouldNotFindTradePartner)?; + if trades.in_trade(&trade_partner.client) { + return Err(TradeError::OtherAlreadyInTrade.into()) + } trades.new_trade(&id, &trade_partner.client); Ok(Box::new(client_location.get_all_clients_by_client(id)?.into_iter() .filter(move |client| client.local_client.id() == target as u8) @@ -180,7 +194,7 @@ where match this.items[trade_item_index].stacked()?.1.cmp(&(amount as usize)) { std::cmp::Ordering::Greater => { - this.items[trade_item_index].stacked()?.1 -= amount as usize; + *this.items[trade_item_index].stacked_mut()?.1 -= amount as usize; }, std::cmp::Ordering::Equal => { this.items.remove(trade_item_index); @@ -267,6 +281,15 @@ where .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {}))))) })) }, + TradeRequestCommand::Cancel => { + trades.remove_trade(&id); + Ok(Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() + .filter(move |client| client.local_client.id() == target as u8) + .map(move |client| { + (client.client, SendShipPacket::CancelTrade(CancelTrade {})) + }) + .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {})))))) + } } } @@ -297,17 +320,31 @@ where } let client = clients.get(&this.client()).ok_or(ShipError::ClientNotFound(this.client()))?; + let other_client = clients.get(&other.client()).ok_or(ShipError::ClientNotFound(other.client()))?; let inventory = item_manager.get_character_inventory(&client.character)?; - let item_blobs = items_to_trade.items.iter().take(items_to_trade.count as usize); - item_blobs + if items_to_trade.count as usize != (this.items.len() + (if this.meseta != 0 { 1 } else { 0 })) { + return Err(TradeError::MismatchedTradeItems.into()) + } + + items_to_trade.items + .iter() + .take(items_to_trade.count as usize) .map(|item| { if ClientItemId(item.item_id) == OTHER_MESETA_ITEM_ID { if item.item_data[0] != 4 { return Err(TradeError::InvalidItemId(ClientItemId(item.item_id)).into()) } let amount = u32::from_le_bytes(item.item_data2); - if amount > client.character.meseta { + let character_meseta = item_manager.get_character_meseta(&client.character.id).map_err(|_| TradeError::InvalidMeseta)?; + let other_character_meseta = item_manager.get_character_meseta(&other_client.character.id).map_err(|_| TradeError::InvalidMeseta)?; + if amount > character_meseta.0 { + return Err(TradeError::InvalidMeseta.into()) + } + if (amount + other_character_meseta.0) > 999999 { + return Err(TradeError::InvalidMeseta.into()) + } + if amount != this.meseta as u32{ return Err(TradeError::InvalidMeseta.into()) } Ok(()) @@ -315,6 +352,10 @@ where else { let real_item = inventory.get_item_by_id(ClientItemId(item.item_id)) .ok_or(ItemManagerError::NoSuchItemId(ClientItemId(item.item_id)))?; + let real_trade_item = this.items + .iter() + .find(|i| i.item_id() == ClientItemId(item.item_id)) + .ok_or_else(|| TradeError::SketchyTrade)?; let trade_item_bytes: [u8; 16] = item.item_data.iter() .chain(item.item_data2.iter()) .cloned().collect::>() @@ -333,7 +374,12 @@ where if real_item.as_client_bytes()[0..4] == trade_item_bytes[0..4] { let amount = trade_item_bytes[5] as usize; if amount <= stacked_inventory_item.entity_ids.len() { - Ok(()) + if real_trade_item.stacked().ok_or_else(|| TradeError::SketchyTrade)?.1 == amount { + Ok(()) + } + else { + Err(TradeError::InvalidStackAmount(stacked_inventory_item.item_id, amount).into()) + } } else { Err(TradeError::InvalidStackAmount(stacked_inventory_item.item_id, amount).into()) diff --git a/src/ship/trade.rs b/src/ship/trade.rs index e6e1ae3..c58fdb2 100644 --- a/src/ship/trade.rs +++ b/src/ship/trade.rs @@ -14,12 +14,19 @@ pub enum TradeItem { impl TradeItem { pub fn stacked(&self) -> Option<(items::ClientItemId, usize)> { match self { - TradeItem::Stacked(item_id, amount) => Some((*item_id, *amount as usize)), + TradeItem::Stacked(item_id, amount) => Some((*item_id, *amount)), _ => None } } - pub fn item_id(&self) -> items::ClientItemId { + pub fn stacked_mut(&mut self) -> Option<(items::ClientItemId, &mut usize)> { + match self { + TradeItem::Stacked(item_id, ref mut amount) => Some((*item_id, amount)), + _ => None + } + } + + pub fn item_id(&self) -> items::ClientItemId { match self { TradeItem::Individual(item_id) => *item_id, TradeItem::Stacked(item_id, _) => *item_id, @@ -125,6 +132,10 @@ impl TradeState { self.trades.insert(*receiver, RefCell::new(state)); } + pub fn in_trade(&self, client: &ClientId) -> bool { + self.trades.contains_key(client) + } + pub fn with (&self, client: &ClientId, func: F) -> Result where F: Fn(&mut ClientTradeState, &mut ClientTradeState) -> T diff --git a/tests/test_trade.rs b/tests/test_trade.rs new file mode 100644 index 0000000..25d4a52 --- /dev/null +++ b/tests/test_trade.rs @@ -0,0 +1,4271 @@ +use std::convert::TryInto; +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::entity::item::{Meseta, ItemEntity}; +use elseware::ship::items::transaction::TransactionError; +use elseware::ship::packet::handler::trade::TradeError; + +use libpso::packet::ship::*; +use libpso::packet::messages::*; + +#[path = "common.rs"] +mod common; +use common::*; + +async fn initialize_trade(ship: &mut ShipServerState, client1: ClientId, client2: ClientId) { + ship.handle(client1, &RecvShipPacket::DirectMessage(DirectMessage::new(client2.0 as u32 -1, GameMessage::TradeRequest(TradeRequest { + client: client1.0 as u8 -1, + target: 0, + trade: TradeRequestCommand::Initialize(TradeRequestInitializeCommand::Initialize, 0) + })))).await.unwrap().for_each(drop); + + ship.handle(client2, &RecvShipPacket::DirectMessage(DirectMessage::new(client1.0 as u32 -1, GameMessage::TradeRequest(TradeRequest { + client: client2.0 as u8 -1, + target: 0, + trade: TradeRequestCommand::Initialize(TradeRequestInitializeCommand::Respond, 0) + })))).await.unwrap().for_each(drop); +} + +async fn confirm_trade(ship: &mut ShipServerState, client1: ClientId, client2: ClientId) { + ship.handle(client1, &RecvShipPacket::DirectMessage(DirectMessage::new(client2.0 as u32 -1, GameMessage::TradeRequest(TradeRequest { + client: client1.0 as u8 -1, + target: 0, + trade: TradeRequestCommand::Confirm + })))).await.unwrap().for_each(drop); + + ship.handle(client2, &RecvShipPacket::DirectMessage(DirectMessage::new(client1.0 as u32 -1, GameMessage::TradeRequest(TradeRequest { + client: client2.0 as u8 -1, + target: 0, + trade: TradeRequestCommand::Confirm + })))).await.unwrap().for_each(drop); +} + +async fn finalconfirm_trade(ship: &mut ShipServerState, client1: ClientId, client2: ClientId) { + ship.handle(client1, &RecvShipPacket::DirectMessage(DirectMessage::new(client2.0 as u32 -1, GameMessage::TradeRequest(TradeRequest { + client: client1.0 as u8 -1, + target: 0, + trade: TradeRequestCommand::FinalConfirm + })))).await.unwrap().for_each(drop); + + ship.handle(client2, &RecvShipPacket::DirectMessage(DirectMessage::new(client1.0 as u32 -1, GameMessage::TradeRequest(TradeRequest { + client: client2.0 as u8 -1, + target: 0, + trade: TradeRequestCommand::FinalConfirm + })))).await.unwrap().for_each(drop); +} + +#[derive(Default)] +struct TradeItemBuilder { + count: usize, + items: [TradeItem; 32], +} + +impl TradeItemBuilder { + fn individual(mut self, item: &elseware::entity::item::InventoryItemEntity, item_id: u32) -> Self { + let idata = item.with_individual(|i| i.item.as_client_bytes()).unwrap(); + self.items[self.count] = TradeItem { + item_data: idata[0..12].try_into().unwrap(), + item_id: item_id, + item_data2: idata[12..16].try_into().unwrap(), + }; + + self.count += 1; + self + } + + fn stacked(mut self, item: &elseware::entity::item::InventoryItemEntity, item_id: u32, amount: u8) -> Self { + let idata = item + .with_stacked(|i| i[0].item.tool().unwrap().as_stacked_bytes(i.len())) + .map(|mut data| { + data[5] = amount; + data + }) + .unwrap(); + self.items[self.count] = TradeItem { + item_data: idata[0..12].try_into().unwrap(), + item_id: item_id, + item_data2: idata[12..16].try_into().unwrap(), + }; + + self.count += 1; + self + } + + fn meseta(mut self, amount: usize) -> Self { + self.items[self.count] = TradeItem { + item_data: [4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + item_id: 0xFFFFFFFF, + item_data2: u32::to_le_bytes(amount as u32), + }; + + self.count += 1; + self + } + + fn build(self) -> [TradeItem; 32] { + self.items + } +} + + +#[async_std::test] +async fn test_trade_one_individual_item() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let mut p1_inv = Vec::new(); + p1_inv.push(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, + } + ), + }).await.unwrap()); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 1) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .individual(&p1_items.items[0], 0x10000) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 0, + items: Default::default(), + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 5); + assert!(matches!(ack[0], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem {..}), + .. + })))); + assert!(matches!(ack[1], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem {..}), + .. + })))); + assert!(matches!(ack[2], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem {..}), + .. + })))); + assert!(matches!(ack[3], (ClientId(2), SendShipPacket::TradeSuccessful {..}))); + assert!(matches!(ack[4], (ClientId(1), SendShipPacket::TradeSuccessful {..}))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 0); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 1); +} + +#[async_std::test] +async fn test_trade_player2_to_player1() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let mut p2_inv = Vec::new(); + p2_inv.push(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, + } + ), + }).await.unwrap()); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(p2_inv)).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 0); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 1); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(2), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x210000, 1) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 0, + items: Default::default(), + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let titems = TradeItemBuilder::default() + .individual(&p2_items.items[0], 0x210000) + .build(); + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 5); + assert!(matches!(ack[0], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem {..}), + .. + })))); + assert!(matches!(ack[1], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem {..}), + .. + })))); + assert!(matches!(ack[2], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem {..}), + .. + })))); + assert!(matches!(ack[3], (ClientId(2), SendShipPacket::TradeSuccessful {..}))); + assert!(matches!(ack[4], (ClientId(1), SendShipPacket::TradeSuccessful {..}))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); +} + +#[async_std::test] +async fn test_reverse_trade_ack_order() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let mut p1_inv = Vec::new(); + p1_inv.push(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, + } + ), + }).await.unwrap()); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); + + initialize_trade(&mut ship, ClientId(2), ClientId(1)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 1) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(2), ClientId(1)).await; + finalconfirm_trade(&mut ship, ClientId(2), ClientId(1)).await; + + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 0, + items: Default::default(), + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let titems = TradeItemBuilder::default() + .individual(&p1_items.items[0], 0x10000) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 5); + assert!(matches!(ack[0], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem {..}), + .. + })))); + assert!(matches!(ack[1], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem {..}), + .. + })))); + assert!(matches!(ack[2], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem {..}), + .. + })))); + assert!(matches!(ack[3], (ClientId(2), SendShipPacket::TradeSuccessful {..}))); + assert!(matches!(ack[4], (ClientId(1), SendShipPacket::TradeSuccessful {..}))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 0); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 1); +} + +#[async_std::test] +async fn test_trade_one_stacked_item() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let p1_stack = futures::future::join_all((0..2).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![p1_stack])).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 2) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .stacked(&p1_items.items[0], 0x10000, 2) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 0, + items: Default::default(), + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 5); + assert!(matches!(ack[0], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem {..}), + .. + })))); + assert!(matches!(ack[1], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem {..}), + .. + })))); + assert!(matches!(ack[2], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem {..}), + .. + })))); + assert!(matches!(ack[3], (ClientId(2), SendShipPacket::TradeSuccessful {..}))); + assert!(matches!(ack[4], (ClientId(1), SendShipPacket::TradeSuccessful {..}))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 0); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 1); +} + +#[async_std::test] +async fn test_trade_partial_stacked_item() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let p1_stack = futures::future::join_all((0..2).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![p1_stack])).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + assert_eq!(p1_items.items[0].with_stacked(|i| i.len()).unwrap(), 2); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 1) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .stacked(&p1_items.items[0], 0x10000, 1) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 0, + items: Default::default(), + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 5); + assert!(matches!(ack[0], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem {..}), + .. + })))); + assert!(matches!(ack[1], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem {..}), + .. + })))); + assert!(matches!(ack[2], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem {..}), + .. + })))); + assert!(matches!(ack[3], (ClientId(2), SendShipPacket::TradeSuccessful {..}))); + assert!(matches!(ack[4], (ClientId(1), SendShipPacket::TradeSuccessful {..}))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + assert_eq!(p1_items.items[0].with_stacked(|i| i.len()).unwrap(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 1); + assert_eq!(p2_items.items[0].with_stacked(|i| i.len()).unwrap(), 1); +} + +#[async_std::test] +async fn test_trade_individual_both() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let p1_inv = vec![ + 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, + } + ), + }).await.unwrap()]; + let p2_inv = vec![ + 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, + } + ), + }).await.unwrap()]; + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(p2_inv)).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 1); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 1) + })))).await.unwrap().for_each(drop); + + ship.handle(ClientId(2), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 0, + target: 0, + trade: TradeRequestCommand::AddItem(0x210000, 1) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .individual(&p1_items.items[0], 0x10000) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let titems = TradeItemBuilder::default() + .individual(&p2_items.items[0], 0x210000) + .build(); + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 8); + assert!(matches!(ack[0], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 0, + item_data: [0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // handgun + item_id: 0x810001, + .. + }), + .. + })))); + assert!(matches!(ack[1], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 0, + item_data: [0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // handgun + item_id: 0x810001, + .. + }), + .. + })))); + assert!(matches!(ack[2], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { + client: 1, + item_id: 0x210000, + .. + }), + .. + })))); + assert!(matches!(ack[3], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 1, + item_data: [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // saber + item_id: 0x810002, + .. + }), + .. + })))); + assert!(matches!(ack[4], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 1, + item_data: [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // saber + item_id: 0x810002, + .. + }), + .. + })))); + assert!(matches!(ack[5], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { + client: 0, + item_id: 0x10000, + .. + }), + .. + })))); + assert!(matches!(ack[6], (ClientId(2), SendShipPacket::TradeSuccessful {..}))); + assert!(matches!(ack[7], (ClientId(1), SendShipPacket::TradeSuccessful {..}))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + assert!(matches!(p1_items.items[0].with_individual(|i| i.clone()).unwrap(), item::ItemEntity{item: item::ItemDetail::Weapon(item::weapon::Weapon {weapon: item::weapon::WeaponType::Handgun, ..}), ..})); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 1); + assert!(matches!(p2_items.items[0].with_individual(|i| i.clone()).unwrap(), item::ItemEntity{item: item::ItemDetail::Weapon(item::weapon::Weapon {weapon: item::weapon::WeaponType::Saber, ..}), ..})); +} + +#[async_std::test] +async fn test_trade_stacked_both() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let p1_stack = futures::future::join_all((0..2).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + let p2_stack = futures::future::join_all((0..3).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monofluid, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![p1_stack])).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(vec![p2_stack])).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 1); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 2) + })))).await.unwrap().for_each(drop); + + ship.handle(ClientId(2), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::TradeRequest(TradeRequest { + client: 0, + target: 0, + trade: TradeRequestCommand::AddItem(0x210000, 3) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .stacked(&p1_items.items[0], 0x10000, 2) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let titems = TradeItemBuilder::default() + .stacked(&p2_items.items[0], 0x210000, 3) + .build(); + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 8); + assert!(matches!(ack[0], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 0, + item_id: 0x810001, + .. + }), + .. + })))); + assert!(matches!(ack[1], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 0, + item_id: 0x810001, + .. + }), + .. + })))); + assert!(matches!(ack[2], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { + client: 1, + item_id: 0x210000, + .. + }), + .. + })))); + assert!(matches!(ack[3], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 1, + item_id: 0x810002, + .. + }), + .. + })))); + assert!(matches!(ack[4], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 1, + item_id: 0x810002, + .. + }), + .. + })))); + assert!(matches!(ack[5], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { + client: 0, + item_id: 0x10000, + .. + }), + .. + })))); + assert!(matches!(ack[6], (ClientId(2), SendShipPacket::TradeSuccessful {..}))); + assert!(matches!(ack[7], (ClientId(1), SendShipPacket::TradeSuccessful {..}))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + assert_eq!(p1_items.items[0].with_stacked(|i| i.clone()).unwrap().len(), 3); + assert!(matches!(p1_items.items[0].with_stacked(|i| i.clone()).unwrap()[0], item::ItemEntity{item: item::ItemDetail::Tool(item::tool::Tool {tool: item::tool::ToolType::Monofluid, ..}), ..})); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 1); + assert_eq!(p2_items.items[0].with_stacked(|i| i.clone()).unwrap().len(), 2); + assert!(matches!(p2_items.items[0].with_stacked(|i| i.clone()).unwrap()[0], item::ItemEntity{item: item::ItemDetail::Tool(item::tool::Tool {tool: item::tool::ToolType::Monomate, ..}), ..})); +} + +#[async_std::test] +async fn test_trade_partial_stack_both() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let p1_stack = futures::future::join_all((0..2).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + let p2_stack = futures::future::join_all((0..3).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monofluid, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![p1_stack])).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(vec![p2_stack])).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 1); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 1) + })))).await.unwrap().for_each(drop); + + ship.handle(ClientId(2), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::TradeRequest(TradeRequest { + client: 0, + target: 0, + trade: TradeRequestCommand::AddItem(0x210000, 2) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .stacked(&p1_items.items[0], 0x10000, 1) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let titems = TradeItemBuilder::default() + .stacked(&p2_items.items[0], 0x210000, 2) + .build(); + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 8); + assert!(matches!(ack[0], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 0, + item_id: 0x810001, + .. + }), + .. + })))); + assert!(matches!(ack[1], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 0, + item_id: 0x810001, + .. + }), + .. + })))); + assert!(matches!(ack[2], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { + client: 1, + item_id: 0x210000, + amount: 2, + .. + }), + .. + })))); + assert!(matches!(ack[3], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 1, + item_id: 0x810002, + .. + }), + .. + })))); + assert!(matches!(ack[4], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 1, + item_id: 0x810002, + .. + }), + .. + })))); + assert!(matches!(ack[5], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { + client: 0, + item_id: 0x10000, + amount: 1, + .. + }), + .. + })))); + assert!(matches!(ack[6], (ClientId(2), SendShipPacket::TradeSuccessful {..}))); + assert!(matches!(ack[7], (ClientId(1), SendShipPacket::TradeSuccessful {..}))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 2); + assert_eq!(p1_items.items[0].with_stacked(|i| i.clone()).unwrap().len(), 1); + assert_eq!(p1_items.items[1].with_stacked(|i| i.clone()).unwrap().len(), 2); + assert!(matches!(p1_items.items[0].with_stacked(|i| i.clone()).unwrap()[0], item::ItemEntity{item: item::ItemDetail::Tool(item::tool::Tool {tool: item::tool::ToolType::Monomate, ..}), ..})); + assert!(matches!(p1_items.items[1].with_stacked(|i| i.clone()).unwrap()[0], item::ItemEntity{item: item::ItemDetail::Tool(item::tool::Tool {tool: item::tool::ToolType::Monofluid, ..}), ..})); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 2); + assert_eq!(p2_items.items[0].with_stacked(|i| i.clone()).unwrap().len(), 1); + assert_eq!(p2_items.items[1].with_stacked(|i| i.clone()).unwrap().len(), 1); + assert!(matches!(p2_items.items[0].with_stacked(|i| i.clone()).unwrap()[0], item::ItemEntity{item: item::ItemDetail::Tool(item::tool::Tool {tool: item::tool::ToolType::Monofluid, ..}), ..})); + assert!(matches!(p2_items.items[1].with_stacked(|i| i.clone()).unwrap()[0], item::ItemEntity{item: item::ItemDetail::Tool(item::tool::Tool {tool: item::tool::ToolType::Monomate, ..}), ..})); +} + +#[async_std::test] +async fn test_trade_same_stacked_item_to_eachother() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let p1_stack = futures::future::join_all((0..3).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + let p2_stack = futures::future::join_all((0..4).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![p1_stack])).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(vec![p2_stack])).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 1); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 1) + })))).await.unwrap().for_each(drop); + + ship.handle(ClientId(2), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::TradeRequest(TradeRequest { + client: 0, + target: 0, + trade: TradeRequestCommand::AddItem(0x210000, 3) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .stacked(&p1_items.items[0], 0x10000, 1) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let titems = TradeItemBuilder::default() + .stacked(&p2_items.items[0], 0x210000, 3) + .build(); + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 8); + assert!(matches!(ack[0], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 0, + item_id: 0x810001, + .. + }), + .. + })))); + assert!(matches!(ack[1], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 0, + item_id: 0x810001, + .. + }), + .. + })))); + assert!(matches!(ack[2], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { + client: 1, + item_id: 0x210000, + amount: 3, + .. + }), + .. + })))); + assert!(matches!(ack[3], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 1, + item_id: 0x810002, + .. + }), + .. + })))); + assert!(matches!(ack[4], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 1, + item_id: 0x810002, + .. + }), + .. + })))); + assert!(matches!(ack[5], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { + client: 0, + item_id: 0x10000, + amount: 1, + .. + }), + .. + })))); + assert!(matches!(ack[6], (ClientId(2), SendShipPacket::TradeSuccessful {..}))); + assert!(matches!(ack[7], (ClientId(1), SendShipPacket::TradeSuccessful {..}))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + assert_eq!(p1_items.items[0].with_stacked(|i| i.clone()).unwrap().len(), 5); + assert!(matches!(p1_items.items[0].with_stacked(|i| i.clone()).unwrap()[0], item::ItemEntity{item: item::ItemDetail::Tool(item::tool::Tool {tool: item::tool::ToolType::Monomate, ..}), ..})); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 1); + assert_eq!(p2_items.items[0].with_stacked(|i| i.clone()).unwrap().len(), 2); + assert!(matches!(p2_items.items[0].with_stacked(|i| i.clone()).unwrap()[0], item::ItemEntity{item: item::ItemDetail::Tool(item::tool::Tool {tool: item::tool::ToolType::Monomate, ..}), ..})); +} + +#[async_std::test] +async fn test_trade_stacked_when_already_have_partial_stack() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let p1_stack = futures::future::join_all((0..3).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + let p2_stack = futures::future::join_all((0..3).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![p1_stack])).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(vec![p2_stack])).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 1); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 2) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .stacked(&p1_items.items[0], 0x10000, 2) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 0, + items: Default::default(), + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 5); + assert!(matches!(ack[0], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 1, + item_id: 0x810001, + item_data: [3, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0], + .. + }), + .. + })))); + assert!(matches!(ack[1], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 1, + item_id: 0x810001, + item_data: [3, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0], + .. + }), + .. + })))); + assert!(matches!(ack[2], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { + client: 0, + item_id: 0x10000, + amount: 2, + .. + }), + .. + })))); + assert!(matches!(ack[3], (ClientId(2), SendShipPacket::TradeSuccessful {..}))); + assert!(matches!(ack[4], (ClientId(1), SendShipPacket::TradeSuccessful {..}))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + assert_eq!(p1_items.items[0].with_stacked(|i| i.clone()).unwrap().len(), 1); + assert!(matches!(p1_items.items[0].with_stacked(|i| i.clone()).unwrap()[0], item::ItemEntity{item: item::ItemDetail::Tool(item::tool::Tool {tool: item::tool::ToolType::Monomate, ..}), ..})); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 1); + assert_eq!(p2_items.items[0].with_stacked(|i| i.clone()).unwrap().len(), 5); + assert!(matches!(p2_items.items[0].with_stacked(|i| i.clone()).unwrap()[0], item::ItemEntity{item: item::ItemDetail::Tool(item::tool::Tool {tool: item::tool::ToolType::Monomate, ..}), ..})); +} + +#[async_std::test] +async fn test_trade_individual_for_stacked() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let p1_inv = vec![ + 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, + } + ), + }).await.unwrap()]; + + let p2_stack = futures::future::join_all((0..2).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(vec![p2_stack])).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 1); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 1) + })))).await.unwrap().for_each(drop); + + ship.handle(ClientId(2), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::TradeRequest(TradeRequest { + client: 0, + target: 0, + trade: TradeRequestCommand::AddItem(0x210000, 2) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .individual(&p1_items.items[0], 0x10000) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let titems = TradeItemBuilder::default() + .stacked(&p2_items.items[0], 0x210000, 2) + .build(); + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 8); + assert!(matches!(ack[0], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 0, + item_data: [3, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0], + item_id: 0x810001, + .. + }), + .. + })))); + assert!(matches!(ack[1], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 0, + item_data: [3, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0], + item_id: 0x810001, + .. + }), + .. + })))); + assert!(matches!(ack[2], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { + client: 1, + item_id: 0x210000, + .. + }), + .. + })))); + assert!(matches!(ack[3], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 1, + item_data: [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + item_id: 0x810002, + .. + }), + .. + })))); + assert!(matches!(ack[4], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 1, + item_data: [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + item_id: 0x810002, + .. + }), + .. + })))); + assert!(matches!(ack[5], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { + client: 0, + item_id: 0x10000, + .. + }), + .. + })))); + assert!(matches!(ack[6], (ClientId(2), SendShipPacket::TradeSuccessful {..}))); + assert!(matches!(ack[7], (ClientId(1), SendShipPacket::TradeSuccessful {..}))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + assert_eq!(p1_items.items[0].with_stacked(|i| i.clone()).unwrap().len(), 2); + assert!(matches!(p1_items.items[0].with_stacked(|i| i.clone()).unwrap()[0], item::ItemEntity{item: item::ItemDetail::Tool(item::tool::Tool {tool: item::tool::ToolType::Monomate, ..}), ..})); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 1); + assert!(matches!(p2_items.items[0].with_individual(|i| i.clone()).unwrap(), item::ItemEntity{item: item::ItemDetail::Weapon(item::weapon::Weapon {weapon: item::weapon::WeaponType::Saber, ..}), ..})); +} + +#[async_std::test] +async fn test_trade_multiple_individual() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let p1_inv = vec![ + 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, + } + ), + }).await.unwrap(), + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Buster, + grind: 0, + special: None, + attrs: [None, None, None], + tekked: true, + } + ), + }).await.unwrap(), + ]; + let p2_inv = vec![ + 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, + } + ), + }).await.unwrap(), + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Autogun, + grind: 0, + special: None, + attrs: [None, None, None], + tekked: true, + } + ), + }).await.unwrap(), + ]; + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(p2_inv)).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 2); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 2); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 1) + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10001, 1) + })))).await.unwrap().for_each(drop); + + ship.handle(ClientId(2), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::TradeRequest(TradeRequest { + client: 0, + target: 0, + trade: TradeRequestCommand::AddItem(0x210000, 1) + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(2), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::TradeRequest(TradeRequest { + client: 0, + target: 0, + trade: TradeRequestCommand::AddItem(0x210001, 1) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .individual(&p1_items.items[0], 0x10000) + .individual(&p1_items.items[1], 0x10001) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 2, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let titems = TradeItemBuilder::default() + .individual(&p2_items.items[0], 0x210000) + .individual(&p2_items.items[1], 0x210001) + .build(); + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 2, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 14); + assert!(matches!(ack[0], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 0, + item_data: [0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // handgun + item_id: 0x810001, + .. + }), + .. + })))); + assert!(matches!(ack[1], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 0, + item_data: [0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // handgun + item_id: 0x810001, + .. + }), + .. + })))); + assert!(matches!(ack[2], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { + client: 1, + item_id: 0x210000, + .. + }), + .. + })))); + assert!(matches!(ack[3], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 0, + item_data: [0, 6, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], // handgun + item_id: 0x810002, + .. + }), + .. + })))); + assert!(matches!(ack[4], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 0, + item_data: [0, 6, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], // handgun + item_id: 0x810002, + .. + }), + .. + })))); + assert!(matches!(ack[5], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { + client: 1, + item_id: 0x210001, + .. + }), + .. + })))); + assert!(matches!(ack[6], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 1, + item_data: [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // saber + item_id: 0x810003, + .. + }), + .. + })))); + assert!(matches!(ack[7], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 1, + item_data: [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // saber + item_id: 0x810003, + .. + }), + .. + })))); + assert!(matches!(ack[8], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { + client: 0, + item_id: 0x10000, + .. + }), + .. + })))); + assert!(matches!(ack[9], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 1, + item_data: [0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0], // saber + item_id: 0x810004, + .. + }), + .. + })))); + assert!(matches!(ack[10], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 1, + item_data: [0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0], // saber + item_id: 0x810004, + .. + }), + .. + })))); + assert!(matches!(ack[11], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { + client: 0, + item_id: 0x10001, + .. + }), + .. + })))); + assert!(matches!(ack[12], (ClientId(2), SendShipPacket::TradeSuccessful {..}))); + assert!(matches!(ack[13], (ClientId(1), SendShipPacket::TradeSuccessful {..}))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 2); + assert!(matches!(p1_items.items[0].with_individual(|i| i.clone()).unwrap(), item::ItemEntity{item: item::ItemDetail::Weapon(item::weapon::Weapon {weapon: item::weapon::WeaponType::Handgun, ..}), ..})); + assert!(matches!(p1_items.items[1].with_individual(|i| i.clone()).unwrap(), item::ItemEntity{item: item::ItemDetail::Weapon(item::weapon::Weapon {weapon: item::weapon::WeaponType::Autogun, ..}), ..})); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 2); + assert!(matches!(p2_items.items[0].with_individual(|i| i.clone()).unwrap(), item::ItemEntity{item: item::ItemDetail::Weapon(item::weapon::Weapon {weapon: item::weapon::WeaponType::Saber, ..}), ..})); + assert!(matches!(p2_items.items[1].with_individual(|i| i.clone()).unwrap(), item::ItemEntity{item: item::ItemDetail::Weapon(item::weapon::Weapon {weapon: item::weapon::WeaponType::Buster, ..}), ..})); +} + + +#[async_std::test] +async fn test_trade_multiple_stacked() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let p1_stack1 = futures::future::join_all((0..2).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + let p1_stack2 = futures::future::join_all((0..2).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Dimate, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + let p2_stack1 = futures::future::join_all((0..3).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monofluid, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + let p2_stack2 = futures::future::join_all((0..3).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Difluid, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![p1_stack1, p1_stack2])).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(vec![p2_stack1, p2_stack2])).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 2); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 2); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 2) + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10001, 2) + })))).await.unwrap().for_each(drop); + + ship.handle(ClientId(2), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::TradeRequest(TradeRequest { + client: 0, + target: 0, + trade: TradeRequestCommand::AddItem(0x210000, 3) + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(2), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::TradeRequest(TradeRequest { + client: 0, + target: 0, + trade: TradeRequestCommand::AddItem(0x210001, 3) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + + let titems = TradeItemBuilder::default() + .stacked(&p1_items.items[0], 0x10000, 2) + .stacked(&p1_items.items[1], 0x10001, 2) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 2, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let titems = TradeItemBuilder::default() + .stacked(&p2_items.items[0], 0x210000, 3) + .stacked(&p2_items.items[1], 0x210001, 3) + .build(); + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 2, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 14); + assert!(matches!(ack[0], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 0, + item_id: 0x810001, + .. + }), + .. + })))); + assert!(matches!(ack[1], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 0, + item_id: 0x810001, + .. + }), + .. + })))); + assert!(matches!(ack[2], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { + client: 1, + item_id: 0x210000, + .. + }), + .. + })))); + assert!(matches!(ack[3], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 0, + item_id: 0x810002, + .. + }), + .. + })))); + assert!(matches!(ack[4], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 0, + item_id: 0x810002, + .. + }), + .. + })))); + assert!(matches!(ack[5], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { + client: 1, + item_id: 0x210001, + .. + }), + .. + })))); + assert!(matches!(ack[6], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 1, + item_id: 0x810003, + .. + }), + .. + })))); + assert!(matches!(ack[7], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 1, + item_id: 0x810003, + .. + }), + .. + })))); + assert!(matches!(ack[8], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { + client: 0, + item_id: 0x10000, + .. + }), + .. + })))); + assert!(matches!(ack[9], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 1, + item_id: 0x810004, + .. + }), + .. + })))); + assert!(matches!(ack[10], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem { + client: 1, + item_id: 0x810004, + .. + }), + .. + })))); + assert!(matches!(ack[11], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem { + client: 0, + item_id: 0x10001, + .. + }), + .. + })))); + assert!(matches!(ack[12], (ClientId(2), SendShipPacket::TradeSuccessful {..}))); + assert!(matches!(ack[13], (ClientId(1), SendShipPacket::TradeSuccessful {..}))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 2); + assert_eq!(p1_items.items[0].with_stacked(|i| i.clone()).unwrap().len(), 3); + assert!(matches!(p1_items.items[0].with_stacked(|i| i.clone()).unwrap()[0], item::ItemEntity{item: item::ItemDetail::Tool(item::tool::Tool {tool: item::tool::ToolType::Monofluid, ..}), ..})); + assert_eq!(p1_items.items[1].with_stacked(|i| i.clone()).unwrap().len(), 3); + assert!(matches!(p1_items.items[1].with_stacked(|i| i.clone()).unwrap()[0], item::ItemEntity{item: item::ItemDetail::Tool(item::tool::Tool {tool: item::tool::ToolType::Difluid, ..}), ..})); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 2); + assert_eq!(p2_items.items[0].with_stacked(|i| i.clone()).unwrap().len(), 2); + assert!(matches!(p2_items.items[0].with_stacked(|i| i.clone()).unwrap()[0], item::ItemEntity{item: item::ItemDetail::Tool(item::tool::Tool {tool: item::tool::ToolType::Monomate, ..}), ..})); + assert_eq!(p2_items.items[1].with_stacked(|i| i.clone()).unwrap().len(), 2); + assert!(matches!(p2_items.items[1].with_stacked(|i| i.clone()).unwrap()[0], item::ItemEntity{item: item::ItemDetail::Tool(item::tool::Tool {tool: item::tool::ToolType::Dimate, ..}), ..})); +} + +#[async_std::test] +async fn test_trade_not_enough_inventory_space_individual() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let p1_inv = futures::future::join_all((0..2).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + 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, + } + ), + } + ).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + let p2_inv = futures::future::join_all((0..30).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + 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, + } + ), + } + ).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(p2_inv)).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 2); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 30); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 1) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .individual(&p1_items.items[0], 0x10000) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 0, + items: Default::default(), + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.err().unwrap(); + match ack.downcast::>().unwrap() { + TransactionError::Action(a) => { + assert_eq!(a.downcast::().unwrap(), TradeError::NoInventorySpace); + }, + _ => panic!() + } + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 2); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 30); +} + +#[async_std::test] +async fn test_trade_not_enough_inventory_space_stacked() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let p1_stack = futures::future::join_all((0..2).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + let p2_inv = futures::future::join_all((0..30).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + 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, + } + ), + } + ).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![p1_stack])).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(p2_inv)).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 30); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 2) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .stacked(&p1_items.items[0], 0x10000, 2) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 0, + items: Default::default(), + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.err().unwrap(); + match ack.downcast::>().unwrap() { + TransactionError::Action(a) => { + assert_eq!(a.downcast::().unwrap(), TradeError::NoInventorySpace); + }, + _ => panic!() + } + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 30); +} + +#[async_std::test] +async fn test_trade_stack_too_big() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let p1_stack = futures::future::join_all((0..8).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + let p2_stack = futures::future::join_all((0..7).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![p1_stack])).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(vec![p2_stack])).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + assert_eq!(p1_items.items[0].with_stacked(|i| i.len()).unwrap(), 8); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 1); + assert_eq!(p2_items.items[0].with_stacked(|i| i.len()).unwrap(), 7); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 4) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .stacked(&p1_items.items[0], 0x10000, 4) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 0, + items: Default::default(), + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.err().unwrap(); + match ack.downcast::>().unwrap() { + TransactionError::Action(a) => { + assert_eq!(a.downcast::().unwrap(), TradeError::NoStackSpace); + }, + _ => panic!() + } + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + assert_eq!(p1_items.items[0].with_stacked(|i| i.len()).unwrap(), 8); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 1); + assert_eq!(p2_items.items[0].with_stacked(|i| i.len()).unwrap(), 7); +} + +#[async_std::test] +async fn test_trade_meseta() { + let mut entity_gateway = InMemoryGateway::default(); + + 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.set_character_meseta(&char1.id, Meseta(2323)).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0xFFFFFF01, 23) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .meseta(23) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 0, + items: Default::default(), + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 5); + assert!(matches!(ack[0], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem {..}), + .. + })))); + assert!(matches!(ack[1], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem {..}), + .. + })))); + assert!(matches!(ack[2], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem {..}), + .. + })))); + assert!(matches!(ack[3], (ClientId(2), SendShipPacket::TradeSuccessful {..}))); + assert!(matches!(ack[4], (ClientId(1), SendShipPacket::TradeSuccessful {..}))); + + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta, Meseta(2300)); + let c2_meseta = entity_gateway.get_character_meseta(&char2.id).await.unwrap(); + assert_eq!(c2_meseta, Meseta(23)); +} + +#[async_std::test] +async fn test_trade_too_much_meseta() { + let mut entity_gateway = InMemoryGateway::default(); + + 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.set_character_meseta(&char1.id, Meseta(4000)).await.unwrap(); + entity_gateway.set_character_meseta(&char2.id, Meseta(999000)).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0xFFFFFF01, 2000) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .meseta(2000) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::CancelTrade(..)))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::CancelTrade(..)))); + + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta, Meseta(4000)); + let c2_meseta = entity_gateway.get_character_meseta(&char2.id).await.unwrap(); + assert_eq!(c2_meseta, Meseta(999000)); +} + +#[async_std::test] +async fn test_trade_invalid_amount_of_meseta() { + let mut entity_gateway = InMemoryGateway::default(); + + 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.set_character_meseta(&char1.id, Meseta(4000)).await.unwrap(); + entity_gateway.set_character_meseta(&char2.id, Meseta(999000)).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0xFFFFFF01, 5000) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .meseta(5000) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::CancelTrade(..)))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::CancelTrade(..)))); + + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta, Meseta(4000)); + let c2_meseta = entity_gateway.get_character_meseta(&char2.id).await.unwrap(); + assert_eq!(c2_meseta, Meseta(999000)); +} + +#[async_std::test] +async fn test_trade_meseta_request_and_items_dont_match() { + let mut entity_gateway = InMemoryGateway::default(); + + 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.set_character_meseta(&char1.id, Meseta(4000)).await.unwrap(); + entity_gateway.set_character_meseta(&char2.id, Meseta(999000)).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0xFFFFFF01, 50) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .meseta(23) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::CancelTrade(..)))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::CancelTrade(..)))); + + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta, Meseta(4000)); + let c2_meseta = entity_gateway.get_character_meseta(&char2.id).await.unwrap(); + assert_eq!(c2_meseta, Meseta(999000)); +} + +#[async_std::test] +async fn test_player_declined_trade() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, _char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, _char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let ack = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::Cancel + })))).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::CancelTrade(..)))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::CancelTrade(..)))); +} + +#[async_std::test] +async fn test_back_out_of_trade_last_minute() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let mut p1_inv = Vec::new(); + p1_inv.push(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, + } + ), + }).await.unwrap()); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 1) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let ack = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::Cancel + })))).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::CancelTrade(..)))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::CancelTrade(..)))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); +} + +#[async_std::test] +async fn test_valid_trade_when_both_inventories_are_full() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let p1_inv = futures::future::join_all((0..30).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + 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, + } + ), + } + ).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + let p2_inv = futures::future::join_all((0..30).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + 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, + } + ), + } + ).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(p2_inv)).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 30); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 30); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 1) + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10001, 1) + })))).await.unwrap().for_each(drop); + + ship.handle(ClientId(2), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::TradeRequest(TradeRequest { + client: 0, + target: 0, + trade: TradeRequestCommand::AddItem(0x210000, 1) + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(2), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::TradeRequest(TradeRequest { + client: 0, + target: 0, + trade: TradeRequestCommand::AddItem(0x210001, 1) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .individual(&p1_items.items[0], 0x10000) + .individual(&p1_items.items[1], 0x10001) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 2, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let titems = TradeItemBuilder::default() + .individual(&p2_items.items[0], 0x210000) + .individual(&p2_items.items[1], 0x210001) + .build(); + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 2, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 14); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 30); + assert_eq!(p1_items.items.iter().filter(|i| matches!(i.individual().unwrap().item, item::ItemDetail::Weapon(item::weapon::Weapon { weapon: item::weapon::WeaponType::Saber, ..}, ..))).count(), 28); + assert_eq!(p1_items.items.iter().filter(|i| matches!(i.individual().unwrap().item, item::ItemDetail::Weapon(item::weapon::Weapon { weapon: item::weapon::WeaponType::Handgun, ..}, ..))).count(), 2); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 30); + assert_eq!(p2_items.items.iter().filter(|i| matches!(i.individual().unwrap().item, item::ItemDetail::Weapon(item::weapon::Weapon { weapon: item::weapon::WeaponType::Saber, ..}, ..))).count(), 2); + assert_eq!(p2_items.items.iter().filter(|i| matches!(i.individual().unwrap().item, item::ItemDetail::Weapon(item::weapon::Weapon { weapon: item::weapon::WeaponType::Handgun, ..}, ..))).count(), 28); +} + +#[async_std::test] +async fn test_invalid_trade_when_both_inventories_are_full() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let p1_inv = futures::future::join_all((0..30).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + 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, + } + ), + } + ).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + let p2_inv = futures::future::join_all((0..30).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + 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, + } + ), + } + ).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(p2_inv)).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 30); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 30); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 1) + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10001, 1) + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10002, 1) + })))).await.unwrap().for_each(drop); + + ship.handle(ClientId(2), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::TradeRequest(TradeRequest { + client: 0, + target: 0, + trade: TradeRequestCommand::AddItem(0x210000, 1) + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(2), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::TradeRequest(TradeRequest { + client: 0, + target: 0, + trade: TradeRequestCommand::AddItem(0x210001, 1) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .individual(&p1_items.items[0], 0x10000) + .individual(&p1_items.items[1], 0x10001) + .individual(&p1_items.items[1], 0x10002) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 3, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let titems = TradeItemBuilder::default() + .individual(&p2_items.items[0], 0x210000) + .individual(&p2_items.items[1], 0x210001) + .build(); + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 2, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.err().unwrap(); + match ack.downcast::>().unwrap() { + TransactionError::Action(a) => { + assert_eq!(a.downcast::().unwrap(), TradeError::NoInventorySpace); + }, + _ => panic!() + } + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 30); + assert_eq!(p1_items.items.iter().filter(|i| matches!(i.individual().unwrap().item, item::ItemDetail::Weapon(item::weapon::Weapon { weapon: item::weapon::WeaponType::Saber, ..}, ..))).count(), 30); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 30); + assert_eq!(p2_items.items.iter().filter(|i| matches!(i.individual().unwrap().item, item::ItemDetail::Weapon(item::weapon::Weapon { weapon: item::weapon::WeaponType::Handgun, ..}, ..))).count(), 30); +} + +#[async_std::test] +async fn test_client_tries_to_start_two_trades() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, _char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, _char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + let (_user2, _char3) = new_user_character(&mut entity_gateway, "a3", "a").await; + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + log_in_char(&mut ship, ClientId(2), "a2", "a").await; + log_in_char(&mut ship, ClientId(3), "a3", "a").await; + + join_lobby(&mut ship, ClientId(1)).await; + join_lobby(&mut ship, ClientId(2)).await; + join_lobby(&mut ship, ClientId(3)).await; + + create_room(&mut ship, ClientId(1), "room", "").await; + join_room(&mut ship, ClientId(2), 0).await; + join_room(&mut ship, ClientId(3), 0).await; + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + let ack = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 0, + target: 0, + trade: TradeRequestCommand::Initialize(TradeRequestInitializeCommand::Initialize, 0) + })))).await.err().unwrap(); + assert_eq!(ack.downcast::().unwrap(), TradeError::ClientAlreadyInTrade); +} + +#[async_std::test] +async fn test_client_tries_trading_with_client_already_trading() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, _char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, _char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + let (_user2, _char3) = new_user_character(&mut entity_gateway, "a3", "a").await; + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + log_in_char(&mut ship, ClientId(2), "a2", "a").await; + log_in_char(&mut ship, ClientId(3), "a3", "a").await; + + join_lobby(&mut ship, ClientId(1)).await; + join_lobby(&mut ship, ClientId(2)).await; + join_lobby(&mut ship, ClientId(3)).await; + + create_room(&mut ship, ClientId(1), "room", "").await; + join_room(&mut ship, ClientId(2), 0).await; + join_room(&mut ship, ClientId(3), 0).await; + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + let ack = ship.handle(ClientId(3), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::TradeRequest(TradeRequest { + client: 2, + target: 0, + trade: TradeRequestCommand::Initialize(TradeRequestInitializeCommand::Initialize, 0) + })))).await.err().unwrap(); + assert_eq!(ack.downcast::().unwrap(), TradeError::OtherAlreadyInTrade); + + let ack = ship.handle(ClientId(3), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 2, + target: 0, + trade: TradeRequestCommand::Initialize(TradeRequestInitializeCommand::Initialize, 1) + })))).await.err().unwrap(); + assert_eq!(ack.downcast::().unwrap(), TradeError::OtherAlreadyInTrade); +} + +#[async_std::test] +async fn test_add_then_remove_individual_item() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let mut p1_inv = Vec::new(); + for _ in 0..2 { + p1_inv.push(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, + } + ), + }).await.unwrap()); + } + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 2); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 1) + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10001, 1) + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::RemoveItem(0x10000, 1) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .individual(&p1_items.items[1], 0x10001) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 0, + items: Default::default(), + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 5); + assert!(matches!(ack[0], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem {..}), + .. + })))); + assert!(matches!(ack[1], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem {..}), + .. + })))); + assert!(matches!(ack[2], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem {..}), + .. + })))); + assert!(matches!(ack[3], (ClientId(2), SendShipPacket::TradeSuccessful {..}))); + assert!(matches!(ack[4], (ClientId(1), SendShipPacket::TradeSuccessful {..}))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 1); +} + +#[async_std::test] +async fn test_add_then_remove_stacked_item() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let p1_stack1 = futures::future::join_all((0..2).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + let p1_stack2 = futures::future::join_all((0..2).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monofluid, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![p1_stack1, p1_stack2])).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 2); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 2) + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10001, 2) + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::RemoveItem(0x10000, 2) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .stacked(&p1_items.items[1], 0x10001, 2) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 0, + items: Default::default(), + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 5); + assert!(matches!(ack[0], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem {..}), + .. + })))); + assert!(matches!(ack[1], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem {..}), + .. + })))); + assert!(matches!(ack[2], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem {..}), + .. + })))); + assert!(matches!(ack[3], (ClientId(2), SendShipPacket::TradeSuccessful {..}))); + assert!(matches!(ack[4], (ClientId(1), SendShipPacket::TradeSuccessful {..}))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + assert!(matches!(p1_items.items[0].with_stacked(|i| i.clone()).unwrap()[0], item::ItemEntity{item: item::ItemDetail::Tool(item::tool::Tool {tool: item::tool::ToolType::Monomate, ..}), ..})); + assert_eq!(p1_items.items[0].with_stacked(|i| i.clone()).unwrap().len(), 2); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 1); + assert_eq!(p2_items.items[0].with_stacked(|i| i.clone()).unwrap().len(), 2); + assert!(matches!(p2_items.items[0].with_stacked(|i| i.clone()).unwrap()[0], item::ItemEntity{item: item::ItemDetail::Tool(item::tool::Tool {tool: item::tool::ToolType::Monofluid, ..}), ..})); +} + +#[async_std::test] +async fn test_add_then_remove_partial_stack() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let p1_stack1 = futures::future::join_all((0..2).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + let p1_stack2 = futures::future::join_all((0..2).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monofluid, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![p1_stack1, p1_stack2])).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 2); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 2) + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10001, 2) + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::RemoveItem(0x10000, 1) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .stacked(&p1_items.items[0], 0x10000, 1) + .stacked(&p1_items.items[1], 0x10001, 2) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 2, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 0, + items: Default::default(), + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 8); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + assert_eq!(p1_items.items[0].with_stacked(|i| i.clone()).unwrap().len(), 1); + assert!(matches!(p1_items.items[0].with_stacked(|i| i.clone()).unwrap()[0], item::ItemEntity{item: item::ItemDetail::Tool(item::tool::Tool {tool: item::tool::ToolType::Monomate, ..}), ..})); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 2); + assert_eq!(p2_items.items[0].with_stacked(|i| i.clone()).unwrap().len(), 1); + assert_eq!(p2_items.items[1].with_stacked(|i| i.clone()).unwrap().len(), 2); + assert!(matches!(p2_items.items[0].with_stacked(|i| i.clone()).unwrap()[0], item::ItemEntity{item: item::ItemDetail::Tool(item::tool::Tool {tool: item::tool::ToolType::Monomate, ..}), ..})); + assert!(matches!(p2_items.items[1].with_stacked(|i| i.clone()).unwrap()[0], item::ItemEntity{item: item::ItemDetail::Tool(item::tool::Tool {tool: item::tool::ToolType::Monofluid, ..}), ..})); +} + +#[async_std::test] +async fn test_add_then_remove_meseta() { + let mut entity_gateway = InMemoryGateway::default(); + + 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.set_character_meseta(&char1.id, Meseta(2323)).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0xFFFFFF01, 23) + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::RemoveItem(0xFFFFFF01, 5) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .meseta(18) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 0, + items: Default::default(), + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 5); + assert!(matches!(ack[0], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem {..}), + .. + })))); + assert!(matches!(ack[1], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem {..}), + .. + })))); + assert!(matches!(ack[2], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem {..}), + .. + })))); + assert!(matches!(ack[3], (ClientId(2), SendShipPacket::TradeSuccessful {..}))); + assert!(matches!(ack[4], (ClientId(1), SendShipPacket::TradeSuccessful {..}))); + + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta, Meseta(2305)); + let c2_meseta = entity_gateway.get_character_meseta(&char2.id).await.unwrap(); + assert_eq!(c2_meseta, Meseta(18)); +} + +#[async_std::test] +async fn test_items_to_trade_data_does_not_match() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let mut p1_inv = Vec::new(); + p1_inv.push(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, + } + ), + }).await.unwrap()); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 1) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let new_item = item::InventoryItemEntity::Individual( + ItemEntity { + id: p1_items.items[0].with_individual(|i| i.id).unwrap(), + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Handgun, + grind: 2, + special: None, + attrs: [None, None, None], + tekked: true, + } + )}); + let titems = TradeItemBuilder::default() + .individual(&new_item, 0x10000) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + dbg!(&ack); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::CancelTrade(..)))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::CancelTrade(..)))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); +} + +#[async_std::test] +async fn test_items_to_trade_id_does_not_match() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let mut p1_inv = Vec::new(); + p1_inv.push(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, + } + ), + }).await.unwrap()); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 1) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .individual(&p1_items.items[0], 0x10001) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::CancelTrade(..)))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::CancelTrade(..)))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); +} + +#[async_std::test] +async fn test_stack_is_same_amount_in_request_and_items_to_trade() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let p1_stack = futures::future::join_all((0..2).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![p1_stack])).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 2) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .stacked(&p1_items.items[0], 0x10000, 1) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::CancelTrade(..)))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::CancelTrade(..)))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); +} + +#[async_std::test] +async fn test_stack_is_same_amount_in_request_and_items_to_trade2() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let p1_stack = futures::future::join_all((0..2).map(|_| { + let mut entity_gateway = entity_gateway.clone(); + async move { + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate, + } + ) + }).await + }})) + .await + .into_iter() + .collect::,_>>() + .unwrap(); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![p1_stack])).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 1) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .stacked(&p1_items.items[0], 0x10000, 2) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::CancelTrade(..)))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::CancelTrade(..)))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); +} + +#[async_std::test] +async fn test_items_to_trade_count_less_than() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let p1_inv = vec![ + 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, + } + ), + }).await.unwrap(), + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Brand, + grind: 0, + special: None, + attrs: [None, None, None], + tekked: true, + } + ), + }).await.unwrap(), + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Buster, + grind: 0, + special: None, + attrs: [None, None, None], + tekked: true, + } + ), + }).await.unwrap(), + ]; + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 3); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 1) + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10001, 1) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .individual(&p1_items.items[0], 0x10000) + .individual(&p1_items.items[1], 0x10001) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::CancelTrade(..)))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::CancelTrade(..)))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 3); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); +} + +#[async_std::test] +async fn test_items_to_trade_count_greater_than() { + let mut entity_gateway = InMemoryGateway::default(); + + 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.set_character_meseta(&char1.id, Meseta(23)).await.unwrap(); + + let p1_inv = vec![ + 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, + } + ), + }).await.unwrap(), + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Brand, + grind: 0, + special: None, + attrs: [None, None, None], + tekked: true, + } + ), + }).await.unwrap(), + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Buster, + grind: 0, + special: None, + attrs: [None, None, None], + tekked: true, + } + ), + }).await.unwrap(), + ]; + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 3); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 1) + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10001, 1) + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0xFFFFFF01, 5) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .individual(&p1_items.items[0], 0x10000) + .individual(&p1_items.items[1], 0x10001) + .meseta(5) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 4, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::CancelTrade(..)))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::CancelTrade(..)))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 3); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); +} + +#[async_std::test] +async fn test_items_to_trade_count_mismatch_with_meseta() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let p1_inv = vec![ + 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, + } + ), + }).await.unwrap(), + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Brand, + grind: 0, + special: None, + attrs: [None, None, None], + tekked: true, + } + ), + }).await.unwrap(), + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Weapon( + item::weapon::Weapon { + weapon: item::weapon::WeaponType::Buster, + grind: 0, + special: None, + attrs: [None, None, None], + tekked: true, + } + ), + }).await.unwrap(), + ]; + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 3); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 1) + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10001, 1) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .individual(&p1_items.items[0], 0x10000) + .individual(&p1_items.items[1], 0x10001) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 3, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::CancelTrade(..)))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::CancelTrade(..)))); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 3); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); +} From 22f630f5629ec5a058fabee3021cd06f37f6dd3e Mon Sep 17 00:00:00 2001 From: jake Date: Tue, 28 Dec 2021 01:08:44 -0700 Subject: [PATCH 40/41] properly set item_id after a trade serverside --- src/ship/items/inventory.rs | 13 ++ src/ship/items/manager.rs | 4 +- tests/test_trade.rs | 249 ++++++++++++++++++++++++++---------- 3 files changed, 195 insertions(+), 71 deletions(-) diff --git a/src/ship/items/inventory.rs b/src/ship/items/inventory.rs index 5bb5c19..4aa5467 100644 --- a/src/ship/items/inventory.rs +++ b/src/ship/items/inventory.rs @@ -654,6 +654,19 @@ impl CharacterInventory { } } + pub fn add_item_with_new_item_id(&mut self, item: InventoryItem, item_id: ClientItemId) { + match item { + InventoryItem::Individual(mut individual_inventory_item) => { + individual_inventory_item.item_id = item_id; + self.add_item(InventoryItem::Individual(individual_inventory_item)); + }, + InventoryItem::Stacked(mut stacked_inventory_item) => { + stacked_inventory_item.item_id = item_id; + self.add_stacked_item(stacked_inventory_item) + } + } + } + pub fn add_individual_floor_item(&mut self, floor_item: &IndividualFloorItem) -> &InventoryItem { self.items.push(InventoryItem::Individual(IndividualInventoryItem { entity_id: floor_item.entity_id, diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs index 3031511..ba6b562 100644 --- a/src/ship/items/manager.rs +++ b/src/ship/items/manager.rs @@ -1277,7 +1277,7 @@ impl ItemAction for TradeIndividualItem { entity_gateway.set_character_inventory(&self.src_character_id, &src_inventory.as_inventory_entity(&self.src_character_id)).await?; let dest_inventory = item_manager.character_inventory.get_mut(&self.dest_character_id).ok_or(ItemManagerError::NoCharacter(self.dest_character_id))?; - dest_inventory.add_item(inventory_item); + dest_inventory.add_item_with_new_item_id(inventory_item, self.new_item_id); entity_gateway.set_character_inventory(&self.dest_character_id, &dest_inventory.as_inventory_entity(&self.dest_character_id)).await?; Ok(()) @@ -1301,7 +1301,7 @@ impl ItemAction for TradeStackedItem { entity_gateway.set_character_inventory(&self.src_character_id, &src_inventory.as_inventory_entity(&self.src_character_id)).await?; let dest_inventory = item_manager.character_inventory.get_mut(&self.dest_character_id).ok_or(ItemManagerError::NoCharacter(self.dest_character_id))?; - dest_inventory.add_stacked_item(inventory_item); + dest_inventory.add_item_with_new_item_id(InventoryItem::Stacked(inventory_item), self.new_item_id); entity_gateway.set_character_inventory(&self.dest_character_id, &dest_inventory.as_inventory_entity(&self.dest_character_id)).await?; Ok(()) diff --git a/tests/test_trade.rs b/tests/test_trade.rs index 25d4a52..2a1d5d8 100644 --- a/tests/test_trade.rs +++ b/tests/test_trade.rs @@ -152,13 +152,13 @@ async fn test_trade_one_individual_item() { assert_eq!(p2_items.items.len(), 0); initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; - + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { client: 1, target: 0, trade: TradeRequestCommand::AddItem(0x10000, 1) })))).await.unwrap().for_each(drop); - + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; @@ -186,7 +186,7 @@ async fn test_trade_one_individual_item() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 5); @@ -253,16 +253,16 @@ async fn test_trade_player2_to_player1() { assert_eq!(p2_items.items.len(), 1); initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; - + ship.handle(ClientId(2), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { client: 1, target: 0, trade: TradeRequestCommand::AddItem(0x210000, 1) })))).await.unwrap().for_each(drop); - + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; - + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { trade_target: 1, unknown2: 0, @@ -287,7 +287,7 @@ async fn test_trade_player2_to_player1() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 5); @@ -363,7 +363,7 @@ async fn test_reverse_trade_ack_order() { confirm_trade(&mut ship, ClientId(2), ClientId(1)).await; finalconfirm_trade(&mut ship, ClientId(2), ClientId(1)).await; - + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { trade_target: 0, unknown2: 0, @@ -388,7 +388,7 @@ async fn test_reverse_trade_ack_order() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 5); @@ -436,7 +436,7 @@ async fn test_trade_one_stacked_item() { .into_iter() .collect::,_>>() .unwrap(); - + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![p1_stack])).await.unwrap(); entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); @@ -492,7 +492,7 @@ async fn test_trade_one_stacked_item() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 5); @@ -540,7 +540,7 @@ async fn test_trade_partial_stacked_item() { .into_iter() .collect::,_>>() .unwrap(); - + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![p1_stack])).await.unwrap(); entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); @@ -597,7 +597,7 @@ async fn test_trade_partial_stacked_item() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 5); @@ -679,7 +679,7 @@ async fn test_trade_individual_both() { assert_eq!(p2_items.items.len(), 1); initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; - + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { client: 1, target: 0, @@ -691,7 +691,7 @@ async fn test_trade_individual_both() { target: 0, trade: TradeRequestCommand::AddItem(0x210000, 1) })))).await.unwrap().for_each(drop); - + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; @@ -722,7 +722,7 @@ async fn test_trade_individual_both() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 8); @@ -894,7 +894,7 @@ async fn test_trade_stacked_both() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 8); @@ -1064,7 +1064,7 @@ async fn test_trade_partial_stack_both() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 8); @@ -1134,7 +1134,7 @@ async fn test_trade_partial_stack_both() { assert!(matches!(p2_items.items[0].with_stacked(|i| i.clone()).unwrap()[0], item::ItemEntity{item: item::ItemDetail::Tool(item::tool::Tool {tool: item::tool::ToolType::Monofluid, ..}), ..})); assert!(matches!(p2_items.items[1].with_stacked(|i| i.clone()).unwrap()[0], item::ItemEntity{item: item::ItemDetail::Tool(item::tool::Tool {tool: item::tool::ToolType::Monomate, ..}), ..})); } - + #[async_std::test] async fn test_trade_same_stacked_item_to_eachother() { let mut entity_gateway = InMemoryGateway::default(); @@ -1240,7 +1240,7 @@ async fn test_trade_same_stacked_item_to_eachother() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 8); @@ -1403,7 +1403,7 @@ async fn test_trade_stacked_when_already_have_partial_stack() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 5); @@ -1549,7 +1549,7 @@ async fn test_trade_individual_for_stacked() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 8); @@ -1753,7 +1753,7 @@ async fn test_trade_multiple_individual() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 14); @@ -2025,7 +2025,7 @@ async fn test_trade_multiple_stacked() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 14); @@ -2214,13 +2214,13 @@ async fn test_trade_not_enough_inventory_space_individual() { assert_eq!(p2_items.items.len(), 30); initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; - + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { client: 1, target: 0, trade: TradeRequestCommand::AddItem(0x10000, 1) })))).await.unwrap().for_each(drop); - + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; @@ -2248,7 +2248,7 @@ async fn test_trade_not_enough_inventory_space_individual() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.err().unwrap(); match ack.downcast::>().unwrap() { @@ -2331,13 +2331,13 @@ async fn test_trade_not_enough_inventory_space_stacked() { assert_eq!(p2_items.items.len(), 30); initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; - + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { client: 1, target: 0, trade: TradeRequestCommand::AddItem(0x10000, 2) })))).await.unwrap().for_each(drop); - + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; @@ -2365,7 +2365,7 @@ async fn test_trade_not_enough_inventory_space_stacked() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.err().unwrap(); match ack.downcast::>().unwrap() { @@ -2445,13 +2445,13 @@ async fn test_trade_stack_too_big() { assert_eq!(p2_items.items[0].with_stacked(|i| i.len()).unwrap(), 7); initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; - + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { client: 1, target: 0, trade: TradeRequestCommand::AddItem(0x10000, 4) })))).await.unwrap().for_each(drop); - + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; @@ -2479,7 +2479,7 @@ async fn test_trade_stack_too_big() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.err().unwrap(); match ack.downcast::>().unwrap() { @@ -2519,13 +2519,13 @@ async fn test_trade_meseta() { join_room(&mut ship, ClientId(2), 0).await; initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; - + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { client: 1, target: 0, trade: TradeRequestCommand::AddItem(0xFFFFFF01, 23) })))).await.unwrap().for_each(drop); - + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; @@ -2553,7 +2553,7 @@ async fn test_trade_meseta() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 5); @@ -2601,13 +2601,13 @@ async fn test_trade_too_much_meseta() { join_room(&mut ship, ClientId(2), 0).await; initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; - + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { client: 1, target: 0, trade: TradeRequestCommand::AddItem(0xFFFFFF01, 2000) })))).await.unwrap().for_each(drop); - + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; @@ -2653,13 +2653,13 @@ async fn test_trade_invalid_amount_of_meseta() { join_room(&mut ship, ClientId(2), 0).await; initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; - + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { client: 1, target: 0, trade: TradeRequestCommand::AddItem(0xFFFFFF01, 5000) })))).await.unwrap().for_each(drop); - + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; @@ -2705,13 +2705,13 @@ async fn test_trade_meseta_request_and_items_dont_match() { join_room(&mut ship, ClientId(2), 0).await; initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; - + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { client: 1, target: 0, trade: TradeRequestCommand::AddItem(0xFFFFFF01, 50) })))).await.unwrap().for_each(drop); - + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; @@ -2754,7 +2754,7 @@ async fn test_player_declined_trade() { join_room(&mut ship, ClientId(2), 0).await; initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; - + let ack = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { client: 1, target: 0, @@ -2807,13 +2807,13 @@ async fn test_back_out_of_trade_last_minute() { assert_eq!(p2_items.items.len(), 0); initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; - + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { client: 1, target: 0, trade: TradeRequestCommand::AddItem(0x10000, 1) })))).await.unwrap().for_each(drop); - + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; let ack = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { @@ -2824,7 +2824,7 @@ async fn test_back_out_of_trade_last_minute() { assert_eq!(ack.len(), 2); assert!(matches!(ack[0], (ClientId(2), SendShipPacket::CancelTrade(..)))); assert!(matches!(ack[1], (ClientId(1), SendShipPacket::CancelTrade(..)))); - + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); assert_eq!(p1_items.items.len(), 1); let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); @@ -2903,7 +2903,7 @@ async fn test_valid_trade_when_both_inventories_are_full() { assert_eq!(p2_items.items.len(), 30); initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; - + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { client: 1, target: 0, @@ -2925,7 +2925,7 @@ async fn test_valid_trade_when_both_inventories_are_full() { target: 0, trade: TradeRequestCommand::AddItem(0x210001, 1) })))).await.unwrap().for_each(drop); - + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; @@ -2958,7 +2958,7 @@ async fn test_valid_trade_when_both_inventories_are_full() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 14); @@ -3045,7 +3045,7 @@ async fn test_invalid_trade_when_both_inventories_are_full() { assert_eq!(p2_items.items.len(), 30); initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; - + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { client: 1, target: 0, @@ -3072,7 +3072,7 @@ async fn test_invalid_trade_when_both_inventories_are_full() { target: 0, trade: TradeRequestCommand::AddItem(0x210001, 1) })))).await.unwrap().for_each(drop); - + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; @@ -3106,7 +3106,7 @@ async fn test_invalid_trade_when_both_inventories_are_full() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.err().unwrap(); match ack.downcast::>().unwrap() { @@ -3239,7 +3239,7 @@ async fn test_add_then_remove_individual_item() { assert_eq!(p2_items.items.len(), 0); initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; - + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { client: 1, target: 0, @@ -3255,7 +3255,7 @@ async fn test_add_then_remove_individual_item() { target: 0, trade: TradeRequestCommand::RemoveItem(0x10000, 1) })))).await.unwrap().for_each(drop); - + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; @@ -3283,7 +3283,7 @@ async fn test_add_then_remove_individual_item() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 5); @@ -3348,7 +3348,7 @@ async fn test_add_then_remove_stacked_item() { .into_iter() .collect::,_>>() .unwrap(); - + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![p1_stack1, p1_stack2])).await.unwrap(); entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); @@ -3414,7 +3414,7 @@ async fn test_add_then_remove_stacked_item() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 5); @@ -3483,7 +3483,7 @@ async fn test_add_then_remove_partial_stack() { .into_iter() .collect::,_>>() .unwrap(); - + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![p1_stack1, p1_stack2])).await.unwrap(); entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); @@ -3550,7 +3550,7 @@ async fn test_add_then_remove_partial_stack() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 8); @@ -3589,7 +3589,7 @@ async fn test_add_then_remove_meseta() { join_room(&mut ship, ClientId(2), 0).await; initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; - + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { client: 1, target: 0, @@ -3600,7 +3600,7 @@ async fn test_add_then_remove_meseta() { target: 0, trade: TradeRequestCommand::RemoveItem(0xFFFFFF01, 5) })))).await.unwrap().for_each(drop); - + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; @@ -3628,7 +3628,7 @@ async fn test_add_then_remove_meseta() { let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 0); - + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { })).await.unwrap().collect::>(); assert_eq!(ack.len(), 5); @@ -3695,13 +3695,13 @@ async fn test_items_to_trade_data_does_not_match() { assert_eq!(p2_items.items.len(), 0); initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; - + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { client: 1, target: 0, trade: TradeRequestCommand::AddItem(0x10000, 1) })))).await.unwrap().for_each(drop); - + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; @@ -3726,7 +3726,6 @@ async fn test_items_to_trade_data_does_not_match() { count: 1, items: titems, })).await.unwrap().collect::>(); - dbg!(&ack); assert_eq!(ack.len(), 2); assert!(matches!(ack[0], (ClientId(2), SendShipPacket::CancelTrade(..)))); assert!(matches!(ack[1], (ClientId(1), SendShipPacket::CancelTrade(..)))); @@ -3779,13 +3778,13 @@ async fn test_items_to_trade_id_does_not_match() { assert_eq!(p2_items.items.len(), 0); initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; - + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { client: 1, target: 0, trade: TradeRequestCommand::AddItem(0x10000, 1) })))).await.unwrap().for_each(drop); - + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; @@ -3831,7 +3830,7 @@ async fn test_stack_is_same_amount_in_request_and_items_to_trade() { .into_iter() .collect::,_>>() .unwrap(); - + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![p1_stack])).await.unwrap(); entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); @@ -3905,7 +3904,7 @@ async fn test_stack_is_same_amount_in_request_and_items_to_trade2() { .into_iter() .collect::,_>>() .unwrap(); - + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(vec![p1_stack])).await.unwrap(); entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); @@ -4269,3 +4268,115 @@ async fn test_items_to_trade_count_mismatch_with_meseta() { let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); assert_eq!(p2_items.items.len(), 0); } + +#[async_std::test] +async fn test_dropping_item_after_trade() { + let mut entity_gateway = InMemoryGateway::default(); + + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + let (_user2, char2) = new_user_character(&mut entity_gateway, "a2", "a").await; + + let mut p1_inv = Vec::new(); + p1_inv.push(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, + } + ), + }).await.unwrap()); + + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + entity_gateway.set_character_inventory(&char2.id, &item::InventoryEntity::new(Vec::::new())).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + 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; + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 1); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); + + initialize_trade(&mut ship, ClientId(1), ClientId(2)).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(1, GameMessage::TradeRequest(TradeRequest { + client: 1, + target: 0, + trade: TradeRequestCommand::AddItem(0x10000, 1) + })))).await.unwrap().for_each(drop); + + confirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + finalconfirm_trade(&mut ship, ClientId(1), ClientId(2)).await; + + let titems = TradeItemBuilder::default() + .individual(&p1_items.items[0], 0x10000) + .build(); + let ack = ship.handle(ClientId(1), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 1, + unknown2: 0, + count: 1, + items: titems, + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::ItemsToTrade(ItemsToTrade { + trade_target: 0, + unknown2: 0, + count: 0, + items: Default::default(), + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 2); + assert!(matches!(ack[0], (ClientId(2), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + assert!(matches!(ack[1], (ClientId(1), SendShipPacket::AcknowledgeTrade(AcknowledgeTrade {})))); + + let ack = ship.handle(ClientId(1), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 0); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::TradeConfirmed(TradeConfirmed { + })).await.unwrap().collect::>(); + assert_eq!(ack.len(), 5); + assert!(matches!(ack[0], (ClientId(1), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem {..}), + .. + })))); + assert!(matches!(ack[1], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::CreateItem(CreateItem {..}), + .. + })))); + assert!(matches!(ack[2], (ClientId(2), SendShipPacket::Message(Message { + msg: GameMessage::PlayerNoLongerHasItem(PlayerNoLongerHasItem {..}), + .. + })))); + assert!(matches!(ack[3], (ClientId(2), SendShipPacket::TradeSuccessful {..}))); + assert!(matches!(ack[4], (ClientId(1), SendShipPacket::TradeSuccessful {..}))); + + let ack = ship.handle(ClientId(2), &RecvShipPacket::Message(Message::new(GameMessage::PlayerDropItem(PlayerDropItem { + client: 0, + target: 0, + unknown1: 0, + map_area: 0, + item_id: 0x810001, + x: 0.0, + y: 0.0, + z: 0.0, + })))).await.unwrap().for_each(drop); + + let p1_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + assert_eq!(p1_items.items.len(), 0); + let p2_items = entity_gateway.get_character_inventory(&char2.id).await.unwrap(); + assert_eq!(p2_items.items.len(), 0); +} From a6e50555de1940a76859ac5b64b835ce830e198f Mon Sep 17 00:00:00 2001 From: jake Date: Tue, 28 Dec 2021 01:37:24 -0700 Subject: [PATCH 41/41] appease the clip --- src/entity/character.rs | 10 +-- src/entity/gateway/inmemory.rs | 4 +- src/entity/gateway/postgres/models.rs | 1 + src/entity/item/mod.rs | 2 +- src/login/character.rs | 2 +- src/ship/character.rs | 34 +--------- src/ship/items/bank.rs | 2 +- src/ship/items/floor.rs | 8 +-- src/ship/items/inventory.rs | 2 +- src/ship/items/manager.rs | 67 ++++++++++---------- src/ship/items/transaction.rs | 9 --- src/ship/map/area.rs | 9 +-- src/ship/map/enemy.rs | 46 +++++++------- src/ship/map/object.rs | 60 +++++++++--------- src/ship/packet/handler/trade.rs | 89 +++++++++++---------------- src/ship/ship.rs | 4 +- src/ship/shops/weapon.rs | 10 +-- src/ship/trade.rs | 51 +-------------- 18 files changed, 142 insertions(+), 268 deletions(-) diff --git a/src/entity/character.rs b/src/entity/character.rs index 76346a9..432017d 100644 --- a/src/entity/character.rs +++ b/src/entity/character.rs @@ -160,19 +160,11 @@ pub struct CharacterAppearance { #[derive(Clone, Debug)] pub struct TechLevel(pub u8); -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct CharacterTechniques { pub techs: HashMap } -impl Default for CharacterTechniques { - fn default() -> CharacterTechniques { - CharacterTechniques { - techs: HashMap::new(), - } - } -} - impl CharacterTechniques { pub fn set_tech(&mut self, tech: Technique, level: TechLevel) { self.techs.insert(tech, TechLevel(level.0 - 1)); diff --git a/src/entity/gateway/inmemory.rs b/src/entity/gateway/inmemory.rs index 57ad039..3d37761 100644 --- a/src/entity/gateway/inmemory.rs +++ b/src/entity/gateway/inmemory.rs @@ -231,7 +231,7 @@ impl EntityGateway for InMemoryGateway { Ok(new_item) } - async fn add_item_note(&mut self, item_id: &ItemEntityId, item_note: ItemNote) -> Result<(), GatewayError> { + async fn add_item_note(&mut self, _item_id: &ItemEntityId, _item_note: ItemNote) -> Result<(), GatewayError> { Ok(()) } @@ -324,7 +324,7 @@ impl EntityGateway for InMemoryGateway { async fn get_character_meseta(&mut self, char_id: &CharacterEntityId) -> Result { let mut character_meseta = self.character_meseta.lock().unwrap(); - if let Some(meseta) = character_meseta.get_mut(&char_id) { + if let Some(meseta) = character_meseta.get_mut(char_id) { Ok(*meseta) } else { diff --git a/src/entity/gateway/postgres/models.rs b/src/entity/gateway/postgres/models.rs index 2e5404e..13df761 100644 --- a/src/entity/gateway/postgres/models.rs +++ b/src/entity/gateway/postgres/models.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] use std::collections::HashMap; use std::convert::Into; use serde::{Serialize, Deserialize}; diff --git a/src/entity/item/mod.rs b/src/entity/item/mod.rs index 78eb1a3..c083d7e 100644 --- a/src/entity/item/mod.rs +++ b/src/entity/item/mod.rs @@ -224,7 +224,7 @@ impl InventoryItemEntity { } } - pub fn individual<'a>(&'a self) -> Option<&'a ItemEntity> { + pub fn individual(&self) -> Option<&ItemEntity> { match self { InventoryItemEntity::Individual(i) => Some(i), _ => None, diff --git a/src/login/character.rs b/src/login/character.rs index 9b3cf1f..69d43e7 100644 --- a/src/login/character.rs +++ b/src/login/character.rs @@ -260,7 +260,7 @@ async fn new_character(entity_gateway: &mut EG, user: &UserAc let (monomates, monofluids) = futures::future::join_all((0..4usize).map(|_| { let mut eg = entity_gateway.clone(); let character_id = character.id; - return async move { + async move { let monomate = eg.create_item( NewItemEntity { item: ItemDetail::Tool ( diff --git a/src/ship/character.rs b/src/ship/character.rs index 2fdeb35..b1235b1 100644 --- a/src/ship/character.rs +++ b/src/ship/character.rs @@ -4,6 +4,8 @@ use crate::entity::character::CharacterEntity; use crate::ship::items::{CharacterInventory, CharacterBank}; use crate::entity::item::Meseta; + +#[derive(Default)] pub struct CharacterBytesBuilder<'a> { character: Option<&'a CharacterEntity>, stats: Option<&'a CharacterStats>, @@ -11,18 +13,6 @@ pub struct CharacterBytesBuilder<'a> { meseta: Option, } -impl<'a> Default for CharacterBytesBuilder<'a> { - fn default() -> CharacterBytesBuilder<'a> { - CharacterBytesBuilder { - character: None, - stats: None, - level: None, - meseta: None, - } - } -} - - impl<'a> CharacterBytesBuilder<'a> { pub fn character(self, character: &'a CharacterEntity) -> CharacterBytesBuilder<'a> { CharacterBytesBuilder { @@ -89,6 +79,7 @@ impl<'a> CharacterBytesBuilder<'a> { } +#[derive(Default)] pub struct FullCharacterBytesBuilder<'a> { character: Option<&'a CharacterEntity>, stats: Option<&'a CharacterStats>, @@ -103,25 +94,6 @@ pub struct FullCharacterBytesBuilder<'a> { option_flags: Option, } -impl<'a> Default for FullCharacterBytesBuilder<'a> { - fn default() -> FullCharacterBytesBuilder<'a> { - FullCharacterBytesBuilder { - character: None, - stats: None, - level: None, - meseta: None, - inventory: None, - bank: None, - key_config: None, - joystick_config: None, - symbol_chat: None, - tech_menu: None, - option_flags: None, - } - } -} - - impl<'a> FullCharacterBytesBuilder<'a> { pub fn character(self, character: &'a CharacterEntity) -> FullCharacterBytesBuilder<'a> { FullCharacterBytesBuilder { diff --git a/src/ship/items/bank.rs b/src/ship/items/bank.rs index b35a758..dab026f 100644 --- a/src/ship/items/bank.rs +++ b/src/ship/items/bank.rs @@ -293,7 +293,7 @@ impl CharacterBank { self.items.last() } - pub fn as_bank_entity(&self, character_id: &CharacterEntityId, bank_name: &BankName) -> BankEntity { + pub fn as_bank_entity(&self, _character_id: &CharacterEntityId, _bank_name: &BankName) -> BankEntity { BankEntity { items: self.items.iter() .map(|item| { diff --git a/src/ship/items/floor.rs b/src/ship/items/floor.rs index 271880c..f2720c0 100644 --- a/src/ship/items/floor.rs +++ b/src/ship/items/floor.rs @@ -159,15 +159,9 @@ impl<'a> FloorItemHandle<'a> { } // TODO: floors should keep track of their own item_ids -#[derive(Debug)] +#[derive(Debug, Default)] pub struct RoomFloorItems(Vec); -impl Default for RoomFloorItems { - fn default() -> RoomFloorItems { - RoomFloorItems(Vec::new()) - } -} - impl RoomFloorItems { pub fn add_item(&mut self, item: FloorItem) { self.0.push(item); diff --git a/src/ship/items/inventory.rs b/src/ship/items/inventory.rs index 4aa5467..7906ee4 100644 --- a/src/ship/items/inventory.rs +++ b/src/ship/items/inventory.rs @@ -897,7 +897,7 @@ impl CharacterInventory { } } - pub fn as_inventory_entity(&self, character_id: &CharacterEntityId) -> InventoryEntity { + pub fn as_inventory_entity(&self, _character_id: &CharacterEntityId) -> InventoryEntity { InventoryEntity { items: self.items.iter() .map(|item| { diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs index ba6b562..2af3cdc 100644 --- a/src/ship/items/manager.rs +++ b/src/ship/items/manager.rs @@ -1,4 +1,3 @@ -use log::warn; use crate::ship::items::ClientItemId; use std::collections::HashMap; use std::cell::RefCell; @@ -6,7 +5,7 @@ use thiserror::Error; use crate::entity::gateway::{EntityGateway, GatewayError}; use crate::entity::character::{CharacterEntity, CharacterEntityId, TechLevel}; use crate::entity::item::{ItemDetail, ItemNote, BankName}; -use crate::entity::item::{Meseta, NewItemEntity, ItemEntity, ItemEntityId, InventoryItemEntity, BankItemEntity}; +use crate::entity::item::{Meseta, NewItemEntity, ItemEntity, InventoryItemEntity, BankItemEntity}; use crate::entity::item::tool::{Tool, ToolType}; use crate::entity::item::weapon; use crate::ship::map::MapArea; @@ -239,38 +238,36 @@ impl ItemManager { } pub fn get_character_bank(&self, character: &CharacterEntity) -> Result<&CharacterBank, anyhow::Error> { - Ok(self.character_bank + self.character_bank .get(&character.id) - .ok_or(ItemManagerError::NoCharacter(character.id))?) - //.get(&BankName("".to_string())) - //.ok_or(ItemManagerError::InvalidBankName(BankName("".to_string())))?) + .ok_or_else(|| ItemManagerError::NoCharacter(character.id).into()) } pub fn get_character_meseta(&self, character_id: &CharacterEntityId) -> Result<&Meseta, ItemManagerError> { - Ok(self.character_meseta.get(&character_id) - .ok_or(ItemManagerError::NoCharacter(*character_id))?) + self.character_meseta.get(character_id) + .ok_or(ItemManagerError::NoCharacter(*character_id)) } pub fn get_character_meseta_mut<'a>(&'a mut self, character_id: &CharacterEntityId) -> Result<&'a mut Meseta, ItemManagerError> { - Ok(self.character_meseta.get_mut(&character_id) - .ok_or(ItemManagerError::NoCharacter(*character_id))?) + self.character_meseta.get_mut(character_id) + .ok_or(ItemManagerError::NoCharacter(*character_id)) } pub fn get_bank_meseta(&self, character_id: &CharacterEntityId) -> Result<&Meseta, ItemManagerError> { - Ok(self.bank_meseta.get(&character_id) - .ok_or(ItemManagerError::NoCharacter(*character_id))?) + self.bank_meseta.get(character_id) + .ok_or(ItemManagerError::NoCharacter(*character_id)) } pub fn get_bank_meseta_mut<'a>(&'a mut self, character_id: &CharacterEntityId) -> Result<&'a mut Meseta, ItemManagerError> { - Ok(self.bank_meseta.get_mut(&character_id) - .ok_or(ItemManagerError::NoCharacter(*character_id))?) + self.bank_meseta.get_mut(character_id) + .ok_or(ItemManagerError::NoCharacter(*character_id)) } pub fn get_character_and_bank_meseta_mut<'a>(&'a mut self, character_id: &CharacterEntityId) -> Result<(&'a mut Meseta, &'a mut Meseta), ItemManagerError> { Ok(( - self.character_meseta.get_mut(&character_id) + self.character_meseta.get_mut(character_id) .ok_or(ItemManagerError::NoCharacter(*character_id))?, - self.bank_meseta.get_mut(&character_id) + self.bank_meseta.get_mut(character_id) .ok_or(ItemManagerError::NoCharacter(*character_id))? )) } @@ -299,7 +296,7 @@ impl ItemManager { pub async fn character_picks_up_item(&mut self, entity_gateway: &mut EG, character: &mut CharacterEntity, item_id: ClientItemId) -> Result { - let it = ItemTransaction::new(&self, (character, item_id)) + let it = ItemTransaction::new(self, (character, item_id)) .act(|it, (character, item_id)| -> Result<_, ItemManagerError> { let local_floor = it.manager.character_floor.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; let inventory = it.manager.character_inventory.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; @@ -310,7 +307,7 @@ impl ItemManager { Some(floor_item) => { it.action(Box::new(RemoveFromLocalFloor { character_id: character.id, - item_id: item_id.clone() + item_id: *item_id })); floor_item }, @@ -319,12 +316,12 @@ impl ItemManager { Some(floor_item) => { it.action(Box::new(RemoveFromSharedFloor { room_id: *room_id, - item_id: item_id.clone() + item_id: *item_id })); floor_item }, None => { - return Err(ItemManagerError::NoSuchItemId(item_id.clone())).into() + return Err(ItemManagerError::NoSuchItemId(*item_id)) } } } @@ -339,7 +336,7 @@ impl ItemManager { })) } else { - return Err(ItemManagerError::CouldNotAddToInventory(*item_id).into()); + return Err(ItemManagerError::CouldNotAddToInventory(*item_id)); } TriggerCreateItem::Yes }, @@ -360,14 +357,14 @@ impl ItemManager { TriggerCreateItem::No }, SpaceForStack::No(_) => { - return Err(ItemManagerError::CouldNotAddToInventory(*item_id).into()); + return Err(ItemManagerError::CouldNotAddToInventory(*item_id)); }, } }, FloorItem::Meseta(meseta_floor_item) => { let character_meseta = it.manager.character_meseta.get(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; if character_meseta.0 >= 999999 { - return Err(ItemManagerError::CouldNotAddToInventory(*item_id).into()); + return Err(ItemManagerError::CouldNotAddToInventory(*item_id)); } it.action(Box::new(AddMesetaFloorItemToInventory { character_id: character.id, @@ -615,7 +612,7 @@ impl ItemManager { .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)?; + let _bank_item = bank.deposit_item(item_to_deposit, amount).ok_or(ItemManagerError::Idunnoman)?; entity_gateway.set_character_inventory(&character.id, &inventory.as_inventory_entity(&character.id)).await?; entity_gateway.set_character_bank(&character.id, &bank.as_bank_entity(&character.id, &BankName("".into())), BankName("".into())).await?; @@ -972,7 +969,7 @@ impl ItemManager { p1: (&AreaClient, &CharacterEntity, &Vec, usize), p2: (&AreaClient, &CharacterEntity, &Vec, usize)) -> Result, anyhow::Error> { - let it = ItemTransaction::new(&self, (p1, p2, room_id)) + let it = ItemTransaction::new(self, (p1, p2, room_id)) .act(|it, (p1, p2, room_id)| -> Result<_, anyhow::Error> { let p1_inventory = it.manager.get_character_inventory(p1.1)?; let p2_inventory = it.manager.get_character_inventory(p2.1)?; @@ -1049,27 +1046,27 @@ impl ItemManager { }) .collect::, _>>()?; - let trade_items = [(p1, p2, p1_inventory, p2_inventory), (p2, p1, p2_inventory, p1_inventory)] - .map(|(src_client, dest_client, src_inventory, dest_inventory)| { + let trade_items = [(p1, p2, p1_inventory), (p2, p1, p2_inventory)] + .map(|(src_client, dest_client, src_inventory)| { src_client.2.iter() .map(|item| -> Option<(Option, Box>)> { match item { TradeItem::Individual(item_id) => { let item = src_inventory.get_item_by_id(*item_id)?.individual()?; - let new_item_id = it.manager.room_item_id_counter.borrow_mut().get_mut(&room_id)?(); + let new_item_id = it.manager.room_item_id_counter.borrow_mut().get_mut(room_id)?(); Some(( Some(ItemToTrade { add_to: *dest_client.0, remove_from: *src_client.0, current_item_id: *item_id, - new_item_id: new_item_id, + new_item_id, item_detail: ItemToTradeDetail::Individual(item.item.clone()) }), Box::new(TradeIndividualItem { src_character_id: src_client.1.id, dest_character_id: dest_client.1.id, current_item_id: *item_id, - new_item_id: new_item_id, + new_item_id, }), )) }, @@ -1079,13 +1076,13 @@ impl ItemManager { None } else { - let new_item_id = it.manager.room_item_id_counter.borrow_mut().get_mut(&room_id)?(); + let new_item_id = it.manager.room_item_id_counter.borrow_mut().get_mut(room_id)?(); Some(( Some(ItemToTrade { add_to: *dest_client.0, remove_from: *src_client.0, current_item_id: *item_id, - new_item_id: new_item_id, + new_item_id, item_detail: ItemToTradeDetail::Stacked(item.tool, *amount) }), Box::new(TradeStackedItem { @@ -1093,7 +1090,7 @@ impl ItemManager { dest_character_id: dest_client.1.id, //item_ids: item.entity_ids.iter().cloned().take(*amount).collect(), current_item_id: *item_id, - new_item_id: new_item_id, + new_item_id, amount: *amount, }), )) @@ -1125,8 +1122,8 @@ impl ItemManager { if let [Some(p1_trades), Some(p2_trades)] = trade_items { - let (p1_item_trades, p1_item_actions): (Vec>, Vec>>) = p1_trades.into_iter().unzip(); - let (p2_item_trades, p2_item_actions): (Vec>, Vec>>) = p2_trades.into_iter().unzip(); + let (p1_item_trades, p1_item_actions): (Vec<_>, Vec<_>) = p1_trades.into_iter().unzip(); + let (p2_item_trades, p2_item_actions): (Vec<_>, Vec<_>) = p2_trades.into_iter().unzip(); let item_trades = p1_item_trades.into_iter().flatten().chain(p2_item_trades.into_iter().flatten()); let item_actions = p1_item_actions.into_iter().chain(p2_item_actions.into_iter()); diff --git a/src/ship/items/transaction.rs b/src/ship/items/transaction.rs index 4d426af..293a8de 100644 --- a/src/ship/items/transaction.rs +++ b/src/ship/items/transaction.rs @@ -1,17 +1,8 @@ use crate::entity::gateway::EntityGateway; use thiserror::Error; -use std::borrow::Cow; use crate::ship::items::manager::{ItemManager, ItemManagerError}; -use std::collections::HashMap; -use crate::entity::character::{CharacterEntity, CharacterEntityId, TechLevel}; -use crate::ship::items::bank::*; -use crate::ship::items::floor::*; -use crate::ship::items::inventory::*; -use crate::ship::items::ClientItemId; use crate::entity::gateway::GatewayError; -use crate::ship::location::{AreaClient, RoomId}; - #[derive(Error, Debug)] pub enum TransactionCommitError { #[error("transaction commit gateway error {0}")] diff --git a/src/ship/map/area.rs b/src/ship/map/area.rs index a26aa19..1bbc2b6 100644 --- a/src/ship/map/area.rs +++ b/src/ship/map/area.rs @@ -287,18 +287,11 @@ impl MapAreaLookup { } +#[derive(Default)] pub struct MapAreaLookupBuilder { map_areas: HashMap, } -impl Default for MapAreaLookupBuilder { - fn default() -> MapAreaLookupBuilder { - MapAreaLookupBuilder { - map_areas: HashMap::new() - } - } -} - impl MapAreaLookupBuilder { pub fn add(mut self, value: u16, map_area: MapArea) -> MapAreaLookupBuilder { self.map_areas.insert(value, map_area); diff --git a/src/ship/map/enemy.rs b/src/ship/map/enemy.rs index 32e53df..f10bc69 100644 --- a/src/ship/map/enemy.rs +++ b/src/ship/map/enemy.rs @@ -14,17 +14,17 @@ pub struct RawMapEnemy { id: u32, _unknown1: u16, pub children: u16, - map_area: u16, + _map_area: u16, _unknown4: u16, - section: u16, - wave_idd: u16, - wave_id: u32, - x: f32, - y: f32, - z: f32, - xrot: u32, - yrot: u32, - zrot: u32, + _section: u16, + _wave_idd: u16, + _wave_id: u32, + _x: f32, + _y: f32, + _z: f32, + _xrot: u32, + _yrot: u32, + _zrot: u32, _field1: u32, field2: u32, _field3: u32, @@ -40,17 +40,17 @@ impl RawMapEnemy { id: cursor.read_u32::()?, _unknown1: cursor.read_u16::()?, children: cursor.read_u16::()?, - map_area: cursor.read_u16::()?, + _map_area: cursor.read_u16::()?, _unknown4: cursor.read_u16::()?, - section: cursor.read_u16::()?, - wave_idd: cursor.read_u16::()?, - wave_id: cursor.read_u32::()?, - x: cursor.read_f32::()?, - y: cursor.read_f32::()?, - z: cursor.read_f32::()?, - xrot: cursor.read_u32::()?, - yrot: cursor.read_u32::()?, - zrot: cursor.read_u32::()?, + _section: cursor.read_u16::()?, + _wave_idd: cursor.read_u16::()?, + _wave_id: cursor.read_u32::()?, + _x: cursor.read_f32::()?, + _y: cursor.read_f32::()?, + _z: cursor.read_f32::()?, + _xrot: cursor.read_u32::()?, + _yrot: cursor.read_u32::()?, + _zrot: cursor.read_u32::()?, _field1: cursor.read_u32::()?, field2: cursor.read_u32::()?, _field3: cursor.read_u32::()?, @@ -75,7 +75,7 @@ pub enum MapEnemyError { pub struct MapEnemy { pub monster: MonsterType, pub map_area: MapArea, - hp: u32, + _hp: u32, // TODO: other stats from battleparam pub player_hit: [bool; 4], pub dropped_item: bool, @@ -254,7 +254,7 @@ impl MapEnemy { Ok(MapEnemy { monster, map_area: *map_area, - hp: 0, + _hp: 0, dropped_item: false, gave_exp: false, player_hit: [false; 4], @@ -265,7 +265,7 @@ impl MapEnemy { MapEnemy { monster, map_area, - hp: 0, + _hp: 0, dropped_item: false, gave_exp: false, player_hit: [false; 4], diff --git a/src/ship/map/object.rs b/src/ship/map/object.rs index 8307bf0..81bce6a 100644 --- a/src/ship/map/object.rs +++ b/src/ship/map/object.rs @@ -13,50 +13,50 @@ use crate::ship::map::*; #[derive(Debug, Copy, Clone)] pub struct RawMapObject { otype: u16, - unknown1: u16, - unknown2: u32, - id: u16, - group: u16, - section: u16, - unknown3: u16, - x: f32, - y: f32, - z: f32, - xrot: u32, - yrot: u32, - zrot: u32, + _unknown1: u16, + _unknown2: u32, + _id: u16, + _group: u16, + _section: u16, + _unknown3: u16, + _x: f32, + _y: f32, + _z: f32, + _xrot: u32, + _yrot: u32, + _zrot: u32, field1: f32, field2: f32, field3: f32, field4: u32, - field5: u32, - field6: u32, - field7: u32, + _field5: u32, + _field6: u32, + _field7: u32, } impl RawMapObject { pub fn from_byte_stream(cursor: &mut R) -> Result { Ok(RawMapObject { otype: cursor.read_u16::()?, - unknown1: cursor.read_u16::()?, - unknown2: cursor.read_u32::()?, - id: cursor.read_u16::()?, - group: cursor.read_u16::()?, - section: cursor.read_u16::()?, - unknown3: cursor.read_u16::()?, - x: cursor.read_f32::()?, - y: cursor.read_f32::()?, - z: cursor.read_f32::()?, - xrot: cursor.read_u32::()?, - yrot: cursor.read_u32::()?, - zrot: cursor.read_u32::()?, + _unknown1: cursor.read_u16::()?, + _unknown2: cursor.read_u32::()?, + _id: cursor.read_u16::()?, + _group: cursor.read_u16::()?, + _section: cursor.read_u16::()?, + _unknown3: cursor.read_u16::()?, + _x: cursor.read_f32::()?, + _y: cursor.read_f32::()?, + _z: cursor.read_f32::()?, + _xrot: cursor.read_u32::()?, + _yrot: cursor.read_u32::()?, + _zrot: cursor.read_u32::()?, field1: cursor.read_f32::()?, field2: cursor.read_f32::()?, field3: cursor.read_f32::()?, field4: cursor.read_u32::()?, - field5: cursor.read_u32::()?, - field6: cursor.read_u32::()?, - field7: cursor.read_u32::()?, + _field5: cursor.read_u32::()?, + _field6: cursor.read_u32::()?, + _field7: cursor.read_u32::()?, }) } } diff --git a/src/ship/packet/handler/trade.rs b/src/ship/packet/handler/trade.rs index 1782546..7792b4e 100644 --- a/src/ship/packet/handler/trade.rs +++ b/src/ship/packet/handler/trade.rs @@ -1,22 +1,14 @@ use std::convert::TryInto; -use log::warn; -use rand::Rng; -use rand::seq::SliceRandom; use libpso::packet::ship::*; use libpso::packet::messages::*; -use crate::common::leveltable::CharacterLevelTable; use crate::common::serverstate::ClientId; -use crate::ship::ship::{SendShipPacket, ShipError, Clients, Rooms, ItemShops}; -use crate::ship::location::{ClientLocation, ClientLocationError, LocalClientId}; -use crate::ship::drops::ItemDrop; -use crate::ship::items::{ItemManager, ItemManagerError, ClientItemId, TriggerCreateItem, FloorItem, FloorType, ItemToTradeDetail}; +use crate::ship::ship::{SendShipPacket, ShipError, Clients}; +use crate::ship::location::{ClientLocation, ClientLocationError}; +use crate::ship::items::{ItemManager, ItemManagerError, ClientItemId, ItemToTradeDetail}; use crate::ship::items::inventory::InventoryItem; use crate::ship::trade::{TradeItem, TradeState, TradeStatus}; use crate::entity::gateway::EntityGateway; -use crate::entity::item; -use libpso::utf8_to_utf16_array; use crate::ship::packet::builder; -use crate::ship::shops::{ShopItem, ToolShopItem, ArmorShopItem}; pub const MESETA_ITEM_ID: ClientItemId = ClientItemId(0xFFFFFF01); pub const OTHER_MESETA_ITEM_ID: ClientItemId = ClientItemId(0xFFFFFFFF); @@ -54,21 +46,18 @@ pub enum TradeError { // TODO: remove target -pub async fn trade_request(id: ClientId, +pub async fn trade_request(id: ClientId, trade_request: &TradeRequest, target: u32, - entity_gateway: &mut EG, client_location: &ClientLocation, clients: &mut Clients, item_manager: &mut ItemManager, trades: &mut TradeState) -> Result + Send>, anyhow::Error> -where - EG: EntityGateway { let trade_request = trade_request.clone(); // TODO: make this function take ownership of packet match trade_request.trade { - TradeRequestCommand::Initialize(ref act, meseta) => { + TradeRequestCommand::Initialize(ref act, _meseta) => { match act { TradeRequestInitializeCommand::Initialize => { if trades.in_trade(&id) { @@ -76,10 +65,9 @@ where } let trade_partner = client_location.get_client_neighbors(id)? .into_iter() - .filter(|ac| { + .find(|ac| { ac.local_client.id() == target as u8 //trade_request.client }) - .next() .ok_or(TradeError::CouldNotFindTradePartner)?; if trades.in_trade(&trade_partner.client) { return Err(TradeError::OtherAlreadyInTrade.into()) @@ -125,7 +113,7 @@ where Ok(trades .with(&id, |this, other| -> Result + Send>, anyhow::Error> { if this.status == TradeStatus::Trading && other.status == TradeStatus::Trading { - let client = clients.get(&this.client()).ok_or(ShipError::ClientNotFound(this.client()))?; + let client = clients.get(&this.client()).ok_or_else(|| ShipError::ClientNotFound(this.client()))?; let inventory = item_manager.get_character_inventory(&client.character)?; if ClientItemId(item_id) == MESETA_ITEM_ID { this.meseta += amount as usize; @@ -157,7 +145,7 @@ where Err(TradeError::MismatchedStatus.into()) } })? - .unwrap_or_else(|err| { + .unwrap_or_else(|_err| { trades.remove_trade(&id); Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() .filter(move |client| client.local_client.id() == target as u8) @@ -184,9 +172,8 @@ where this.items.retain(|item| { item.item_id() != ClientItemId(item_id) }) - //this.items.push(TradeItem::Individual(ClientItemId(item_id))); }, - InventoryItem::Stacked(stacked_item) => { + InventoryItem::Stacked(_stacked_item) => { let trade_item_index = this.items.iter() .position(|item| { item.item_id() == ClientItemId(item_id) @@ -302,16 +289,13 @@ fn status_is_not(status: &TradeStatus, statuses: &[TradeStatus; !status_is(status, statuses) } -pub async fn inner_items_to_trade(id: ClientId, - items_to_trade: &ItemsToTrade, - entity_gateway: &mut EG, - client_location: &ClientLocation, - clients: &mut Clients, - item_manager: &mut ItemManager, - trades: &mut TradeState) - -> Result + Send>, anyhow::Error> -where - EG: EntityGateway +pub async fn inner_items_to_trade(id: ClientId, + items_to_trade: &ItemsToTrade, + client_location: &ClientLocation, + clients: &mut Clients, + item_manager: &mut ItemManager, + trades: &mut TradeState) + -> Result + Send>, anyhow::Error> { Ok(trades .with(&id, |this, other| -> Result + Send>, anyhow::Error> { @@ -319,8 +303,8 @@ where return Err(TradeError::MismatchedStatus.into()) } - let client = clients.get(&this.client()).ok_or(ShipError::ClientNotFound(this.client()))?; - let other_client = clients.get(&other.client()).ok_or(ShipError::ClientNotFound(other.client()))?; + let client = clients.get(&this.client()).ok_or_else(|| ShipError::ClientNotFound(this.client()))?; + let other_client = clients.get(&other.client()).ok_or_else(|| ShipError::ClientNotFound(other.client()))?; let inventory = item_manager.get_character_inventory(&client.character)?; if items_to_trade.count as usize != (this.items.len() + (if this.meseta != 0 { 1 } else { 0 })) { @@ -355,14 +339,14 @@ where let real_trade_item = this.items .iter() .find(|i| i.item_id() == ClientItemId(item.item_id)) - .ok_or_else(|| TradeError::SketchyTrade)?; + .ok_or(TradeError::SketchyTrade)?; let trade_item_bytes: [u8; 16] = item.item_data.iter() .chain(item.item_data2.iter()) .cloned().collect::>() .try_into() .unwrap(); match real_item { - InventoryItem::Individual(individual_inventory_item) => { + InventoryItem::Individual(_individual_inventory_item) => { if real_item.as_client_bytes() == trade_item_bytes { Ok(()) } @@ -374,7 +358,7 @@ where if real_item.as_client_bytes()[0..4] == trade_item_bytes[0..4] { let amount = trade_item_bytes[5] as usize; if amount <= stacked_inventory_item.entity_ids.len() { - if real_trade_item.stacked().ok_or_else(|| TradeError::SketchyTrade)?.1 == amount { + if real_trade_item.stacked().ok_or(TradeError::SketchyTrade)?.1 == amount { Ok(()) } else { @@ -407,7 +391,7 @@ where })? .unwrap_or_else(|err| { log::warn!("trade error: {:?}", err); - let (this, other) = trades.remove_trade(&id); + let (_this, other) = trades.remove_trade(&id); Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() .filter(move |client| other.as_ref().map(|other| client.client == other.client() ).unwrap_or_else(|| false)) .map(move |client| { @@ -417,23 +401,20 @@ where })) } -pub async fn items_to_trade(id: ClientId, - items_to_trade_pkt: &ItemsToTrade, - entity_gateway: &mut EG, - client_location: &ClientLocation, - clients: &mut Clients, - item_manager: &mut ItemManager, - trades: &mut TradeState) - -> Result + Send>, anyhow::Error> -where - EG: EntityGateway +pub async fn items_to_trade(id: ClientId, + items_to_trade_pkt: &ItemsToTrade, + client_location: &ClientLocation, + clients: &mut Clients, + item_manager: &mut ItemManager, + trades: &mut TradeState) + -> Result + Send>, anyhow::Error> { - let t = inner_items_to_trade(id, items_to_trade_pkt, entity_gateway, client_location, clients, item_manager, trades).await; + let t = inner_items_to_trade(id, items_to_trade_pkt, client_location, clients, item_manager, trades).await; match t { Ok(p) => Ok(p), Err(err) => { log::warn!("atrade error: {:?}", err); - let (this, other) = trades.remove_trade(&id); + let (_this, other) = trades.remove_trade(&id); Ok(Box::new(client_location.get_all_clients_by_client(id)?.into_iter() .filter(move |client| other.as_ref().map(|other| client.client == other.client()).unwrap_or_else(|| false)) .map(move |client| { @@ -469,8 +450,8 @@ where this.status = TradeStatus::TradeComplete; if this.status == TradeStatus::TradeComplete && other.status == TradeStatus::TradeComplete { - let this_client = clients.get(&this.client()).ok_or(ShipError::ClientNotFound(this.client()))?; - let other_client = clients.get(&other.client()).ok_or(ShipError::ClientNotFound(other.client()))?; + let this_client = clients.get(&this.client()).ok_or_else(|| ShipError::ClientNotFound(this.client()))?; + let other_client = clients.get(&other.client()).ok_or_else(|| ShipError::ClientNotFound(other.client()))?; let this_local_client = client_location.get_local_client(this.client())?; let other_local_client = client_location.get_local_client(other.client())?; let room_id = client_location.get_room(id).map_err(|err| -> ClientLocationError { err.into() })?; @@ -550,8 +531,8 @@ where } } }, - err @ _ => { - let (this, other) = trades.remove_trade(&id); + _ => { + let (_this, other) = trades.remove_trade(&id); Ok(Box::new(client_location.get_all_clients_by_client(id).unwrap().into_iter() .filter(move |client| other.as_ref().map(|other| client.client == other.client()).unwrap_or_else(|| false)) .map(move |client| { diff --git a/src/ship/ship.rs b/src/ship/ship.rs index 8b0aa17..60e64f4 100644 --- a/src/ship/ship.rs +++ b/src/ship/ship.rs @@ -556,7 +556,7 @@ impl ShipServerState { handler::direct_message::accept_tek_item(id, tek_accept, &mut self.entity_gateway, &block.client_location, &mut self.clients, &mut self.item_manager).await? }, GameMessage::TradeRequest(trade_request) => { - handler::trade::trade_request(id, trade_request, target, &mut self.entity_gateway, &block.client_location, &mut self.clients, &mut self.item_manager, &mut self.trades).await? + handler::trade::trade_request(id, trade_request, target, &block.client_location, &mut self.clients, &mut self.item_manager, &mut self.trades).await? }, _ => { let cmsg = msg.clone(); @@ -727,7 +727,7 @@ impl ServerState for ShipServerState { }, RecvShipPacket::ItemsToTrade(items_to_trade) => { let block = self.blocks.with_client(id, &self.clients)?; - handler::trade::items_to_trade(id, items_to_trade, &mut self.entity_gateway, &block.client_location, &mut self.clients, &mut self.item_manager, &mut self.trades).await? + handler::trade::items_to_trade(id, items_to_trade, &block.client_location, &mut self.clients, &mut self.item_manager, &mut self.trades).await? }, RecvShipPacket::TradeConfirmed(_) => { let block = self.blocks.with_client(id, &self.clients)?; diff --git a/src/ship/shops/weapon.rs b/src/ship/shops/weapon.rs index 6818f23..0e2e8e4 100644 --- a/src/ship/shops/weapon.rs +++ b/src/ship/shops/weapon.rs @@ -315,7 +315,7 @@ fn number_of_weapons_to_generate(character_level: usize) -> usize { #[derive(Debug)] pub struct WeaponShop { - difficulty: Difficulty, + _difficulty: Difficulty, section_id: SectionID, weapon: WeaponTable, special: SpecialTable, @@ -329,7 +329,7 @@ pub struct WeaponShop { impl WeaponShop { pub fn new(difficulty: Difficulty, section_id: SectionID) -> WeaponShop { WeaponShop { - difficulty, + _difficulty: difficulty, section_id, weapon: load_weapon_table(difficulty, section_id), special: load_special_table(), @@ -358,8 +358,10 @@ impl WeaponShop { .last() .unwrap(); - let special_tier = WeightedIndex::new(tier.special.iter().map(|t| t.probability)).unwrap(); - match special_tier.sample(&mut self.rng) { + let special_tier_choice = WeightedIndex::new(tier.special.iter().map(|t| t.probability)).unwrap(); + let special_tier_index = special_tier_choice.sample(&mut self.rng); + let special_tier = tier.special.get(special_tier_index)?; + match special_tier.tier { 1 => TIER1_SPECIAL.choose(&mut self.rng).cloned(), 2 => TIER2_SPECIAL.choose(&mut self.rng).cloned(), _ => None diff --git a/src/ship/trade.rs b/src/ship/trade.rs index c58fdb2..9f600a8 100644 --- a/src/ship/trade.rs +++ b/src/ship/trade.rs @@ -1,6 +1,5 @@ use std::collections::HashMap; use std::cell::RefCell; -use async_std::sync::{Arc, Mutex}; use crate::common::serverstate::ClientId; use crate::ship::items; @@ -67,38 +66,6 @@ impl ClientTradeState { } } -/* -pub struct ClientTradeHandle<'a> { - handle: std::cell::Ref<'a, ClientTradeState>, -} - -impl<'a> ClientTradeHandle<'a> { - fn new(handle: std::cell::Ref<'a, ClientTradeState>) -> ClientTradeHandle<'a> { - ClientTradeHandle { - handle: handle, - } - } -} - -impl<'a> std::ops::Deref for ClientTradeHandle<'a> { - type Target = ClientTradeState; - fn deref(&self) -> &ClientTradeState { - &self.handle - } -} - -impl<'a> std::ops::DerefMut for ClientTradeHandle<'a> { - fn deref_mut(&mut self) -> &mut ClientTradeState { - &mut self.handle - } -} - -impl<'a> Clone for ClientTradeHandle<'a> { - -} -*/ - - #[derive(thiserror::Error, Debug)] #[error("")] pub enum TradeStateError { @@ -141,7 +108,7 @@ impl TradeState { F: Fn(&mut ClientTradeState, &mut ClientTradeState) -> T { let mut c1 = self.trades.get(client).ok_or_else(|| TradeStateError::ClientNotInTrade(*client))?.borrow_mut(); - let mut c2 = self.trades.get(&c1.other_client).ok_or_else(|| TradeStateError::ClientNotInTrade(c1.other_client))?.borrow_mut(); + let mut c2 = self.trades.get(&c1.other_client).ok_or(TradeStateError::ClientNotInTrade(c1.other_client))?.borrow_mut(); // sanity check if c1.client != c2.other_client { @@ -150,22 +117,6 @@ impl TradeState { Ok(func(&mut *c1, &mut *c2)) } - /* - pub fn with (&self, client: &ClientId, func: F) -> Result - where - F: Fn(&mut ClientTradeHandle, &mut ClientTradeHandle) -> T - { - let c1 = ClientTradeHandle::new(self.trades.get(client).ok_or_else(|| TradeStateError::ClientNotInTrade(*client))?.borrow_mut()); - let c2 = ClientTradeHandle::new(self.trades.get(&c1.other_client).ok_or_else(|| TradeStateError::ClientNotInTrade(c1.other_client))?.borrow_mut()); - - // sanity check - if c1.client != c2.other_client { - return Err(TradeStateError::MismatchedTrade(c1.client, c2.client)); - } - - Ok(func(&mut c1, &mut c2)) - } - */ // TODO: is it possible for this to not return Options? pub fn remove_trade(&mut self, client: &ClientId) -> (Option, Option) {