diff --git a/Cargo.toml b/Cargo.toml index 33acf8d..582634f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,12 +24,12 @@ 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"] } 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"] } diff --git a/src/bin/main.rs b/src/bin/main.rs index 05fc80d..931af87 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, 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( @@ -87,10 +90,6 @@ fn main() { tekked: true, } ), - location: item::ItemLocation::Bank { - character_id: character.id, - name: item::BankName("".to_string()) - } }).await.unwrap(); } @@ -102,10 +101,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 +117,6 @@ fn main() { tekked: false, } ), - location: ItemLocation::Inventory { - character_id: character.id, - } }).await.unwrap(); let item1 = entity_gateway.create_item( NewItemEntity { @@ -139,9 +131,6 @@ fn main() { tekked: true, } ), - location: ItemLocation::Inventory { - character_id: character.id, - } }).await.unwrap(); let item2_w = entity_gateway.create_item( NewItemEntity { @@ -156,9 +145,6 @@ fn main() { tekked: true, } ), - location: ItemLocation::Inventory { - character_id: character.id, - } }).await.unwrap(); let item3 = entity_gateway.create_item( NewItemEntity { @@ -173,34 +159,25 @@ fn main() { tekked: true, } ), - location: ItemLocation::Inventory { - character_id: character.id, - } }).await.unwrap(); let item4 = entity_gateway.create_item( NewItemEntity { 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}),], 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 +188,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 +200,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 +208,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(); @@ -246,9 +216,6 @@ fn main() { item: ItemDetail::ESWeapon( item::esweapon::ESWeapon::new(item::esweapon::ESWeaponType::Saber) ), - location: ItemLocation::Inventory { - character_id: character.id, - } }).await.unwrap(); let item7_a = entity_gateway.create_item( NewItemEntity { @@ -260,9 +227,6 @@ fn main() { slots: 4, } ), - location: ItemLocation::Inventory { - character_id: character.id, - } } ).await.unwrap(); let item8_s = entity_gateway.create_item( @@ -274,9 +238,6 @@ fn main() { evp: 5, } ), - location: ItemLocation::Inventory { - character_id: character.id, - } } ).await.unwrap(); let item9_u0 = entity_gateway.create_item( @@ -287,9 +248,6 @@ fn main() { modifier: Some(item::unit::UnitModifier::PlusPlus), } ), - location: ItemLocation::Inventory { - character_id: character.id, - } } ).await.unwrap(); let item10_u1 = entity_gateway.create_item( @@ -300,9 +258,6 @@ fn main() { modifier: Some(item::unit::UnitModifier::Plus), } ), - location: ItemLocation::Inventory { - character_id: character.id, - } } ).await.unwrap(); let item11_u2 = entity_gateway.create_item( @@ -313,9 +268,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( @@ -326,9 +278,6 @@ fn main() { modifier: Some(item::unit::UnitModifier::MinusMinus), } ), - location: ItemLocation::Inventory { - character_id: character.id, - } } ).await.unwrap(); let item13 = entity_gateway.create_item( @@ -336,9 +285,6 @@ fn main() { item: ItemDetail::Mag( item::mag::Mag::baby_mag(5) ), - location: ItemLocation::Inventory { - character_id: character.id, - } } ).await.unwrap(); let item14 = entity_gateway.create_item( @@ -354,11 +300,22 @@ fn main() { tekked: true, } ), - location: ItemLocation::Inventory { - character_id: character.id, - } }).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), @@ -368,7 +325,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, item6_1, item7_a, item8_s, item9_u0, item10_u1, item11_u2, item12_u3, item13, item14]); + 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(), item14.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/common/mainloop/client.rs b/src/common/mainloop/client.rs index 32562e9..52cc503 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?; @@ -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); } 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/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..432017d 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, @@ -158,18 +160,12 @@ 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 CharacterTechniques { - fn new() -> CharacterTechniques { - CharacterTechniques { - techs: HashMap::new(), - } - } - pub fn set_tech(&mut self, tech: Technique, level: TechLevel) { self.techs.insert(tech, TechLevel(level.0 - 1)); } @@ -187,18 +183,20 @@ impl CharacterTechniques { } -#[derive(Clone)] +#[derive(Debug, Clone)] 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; } @@ -208,18 +206,20 @@ impl CharacterConfig { } } -#[derive(Clone)] +#[derive(Debug, Clone)] 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 } @@ -229,29 +229,31 @@ 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], } -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 } } -#[derive(Clone, Default)] +#[derive(Clone, Default, Debug)] pub struct CharacterMaterials { pub power: u32, pub mind: u32, @@ -262,7 +264,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, derive_more::Display)] pub struct CharacterEntityId(pub u32); #[derive(Clone)] @@ -284,8 +286,6 @@ pub struct NewCharacterEntity { pub materials: CharacterMaterials, pub tech_menu: CharacterTechMenu, - pub meseta: u32, - pub bank_meseta: u32, pub option_flags: u32, } @@ -299,20 +299,18 @@ 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(), - meseta: 0, - bank_meseta: 0, + tech_menu: CharacterTechMenu::default(), option_flags: 0, } } } -#[derive(Clone)] +#[derive(Clone, Default, Debug)] pub struct CharacterEntity { pub id: CharacterEntityId, pub user_id: UserAccountId, @@ -332,8 +330,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 261aadc..fd8b1aa 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!(); } @@ -115,4 +115,20 @@ pub trait EntityGateway: Send + Sync + Clone { async fn set_character_equips(&mut self, _char_id: &CharacterEntityId, _equips: &EquippedEntity) -> Result<(), GatewayError> { unimplemented!(); } + + 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 d80dca7..3d37761 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()); @@ -223,17 +225,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(()) } @@ -272,7 +270,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() @@ -318,4 +315,36 @@ impl EntityGateway for InMemoryGateway { equips.insert(*char_id, equipped.clone()); Ok(()) } + + 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/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 +); 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 5f90986..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}; @@ -216,8 +217,6 @@ pub struct PgCharacter { tp: i16, tech_menu: Vec, - meseta: i32, - bank_meseta: i32, } impl From for CharacterEntity { @@ -267,8 +266,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, } } } @@ -571,22 +568,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,45 +592,92 @@ pub enum PgItemLocationDetail { FedToMag { mag: u32, }, - Shop, + BoughtAtShop { + character_id: u32, + }, SoldToShop, + Trade { + 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{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::Shop => PgItemLocationDetail::Shop, - ItemLocation::SoldToShop => PgItemLocationDetail::SoldToShop, + ItemNote::CharacterCreation{character_id} => PgItemNoteDetail::CharacterCreation { + character_id: character_id.0, + }, + ItemNote::EnemyDrop{character_id, map_area, x, y, z} => PgItemNoteDetail::EnemyDrop { + character_id: character_id.0, + map_area, + x,y,z, + }, + ItemNote::Pickup{character_id} => PgItemNoteDetail::Pickup { + character_id: character_id.0, + }, + ItemNote::PlayerDrop{map_area, x, y, z} => PgItemNoteDetail::PlayerDrop { + map_area, + x,y,z, + }, + ItemNote::Consumed => PgItemNoteDetail::Consumed, + ItemNote::FedToMag{mag} => PgItemNoteDetail::FedToMag{ + mag: mag.0 + }, + 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 { - 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::Shop => ItemLocation::Shop, - PgItemLocationDetail::SoldToShop => ItemLocation::SoldToShop, +impl From for ItemNote { + fn from(other: PgItemNoteDetail) -> ItemNote { + match other { + PgItemNoteDetail::CharacterCreation{character_id} => ItemNote::CharacterCreation { + character_id: CharacterEntityId(character_id as u32), + }, + PgItemNoteDetail::EnemyDrop{character_id, map_area, x, y, z} => ItemNote::EnemyDrop { + character_id: CharacterEntityId(character_id as u32), + map_area, + x,y,z, + }, + PgItemNoteDetail::Pickup{character_id} => ItemNote::Pickup { + character_id: CharacterEntityId(character_id as u32), + }, + PgItemNoteDetail::PlayerDrop{map_area, x, y, z} => ItemNote::PlayerDrop { + map_area, + x,y,z, + }, + PgItemNoteDetail::Consumed => ItemNote::Consumed, + PgItemNoteDetail::FedToMag{mag} => ItemNote::FedToMag{ + mag: ItemEntityId(mag) + }, + 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), + } } } } #[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, } @@ -683,19 +726,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 53609cf..2fefb03 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 } } } @@ -183,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 *;"#; @@ -216,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?; @@ -242,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) @@ -273,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?; @@ -294,64 +289,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 +414,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 +425,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 +450,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 +461,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()) @@ -597,4 +546,44 @@ impl EntityGateway for PostgresGateway { .await?; Ok(()) } + + 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(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/mag.rs b/src/entity/item/mag.rs index 087ee88..64efc2e 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()); diff --git a/src/entity/item/mod.rs b/src/entity/item/mod.rs index 8e8c6d4..c083d7e 100644 --- a/src/entity/item/mod.rs +++ b/src/entity/item/mod.rs @@ -17,26 +17,29 @@ 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); #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub enum ItemLocation { - Inventory { +pub enum ItemNote { + CharacterCreation { character_id: CharacterEntityId, }, - Bank { - 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,19 +49,18 @@ pub enum ItemLocation { FedToMag { mag: ItemEntityId, }, - Shop, + BoughtAtShop { + character_id: CharacterEntityId, + }, SoldToShop, - /*Destroyed { - // marks an item that has been consumed in some way + Trade { + id: TradeId, + character_to: CharacterEntityId, + character_from: CharacterEntityId, }, - Transformed { - item_id, - change_event - } -*/ } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Copy, Clone, PartialEq)] pub struct Meseta(pub u32); impl Meseta { @@ -160,18 +162,23 @@ impl ItemDetail { _ => None, } } + + pub fn tool(&self) -> Option<&tool::Tool> { + match self { + ItemDetail::Tool(tool) => Some(tool), + _ => None, + } + } } #[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, } @@ -216,6 +223,13 @@ impl InventoryItemEntity { _ => None, } } + + pub fn individual(&self) -> Option<&ItemEntity> { + match self { + InventoryItemEntity::Individual(i) => Some(i), + _ => None, + } + } } #[derive(Clone, Debug, Default)] diff --git a/src/lib.rs b/src/lib.rs index dbf3afa..76e1020 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,8 @@ #![feature(maybe_uninit_extra)] #![feature(inline_const)] #![feature(drain_filter)] - +#![feature(derive_default_enum)] +#![feature(try_blocks)] diff --git a/src/login/character.rs b/src/login/character.rs index 12e874a..69d43e7 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, 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, @@ -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; + 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/login/login.rs b/src/login/login.rs index 617fad2..4181136 100644 --- a/src/login/login.rs +++ b/src/login/login.rs @@ -232,7 +232,7 @@ mod test { async fn save_user(&mut self, _user: &UserAccountEntity) -> Result<(), GatewayError> { Ok(()) } - }; + } let mut server = LoginServerState::new(TestData {}, "127.0.0.1".parse().unwrap()); diff --git a/src/ship/character.rs b/src/ship/character.rs index bbd2ddc..b1235b1 100644 --- a/src/ship/character.rs +++ b/src/ship/character.rs @@ -2,24 +2,17 @@ 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; + +#[derive(Default)] pub struct CharacterBytesBuilder<'a> { character: Option<&'a CharacterEntity>, stats: Option<&'a CharacterStats>, level: Option, + meseta: Option, } -impl<'a> Default for CharacterBytesBuilder<'a> { - fn default() -> CharacterBytesBuilder<'a> { - CharacterBytesBuilder { - character: None, - stats: None, - level: None, - } - } -} - - impl<'a> CharacterBytesBuilder<'a> { pub fn character(self, character: &'a CharacterEntity) -> CharacterBytesBuilder<'a> { CharacterBytesBuilder { @@ -42,10 +35,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 +71,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() } @@ -78,10 +79,12 @@ impl<'a> CharacterBytesBuilder<'a> { } +#[derive(Default)] 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]>, @@ -91,24 +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, - 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 { @@ -131,6 +116,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 +176,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 +197,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/bank.rs b/src/ship/items/bank.rs index f3d5d10..dab026f 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}; @@ -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| { @@ -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/floor.rs b/src/ship/items/floor.rs index c976048..f2720c0 100644 --- a/src/ship/items/floor.rs +++ b/src/ship/items/floor.rs @@ -159,24 +159,24 @@ 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); } + 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/inventory.rs b/src/ship/items/inventory.rs index 7369c40..da4c4b0 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; 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, ToolType}; use crate::entity::item::mag::Mag; use crate::entity::item::weapon::Weapon; @@ -88,6 +88,24 @@ pub enum InventoryItemAddToError { pub enum InventoryAddError { } +#[derive(Debug, Clone)] +pub enum YesThereIsSpace { + NewStack, + ExistingStack, +} + +#[derive(Debug, Clone)] +pub enum NoThereIsNotSpace { + FullStack, + FullInventory, +} + +#[derive(Debug, Clone)] +pub enum SpaceForStack { + Yes(YesThereIsSpace), + No(NoThereIsNotSpace), +} + impl InventoryItem { pub fn entity_ids(&self) -> Vec { match self { @@ -277,6 +295,27 @@ impl InventoryItem { }, } } + + pub fn stacked(&self) -> Option<&StackedInventoryItem> { + match self { + InventoryItem::Stacked(ref stacked_inventory_item) => Some(stacked_inventory_item), + _ => None + } + } + + 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(), + _ => None + } + } } @@ -470,6 +509,62 @@ 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(NoThereIsNotSpace::FullStack) + } + } + None => { + if self.count() < INVENTORY_CAPACITY { + SpaceForStack::Yes(YesThereIsSpace::NewStack) + } + else { + SpaceForStack::No(NoThereIsNotSpace::FullInventory) + } + } + } + } + + 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() .enumerate() @@ -563,10 +658,80 @@ 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_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, + 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? @@ -589,6 +754,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() @@ -762,7 +954,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| { @@ -770,9 +962,6 @@ impl CharacterInventory { InventoryItem::Individual(item) => { InventoryItemEntity::Individual(ItemEntity { id: item.entity_id, - location: ItemLocation::Inventory { - character_id: *character_id, - }, item: item.item.clone(), }) }, @@ -781,9 +970,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 f22c3a5..78b2749 100644 --- a/src/ship/items/manager.rs +++ b/src/ship/items/manager.rs @@ -1,23 +1,27 @@ use crate::ship::items::ClientItemId; use std::collections::HashMap; use std::cmp::Ordering; +use std::cell::RefCell; 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::{ItemDetail, ItemNote, BankName}; 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; 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; +use crate::ship::packet::handler::trade::{TradeError, OTHER_MESETA_ITEM_ID}; 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 { @@ -31,47 +35,92 @@ 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] crate::entity::gateway::GatewayError), + GatewayError(#[from] GatewayError), + #[error("stacked item")] StackedItemError(Vec), + #[error("item not sellable")] ItemNotSellable(InventoryItem), + #[error("wallet full")] WalletFull, + #[error("invalid sale")] InvalidSale, + ItemTransactionAction(Box), + #[error("invalid trade")] + 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 { - id_counter: u32, + pub(super) id_counter: u32, - character_inventory: HashMap, + pub(self) character_inventory: HashMap, + pub(self) character_meseta: HashMap, + pub(self) bank_meseta: 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: RefCell ClientItemId + Send>>>, } impl Default for ItemManager { @@ -79,11 +128,13 @@ 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(), room_floor: HashMap::new(), - room_item_id_counter: HashMap::new(), + room_item_id_counter: RefCell::new(HashMap::new()), } } } @@ -94,7 +145,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?; @@ -154,8 +204,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(()) } @@ -173,7 +228,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) })); @@ -184,22 +239,45 @@ 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 + 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> { + 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> { + 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> { + 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> { + 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) - .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 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 remove_character_from_room(&mut self, character: &CharacterEntity) { self.character_inventory.remove(&character.id); @@ -225,80 +303,89 @@ 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 + })); + 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 + })); + floor_item + }, + None => { + return Err(ItemManagerError::NoSuchItemId(*item_id)) + } } + } + }; - 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)); } + 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)); + }, + } + }, + 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)); + } + it.action(Box::new(AddMesetaFloorItemToInventory { + character_id: character.id, + 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> { @@ -325,18 +412,18 @@ 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 { 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, @@ -351,13 +438,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], @@ -402,9 +489,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, @@ -415,9 +502,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, @@ -440,13 +527,14 @@ 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.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), @@ -474,14 +562,14 @@ 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))?; 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, @@ -503,7 +591,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)); @@ -511,8 +599,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?; @@ -531,26 +619,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)?; - - 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?; - } - } - } + 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?; @@ -572,23 +641,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 }; @@ -626,7 +678,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?; } @@ -764,10 +816,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(), @@ -781,21 +836,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, @@ -808,10 +860,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))? @@ -819,9 +867,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, @@ -834,10 +884,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))? @@ -849,37 +895,34 @@ impl ItemManager { } pub async fn player_sells_item(&mut self, - entity_gateway: &mut EG, - character: &mut CharacterEntity, - item_id: ClientItemId, - amount: usize) - -> Result<(), anyhow::Error> { + entity_gateway: &mut EG, + character: &mut CharacterEntity, + item_id: ClientItemId, + amount: usize) + -> Result<(), anyhow::Error> { let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; let sold_item_handle = inventory.get_item_handle_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?; if let Some(item_sold) = sold_item_handle.item() { let unit_price = item_sold.get_sell_price()?; { let total_sale = unit_price * amount as u32; - if character.meseta + total_sale <= 999999 { - character.meseta += total_sale; - match item_sold { - InventoryItem::Individual(i) => { - entity_gateway.change_item_location(&i.entity_id, ItemLocation::SoldToShop).await?; - inventory.remove_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?; - }, - InventoryItem::Stacked(s) => { - match amount.cmp(&s.count()) { - Ordering::Less | Ordering::Equal => { - sold_item_handle.consume(amount)?; - }, - Ordering::Greater => return Err(ItemManagerError::InvalidSale.into()), - }; - }, - } - entity_gateway.set_character_inventory(&character.id, &inventory.as_inventory_entity(&character.id)).await?; - entity_gateway.save_character(character).await?; - } else { - return Err(ItemManagerError::WalletFull.into()); + match item_sold { + InventoryItem::Individual(i) => { + entity_gateway.add_item_note(&i.entity_id, ItemNote::SoldToShop).await?; + inventory.remove_by_id(item_id).ok_or(ItemManagerError::NoSuchItemId(item_id))?; + }, + InventoryItem::Stacked(s) => { + match amount.cmp(&s.count()) { + Ordering::Less | Ordering::Equal => { + sold_item_handle.consume(amount)?; + }, + Ordering::Greater => return Err(ItemManagerError::InvalidSale.into()), + }; + }, } + entity_gateway.set_character_inventory(&character.id, &inventory.as_inventory_entity(&character.id)).await?; + let character_meseta = self.get_character_meseta_mut(&character.id)?; + character_meseta.0 += total_sale; + entity_gateway.set_character_meseta(&character.id, *character_meseta).await?; } } else { return Err(ItemManagerError::ItemIdNotInInventory(item_id).into()) @@ -954,11 +997,376 @@ impl ItemManager { inventory.add_item(InventoryItem::Individual(IndividualInventoryItem { entity_id, item_id, - item: ItemDetail::Weapon(weapon), - }))?; + item: ItemDetail::Weapon(weapon.clone()), + })); entity_gateway.set_character_inventory(&character.id, &inventory.as_inventory_entity(&character.id)).await?; Ok(weapon) } + + pub async fn trade_items(&mut self, + entity_gateway: &mut EG, + room_id: RoomId, + 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> { + 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.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 + item_slots_lost_to_trade) { + 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) => { + if acc >= (30 + item_slots_lost_to_trade) { + Err(TradeError::NoInventorySpace) + } + else { + Ok(acc + 1) + } + }, + } + } + } + }) + }) + .collect::, _>>()?; + + 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)?(); + Some(( + Some(ItemToTrade { + add_to: *dest_client.0, + remove_from: *src_client.0, + current_item_id: *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, + }), + )) + }, + TradeItem::Stacked(item_id, amount) => { + let item = src_inventory.get_item_by_id(*item_id)?.stacked()?; + if item.count() < *amount { + 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, + current_item_id: *item_id, + new_item_id, + item_detail: ItemToTradeDetail::Stacked(item.tool, *amount) + }), + 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, + 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::>>() + }); + + + 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().chain(p2_item_actions.into_iter()); + + 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()) + } +} + +#[derive(Debug)] +pub enum ItemToTradeDetail { + Individual(ItemDetail), + Stacked(Tool, usize), + Meseta(usize), +} + +#[derive(Debug)] +pub struct ItemToTrade { + pub add_to: AreaClient, + pub remove_from: AreaClient, + pub current_item_id: ClientItemId, + pub new_item_id: ClientItemId, + pub item_detail: ItemToTradeDetail, +} + + +#[derive(Debug)] +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(()) + } +} + + +#[derive(Debug)] +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(()) + } +} + + +#[derive(Debug)] +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.add_item_note( + &self.item.entity_id, + ItemNote::Pickup { + 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(()) + } +} + + +#[derive(Debug)] +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(()) + } +} + + +#[derive(Debug)] +struct AddMesetaFloorItemToInventory{ + 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 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(()) + } +} + + +#[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 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?; + + 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_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(()) + } +} + +#[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 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?; + + 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_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(()) + } +} + +#[derive(Debug)] +struct TradeMeseta { + src_character_id: CharacterEntityId, + dest_character_id: CharacterEntityId, + amount: usize, +} + +#[async_trait::async_trait] +impl ItemAction for TradeMeseta { + 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/items/mod.rs b/src/ship/items/mod.rs index d70a7f4..0d76eb3 100644 --- a/src/ship/items/mod.rs +++ b/src/ship/items/mod.rs @@ -2,10 +2,11 @@ mod bank; mod floor; pub mod inventory; mod manager; +pub 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/items/transaction.rs b/src/ship/items/transaction.rs new file mode 100644 index 0000000..293a8de --- /dev/null +++ b/src/ship/items/transaction.rs @@ -0,0 +1,337 @@ +use crate::entity::gateway::EntityGateway; +use thiserror::Error; +use crate::ship::items::manager::{ItemManager, ItemManagerError}; +use crate::entity::gateway::GatewayError; + +#[derive(Error, Debug)] +pub enum TransactionCommitError { + #[error("transaction commit gateway error {0}")] + Gateway(#[from] GatewayError), + #[error("transaction commit itemmanager error {0}")] + ItemManager(#[from] ItemManagerError), +} + +#[async_trait::async_trait] +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>; +} + +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(&mut self, action: Box>) { + self.action_queue.push(action) + } +} + + +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) -> 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(), + } + } + } + } +} + + +#[derive(Error, Debug)] +pub enum TransactionError { + #[error("transaction action error {0:?}")] + Action(E), + #[error("transaction commit error {0}")] + 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 + 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() { + #[derive(Debug)] + struct DummyAction1 { + name: String, + } + #[derive(Debug)] + 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(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) + } + 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() { + #[derive(Debug)] + struct DummyAction1 { + } + #[derive(Debug)] + 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(Box::new(DummyAction1 {})); + it.action(Box::new(DummyAction2 {})); + it.action(Box::new(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() { + #[derive(Debug)] + struct DummyAction1 { + } + #[derive(Debug)] + 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(Box::new(DummyAction1 {})); + it.action(Box::new(DummyAction2 {})); + it.action(Box::new(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))))); + } +} + diff --git a/src/ship/location.rs b/src/ship/location.rs index a0c05f5..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), @@ -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, 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/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/builder/message.rs b/src/ship/packet/builder/message.rs index b7d7f97..186e637 100644 --- a/src/ship/packet/builder/message.rs +++ b/src/ship/packet/builder/message.rs @@ -27,7 +27,8 @@ pub fn item_drop(client: u8, target: u8, item_drop: &FloorItem) -> Result Result { +// TODO: this doesn't need to be a Result, just unwrap try_intos they are guaranteed to succeed +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 +40,31 @@ pub fn create_item(area_client: AreaClient, item_id: ClientItemId, item: &item:: }) } +// 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 { + 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_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 { @@ -142,6 +168,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/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/builder/trade.rs b/src/ship/packet/builder/trade.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/ship/packet/handler/direct_message.rs b/src/ship/packet/handler/direct_message.rs index 2268bb7..fcceb43 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)?), }; @@ -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() * buy_item.amount as usize) as u32 { + let character_meseta = item_manager.get_character_meseta_mut(&client.character.id)?; + if character_meseta.0 < (item.price() * buy_item.amount as usize) as u32 { return Err(ShipError::ShopError.into()) } - client.character.meseta -= (item.price() * buy_item.amount as usize) as u32; - entity_gateway.save_character(&client.character).await?; + character_meseta.0 -= (item.price() * buy_item.amount as usize) 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)?; @@ -446,8 +457,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)?; @@ -479,7 +491,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() 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 1ded48e..1f405e4 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?; @@ -181,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; @@ -245,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?; - Ok(Box::new(None.into_iter())) // TODO: should probably tell other players we used charge and lost money? + 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()) } } @@ -280,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/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..7792b4e --- /dev/null +++ b/src/ship/packet/handler/trade.rs @@ -0,0 +1,544 @@ +use std::convert::TryInto; +use libpso::packet::ship::*; +use libpso::packet::messages::*; +use crate::common::serverstate::ClientId; +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::ship::packet::builder; + +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")] + CouldNotFindTradePartner, + #[error("invalid item id")] + InvalidItemId(ClientItemId), + #[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, + #[error("no space in inventory")] + NoInventorySpace, + #[error("no space in stack")] + 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, +} + + +// TODO: remove target +pub async fn trade_request(id: ClientId, + trade_request: &TradeRequest, + target: u32, + client_location: &ClientLocation, + clients: &mut Clients, + item_manager: &mut ItemManager, + trades: &mut TradeState) + -> Result + Send>, anyhow::Error> +{ + 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 => { + if trades.in_trade(&id) { + return Err(TradeError::ClientAlreadyInTrade.into()) + } + let trade_partner = client_location.get_client_neighbors(id)? + .into_iter() + .find(|ac| { + ac.local_client.id() == target as u8 //trade_request.client + }) + .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) + .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| -> Result + Send>, anyhow::Error> { + if this.status == TradeStatus::Trading && other.status == TradeStatus::Trading { + 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; + } + 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)); + }, + } + } + + let trade_request = trade_request.clone(); + 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 { + Err(TradeError::MismatchedStatus.into()) + } + })? + .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) + .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()?; + 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) + }) + }, + 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_mut()?.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 { + 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 status_is(&this.status, &[TradeStatus::Trading]) && status_is(&other.status, &[TradeStatus::Trading, 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 {}))))) + })) + }, + 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 {})))))) + } + } +} + + +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, + 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> { + if status_is_not(&this.status, &[TradeStatus::FinalConfirm]) || status_is_not(&other.status, &[TradeStatus::FinalConfirm, TradeStatus::ItemsChecked]) { + return Err(TradeError::MismatchedStatus.into()) + } + + 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 })) { + 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); + 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(()) + } + 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(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) => { + if real_item.as_client_bytes() == trade_item_bytes { + Ok(()) + } + 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() { + if real_trade_item.stacked().ok_or(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()) + } + } + else { + Err(TradeError::ClientItemIdDidNotMatchItem(ClientItemId(item.item_id), trade_item_bytes).into()) + } + } + } + } + }) + .collect::, anyhow::Error>>()?; + + this.status = TradeStatus::ItemsChecked; + 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); + 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, + 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, 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); + 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 {})))))) + } + } +} + +pub async fn trade_confirmed(id: ClientId, + entity_gateway: &mut EG, + client_location: &ClientLocation, + clients: &mut Clients, + item_manager: &mut ItemManager, + trades: &mut TradeState) + -> Result + Send>, anyhow::Error> +where + EG: EntityGateway +{ + 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 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()) + } + this.status = TradeStatus::TradeComplete; + + if this.status == TradeStatus::TradeComplete && other.status == TradeStatus::TradeComplete { + 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() })?; + + 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, 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 + .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)) + ] + }, + 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() + .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))) + } + } + }, + _ => { + 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::CancelTrade(CancelTrade {})) + }) + .chain(std::iter::once((id, SendShipPacket::CancelTrade(CancelTrade {})))))) + } + } +} diff --git a/src/ship/ship.rs b/src/ship/ship.rs index 7e79552..98b26ca 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,11 +42,12 @@ pub type Rooms = [Option; MAX_ROOMS]; pub type Clients = HashMap; #[derive(Error, Debug)] -#[error("")] +#[error("shiperror {0:?}")] pub enum ShipError { ClientNotFound(ClientId), NoCharacterInSlot(ClientId, u32), InvalidSlot(ClientId, u32), + #[error("")] TooManyClients, ClientLocationError(#[from] ClientLocationError), GetNeighborError(#[from] GetNeighborError), @@ -56,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), @@ -67,12 +71,15 @@ 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), } #[derive(Debug)] @@ -106,6 +113,8 @@ pub enum RecvShipPacket { SaveOptions(SaveOptions), RequestShipList(RequestShipList), RequestShipBlockList(RequestShipBlockList), + ItemsToTrade(ItemsToTrade), + TradeConfirmed(TradeConfirmed), } impl RecvServerPacket for RecvShipPacket { @@ -143,6 +152,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 +195,9 @@ pub enum SendShipPacket { DoneLoadingQuest(DoneLoadingQuest), BankItemList(BankItemList), RedirectClient(RedirectClient), + AcknowledgeTrade(AcknowledgeTrade), + CancelTrade(CancelTrade), + TradeSuccessful(TradeSuccessful), } impl SendServerPacket for SendShipPacket { @@ -221,6 +235,9 @@ 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(), + SendShipPacket::TradeSuccessful(pkt) => pkt.as_bytes(), } } } @@ -239,6 +256,12 @@ pub struct LoadingQuest { //pub quest_chunk_bin: Option>>, } + + + + + + pub struct ClientState { pub user: UserAccountEntity, pub settings: UserSettingsEntity, @@ -380,6 +403,7 @@ impl ShipServerStateBuilder { auth_token: self.auth_token.unwrap_or_else(|| AuthToken("".into())), ship_list: Vec::new(), shipgate_sender: None, + trades: Default::default(), } } } @@ -426,6 +450,7 @@ pub struct ShipServerState { auth_token: AuthToken, ship_list: Vec, shipgate_sender: Option>, + trades: TradeState, } impl ShipServerState { @@ -463,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)?; @@ -533,6 +558,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, &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() @@ -699,7 +727,15 @@ impl ServerState for ShipServerState { }, RecvShipPacket::RequestShipBlockList(_) => { handler::ship::block_list(id, &self.name, self.blocks.0.len()) - } + }, + RecvShipPacket::ItemsToTrade(items_to_trade) => { + let block = self.blocks.with_client(id, &self.clients)?; + 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)?; + 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/shops/weapon.rs b/src/ship/shops/weapon.rs index 1f53cf7..14509c8 100644 --- a/src/ship/shops/weapon.rs +++ b/src/ship/shops/weapon.rs @@ -336,7 +336,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, @@ -350,7 +350,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(), @@ -379,8 +379,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 new file mode 100644 index 0000000..9f600a8 --- /dev/null +++ b/src/ship/trade.rs @@ -0,0 +1,133 @@ +use std::collections::HashMap; +use std::cell::RefCell; + +use crate::common::serverstate::ClientId; +use crate::ship::items; + +#[derive(Debug, Clone)] +pub enum TradeItem { + Individual(items::ClientItemId), + Stacked(items::ClientItemId, usize), +} + +impl TradeItem { + pub fn stacked(&self) -> Option<(items::ClientItemId, usize)> { + match self { + TradeItem::Stacked(item_id, amount) => Some((*item_id, *amount)), + _ => None + } + } + + 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, + } + } +} + + +#[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 meseta: usize, + pub status: TradeStatus, +} + + +impl ClientTradeState { + pub fn client(&self) -> ClientId { + self.client + } + + pub fn other_client(&self) -> ClientId { + self.other_client + } +} + +#[derive(thiserror::Error, Debug)] +#[error("")] +pub enum TradeStateError { + ClientNotInTrade(ClientId), + MismatchedTrade(ClientId, ClientId), +} + +#[derive(Default, Debug)] +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(), + meseta: 0, + status: TradeStatus::SentRequest, + }; + self.trades.insert(*sender, RefCell::new(state)); + + let state = ClientTradeState { + client: *receiver, + other_client: *sender, + items: Default::default(), + meseta: 0, + status: TradeStatus::ReceivedRequest, + }; + 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 + { + 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(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) + } +} 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) } diff --git a/tests/test_bank.rs b/tests/test_bank.rs index c0a1d64..605a3aa 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()); @@ -860,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()) @@ -887,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()) @@ -925,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()) @@ -964,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); } @@ -990,10 +911,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 +970,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 +1030,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 +1097,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 +1106,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 +1169,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 +1181,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 +1244,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 +1259,6 @@ async fn test_withdraw_individual_item_in_full_inventory() { tekked: true, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } @@ -1423,10 +1311,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 +1327,6 @@ async fn test_withdraw_stacked_item_in_full_inventory() { tekked: true, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } @@ -1504,10 +1385,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 +1402,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 +1414,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)); @@ -1589,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()) @@ -1616,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()) @@ -1654,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()) @@ -1692,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_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 e54f0ec..7feb377 100644 --- a/tests/test_item_pickup.rs +++ b/tests/test_item_pickup.rs @@ -10,6 +10,77 @@ 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, + } + ), + }).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(); @@ -25,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(); @@ -41,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); @@ -107,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(); @@ -159,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 { @@ -174,16 +236,11 @@ async fn test_pick_up_meseta_when_inventory_full() { tekked: true, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } 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()) @@ -225,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] @@ -253,9 +308,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()); } @@ -266,9 +318,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(); @@ -279,9 +328,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(); @@ -345,9 +391,6 @@ async fn test_can_not_pick_up_item_when_inventory_full() { tekked: true, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } @@ -363,9 +406,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(); @@ -425,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()) @@ -457,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] @@ -478,9 +516,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()); } @@ -493,9 +528,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(); @@ -546,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()) @@ -592,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()) @@ -649,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] @@ -673,9 +697,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()); } @@ -732,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_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 3555c8c..987f43c 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_eq!(c1.meseta, 999749); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 999749); 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); } @@ -279,9 +275,6 @@ async fn test_player_sells_3_attr_weapon_to_shop() { 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(); @@ -301,9 +294,8 @@ async fn test_player_sells_3_attr_weapon_to_shop() { amount: 1, })))).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_eq!(c1.meseta, 4406); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 4406); } #[async_std::test] @@ -313,7 +305,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() @@ -356,8 +348,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( @@ -365,9 +357,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() @@ -432,9 +421,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); } @@ -445,8 +433,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()) @@ -488,9 +476,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| { @@ -511,8 +498,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()) @@ -573,8 +560,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()) @@ -649,9 +636,6 @@ async fn test_player_sells_untekked_weapon() { tekked: false, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); @@ -671,9 +655,8 @@ async fn test_player_sells_untekked_weapon() { amount: 1, })))).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_eq!(c1.meseta, 1); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 1); } #[async_std::test] @@ -697,9 +680,6 @@ async fn test_player_sells_rare_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(); @@ -719,9 +699,8 @@ async fn test_player_sells_rare_item() { amount: 1, })))).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_eq!(c1.meseta, 10); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 10); } #[async_std::test] @@ -741,9 +720,6 @@ async fn test_player_sells_partial_photon_drop_stack() { tool: item::tool::ToolType::PhotonDrop, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); } @@ -766,9 +742,8 @@ async fn test_player_sells_partial_photon_drop_stack() { amount: 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(); - assert_eq!(c1.meseta, 3000); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 3000); } #[async_std::test] @@ -789,9 +764,6 @@ async fn test_player_sells_basic_frame() { slots: 0, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); @@ -811,9 +783,8 @@ async fn test_player_sells_basic_frame() { amount: 1, })))).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_eq!(c1.meseta, 24); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 24); } #[async_std::test] @@ -834,9 +805,6 @@ async fn test_player_sells_max_frame() { slots: 4, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); @@ -856,9 +824,8 @@ async fn test_player_sells_max_frame() { amount: 1, })))).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_eq!(c1.meseta, 74); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 74); } #[async_std::test] @@ -878,9 +845,6 @@ async fn test_player_sells_basic_barrier() { evp: 0, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); @@ -900,9 +864,8 @@ async fn test_player_sells_basic_barrier() { amount: 1, })))).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_eq!(c1.meseta, 69); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 69); } #[async_std::test] @@ -922,9 +885,6 @@ async fn test_player_sells_max_barrier() { evp: 5, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); @@ -944,9 +904,8 @@ async fn test_player_sells_max_barrier() { amount: 1, })))).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_eq!(c1.meseta, 122); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 122); } #[async_std::test] @@ -965,9 +924,6 @@ async fn test_player_sells_1_star_minusminus_unit() { modifier: Some(item::unit::UnitModifier::MinusMinus), } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); @@ -987,9 +943,8 @@ async fn test_player_sells_1_star_minusminus_unit() { amount: 1, })))).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_eq!(c1.meseta, 125); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 125); } #[async_std::test] @@ -1008,9 +963,6 @@ async fn test_player_sells_5_star_plusplus_unit() { modifier: Some(item::unit::UnitModifier::PlusPlus), } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); @@ -1030,9 +982,8 @@ async fn test_player_sells_5_star_plusplus_unit() { amount: 1, })))).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_eq!(c1.meseta, 625); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 625); } #[async_std::test] @@ -1053,9 +1004,6 @@ async fn test_player_sells_rare_frame() { slots: 3, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); @@ -1075,9 +1023,8 @@ async fn test_player_sells_rare_frame() { amount: 1, })))).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_eq!(c1.meseta, 10); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 10); } #[async_std::test] @@ -1097,9 +1044,6 @@ async fn test_player_sells_rare_barrier() { evp: 20, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); @@ -1119,9 +1063,8 @@ async fn test_player_sells_rare_barrier() { amount: 1, })))).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_eq!(c1.meseta, 10); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 10); } #[async_std::test] @@ -1140,9 +1083,6 @@ async fn test_player_sells_rare_unit() { modifier: None, } ), - location: item::ItemLocation::Inventory { - character_id: char1.id, - } }).await.unwrap()); entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); @@ -1162,7 +1102,6 @@ async fn test_player_sells_rare_unit() { amount: 1, })))).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_eq!(c1.meseta, 10); + let c1_meseta = entity_gateway.get_character_meseta(&char1.id).await.unwrap(); + assert_eq!(c1_meseta.0, 10); } diff --git a/tests/test_trade.rs b/tests/test_trade.rs new file mode 100644 index 0000000..2a1d5d8 --- /dev/null +++ b/tests/test_trade.rs @@ -0,0 +1,4382 @@ +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::>(); + 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); +} + +#[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); +}