diff --git a/shops/Cargo.toml b/shops/Cargo.toml index 495bf2f..25e3055 100644 --- a/shops/Cargo.toml +++ b/shops/Cargo.toml @@ -8,6 +8,10 @@ maps = { workspace = true } stats = { workspace = true } entity = { workspace = true } +async-std = { workspace = true } +async-trait = { workspace = true } +futures = { workspace = true } rand = { workspace = true } +rand_chacha = { workspace = true } toml = { workspace = true } serde = { workspace = true } \ No newline at end of file diff --git a/shops/src/lib.rs b/shops/src/lib.rs index 7a999b7..94abdec 100644 --- a/shops/src/lib.rs +++ b/shops/src/lib.rs @@ -2,7 +2,16 @@ mod weapon; mod tool; mod armor; +use async_std::sync::{Arc, Mutex}; +use futures::future::OptionFuture; +use std::collections::HashMap; use entity::item::ItemDetail; +use maps::room::Difficulty; +use entity::character::SectionID; + +pub use weapon::{WeaponShop, WeaponShopItem}; +pub use tool::{ToolShop, ToolShopItem}; +pub use armor::{ArmorShop, ArmorShopItem}; pub trait ShopItem { fn price(&self) -> usize; @@ -10,12 +19,77 @@ pub trait ShopItem { fn as_item(&self) -> ItemDetail; } +#[async_trait::async_trait] +pub trait ItemShops { + async fn generate_weapon_list(&self, difficulty: Difficulty, section_id: SectionID, char_level: usize) -> Option>; + async fn generate_tool_list(&self, char_level: usize) -> Vec; + async fn generate_armor_list(&self, char_level: usize) -> Vec; +} + + +#[derive(Clone)] +pub struct StandardItemShops { + weapon_shop: HashMap<(Difficulty, SectionID), Arc>>>, + tool_shop: Arc>>, + armor_shop: Arc>>, +} + +impl Default for StandardItemShops { + fn default() -> StandardItemShops { + let difficulty = [Difficulty::Normal, Difficulty::Hard, Difficulty::VeryHard, 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), Arc::new(Mutex::new(WeaponShop::new(*d, *id)))); + } + } + + StandardItemShops { + weapon_shop, + tool_shop: Arc::new(Mutex::new(ToolShop::default())), + armor_shop: Arc::new(Mutex::new(ArmorShop::default())), + } + } +} + +#[async_trait::async_trait] +impl ItemShops for StandardItemShops { + async fn generate_weapon_list(&self, difficulty: Difficulty, section_id: SectionID, char_level: usize) -> Option> { + OptionFuture::from( + self.weapon_shop + .get(&(difficulty, section_id)) + .map(|shop| async { + shop + .lock() + .await + .generate_weapon_list(char_level) + })).await + } + + async fn generate_tool_list(&self, char_level: usize) -> Vec { + self.tool_shop + .lock() + .await + .generate_tool_list(char_level) + } + + async fn generate_armor_list(&self, char_level: usize) -> Vec { + self.armor_shop + .lock() + .await + .generate_armor_list(char_level) + } +} + + + + 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/packet/handler/direct_message.rs b/src/ship/packet/handler/direct_message.rs index 090d77d..c93ae0d 100644 --- a/src/ship/packet/handler/direct_message.rs +++ b/src/ship/packet/handler/direct_message.rs @@ -1,11 +1,12 @@ use log::warn; +use async_std::sync::Arc; use rand::Rng; use rand::seq::SliceRandom; use libpso::packet::ship::*; use libpso::packet::messages::*; use stats::leveltable::LEVEL_TABLE; use networking::serverstate::ClientId; -use crate::ship::ship::{SendShipPacket, ShipError, Clients, ItemShops}; +use crate::ship::ship::{SendShipPacket, ShipError, Clients}; use location::ClientLocation; use drops::ItemDrop; use room::Rooms; @@ -14,7 +15,7 @@ use entity::gateway::EntityGateway; use entity::item; use libpso::utf8_to_utf16_array; use pktbuilder as builder; -use shops::{ShopItem, ToolShopItem, ArmorShopItem}; +use shops::{ItemShops, ShopItem, ToolShopItem, ArmorShopItem}; use items::state::{ItemState, ItemStateError}; use items::floor::{FloorType, FloorItemDetail}; use items::actions::TriggerCreateItem; @@ -320,45 +321,30 @@ pub async fn shop_request(id: ClientId, client_location: &ClientLocation, clients: &Clients, rooms: &Rooms, - shops: &ItemShops) + shops: &Arc>) -> Result, anyhow::Error> { - //let client = clients.get_mut(&id).ok_or(ShipError::ClientNotFound(id))?; let room_id = client_location.get_room(id).await?; - /* - let room = rooms.get(room_id.0) - .ok_or(ShipError::InvalidRoom(room_id.0 as u32))? - .as_ref() - .ok_or(ShipError::InvalidRoom(room_id.0 as u32))?; - */ let difficulty = rooms.with(room_id, |room| Box::pin(async move { room.mode.difficulty() })).await?; let shop_list = clients.with_mut(id, |client| { - let mut shops = shops.clone(); + let shops = shops.clone(); Box::pin(async move { let level = LEVEL_TABLE.get_level_from_exp(client.character.char_class, client.character.exp) as usize; match shop_request.shop_type { SHOP_OPTION_WEAPON => { - client.weapon_shop = shops.weapon_shop.get_mut(&(difficulty, client.character.section_id)) - .ok_or(ShipError::ShopError)? - .lock() + client.weapon_shop = shops.generate_weapon_list(difficulty, client.character.section_id, level) .await - .generate_weapon_list(level); + .ok_or(ShipError::ShopError)?; Ok(builder::message::shop_list(shop_request.shop_type, &client.weapon_shop)) }, SHOP_OPTION_TOOL => { - client.tool_shop = shops.tool_shop - .lock() - .await - .generate_tool_list(level); + client.tool_shop = shops.generate_tool_list(level).await; Ok(builder::message::shop_list(shop_request.shop_type, &client.tool_shop)) }, SHOP_OPTION_ARMOR => { - client.armor_shop = shops.armor_shop - .lock() - .await - .generate_armor_list(level); + client.armor_shop = shops.generate_armor_list(level).await; Ok(builder::message::shop_list(shop_request.shop_type, &client.armor_shop)) }, _ => { diff --git a/src/ship/ship.rs b/src/ship/ship.rs index c920376..85ca6e5 100644 --- a/src/ship/ship.rs +++ b/src/ship/ship.rs @@ -1,9 +1,8 @@ #![allow(dead_code, unused_must_use)] use std::net::Ipv4Addr; -use std::collections::HashMap; use async_std::channel; -use async_std::sync::{Arc, Mutex, RwLock}; +use async_std::sync::{Arc, RwLock}; use rand::Rng; use thiserror::Error; @@ -32,7 +31,7 @@ use maps::Holiday; use maps::area::MapAreaError; use maps::maps::{Maps, MapsError, generate_free_roam_maps}; use crate::ship::packet::handler; -use shops::{WeaponShop, ToolShop, ArmorShop}; +use shops::{ItemShops, StandardItemShops}; use trade::TradeState; use crate::ship::chatcommand; use pktbuilder::quest::{QUEST_CATEGORY_MENU_ID, QUEST_SELECT_MENU_ID}; @@ -294,34 +293,6 @@ impl SendServerPacket for SendShipPacket { } } -#[derive(Clone)] -pub struct ItemShops { - pub weapon_shop: HashMap<(Difficulty, SectionID), Arc>>>, - pub tool_shop: Arc>>, - pub armor_shop: Arc>>, -} - -impl Default for ItemShops { - fn default() -> ItemShops { - let difficulty = [Difficulty::Normal, Difficulty::Hard, Difficulty::VeryHard, 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), Arc::new(Mutex::new(WeaponShop::new(*d, *id)))); - } - } - - ItemShops { - weapon_shop, - tool_shop: Arc::new(Mutex::new(ToolShop::default())), - armor_shop: Arc::new(Mutex::new(ArmorShop::default())), - } - } -} - pub struct ShipServerStateBuilder { entity_gateway: Option, @@ -330,6 +301,7 @@ pub struct ShipServerStateBuilder { port: Option, auth_token: Option, event: Option, + shops: Option>, map_builder: Option Maps + Send + Sync>>, drop_table_builder: Option Box + Send + Sync>>, standard_quest_builder: Option Result + Send + Sync>>, @@ -346,6 +318,7 @@ impl Default for ShipServerStateBuilder port: None, auth_token: None, event: None, + shops: None, map_builder: None, drop_table_builder: None, standard_quest_builder: None, @@ -416,6 +389,12 @@ impl ShipServerStateBuilder { self } + #[must_use] + pub fn item_shops(mut self, item_shops: impl ItemShops + Send + Sync + 'static) -> ShipServerStateBuilder { + self.shops = Some(Box::new(item_shops)); + self + } + #[must_use] pub fn blocks(mut self, num_blocks: usize) -> ShipServerStateBuilder { self.num_blocks = num_blocks; @@ -431,7 +410,8 @@ impl ShipServerStateBuilder { item_state: items::state::ItemState::default(), ip: self.ip.unwrap_or_else(|| Ipv4Addr::new(127,0,0,1)), port: self.port.unwrap_or(SHIP_PORT), - shops: ItemShops::default(), + #[allow(clippy::box_default)] // this lint leads to another which just doesn't work + shops: Arc::new(self.shops.unwrap_or(Box::new(StandardItemShops::default()))), blocks: Blocks(blocks), event: self.event.unwrap_or(Holiday::None), map_builder: Arc::new(self.map_builder.unwrap_or(Box::new(generate_free_roam_maps))), @@ -474,7 +454,7 @@ pub struct ShipServerState { pub clients: Clients, name: String, pub(crate) item_state: items::state::ItemState, - shops: ItemShops, + shops: Arc>, pub blocks: Blocks, event: Holiday, diff --git a/tests/common.rs b/tests/common.rs index f28d318..f446d37 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -13,6 +13,7 @@ use maps::object::MapObject; use maps::monster::MonsterType; use quests::{QuestList, QuestLoadError}; use drops::{DropTable, ItemDropType}; +use shops::{ItemShops, WeaponShopItem, ToolShopItem, ArmorShopItem}; use entity::item; @@ -35,6 +36,23 @@ impl DropTable for NullDropTable { } } +struct NullItemShops; + +#[async_trait::async_trait] +impl ItemShops for NullItemShops { + async fn generate_weapon_list(&self, _difficulty: Difficulty, _section_id: SectionID, _char_level: usize) -> Option> { + Some(Vec::new()) + } + async fn generate_tool_list(&self, _char_level: usize) -> Vec { + Vec::new() + } + async fn generate_armor_list(&self, _char_level: usize) -> Vec { + Vec::new() + } +} + + + pub fn null_drop_table_builder(_episode: Episode, _difficult: Difficulty, _section_id: SectionID) -> Box { Box::new(NullDropTable) } @@ -46,6 +64,7 @@ pub fn standard_ship_buildable(gateway: EG) -> ShipS .government_quest_builder(Box::new(null_quest_builder)) .drop_table_builder(Box::new(null_drop_table_builder)) .map_builder(Box::new(null_free_roam_maps)) + .item_shops(NullItemShops) } pub fn standard_ship(gateway: EG) -> ShipServerState { @@ -55,6 +74,7 @@ pub fn standard_ship(gateway: EG) -> ShipServerState .government_quest_builder(Box::new(null_quest_builder)) .drop_table_builder(Box::new(null_drop_table_builder)) .map_builder(Box::new(null_free_roam_maps)) + .item_shops(NullItemShops) .build() } diff --git a/tests/test_bank.rs b/tests/test_bank.rs index 2a97d6b..07a6ee8 100644 --- a/tests/test_bank.rs +++ b/tests/test_bank.rs @@ -3,6 +3,7 @@ use networking::serverstate::{ClientId, ServerState}; use entity::gateway::{EntityGateway, InMemoryGateway}; use entity::item; use elseware::ship::ship::{ShipServerState, RecvShipPacket, SendShipPacket}; +use shops::StandardItemShops; use libpso::packet::ship::*; use libpso::packet::messages::*; @@ -1538,7 +1539,9 @@ async fn test_withdraw_meseta_and_buy_a_few_monomates_with_it() { entity_gateway.set_character_meseta(&char1.id, item::Meseta(100)).await.unwrap(); entity_gateway.set_bank_meseta(&char1.id, &item::BankIdentifier::Character, item::Meseta(300)).await.unwrap(); - let mut ship = standard_ship(entity_gateway.clone()); + let mut ship = standard_ship_buildable(entity_gateway.clone()) + .item_shops(StandardItemShops::default()) + .build(); log_in_char(&mut ship, ClientId(1), "a1", "a").await; join_lobby(&mut ship, ClientId(1)).await; create_room(&mut ship, ClientId(1), "room", "").await; diff --git a/tests/test_shops.rs b/tests/test_shops.rs index 392fa5a..00d8f5c 100644 --- a/tests/test_shops.rs +++ b/tests/test_shops.rs @@ -4,6 +4,7 @@ use entity::item; use elseware::ship::ship::{ShipServerState, RecvShipPacket, SendShipPacket}; use maps::room::Difficulty; use items::state::ItemStateError; +use shops::StandardItemShops; use libpso::packet::ship::*; use libpso::packet::messages::*; @@ -20,7 +21,9 @@ async fn test_player_opens_weapon_shop() { char1.exp = 80000000; entity_gateway.save_character(&char1).await.unwrap(); - let mut ship = standard_ship(entity_gateway.clone()); + let mut ship = standard_ship_buildable(entity_gateway.clone()) + .item_shops(StandardItemShops::default()) + .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; @@ -48,7 +51,9 @@ async fn test_player_opens_tool_shop() { char1.exp = 80000000; entity_gateway.save_character(&char1).await.unwrap(); - let mut ship = standard_ship(entity_gateway.clone()); + let mut ship = standard_ship_buildable(entity_gateway.clone()) + .item_shops(StandardItemShops::default()) + .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; @@ -76,7 +81,9 @@ async fn test_player_opens_armor_shop() { char1.exp = 80000000; entity_gateway.save_character(&char1).await.unwrap(); - let mut ship = standard_ship(entity_gateway.clone()); + let mut ship = standard_ship_buildable(entity_gateway.clone()) + .item_shops(StandardItemShops::default()) + .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; @@ -105,7 +112,9 @@ async fn test_player_buys_from_weapon_shop() { entity_gateway.save_character(&char1).await.unwrap(); entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap(); - let mut ship = standard_ship(entity_gateway.clone()); + let mut ship = standard_ship_buildable(entity_gateway.clone()) + .item_shops(StandardItemShops::default()) + .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; @@ -141,7 +150,9 @@ async fn test_player_buys_from_tool_shop() { entity_gateway.save_character(&char1).await.unwrap(); entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap(); - let mut ship = standard_ship(entity_gateway.clone()); + let mut ship = standard_ship_buildable(entity_gateway.clone()) + .item_shops(StandardItemShops::default()) + .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; @@ -176,7 +187,9 @@ async fn test_player_buys_multiple_from_tool_shop() { entity_gateway.save_character(&char1).await.unwrap(); entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap(); - let mut ship = standard_ship(entity_gateway.clone()); + let mut ship = standard_ship_buildable(entity_gateway.clone()) + .item_shops(StandardItemShops::default()) + .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; @@ -215,7 +228,9 @@ async fn test_player_buys_from_armor_shop() { entity_gateway.save_character(&char1).await.unwrap(); entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap(); - let mut ship = standard_ship(entity_gateway.clone()); + let mut ship = standard_ship_buildable(entity_gateway.clone()) + .item_shops(StandardItemShops::default()) + .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; @@ -293,7 +308,9 @@ async fn test_other_clients_see_purchase() { entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap(); entity_gateway.save_character(&char1).await.unwrap(); - let mut ship = standard_ship(entity_gateway.clone()); + let mut ship = standard_ship_buildable(entity_gateway.clone()) + .item_shops(StandardItemShops::default()) + .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; @@ -342,7 +359,9 @@ async fn test_other_clients_see_stacked_purchase() { ), }).await.unwrap(); - let mut ship = standard_ship(entity_gateway.clone()); + let mut ship = standard_ship_buildable(entity_gateway.clone()) + .item_shops(StandardItemShops::default()) + .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; @@ -415,7 +434,9 @@ async fn test_player_double_buys_from_tool_shop() { entity_gateway.save_character(&char1).await.unwrap(); entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap(); - let mut ship = standard_ship(entity_gateway.clone()); + let mut ship = standard_ship_buildable(entity_gateway.clone()) + .item_shops(StandardItemShops::default()) + .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; @@ -478,7 +499,9 @@ async fn test_techs_disappear_from_shop_when_bought() { entity_gateway.save_character(&char1).await.unwrap(); entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap(); - let mut ship = standard_ship(entity_gateway.clone()); + let mut ship = standard_ship_buildable(entity_gateway.clone()) + .item_shops(StandardItemShops::default()) + .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; @@ -538,7 +561,9 @@ async fn test_units_disappear_from_shop_when_bought() { entity_gateway.save_character(&char1).await.unwrap(); entity_gateway.set_character_meseta(&char1.id, item::Meseta(999999)).await.unwrap(); - let mut ship = standard_ship(entity_gateway.clone()); + let mut ship = standard_ship_buildable(entity_gateway.clone()) + .item_shops(StandardItemShops::default()) + .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;