diff --git a/src/entity/item/mod.rs b/src/entity/item/mod.rs index a594d3a..165f207 100644 --- a/src/entity/item/mod.rs +++ b/src/entity/item/mod.rs @@ -46,7 +46,8 @@ pub enum ItemLocation { Consumed, FedToMag { mag: ItemEntityId, - } + }, + Shop, /*Destroyed { // marks an item that has been consumed in some way }, diff --git a/src/ship/items/manager.rs b/src/ship/items/manager.rs index 27ab699..4aa5a40 100644 --- a/src/ship/items/manager.rs +++ b/src/ship/items/manager.rs @@ -10,6 +10,7 @@ use crate::ship::map::MapArea; use crate::ship::ship::ItemDropLocation; use crate::ship::drops::{ItemDrop, ItemDropType}; use crate::ship::location::{AreaClient, RoomId}; +use crate::ship::shops::ShopItem; use crate::ship::items::bank::*; use crate::ship::items::floor::*; @@ -39,6 +40,9 @@ pub enum ItemManagerError { BankFull, WrongItemType(ClientItemId), UseItemError(#[from] use_tool::UseItemError), + CouldNotBuyItem, + CouldNotAddBoughtItemToInventory, + ItemIdNotInInventory(ClientItemId) } pub struct ItemManager { @@ -752,4 +756,107 @@ impl ItemManager { } Ok(()) } + + pub async fn player_buys_item(&mut self, + entity_gateway: &mut EG, + character: &CharacterEntity, + shop_item: &(dyn ShopItem + Send + Sync), + item_id: ClientItemId, + amount: usize) + -> Result<(&InventoryItem), ItemManagerError> { + let inventory = self.character_inventory.get_mut(&character.id).ok_or(ItemManagerError::NoCharacter(character.id))?; + + let item_detail = shop_item.as_item(); + let inventory_item = match item_detail { + ItemDetail::Tool(tool) => { + 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, + item: ItemDetail::Tool(tool), + }).await.ok_or(ItemManagerError::EntityGatewayError)?); + } + let floor_item = StackedFloorItem { + entity_ids: item_entities.into_iter().map(|i| i.id).collect(), + item_id: item_id, + tool: tool, + // TODO: this is gonna choke if I ever require the item being near the player for pickup + map_area: MapArea::Pioneer2Ep1, + x: 0.0, + y: 0.0, + z: 0.0, + }; + 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, + slot: slot.0, + equipped: false, + }).await;//.ok_or(ItemManagerError::EntityGatewayError)?; + } + 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.ok_or(ItemManagerError::EntityGatewayError)?; + let floor_item = IndividualFloorItem { + entity_id: item_entity.id, + item_id: item_id, + item: ItemDetail::Tool(tool), + // TODO: this is gonna choke if I ever require the item being near the player for pickup + map_area: MapArea::Pioneer2Ep1, + x: 0.0, + y: 0.0, + z: 0.0, + }; + 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, + slot: slot.0, + equipped: false, + }).await;//.ok_or(ItemManagerError::EntityGatewayError)?; + picked_up_item.item_id + }; + inventory.get_item_by_id(item_id).ok_or(ItemManagerError::ItemIdNotInInventory(item_id))? + } + }, + item_detail @ _ => { + let item_entity = entity_gateway.create_item(NewItemEntity { + location: ItemLocation::Shop, + item: item_detail.clone(), + }).await.ok_or(ItemManagerError::EntityGatewayError)?; + let floor_item = IndividualFloorItem { + entity_id: item_entity.id, + item_id: item_id, + item: item_detail, + // TODO: this is gonna choke if I ever require the item being near the player for pickup + map_area: MapArea::Pioneer2Ep1, + x: 0.0, + y: 0.0, + z: 0.0, + }; + 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, + slot: slot.0, + equipped: false, + }).await;//.ok_or(ItemManagerError::EntityGatewayError)?; + picked_up_item.item_id + }; + inventory.get_item_by_id(item_id).ok_or(ItemManagerError::ItemIdNotInInventory(item_id))? + }, + }; + Ok(inventory_item) + } } diff --git a/src/ship/packet/builder/message.rs b/src/ship/packet/builder/message.rs index 69ad26c..d7bfe75 100644 --- a/src/ship/packet/builder/message.rs +++ b/src/ship/packet/builder/message.rs @@ -5,6 +5,7 @@ use crate::ship::ship::{ShipError}; use crate::ship::items::{ClientItemId, InventoryItem, StackedFloorItem, FloorItem, CharacterBank}; use crate::ship::location::AreaClient; use std::convert::TryInto; +use crate::ship::shops::ShopItem; pub fn item_drop(client: u8, target: u8, item_drop: &FloorItem) -> Result { @@ -139,3 +140,25 @@ pub fn player_no_longer_has_item(area_client: AreaClient, item_id: ClientItemId, amount: amount, } } + +pub fn shop_list(shop_type: u8, items: &Vec) -> ShopList { + let items = items.into_iter() + .enumerate() + .map(|(i, item)| { + ShopListItem { + item_bytes: item.as_bytes(), + unknown: i as u32 + 23, + price: item.price() as u32, + } + }) + .collect(); + + ShopList { + client: 0, + target: 0, + shop_type: shop_type, + num_items: 0, + unused: 0, + items: items, + } +} diff --git a/src/ship/packet/handler/direct_message.rs b/src/ship/packet/handler/direct_message.rs index 18edad8..8c2dd21 100644 --- a/src/ship/packet/handler/direct_message.rs +++ b/src/ship/packet/handler/direct_message.rs @@ -1,17 +1,24 @@ use log::warn; use libpso::packet::ship::*; use libpso::packet::messages::*; +use crate::common::leveltable::CharacterLevelTable; use crate::common::serverstate::ClientId; -use crate::ship::ship::{SendShipPacket, ShipError, Clients, Rooms}; +use crate::ship::ship::{SendShipPacket, ShipError, Clients, Rooms, ItemShops}; use crate::ship::location::{ClientLocation, ClientLocationError}; use crate::ship::drops::ItemDrop; use crate::ship::items::{ItemManager, ClientItemId, TriggerCreateItem, FloorItem}; use crate::entity::gateway::EntityGateway; use libpso::utf8_to_utf16_array; use crate::ship::packet::builder; +use crate::ship::shops::ShopItem; const BANK_ACTION_DEPOSIT: u8 = 0; const BANK_ACTION_WITHDRAW: u8 = 1; + +const SHOP_OPTION_TOOL: u8 = 0; +const SHOP_OPTION_WEAPON: u8 = 1; +const SHOP_OPTION_ARMOR: u8 = 2; + //const BANK_ACTION_: u8 = 1; fn send_to_client(id: ClientId, target: u8, msg: DirectMessage, client_location: &ClientLocation) @@ -269,3 +276,90 @@ where .flatten() )) } + +pub async fn shop_request(id: ClientId, + shop_request: &ShopRequest, + client_location: &ClientLocation, + clients: &mut Clients, + rooms: &Rooms, + level_table: &CharacterLevelTable, + shops: &mut ItemShops) + -> Result + Send>, ShipError> +{ + let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; + let room_id = client_location.get_room(id).map_err(|err| -> ClientLocationError { err.into() })?; + let room = rooms.get(room_id.0) + .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))? + .as_ref() + .ok_or_else(|| ShipError::InvalidRoom(room_id.0 as u32))?; + let level = level_table.get_level_from_exp(client.character.char_class, client.character.exp) as usize; + let shop_list = match shop_request.shop_type { + SHOP_OPTION_WEAPON => { + client.weapon_shop = shops.weapon_shop.get_mut(&(room.mode.difficulty(), client.character.section_id)) + .ok_or(ShipError::ShopError)? + .generate_weapon_list(level); + builder::message::shop_list(shop_request.shop_type, &client.weapon_shop) + }, + SHOP_OPTION_TOOL => { + client.tool_shop = shops.tool_shop.generate_tool_list(level); + builder::message::shop_list(shop_request.shop_type, &client.tool_shop) + }, + SHOP_OPTION_ARMOR => { + client.armor_shop = shops.armor_shop.generate_armor_list(level); + builder::message::shop_list(shop_request.shop_type, &client.armor_shop) + }, + _ => { + return Err(ShipError::ShopError) + } + }; + + Ok(Box::new(vec![(id, SendShipPacket::Message(Message::new(GameMessage::ShopList(shop_list))))].into_iter())) +} + + +pub async fn buy_item(id: ClientId, + buy_item: &BuyItem, + entity_gateway: &mut EG, + client_location: &ClientLocation, + clients: &mut Clients, + item_manager: &mut ItemManager) + -> Result + Send>, ShipError> +where + EG: EntityGateway +{ + let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; + let area_client = client_location.get_local_client(id).map_err(|err| -> ClientLocationError { err.into() })?; + + let item: &(dyn ShopItem + Send + Sync) = match buy_item.shop_type { + SHOP_OPTION_WEAPON => { + client.weapon_shop.get(buy_item.shop_index as usize).ok_or(ShipError::ShopError)? + }, + SHOP_OPTION_TOOL => { + client.tool_shop.get(buy_item.shop_index as usize).ok_or(ShipError::ShopError)? + }, + SHOP_OPTION_ARMOR => { + client.armor_shop.get(buy_item.shop_index as usize).ok_or(ShipError::ShopError)? + }, + _ => { + return Err(ShipError::ShopError) + } + }; + + if client.character.meseta < item.price() as u32{ + return Err(ShipError::ShopError) + } + + client.character.meseta -= item.price() as u32; + entity_gateway.save_character(&client.character).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)?; + + let other_clients_in_area = client_location.get_client_neighbors(id).map_err(|err| -> ClientLocationError { err.into() })?; + Ok(Box::new(other_clients_in_area.into_iter() + .map(move |c| { + (c.client, SendShipPacket::Message(Message::new(GameMessage::CreateItem(create.clone())))) + }))) + +} diff --git a/src/ship/room.rs b/src/ship/room.rs index 7ab224f..c2d1c8a 100644 --- a/src/ship/room.rs +++ b/src/ship/room.rs @@ -60,7 +60,7 @@ impl Episode { } } -#[derive(Debug, Copy, Clone, derive_more::Display)] +#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, derive_more::Display)] pub enum Difficulty { Normal, Hard, diff --git a/src/ship/ship.rs b/src/ship/ship.rs index 643d662..e654bc3 100644 --- a/src/ship/ship.rs +++ b/src/ship/ship.rs @@ -20,7 +20,7 @@ use crate::common::interserver::{AuthToken, Ship, ServerId, InterserverActor, Lo use crate::entity::gateway::EntityGateway; use crate::entity::account::{UserAccountEntity, UserSettingsEntity}; -use crate::entity::character::CharacterEntity; +use crate::entity::character::{CharacterEntity, SectionID}; use crate::ship::location::{ClientLocation, RoomLobby, MAX_ROOMS, ClientLocationError}; @@ -29,6 +29,7 @@ use crate::ship::room; use crate::ship::quests; use crate::ship::map::{MapsError, MapAreaError, MapArea}; use crate::ship::packet::handler; +use crate::ship::shops::{ShopType, WeaponShop, ToolShop, ArmorShop, WeaponShopItem, ToolShopItem, ArmorShopItem}; pub const SHIP_PORT: u16 = 23423; pub const QUEST_CATEGORY_MENU_ID: u32 = 0xA2; @@ -60,6 +61,7 @@ pub enum ShipError { InvalidQuestFilename(String), IoError(#[from] std::io::Error), NotEnoughMeseta(ClientId, u32), + ShopError, } #[derive(Debug)] @@ -228,6 +230,9 @@ pub struct ClientState { pub x: f32, pub y: f32, pub z: f32, + pub weapon_shop: Vec, + pub tool_shop: Vec, + pub armor_shop: Vec, } impl ClientState { @@ -244,10 +249,41 @@ impl ClientState { x: 0.0, y: 0.0, z: 0.0, + weapon_shop: Vec::new(), + tool_shop: Vec::new(), + armor_shop: Vec::new(), } } } +pub struct ItemShops { + pub weapon_shop: HashMap<(room::Difficulty, SectionID), WeaponShop>, + pub tool_shop: ToolShop, + pub armor_shop: ArmorShop, +} + +impl ItemShops { + pub fn new() -> ItemShops { + let difficulty = [room::Difficulty::Normal, room::Difficulty::Hard, room::Difficulty::VeryHard, room::Difficulty::Ultimate]; + let section_id = [SectionID::Viridia, SectionID::Greenill, SectionID::Skyly, SectionID::Bluefull, SectionID::Purplenum, + SectionID::Pinkal, SectionID::Redria, SectionID::Oran, SectionID::Yellowboze, SectionID::Whitill]; + + let mut weapon_shop = HashMap::new(); + for d in difficulty.iter() { + for id in section_id.iter() { + weapon_shop.insert((*d, *id), WeaponShop::new(*d, *id)); + } + } + + ItemShops { + weapon_shop: weapon_shop, + tool_shop: ToolShop::new(), + armor_shop: ArmorShop::new(), + } + } +} + + pub struct ShipServerStateBuilder { entity_gateway: Option, name: Option, @@ -297,6 +333,7 @@ impl ShipServerStateBuilder { quests: quests::load_quests("data/quests.toml".into()).unwrap(), ip: self.ip.unwrap_or(Ipv4Addr::new(127,0,0,1)), port: self.port.unwrap_or(SHIP_PORT), + shops: Box::new(ItemShops::new()), } } } @@ -312,6 +349,7 @@ pub struct ShipServerState { quests: quests::QuestList, ip: Ipv4Addr, port: u16, + shops: Box, } impl ShipServerState { @@ -382,6 +420,12 @@ impl ShipServerState { GameMessage::BankInteraction(bank_interaction) => { handler::direct_message::bank_interaction(id, bank_interaction, &mut self.entity_gateway, &self.client_location, &mut self.clients, &mut self.item_manager).await }, + GameMessage::ShopRequest(shop_request) => { + handler::direct_message::shop_request(id, shop_request, &self.client_location, &mut self.clients, &self.rooms, &self.level_table, &mut self.shops).await + }, + GameMessage::BuyItem(buy_item) => { + handler::direct_message::buy_item(id, buy_item, &mut self.entity_gateway, &self.client_location, &mut self.clients, &mut self.item_manager).await + }, _ => { let cmsg = msg.clone(); Ok(Box::new(self.client_location.get_all_clients_by_client(id).unwrap().into_iter() diff --git a/src/ship/shops/armor.rs b/src/ship/shops/armor.rs index 79dade5..657b732 100644 --- a/src/ship/shops/armor.rs +++ b/src/ship/shops/armor.rs @@ -2,20 +2,22 @@ use std::collections::HashMap; use std::fs::File; use std::io::Read; use std::path::PathBuf; +use std::convert::TryInto; use serde::Deserialize; use rand::{Rng, SeedableRng}; use rand::distributions::{WeightedIndex, Distribution}; use rand::seq::{SliceRandom, IteratorRandom}; use crate::entity::character::SectionID; use crate::ship::room::Difficulty; -use crate::entity::item::armor::ArmorType; -use crate::entity::item::shield::ShieldType; -use crate::entity::item::unit::UnitType; +use crate::entity::item::ItemDetail; +use crate::entity::item::armor::{Armor, ArmorType}; +use crate::entity::item::shield::{Shield, ShieldType}; +use crate::entity::item::unit::{Unit, UnitType}; use crate::ship::shops::ShopItem; use crate::ship::item_stats::{ARMOR_STATS, SHIELD_STATS, UNIT_STATS}; #[derive(Debug)] -enum ShopArmor { +pub enum ArmorShopItem { Frame(ArmorType, usize), Barrier(ShieldType), Unit(UnitType), @@ -25,10 +27,10 @@ const ARMOR_MULTIPLIER: f32 = 0.799999952; const SHIELD_MULTIPLIER: f32 = 1.5; const UNIT_MULTIPLIER: f32 = 1000.0; -impl ShopItem for ShopArmor { +impl ShopItem for ArmorShopItem { fn price(&self) -> usize { match self { - ShopArmor::Frame(frame, slot) => { + ArmorShopItem::Frame(frame, slot) => { ARMOR_STATS.get(&frame) .map(|frame_stats| { let mut price = (frame_stats.dfp + frame_stats.evp) as f32; @@ -40,7 +42,7 @@ impl ShopItem for ShopArmor { }) .unwrap_or(0xFFFF) }, - ShopArmor::Barrier(barrier) => { + ArmorShopItem::Barrier(barrier) => { SHIELD_STATS.get(&barrier) .map(|barrier_stats| { let mut price = (barrier_stats.dfp + barrier_stats.evp) as f32; @@ -51,7 +53,7 @@ impl ShopItem for ShopArmor { }) .unwrap_or(0xFFFF) }, - ShopArmor::Unit(unit) => { + ArmorShopItem::Unit(unit) => { UNIT_STATS.get(&unit) .map(|unit_stats| { (unit_stats.stars as f32 * UNIT_MULTIPLIER) as usize @@ -60,6 +62,37 @@ impl ShopItem for ShopArmor { } } } + + fn as_bytes(&self) -> [u8; 12] { + self.as_item().as_client_bytes()[0..12].try_into().unwrap() + } + + fn as_item(&self) -> ItemDetail { + match self { + ArmorShopItem::Frame(frame, slot) => { + ItemDetail::Armor(Armor { + armor: *frame, + dfp: 0, + evp: 0, + slots: *slot as u8, + modifiers: Vec::new() + }) + }, + ArmorShopItem::Barrier(barrier) => { + ItemDetail::Shield(Shield { + shield: *barrier, + dfp: 0, + evp: 0, + }) + }, + ArmorShopItem::Unit(unit) => { + ItemDetail::Unit(Unit { + unit: *unit, + modifier: None, + }) + }, + } + } } @@ -201,7 +234,7 @@ fn number_of_units_to_generate(character_level: usize) -> usize { } #[derive(Debug)] -struct ArmorShop { +pub struct ArmorShop { frame: FrameTable, barrier: BarrierTable, unit: UnitTable, @@ -218,7 +251,7 @@ impl ArmorShop { } } - fn generate_frame_list(&mut self, character_level: usize) -> Vec { + fn generate_frame_list(&mut self, character_level: usize) -> Vec { let tier = self.frame.frame.iter() .filter(|t| t.level <= character_level) .last() @@ -231,13 +264,13 @@ impl ArmorShop { .map(|_| { let frame_detail = tier.item.get(frame_choice.sample(&mut self.rng)).unwrap(); let slot = self.frame.slot_rate.get(slot_choice.sample(&mut self.rng)).unwrap(); - - ShopArmor::Frame(frame_detail.item, slot.slot) + + ArmorShopItem::Frame(frame_detail.item, slot.slot) }) .collect() } - fn generate_barrier_list(&mut self, character_level: usize) -> Vec { + fn generate_barrier_list(&mut self, character_level: usize) -> Vec { let tier = self.barrier.barrier.iter() .filter(|t| t.level <= character_level) .last() @@ -248,13 +281,13 @@ impl ArmorShop { (0..number_of_barriers_to_generate(character_level)) .map(|_| { let barrier_detail = tier.item.get(barrier_choice.sample(&mut self.rng)).unwrap(); - - ShopArmor::Barrier(barrier_detail.item) + + ArmorShopItem::Barrier(barrier_detail.item) }) .collect() } - fn generate_unit_list(&mut self, character_level: usize) -> Vec { + fn generate_unit_list(&mut self, character_level: usize) -> Vec { self.unit.unit.iter() .filter(|t| t.level <= character_level) .last() @@ -264,15 +297,15 @@ impl ArmorShop { (0..number_of_units_to_generate(character_level)) .map(|_| { let unit_detail = tier.item.get(unit_choice.sample(&mut self.rng)).unwrap(); - - ShopArmor::Unit(unit_detail.item) + + ArmorShopItem::Unit(unit_detail.item) }) .collect() }) .unwrap_or(Vec::new()) } - pub fn generate_armor_list(&mut self, character_level: usize) -> Vec { + pub fn generate_armor_list(&mut self, character_level: usize) -> Vec { self.generate_frame_list(character_level).into_iter() .chain(self.generate_barrier_list(character_level).into_iter()) .chain(self.generate_unit_list(character_level).into_iter()) diff --git a/src/ship/shops/mod.rs b/src/ship/shops/mod.rs index bfff30d..573b667 100644 --- a/src/ship/shops/mod.rs +++ b/src/ship/shops/mod.rs @@ -1,7 +1,21 @@ -pub mod weapon; -pub mod tool; -pub mod armor; +mod weapon; +mod tool; +mod armor; + +use crate::entity::item::ItemDetail; pub trait ShopItem { fn price(&self) -> usize; + fn as_bytes(&self) -> [u8; 12]; + fn as_item(&self) -> ItemDetail; +} + +pub enum ShopType { + Weapon, + Tool, + Armor } + +pub use weapon::{WeaponShop, WeaponShopItem}; +pub use tool::{ToolShop, ToolShopItem}; +pub use armor::{ArmorShop, ArmorShopItem}; diff --git a/src/ship/shops/tool.rs b/src/ship/shops/tool.rs index dbabc11..c3ecaa9 100644 --- a/src/ship/shops/tool.rs +++ b/src/ship/shops/tool.rs @@ -2,12 +2,14 @@ use std::collections::HashMap; use std::fs::File; use std::io::Read; use std::path::PathBuf; +use std::convert::TryInto; use serde::Deserialize; use rand::{Rng, SeedableRng}; use rand::distributions::{WeightedIndex, Distribution}; use rand::seq::{SliceRandom, IteratorRandom}; use crate::entity::character::SectionID; use crate::ship::room::Difficulty; +use crate::entity::item::ItemDetail; use crate::entity::item::tool::{Tool, ToolType}; use crate::entity::item::tech::{Technique, TechniqueDisk}; use crate::ship::shops::ShopItem; @@ -15,52 +17,52 @@ use crate::ship::item_stats::{TOOL_STATS, TECH_STATS}; #[derive(Debug, PartialEq, Eq)] -pub enum ShopTool { +pub enum ToolShopItem { Tool(ToolType), Tech(TechniqueDisk), } -impl Ord for ShopTool { - fn cmp(&self, other: &ShopTool) -> std::cmp::Ordering { +impl Ord for ToolShopItem { + fn cmp(&self, other: &ToolShopItem) -> std::cmp::Ordering { let a = match self { - ShopTool::Tool(t) => Tool{tool : *t}.as_individual_bytes(), - ShopTool::Tech(t) => t.as_bytes(), + ToolShopItem::Tool(t) => Tool{tool : *t}.as_individual_bytes(), + ToolShopItem::Tech(t) => t.as_bytes(), }; let b = match other { - ShopTool::Tool(t) => Tool{tool : *t}.as_individual_bytes(), - ShopTool::Tech(t) => t.as_bytes(), + ToolShopItem::Tool(t) => Tool{tool : *t}.as_individual_bytes(), + ToolShopItem::Tech(t) => t.as_bytes(), }; a.cmp(&b) } } -impl PartialOrd for ShopTool { - fn partial_cmp(&self, other: &ShopTool) -> Option { +impl PartialOrd for ToolShopItem { + fn partial_cmp(&self, other: &ToolShopItem) -> Option { let a = match self { - ShopTool::Tool(t) => Tool{tool : *t}.as_individual_bytes(), - ShopTool::Tech(t) => t.as_bytes(), + ToolShopItem::Tool(t) => Tool{tool : *t}.as_individual_bytes(), + ToolShopItem::Tech(t) => t.as_bytes(), }; let b = match other { - ShopTool::Tool(t) => Tool{tool : *t}.as_individual_bytes(), - ShopTool::Tech(t) => t.as_bytes(), + ToolShopItem::Tool(t) => Tool{tool : *t}.as_individual_bytes(), + ToolShopItem::Tech(t) => t.as_bytes(), }; a.partial_cmp(&b) } } -impl ShopItem for ShopTool { +impl ShopItem for ToolShopItem { fn price(&self) -> usize { match self { - ShopTool::Tool(tool) => { + ToolShopItem::Tool(tool) => { TOOL_STATS.get(&tool) .map(|tool_stats| { tool_stats.price }) .unwrap_or(0xFFFF) }, - ShopTool::Tech(tech) => { + ToolShopItem::Tech(tech) => { TECH_STATS.get(&tech.tech) .map(|tech_stats| { tech_stats.price * tech.level as usize @@ -69,6 +71,32 @@ impl ShopItem for ShopTool { } } } + + fn as_bytes(&self) -> [u8; 12] { + match self { + ToolShopItem::Tool(tool) => { + Tool { + tool: *tool + }.as_individual_bytes()[0..12].try_into().unwrap() + }, + ToolShopItem::Tech(tech) => { + tech.as_bytes()[0..12].try_into().unwrap() + }, + } + } + + fn as_item(&self) -> ItemDetail { + match self { + ToolShopItem::Tool(tool) => { + ItemDetail::Tool(Tool { + tool: *tool + }) + }, + ToolShopItem::Tech(tech) => { + ItemDetail::TechniqueDisk(*tech) + }, + } + } } @@ -163,7 +191,7 @@ fn number_of_techs_to_generate(character_level: usize) -> usize { #[derive(Debug)] -struct ToolShop { +pub struct ToolShop { tools: ToolTable, techs: TechTable, rng: R, @@ -178,7 +206,7 @@ impl ToolShop { } } - fn generate_tech(&mut self, character_level: usize) -> ShopTool { + fn generate_tech(&mut self, character_level: usize) -> ToolShopItem { let tier = self.techs.0.iter() .filter(|t| t.level <= character_level) .last() @@ -198,7 +226,7 @@ impl ToolShop { TechLevel::Range{min, max} => self.rng.gen_range(min, max+1), }; - ShopTool::Tech( + ToolShopItem::Tech( TechniqueDisk { tech: *tech_detail.0, level: tech_level as u32, @@ -245,7 +273,7 @@ impl ToolShop { } - fn generate_techs(&mut self, character_level: usize) -> Vec { + fn generate_techs(&mut self, character_level: usize) -> Vec { let tier = self.techs.0.iter() .filter(|t| t.level <= character_level) .last() @@ -262,7 +290,7 @@ impl ToolShop { TechLevel::Range{min, max} => self.rng.gen_range(min, max+1), }; - ShopTool::Tech(TechniqueDisk { + ToolShopItem::Tech(TechniqueDisk { tech: tech, level: level as u32, }) @@ -270,9 +298,9 @@ impl ToolShop { .collect() } - pub fn generate_tool_list(&mut self, character_level: usize) -> Vec { + pub fn generate_tool_list(&mut self, character_level: usize) -> Vec { let mut tools = Vec::new().into_iter() - .chain(self.tools.0.clone().into_iter().map(|t| ShopTool::Tool(t))) + .chain(self.tools.0.clone().into_iter().map(|t| ToolShopItem::Tool(t))) .chain(self.generate_techs(character_level).into_iter()) .collect::>(); tools.sort(); diff --git a/src/ship/shops/weapon.rs b/src/ship/shops/weapon.rs index f108e12..877ce52 100644 --- a/src/ship/shops/weapon.rs +++ b/src/ship/shops/weapon.rs @@ -2,13 +2,15 @@ use std::collections::HashMap; use std::fs::File; use std::io::Read; use std::path::PathBuf; +use std::convert::TryInto; use serde::Deserialize; use rand::{Rng, SeedableRng}; use rand::distributions::{WeightedIndex, Distribution}; use rand::seq::{SliceRandom, IteratorRandom}; use crate::entity::character::SectionID; use crate::ship::room::Difficulty; -use crate::entity::item::weapon::{WeaponType, WeaponSpecial, Attribute, WeaponAttribute}; +use crate::entity::item::ItemDetail; +use crate::entity::item::weapon::{Weapon, WeaponType, WeaponSpecial, Attribute, WeaponAttribute}; use crate::ship::shops::ShopItem; use crate::ship::item_stats::WEAPON_STATS; @@ -20,7 +22,7 @@ const TIER2_SPECIAL: [WeaponSpecial; 10] = [WeaponSpecial::Drain, WeaponSpecial: WeaponSpecial::Hold, WeaponSpecial::Fire, WeaponSpecial::Thunder, WeaponSpecial::Shadow, WeaponSpecial::Riot]; #[derive(Debug)] -pub struct ShopWeapon { +pub struct WeaponShopItem { weapon: WeaponType, special: Option, grind: usize, @@ -74,7 +76,7 @@ fn special_stars(special: &WeaponSpecial) -> usize { } -impl ShopItem for ShopWeapon { +impl ShopItem for WeaponShopItem { fn price(&self) -> usize { WEAPON_STATS.get(&self.weapon) .map(|weapon_stat| { @@ -99,6 +101,23 @@ impl ShopItem for ShopWeapon { }) .unwrap_or(0xFFFF) } + + fn as_bytes(&self) -> [u8; 12] { + self.as_item().as_client_bytes()[0..12].try_into().unwrap() + } + + fn as_item(&self) -> ItemDetail { + ItemDetail::Weapon( + Weapon { + weapon: self.weapon, + special: self.special, + grind: self.grind as u8, + attrs: [self.attributes[0], self.attributes[1], None], + tekked: true, + modifiers: Vec::new(), + } + ) + } } @@ -259,8 +278,20 @@ fn load_attribute2_table() -> AttributeTable { AttributeTable(table.remove("attributes".into()).unwrap()) } +fn number_of_weapons_to_generate(character_level: usize) -> usize { + if character_level <= 10 { + 10 + } + else if character_level <= 42 { + 12 + } + else { + 16 + } +} + #[derive(Debug)] -struct WeaponShop { +pub struct WeaponShop { difficulty: Difficulty, section_id: SectionID, weapon: WeaponTable, @@ -429,7 +460,7 @@ impl WeaponShop { } } - pub fn generate_weapon(&mut self, level: usize) -> ShopWeapon { + fn generate_weapon(&mut self, level: usize) -> WeaponShopItem { let weapon = self.generate_type(level); let grind = if self.is_alt_grind(&weapon) { self.generate_alt_grind(level) @@ -460,13 +491,21 @@ impl WeaponShop { } }; - ShopWeapon { + WeaponShopItem { weapon: weapon, grind: grind, special: special, attributes: [attr1, attr2], } } + + pub fn generate_weapon_list(&mut self, level: usize) -> Vec { + (0..number_of_weapons_to_generate(level)) + .map(|_| { + self.generate_weapon(level) + }) + .collect() + } } #[cfg(test)] @@ -481,7 +520,7 @@ mod test { fn test_generating_some_weapons() { let mut ws = WeaponShop::::new(Difficulty::Ultimate, SectionID::Pinkal); for i in 0..200 { - ws.generate_weapon(i); + ws.generate_weapon_list(i); } } } diff --git a/tests/common.rs b/tests/common.rs index 693f1f8..bce80d7 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -6,6 +6,7 @@ use elseware::entity::gateway::EntityGateway; use elseware::entity::account::{UserAccountEntity, NewUserAccountEntity, NewUserSettingsEntity}; use elseware::entity::character::{CharacterEntity, NewCharacterEntity}; use elseware::ship::ship::{ShipServerState, RecvShipPacket}; +use elseware::ship::room::Difficulty; use libpso::packet::ship::*; use libpso::packet::login::{Login, Session}; @@ -58,11 +59,15 @@ pub async fn join_lobby(ship: &mut ShipServerState, id: C } pub async fn create_room(ship: &mut ShipServerState, id: ClientId, name: &str, password: &str) { + create_room_with_difficulty(ship, id, name, password, Difficulty::Normal).await; +} + +pub async fn create_room_with_difficulty(ship: &mut ShipServerState, id: ClientId, name: &str, password: &str, difficulty: Difficulty) { ship.handle(id, &RecvShipPacket::CreateRoom(CreateRoom { unknown: [0; 2], name: utf8_to_utf16_array!(name, 16), password: utf8_to_utf16_array!(password, 16), - difficulty: 0, + difficulty: difficulty.into(), battle: 0, challenge: 0, episode: 1, diff --git a/tests/test_shops.rs b/tests/test_shops.rs new file mode 100644 index 0000000..4044607 --- /dev/null +++ b/tests/test_shops.rs @@ -0,0 +1,449 @@ +use elseware::common::serverstate::{ClientId, ServerState}; +use elseware::entity::gateway::{EntityGateway, InMemoryGateway}; +use elseware::entity::item; +use elseware::ship::ship::{ShipServerState, RecvShipPacket, SendShipPacket}; +use elseware::ship::room::Difficulty; + +use libpso::packet::ship::*; +use libpso::packet::messages::*; + +#[path = "common.rs"] +mod common; +use common::*; + +#[async_std::test] +async fn test_player_opens_weapon_shop() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + char1.exp = 80000000; + entity_gateway.save_character(&char1).await; + + let mut ship = ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build(); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + create_room_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Ultimate).await; + + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::ShopRequest(ShopRequest { + client: 255, + target: 255, + shop_type: 1 + })))).await.unwrap().collect::>(); + + assert_eq!(packets.len(), 1); + match &packets[0].1 { + SendShipPacket::Message(Message {msg: GameMessage::ShopList(shop_list)}) => { + assert_eq!(shop_list.items.len(), 16) + } + _ => panic!("") + } +} + +#[async_std::test] +async fn test_player_opens_tool_shop() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + char1.exp = 80000000; + entity_gateway.save_character(&char1).await; + + let mut ship = ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build(); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + create_room_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Ultimate).await; + + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::ShopRequest(ShopRequest { + client: 255, + target: 255, + shop_type: 0 + })))).await.unwrap().collect::>(); + + assert_eq!(packets.len(), 1); + match &packets[0].1 { + SendShipPacket::Message(Message {msg: GameMessage::ShopList(shop_list)}) => { + assert_eq!(shop_list.items.len(), 18) + } + _ => panic!("") + } +} + +#[async_std::test] +async fn test_player_opens_armor_shop() { + let mut entity_gateway = InMemoryGateway::new(); + + let (_user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + char1.exp = 80000000; + entity_gateway.save_character(&char1).await; + + let mut ship = ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build(); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + create_room_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Ultimate).await; + + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::ShopRequest(ShopRequest { + client: 255, + target: 255, + shop_type: 2 + })))).await.unwrap().collect::>(); + + assert_eq!(packets.len(), 1); + match &packets[0].1 { + SendShipPacket::Message(Message {msg: GameMessage::ShopList(shop_list)}) => { + assert_eq!(shop_list.items.len(), 21) + } + _ => panic!("") + } +} + +#[async_std::test] +async fn test_player_buys_from_weapon_shop() { + let mut entity_gateway = InMemoryGateway::new(); + + 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; + + let mut ship = ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build(); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + create_room_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Ultimate).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::ShopRequest(ShopRequest { + client: 255, + target: 255, + shop_type: 1 + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BuyItem(BuyItem { + client: 255, + target: 255, + item_id: 0x10000, + shop_type: 1, + shop_index: 0, + amount: 1, + unknown1: 0, + })))).await.unwrap().for_each(drop); + + let characters1 = entity_gateway.get_characters_by_user(&user1).await; + let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap(); + assert!(c1.meseta < 999999); + let p1_items = entity_gateway.get_items_by_character(&char1).await; + assert_eq!(p1_items.len(), 1); +} + +#[async_std::test] +async fn test_player_buys_from_tool_shop() { + let mut entity_gateway = InMemoryGateway::new(); + + 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; + + let mut ship = ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build(); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + create_room_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Ultimate).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::ShopRequest(ShopRequest { + client: 255, + target: 255, + shop_type: 0, + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BuyItem(BuyItem { + client: 255, + target: 255, + item_id: 0x10000, + shop_type: 0, + shop_index: 0, + amount: 1, + unknown1: 0, + })))).await.unwrap().for_each(drop); + + let characters1 = entity_gateway.get_characters_by_user(&user1).await; + let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap(); + assert!(c1.meseta < 999999); + let p1_items = entity_gateway.get_items_by_character(&char1).await; + assert_eq!(p1_items.len(), 1); +} + +#[async_std::test] +async fn test_player_buys_multiple_from_tool_shop() { + let mut entity_gateway = InMemoryGateway::new(); + + 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; + + let mut ship = ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build(); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + create_room_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Ultimate).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::ShopRequest(ShopRequest { + client: 255, + target: 255, + shop_type: 0, + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BuyItem(BuyItem { + client: 255, + target: 255, + item_id: 0x10000, + shop_type: 0, + shop_index: 0, + amount: 5, + unknown1: 0, + })))).await.unwrap().for_each(drop); + + let characters1 = entity_gateway.get_characters_by_user(&user1).await; + let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap(); + assert!(c1.meseta < 999999); + let p1_items = entity_gateway.get_items_by_character(&char1).await; + assert_eq!(p1_items.len(), 5); +} + +#[async_std::test] +async fn test_player_buys_from_armor_shop() { + let mut entity_gateway = InMemoryGateway::new(); + + 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; + + let mut ship = ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build(); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + create_room_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Ultimate).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::ShopRequest(ShopRequest { + client: 255, + target: 255, + shop_type: 2 + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BuyItem(BuyItem { + client: 255, + target: 255, + item_id: 0x10000, + shop_type: 2, + shop_index: 0, + amount: 1, + unknown1: 0, + })))).await.unwrap().for_each(drop); + + let characters1 = entity_gateway.get_characters_by_user(&user1).await; + let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap(); + assert!(c1.meseta < 999999); + let p1_items = entity_gateway.get_items_by_character(&char1).await; + assert_eq!(p1_items.len(), 1); +} + +#[async_std::test] +async fn test_player_sells_to_shop() { +} + +#[async_std::test] +async fn test_other_clients_see_purchase() { + let mut entity_gateway = InMemoryGateway::new(); + + 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; + + let mut ship = 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_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Ultimate).await; + join_room(&mut ship, ClientId(2), 0).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::ShopRequest(ShopRequest { + client: 255, + target: 255, + shop_type: 1 + })))).await.unwrap().for_each(drop); + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BuyItem(BuyItem { + client: 255, + target: 255, + item_id: 0x10000, + shop_type: 1, + shop_index: 0, + amount: 1, + unknown1: 0, + })))).await.unwrap().collect::>(); + + assert_eq!(packets.len(), 1); + assert_eq!(packets[0].0, ClientId(2)); + match &packets[0].1 { + SendShipPacket::Message(Message{msg: GameMessage::CreateItem(_)}) => {}, + _ => panic!(""), + } +} + +#[async_std::test] +async fn test_other_clients_see_stacked_purchase() { + let mut entity_gateway = InMemoryGateway::new(); + + 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; + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::Monomate + } + ), + location: item::ItemLocation::Inventory { + character_id: char1.id, + slot: 0, + equipped: false, + } + }).await; + + let mut ship = ShipServerState::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_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Ultimate).await; + join_room(&mut ship, ClientId(2), 0).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::ShopRequest(ShopRequest { + client: 255, + target: 255, + shop_type: 1 + })))).await.unwrap().for_each(drop); + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BuyItem(BuyItem { + client: 255, + target: 255, + item_id: 0x10000, + shop_type: 1, + shop_index: 0, + amount: 1, + unknown1: 0, + })))).await.unwrap().collect::>(); + + assert_eq!(packets.len(), 1); + assert_eq!(packets[0].0, ClientId(2)); + match &packets[0].1 { + SendShipPacket::Message(Message{msg: GameMessage::CreateItem(_)}) => {}, + _ => panic!(""), + } +} + +#[async_std::test] +async fn test_buying_item_without_enough_mseseta() { + let mut entity_gateway = InMemoryGateway::new(); + + let (user1, mut char1) = new_user_character(&mut entity_gateway, "a1", "a").await; + + let mut ship = ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build(); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + create_room_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Ultimate).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::ShopRequest(ShopRequest { + client: 255, + target: 255, + shop_type: 1 + })))).await.unwrap().for_each(drop); + let packets = ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BuyItem(BuyItem { + client: 255, + target: 255, + item_id: 0x10000, + shop_type: 1, + shop_index: 0, + amount: 1, + unknown1: 0, + })))).await; + + assert!(packets.is_err()); + let characters1 = entity_gateway.get_characters_by_user(&user1).await; + let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap(); + assert_eq!(c1.meseta, 0); + let p1_items = entity_gateway.get_items_by_character(&char1).await; + assert_eq!(p1_items.len(), 0); +} + +#[async_std::test] +async fn test_player_double_buys_from_tool_shop() { + let mut entity_gateway = InMemoryGateway::new(); + + 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; + + let mut ship = ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build(); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + create_room_with_difficulty(&mut ship, ClientId(1), "room", "", Difficulty::Ultimate).await; + + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::ShopRequest(ShopRequest { + client: 255, + target: 255, + shop_type: 0, + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BuyItem(BuyItem { + client: 255, + target: 255, + item_id: 0x10000, + shop_type: 0, + shop_index: 0, + amount: 3, + unknown1: 0, + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BuyItem(BuyItem { + client: 255, + target: 255, + item_id: 0x10001, + shop_type: 0, + shop_index: 1, + amount: 2, + unknown1: 0, + })))).await.unwrap().for_each(drop); + ship.handle(ClientId(1), &RecvShipPacket::DirectMessage(DirectMessage::new(0, GameMessage::BuyItem(BuyItem { + client: 255, + target: 255, + item_id: 0x10002, + shop_type: 0, + shop_index: 0, + amount: 4, + unknown1: 0, + })))).await.unwrap().for_each(drop); + + let characters1 = entity_gateway.get_characters_by_user(&user1).await; + let c1 = characters1.get(0).as_ref().unwrap().as_ref().unwrap(); + assert!(c1.meseta < 999999); + let p1_items = entity_gateway.get_items_by_character(&char1).await; + assert_eq!(p1_items.len(), 10); +}