diff --git a/src/entity/gateway/entitygateway.rs b/src/entity/gateway/entitygateway.rs index 99c65bb..df922ac 100644 --- a/src/entity/gateway/entitygateway.rs +++ b/src/entity/gateway/entitygateway.rs @@ -4,10 +4,10 @@ use futures::future::{Future, BoxFuture}; use crate::entity::account::*; use crate::entity::character::*; use crate::entity::item::*; +use crate::entity::room::*; // TODO: better granularity? -//#[derive(Error, Debug)] #[derive(Error, Debug)] pub enum GatewayError { #[error("unknown error")] @@ -147,6 +147,14 @@ pub trait EntityGateway: Send + Sync { async fn set_character_playtime(&mut self, _char_id: &CharacterEntityId, _playtime: u32) -> Result<(), GatewayError> { unimplemented!(); } + + async fn create_room(&mut self, _room: NewRoomEntity) -> Result { + unimplemented!(); + } + + async fn add_room_note(&mut self, _room_id: RoomEntityId, _note: RoomNote) -> Result<(), GatewayError> { + unimplemented!(); + } } diff --git a/src/entity/gateway/inmemory.rs b/src/entity/gateway/inmemory.rs index 75df8a1..50a650e 100644 --- a/src/entity/gateway/inmemory.rs +++ b/src/entity/gateway/inmemory.rs @@ -6,6 +6,7 @@ use crate::entity::account::*; use crate::entity::character::*; use crate::entity::gateway::{EntityGateway, EntityGatewayTransaction, GatewayError}; use crate::entity::item::*; +use crate::entity::room::*; use async_std::sync::{Arc, Mutex}; @@ -766,4 +767,21 @@ impl EntityGateway for InMemoryGateway { Err(GatewayError::Error) } } + + // I do not care to replicate this in testing + async fn create_room(&mut self, room: NewRoomEntity) -> Result { + Ok(RoomEntity { + id: RoomEntityId(0), + name: room.name, + section_id: room.section_id, + episode: room.episode, + difficulty: room.difficulty, + mode: room.mode, + }) + } + + // I do not care to replicate this in testing + async fn add_room_note(&mut self, _room_id: RoomEntityId, _note: RoomNote) -> Result<(), GatewayError> { + Ok(()) + } } diff --git a/src/entity/gateway/postgres/migrations/V0012__room.sql b/src/entity/gateway/postgres/migrations/V0012__room.sql new file mode 100644 index 0000000..750493c --- /dev/null +++ b/src/entity/gateway/postgres/migrations/V0012__room.sql @@ -0,0 +1,14 @@ +create table room { + id serial primary key not null, + name varchar(32) not null, + section_id char not null, + mode char not null, + episode char not null, + difficulty char not null, +}; + +create table room_note ( + room integer references room (id) not null, + note jsonb not null, + created_at timestamptz default current_timestamp not null +); diff --git a/src/entity/gateway/postgres/models.rs b/src/entity/gateway/postgres/models.rs index e1a6c8c..f3a2f39 100644 --- a/src/entity/gateway/postgres/models.rs +++ b/src/entity/gateway/postgres/models.rs @@ -7,7 +7,10 @@ use libpso::util::vec_to_array; use crate::entity::account::*; use crate::entity::character::*; use crate::entity::item::*; +use crate::entity::room::*; use crate::ship::map::MapArea; +use crate::ship::room::{Episode, Difficulty}; +use crate::ship::monster::MonsterType; #[derive(Debug, sqlx::FromRow)] pub struct PgUserAccount { @@ -577,6 +580,16 @@ pub enum PgItemNoteDetail { }, EnemyDrop { character_id: u32, + room_id: u32, + monster_type: MonsterType, + map_area: MapArea, + x: f32, + y: f32, + z: f32, + }, + BoxDrop { + character_id: u32, + room_id: u32, map_area: MapArea, x: f32, y: f32, @@ -592,14 +605,19 @@ pub enum PgItemNoteDetail { y: f32, z: f32, }, - Consumed, + Consumed { + character_id: u32, + }, FedToMag { + character_id: u32, mag: u32, }, BoughtAtShop { character_id: u32, }, - SoldToShop, + SoldToShop { + character_id: u32, + }, Trade { trade_id: u32, character_to: u32, @@ -624,8 +642,16 @@ impl From for PgItemNoteDetail { ItemNote::CharacterCreation{character_id} => PgItemNoteDetail::CharacterCreation { character_id: character_id.0, }, - ItemNote::EnemyDrop{character_id, map_area, x, y, z} => PgItemNoteDetail::EnemyDrop { + ItemNote::EnemyDrop{character_id, room_id, monster_type, map_area, x, y, z} => PgItemNoteDetail::EnemyDrop { + character_id: character_id.0, + room_id: room_id.0, + monster_type, + map_area, + x,y,z, + }, + ItemNote::BoxDrop{character_id, room_id, map_area, x, y, z} => PgItemNoteDetail::BoxDrop { character_id: character_id.0, + room_id: room_id.0, map_area, x,y,z, }, @@ -637,14 +663,19 @@ impl From for PgItemNoteDetail { map_area, x,y,z, }, - ItemNote::Consumed => PgItemNoteDetail::Consumed, - ItemNote::FedToMag{mag} => PgItemNoteDetail::FedToMag{ + ItemNote::Consumed{character_id} => PgItemNoteDetail::Consumed { + character_id: character_id.0, + }, + ItemNote::FedToMag{character_id, mag} => PgItemNoteDetail::FedToMag{ + character_id: character_id.0, mag: mag.0 }, ItemNote::BoughtAtShop{character_id} => PgItemNoteDetail::BoughtAtShop { character_id: character_id.0, }, - ItemNote::SoldToShop => PgItemNoteDetail::SoldToShop, + ItemNote::SoldToShop{character_id} => PgItemNoteDetail::SoldToShop { + character_id: character_id.0, + }, ItemNote::Trade{trade_id, character_to, character_from} => PgItemNoteDetail::Trade { trade_id: trade_id.0, character_to: character_to.0, @@ -677,8 +708,16 @@ impl From for ItemNote { PgItemNoteDetail::CharacterCreation{character_id} => ItemNote::CharacterCreation { character_id: CharacterEntityId(character_id), }, - PgItemNoteDetail::EnemyDrop{character_id, map_area, x, y, z} => ItemNote::EnemyDrop { + PgItemNoteDetail::EnemyDrop{character_id, room_id, monster_type, map_area, x, y, z} => ItemNote::EnemyDrop { character_id: CharacterEntityId(character_id), + room_id: RoomEntityId(room_id), + monster_type, + map_area, + x,y,z, + }, + PgItemNoteDetail::BoxDrop{character_id, room_id, map_area, x, y, z} => ItemNote::BoxDrop { + character_id: CharacterEntityId(character_id), + room_id: RoomEntityId(room_id), map_area, x,y,z, }, @@ -690,14 +729,19 @@ impl From for ItemNote { map_area, x,y,z, }, - PgItemNoteDetail::Consumed => ItemNote::Consumed, - PgItemNoteDetail::FedToMag{mag} => ItemNote::FedToMag{ + PgItemNoteDetail::Consumed{character_id} => ItemNote::Consumed { + character_id: CharacterEntityId(character_id), + }, + PgItemNoteDetail::FedToMag{character_id, mag} => ItemNote::FedToMag{ + character_id: CharacterEntityId(character_id), mag: ItemEntityId(mag) }, PgItemNoteDetail::BoughtAtShop{character_id} => ItemNote::BoughtAtShop { character_id: CharacterEntityId(character_id), }, - PgItemNoteDetail::SoldToShop => ItemNote::SoldToShop, + PgItemNoteDetail::SoldToShop{character_id} => ItemNote::SoldToShop { + character_id: CharacterEntityId(character_id), + }, PgItemNoteDetail::Trade {trade_id, character_to, character_from} => ItemNote::Trade { trade_id: TradeId(trade_id), character_to: CharacterEntityId(character_to), @@ -880,3 +924,27 @@ impl From for TradeEntity { } } } + + +#[derive(Debug, sqlx::FromRow, Serialize)] +pub struct PgRoomEntity { + id: i32, + name: String, + section_id: i8, + mode: i8, + episode: i8, + difficulty: i8, +} + +impl From for RoomEntity { + fn from(other: PgRoomEntity) -> RoomEntity { + RoomEntity { + id: RoomEntityId(other.id as u32), + name: other.name, + section_id: SectionID::from(other.section_id as u8), + mode: RoomEntityMode::from(other.mode as u8), + episode: Episode::try_from(other.episode as u8).unwrap(), + difficulty: Difficulty::try_from(other.difficulty as u8).unwrap(), + } + } +} diff --git a/src/entity/gateway/postgres/postgres.rs b/src/entity/gateway/postgres/postgres.rs index 717f8ca..9bb3be5 100644 --- a/src/entity/gateway/postgres/postgres.rs +++ b/src/entity/gateway/postgres/postgres.rs @@ -10,6 +10,7 @@ use crate::entity::account::*; use crate::entity::character::*; use crate::entity::gateway::{EntityGateway, EntityGatewayTransaction, GatewayError}; use crate::entity::item::*; +use crate::entity::room::*; use super::models::*; use sqlx::postgres::PgPoolOptions; @@ -658,6 +659,28 @@ async fn set_character_playtime(conn: &mut sqlx::PgConnection, char_id: &Charact Ok(()) } +async fn create_room(conn: &mut sqlx::PgConnection, room: NewRoomEntity) -> Result { + sqlx::query_as::<_, PgRoomEntity>("insert into room (name, section_id, mode, episode, difficulty) values ($1, $2, $3, $4, $5) returning *") + .bind(room.name) + .bind(u8::from(room.section_id) as i8) + .bind(u8::from(room.mode) as i8) + .bind(u8::from(room.episode) as i8) + .bind(u8::from(room.difficulty) as i8) + .fetch_one(conn) + .await + .map(|room| room.into()) + .map_err(|err| err.into()) +} + +async fn add_room_note(conn: &mut sqlx::PgConnection, room_id: RoomEntityId, note: RoomNote) -> Result<(), GatewayError> { + sqlx::query("insert into room_note (room, note) values ($1, $2)") + .bind(room_id.0) + .bind(sqlx::types::Json(note)) + .execute(conn) + .await?; + Ok(()) +} + #[async_trait::async_trait] impl EntityGateway for PostgresGateway { type Transaction<'t> = PostgresTransaction<'t> where Self: 't; @@ -797,6 +820,14 @@ impl EntityGateway for PostgresGateway { async fn set_character_playtime(&mut self, char_id: &CharacterEntityId, playtime: u32) -> Result<(), GatewayError> { set_character_playtime(&mut *self.pool.acquire().await?, char_id, playtime).await } + + async fn create_room(&mut self, room: NewRoomEntity) -> Result { + create_room(&mut *self.pool.acquire().await?, room).await + } + + async fn add_room_note(&mut self, room_id: RoomEntityId, note: RoomNote) -> Result<(), GatewayError> { + add_room_note(&mut *self.pool.acquire().await?, room_id, note).await + } } @@ -923,5 +954,13 @@ impl<'c> EntityGateway for PostgresTransaction<'c> { async fn set_character_playtime(&mut self, char_id: &CharacterEntityId, playtime: u32) -> Result<(), GatewayError> { set_character_playtime(&mut *self.pgtransaction.lock().await, char_id, playtime).await } + + async fn create_room(&mut self, room: NewRoomEntity) -> Result { + create_room(&mut *self.pgtransaction.lock().await, room).await + } + + async fn add_room_note(&mut self, room_id: RoomEntityId, note: RoomNote) -> Result<(), GatewayError> { + add_room_note(&mut *self.pgtransaction.lock().await, room_id, note).await + } } diff --git a/src/entity/item/mod.rs b/src/entity/item/mod.rs index 8b1d967..7023f7c 100644 --- a/src/entity/item/mod.rs +++ b/src/entity/item/mod.rs @@ -10,7 +10,9 @@ pub mod esweapon; use serde::{Serialize, Deserialize}; use crate::entity::character::CharacterEntityId; +use crate::entity::room::RoomEntityId; use crate::ship::map::MapArea; +use crate::ship::monster::MonsterType; use crate::ship::drops::ItemDropType; #[derive(PartialEq, Eq, Copy, Clone, Debug, Hash, PartialOrd, Ord, Serialize, Deserialize)] @@ -35,8 +37,16 @@ pub enum ItemNote { }, EnemyDrop { character_id: CharacterEntityId, - //monster_type: MonsterType, - //droprate: f32, + room_id: RoomEntityId, + monster_type: MonsterType, + map_area: MapArea, + x: f32, + y: f32, + z: f32, + }, + BoxDrop { + character_id: CharacterEntityId, + room_id: RoomEntityId, map_area: MapArea, x: f32, y: f32, @@ -52,15 +62,19 @@ pub enum ItemNote { y: f32, z: f32, }, - Consumed, // TODO: character_id + Consumed { + character_id: CharacterEntityId, + }, FedToMag { - //character_id: CharacterEntityId, + character_id: CharacterEntityId, mag: ItemEntityId, }, BoughtAtShop { character_id: CharacterEntityId, }, - SoldToShop, + SoldToShop { + character_id: CharacterEntityId, + }, Trade { trade_id: TradeId, character_to: CharacterEntityId, diff --git a/src/entity/room.rs b/src/entity/room.rs index 9db194e..d59327e 100644 --- a/src/entity/room.rs +++ b/src/entity/room.rs @@ -11,12 +11,35 @@ pub struct RoomEntityId(pub u32); #[derive(Debug, Copy, Clone)] pub enum RoomEntityMode { - Single, Multi, + Single, Challenge, Battle, } +impl From for RoomEntityMode { + fn from(other: u8) -> RoomEntityMode { + match other { + 0 => RoomEntityMode::Multi, + 1 => RoomEntityMode::Single, + 2 => RoomEntityMode::Challenge, + 3 => RoomEntityMode::Battle, + _ => unreachable!() + } + } +} + +impl From for u8 { + fn from(other: RoomEntityMode) -> u8 { + match other { + RoomEntityMode::Multi => 0, + RoomEntityMode::Single => 1, + RoomEntityMode::Challenge => 2, + RoomEntityMode::Battle => 3, + } + } +} + #[derive(Debug, Clone)] pub struct RoomEntity { @@ -29,6 +52,7 @@ pub struct RoomEntity { } + #[derive(Debug, Clone)] pub struct NewRoomEntity { pub name: String, @@ -66,7 +90,7 @@ impl NewRoomEntity { } -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, Serialize)] pub enum RoomNote { Create { character_id: CharacterEntityId, diff --git a/src/ship/items/actions.rs b/src/ship/items/actions.rs index afdd044..ad36f52 100644 --- a/src/ship/items/actions.rs +++ b/src/ship/items/actions.rs @@ -9,22 +9,23 @@ use std::iter::IntoIterator; use anyhow::Context; use libpso::packet::{ship::Message, messages::GameMessage}; -use crate::ship::map::MapArea; -use crate::ship::ship::SendShipPacket; use crate::entity::character::{CharacterEntity, CharacterEntityId}; use crate::entity::gateway::{EntityGateway, EntityGatewayTransaction}; +use crate::entity::item::{ItemDetail, NewItemEntity, TradeId, ItemModifier}; +use crate::entity::item::tool::Tool; +use crate::entity::room::RoomEntityId; +use crate::ship::map::MapArea; +use crate::ship::ship::SendShipPacket; use crate::ship::items::state::{ItemStateProxy, ItemStateError, AddItemResult, StackedItemDetail, IndividualItemDetail}; use crate::ship::items::bank::{BankItem, BankItemDetail}; use crate::ship::items::inventory::{InventoryItem, InventoryItemDetail}; use crate::ship::items::floor::{FloorItem, FloorItemDetail}; use crate::ship::items::apply_item::{apply_item, ApplyItemAction}; -use crate::entity::item::{ItemDetail, NewItemEntity, TradeId}; -use crate::entity::item::tool::Tool; -use crate::entity::item::ItemModifier; use crate::ship::shops::ShopItem; use crate::ship::drops::{ItemDrop, ItemDropType}; use crate::ship::packet::builder; use crate::ship::location::AreaClient; +use crate::ship::monster::MonsterType; pub enum TriggerCreateItem { Yes, @@ -513,7 +514,9 @@ where Box::pin(async move { let mut transaction = inventory_item.with_entity_id(transaction, |mut transaction, entity_id| { async move { - transaction.gateway().add_item_note(&entity_id, ItemNote::Consumed).await?; + transaction.gateway().add_item_note(&entity_id, ItemNote::Consumed { + character_id: character.id, + }).await?; Ok(transaction) }}).await?; @@ -660,7 +663,9 @@ where let mut transaction = inventory_item.with_entity_id(transaction, |mut transaction, entity_id| { async move { - transaction.gateway().add_item_note(&entity_id, ItemNote::SoldToShop).await?; + transaction.gateway().add_item_note(&entity_id, ItemNote::SoldToShop { + character_id, + }).await?; Ok(transaction) }}).await?; transaction.gateway().set_character_meseta(&character_id, inventory.meseta).await?; @@ -946,13 +951,6 @@ where let entity = transaction.gateway().create_item(NewItemEntity { item: item_detail.clone(), }).await?; - transaction.gateway().add_item_note(&entity.id, ItemNote::EnemyDrop { - character_id, - map_area: item_drop.map_area, - x: item_drop.x, - y: item_drop.y, - z: item_drop.z, - }).await?; FloorItem { item_id, item: FloorItemDetail::Individual(IndividualItemDetail { @@ -969,13 +967,6 @@ where let entity = transaction.gateway().create_item(NewItemEntity { item: ItemDetail::Tool(tool), }).await?; - transaction.gateway().add_item_note(&entity.id, ItemNote::EnemyDrop { - character_id, - map_area: item_drop.map_area, - x: item_drop.x, - y: item_drop.y, - z: item_drop.z, - }).await?; FloorItem { item_id, item: FloorItemDetail::Stacked(StackedItemDetail{ @@ -1005,6 +996,88 @@ where } } + +pub(super) fn item_note_enemy_drop<'a, EG, TR>( + character_id: CharacterEntityId, + room_id: RoomEntityId, + monster_type: MonsterType, +) -> impl Fn((ItemStateProxy, TR), FloorItem) + -> BoxFuture<'a, Result<((ItemStateProxy, TR), FloorItem), anyhow::Error>> + Clone +where + EG: EntityGateway, + TR: EntityGatewayTransaction + 'a, +{ + move |(item_state, mut transaction), floor_item| { + Box::pin(async move { + match &floor_item.item { + FloorItemDetail::Individual(individual) => { + transaction.gateway().add_item_note(&individual.entity_id, ItemNote::EnemyDrop { + character_id, + room_id, + monster_type, + map_area: floor_item.map_area, + x: floor_item.x, + y: floor_item.y, + z: floor_item.z, + }).await?; + }, + FloorItemDetail::Stacked(stacked) => { + transaction.gateway().add_item_note(&stacked.entity_ids[0], ItemNote::EnemyDrop { + character_id, + room_id, + monster_type, + map_area: floor_item.map_area, + x: floor_item.x, + y: floor_item.y, + z: floor_item.z, + }).await?; + }, + _ => {}, + } + Ok(((item_state, transaction), floor_item)) + }) + } +} + +pub(super) fn item_note_box_drop<'a, EG, TR>( + character_id: CharacterEntityId, + room_id: RoomEntityId, +) -> impl Fn((ItemStateProxy, TR), FloorItem) + -> BoxFuture<'a, Result<((ItemStateProxy, TR), FloorItem), anyhow::Error>> + Clone +where + EG: EntityGateway, + TR: EntityGatewayTransaction + 'a, +{ + move |(item_state, mut transaction), floor_item| { + Box::pin(async move { + match &floor_item.item { + FloorItemDetail::Individual(individual) => { + transaction.gateway().add_item_note(&individual.entity_id, ItemNote::BoxDrop { + character_id, + room_id, + map_area: floor_item.map_area, + x: floor_item.x, + y: floor_item.y, + z: floor_item.z, + }).await?; + }, + FloorItemDetail::Stacked(stacked) => { + transaction.gateway().add_item_note(&stacked.entity_ids[0], ItemNote::BoxDrop { + character_id, + room_id, + map_area: floor_item.map_area, + x: floor_item.x, + y: floor_item.y, + z: floor_item.z, + }).await?; + }, + _ => {}, + } + Ok(((item_state, transaction), floor_item)) + }) + } +} + pub(super) fn add_item_to_local_floor<'a, EG, TR>( character_id: CharacterEntityId, ) -> impl Fn((ItemStateProxy, TR), FloorItem) diff --git a/src/ship/items/tasks.rs b/src/ship/items/tasks.rs index 2bde925..d8603f0 100644 --- a/src/ship/items/tasks.rs +++ b/src/ship/items/tasks.rs @@ -6,15 +6,17 @@ use crate::ship::ship::SendShipPacket; use crate::ship::map::MapArea; use crate::entity::character::{CharacterEntity, CharacterEntityId}; use crate::entity::gateway::{EntityGateway, EntityGatewayTransaction}; +use crate::entity::item::ItemModifier; +use crate::entity::room::RoomEntityId; use crate::ship::items::state::{ItemState, ItemStateProxy, IndividualItemDetail}; use crate::ship::items::itemstateaction::{ItemStateAction, ItemAction}; use crate::ship::items::inventory::InventoryItem; use crate::ship::items::floor::FloorItem; -use crate::entity::item::ItemModifier; use crate::ship::shops::ShopItem; use crate::ship::trade::TradeItem; use crate::ship::location::AreaClient; use crate::ship::drops::ItemDrop; +use crate::ship::monster::MonsterType; use crate::ship::items::actions; @@ -465,6 +467,32 @@ pub fn enemy_drops_item<'a, EG> ( item_state: &'a mut ItemState, entity_gateway: &'a mut EG, character_id: CharacterEntityId, + room_id: RoomEntityId, + monster_type: MonsterType, + item_drop: ItemDrop) + -> BoxFuture<'a, Result> +where + EG: EntityGateway + 'static, +{ + entity_gateway.with_transaction(move |transaction| async move { + let item_state_proxy = ItemStateProxy::new(item_state.clone()); + let ((item_state_proxy, transaction), floor_item) = ItemStateAction::default() + .act(actions::convert_item_drop_to_floor_item(character_id, item_drop)) + .act(actions::item_note_enemy_drop(character_id, room_id, monster_type)) + .act(actions::add_item_to_local_floor(character_id)) + .commit((item_state_proxy, transaction)) + .await?; + + item_state_proxy.commit().await; + Ok((transaction, floor_item)) + }) +} + +pub fn box_drops_item<'a, EG> ( + item_state: &'a mut ItemState, + entity_gateway: &'a mut EG, + character_id: CharacterEntityId, + room_id: RoomEntityId, item_drop: ItemDrop) -> BoxFuture<'a, Result> where @@ -474,6 +502,7 @@ where let item_state_proxy = ItemStateProxy::new(item_state.clone()); let ((item_state_proxy, transaction), floor_item) = ItemStateAction::default() .act(actions::convert_item_drop_to_floor_item(character_id, item_drop)) + .act(actions::item_note_box_drop(character_id, room_id)) .act(actions::add_item_to_local_floor(character_id)) .commit((item_state_proxy, transaction)) .await?; diff --git a/src/ship/packet/handler/direct_message.rs b/src/ship/packet/handler/direct_message.rs index 81f0146..3c914e8 100644 --- a/src/ship/packet/handler/direct_message.rs +++ b/src/ship/packet/handler/direct_message.rs @@ -18,7 +18,7 @@ use crate::ship::shops::{ShopItem, ToolShopItem, ArmorShopItem}; use crate::ship::items::state::{ItemState, ItemStateError}; use crate::ship::items::floor::{FloorType, FloorItemDetail}; use crate::ship::items::actions::TriggerCreateItem; -use crate::ship::items::tasks::{pick_up_item, withdraw_meseta, deposit_meseta, withdraw_item, deposit_item, buy_shop_item, enemy_drops_item, take_meseta, apply_modifier}; +use crate::ship::items::tasks::{pick_up_item, withdraw_meseta, deposit_meseta, withdraw_item, deposit_item, buy_shop_item, enemy_drops_item, box_drops_item, take_meseta, apply_modifier}; const BANK_ACTION_DEPOSIT: u8 = 0; const BANK_ACTION_WITHDRAW: u8 = 1; @@ -89,8 +89,8 @@ where EG: EntityGateway + 'static, { let room_id = client_location.get_room(id).await?; - let monster = rooms.with(room_id, |room| Box::pin(async move { - room.maps.enemy_by_id(request_item.enemy_id as usize) + let (room_entity_id, monster) = rooms.with(room_id, |room| Box::pin(async move { + Ok::<_, anyhow::Error>((room.room_id, room.maps.enemy_by_id(request_item.enemy_id as usize)?)) })).await??; if monster.dropped_item { @@ -121,7 +121,7 @@ where client.character.id })).await?; - let floor_item = enemy_drops_item(item_state, entity_gateway, character_id, item_drop).await?; + let floor_item = enemy_drops_item(item_state, entity_gateway, character_id, room_entity_id, monster.monster, item_drop).await?; let item_drop_msg = builder::message::item_drop(request_item.client, request_item.target, &floor_item)?; item_drop_packets.push((area_client.client, SendShipPacket::Message(Message::new(GameMessage::ItemDrop(item_drop_msg))))); @@ -200,8 +200,8 @@ where EG: EntityGateway + Clone + 'static { let room_id = client_location.get_room(id).await?; - let box_object = rooms.with(room_id, |room| Box::pin(async move { - room.maps.object_by_id(box_drop_request.object_id as usize) + let (room_entity_id, box_object) = rooms.with(room_id, |room| Box::pin(async move { + Ok::<_, anyhow::Error>((room.room_id, room.maps.object_by_id(box_drop_request.object_id as usize)?)) })).await??; if box_object.dropped_item { @@ -232,7 +232,7 @@ where let character_id = clients.with(area_client.client, |client| Box::pin(async move { client.character.id })).await?; - let floor_item = enemy_drops_item(item_state, entity_gateway, character_id, item_drop).await?; + let floor_item = box_drops_item(item_state, entity_gateway, character_id, room_entity_id, item_drop).await?; //let floor_item = enemy_drops_item(item_state, &mut entity_gateway, client.character.id, item_drop).await?; let item_drop_msg = builder::message::item_drop(box_drop_request.client, box_drop_request.target, &floor_item)?; item_drop_packets.push((area_client.client, SendShipPacket::Message(Message::new(GameMessage::ItemDrop(item_drop_msg))))) diff --git a/src/ship/packet/handler/quest.rs b/src/ship/packet/handler/quest.rs index e451f9f..3dee2ff 100644 --- a/src/ship/packet/handler/quest.rs +++ b/src/ship/packet/handler/quest.rs @@ -3,7 +3,7 @@ use futures::stream::{FuturesOrdered, StreamExt}; use libpso::packet::ship::*; use crate::common::serverstate::ClientId; use crate::ship::ship::{SendShipPacket, ShipError, Clients, ShipEvent}; -use crate::ship::room::Rooms; +use crate::ship::room::{Rooms, QuestCategoryType}; use crate::ship::map::enemy::RareMonsterAppearTable; use crate::ship::location::{ClientLocation}; use crate::ship::packet::builder::quest; @@ -46,8 +46,9 @@ pub async fn send_quest_category_list(id: ClientId, let room_id = client_location.get_room(id).await?; let rql = rql.clone(); rooms.with_mut(room_id, |room| Box::pin(async move { - let qcl = quest::quest_category_list(&room.quests[rql.flag.clamp(0, (room.quests.len() - 1) as u32) as usize]); - room.set_quest_group(rql.flag as usize); + //let qcl = quest::quest_category_list(&room.quests[rql.flag.clamp(0, (room.quests.len() - 1) as u32) as usize]); + room.quest_group = rql.flag.into(); + let qcl = quest::quest_category_list(room.quests()); Ok(vec![(id, SendShipPacket::QuestCategoryList(qcl))]) })).await? } @@ -59,10 +60,10 @@ pub async fn select_quest_category(id: ClientId, -> Result, anyhow::Error> { let room_id = client_location.get_room(id).await?; rooms.with(room_id, |room| Box::pin(async move { - let (_, category_quests) = room.quests[room.quest_group.value()].iter() + let (_, category_quests) = room.quests() + .iter() .nth(menuselect.item as usize) .ok_or_else(|| ShipError::InvalidQuestCategory(menuselect.item as u16))?; - let ql = quest::quest_list(menuselect.item, category_quests); Ok(vec![(id, SendShipPacket::QuestOptionList(ql))]) })).await? @@ -76,7 +77,7 @@ pub async fn quest_detail(id: ClientId, -> Result, anyhow::Error> { let room_id = client_location.get_room(id).await?; rooms.with(room_id, |room| Box::pin(async move { - let (_, category_quests) = room.quests[room.quest_group.value()].iter() + let (_, category_quests) = room.quests().iter() .nth(questdetailrequest.category as usize) .ok_or_else(|| ShipError::InvalidQuestCategory(questdetailrequest.category))?; @@ -105,7 +106,7 @@ pub async fn player_chose_quest(id: ClientId, rooms.with_mut(room_id, |room| { let clients = clients.clone(); Box::pin(async move { - let quest = room.quests[room.quest_group.value()].iter() + let quest = room.quests().iter() .nth(questmenuselect.category as usize) .ok_or_else(|| ShipError::InvalidQuestCategory(questmenuselect.category))? .1 @@ -149,7 +150,7 @@ pub async fn quest_file_request(id: ClientId, let quest_file_request = quest_file_request.clone(); rooms.with(room_id, |room| Box::pin(async move { let (category_id, quest_id, datatype) = parse_filename(&quest_file_request.filename)?; - let (_, category_quests) = room.quests[room.quest_group.value()].iter() + let (_, category_quests) = room.quests().iter() .nth(category_id as usize) .ok_or_else(|| ShipError::InvalidQuestCategory(category_id))?; @@ -182,7 +183,7 @@ pub async fn quest_chunk_ack(id: ClientId, let quest_chunk_ack = quest_chunk_ack.clone(); rooms.with(room_id, |room| Box::pin(async move { let (category_id, quest_id, datatype) = parse_filename(&quest_chunk_ack.filename)?; - let (_, category_quests) = room.quests[room.quest_group.value()].iter() + let (_, category_quests) = room.quests().iter() .nth(category_id as usize) .ok_or_else(|| ShipError::InvalidQuestCategory(category_id))?; diff --git a/src/ship/packet/handler/room.rs b/src/ship/packet/handler/room.rs index 1055b28..ca9ea5b 100644 --- a/src/ship/packet/handler/room.rs +++ b/src/ship/packet/handler/room.rs @@ -7,7 +7,9 @@ use libpso::packet::ship::*; use libpso::packet::messages::*; use crate::common::serverstate::ClientId; use crate::common::leveltable::LEVEL_TABLE; +use crate::entity::gateway::EntityGateway; use crate::entity::character::SectionID; +use crate::entity::room::{RoomEntity, RoomEntityId, NewRoomEntity, RoomEntityMode}; use crate::ship::drops::DropTable; use crate::ship::ship::{SendShipPacket, Clients, ShipEvent}; use crate::ship::room::{Rooms, Episode, Difficulty, RoomState, RoomMode}; @@ -17,20 +19,25 @@ use crate::ship::packet::builder; use crate::ship::items::state::ItemState; #[allow(clippy::too_many_arguments)] -pub async fn create_room(id: ClientId, - create_room: CreateRoom, - client_location: &mut ClientLocation, - clients: &Clients, - item_state: &mut ItemState, - rooms: &Rooms, - map_builder: Arc Maps + Send + Sync>>, - drop_table_builder: Arc DropTable + Send + Sync>>, - event: ShipEvent) - -> Result, anyhow::Error> { +pub async fn create_room(id: ClientId, + create_room: CreateRoom, + entity_gateway: &mut EG, + client_location: &mut ClientLocation, + clients: &Clients, + item_state: &mut ItemState, + rooms: &Rooms, + map_builder: Arc Maps + Send + Sync>>, + drop_table_builder: Arc DropTable + Send + Sync>>, + event: ShipEvent) + -> Result, anyhow::Error> +where + EG: EntityGateway + Clone + 'static, +{ let level = clients.with(id, |client| Box::pin(async move { LEVEL_TABLE.get_level_from_exp(client.character.char_class, client.character.exp) })).await?; - match Difficulty::try_from(create_room.difficulty)? { + let difficulty = Difficulty::try_from(create_room.difficulty)?; + match difficulty { Difficulty::Ultimate if level < 80 => { return Ok(vec![(id, SendShipPacket::SmallDialog(SmallDialog::new("You must be at least level 80 \nto create Ultimate rooms.".into())))]) }, @@ -49,11 +56,34 @@ pub async fn create_room(id: ClientId, let room_id = client_location.create_new_room(id).await?; let new_area_client = client_location.get_local_client(id).await?; + + + let name = String::from_utf16_lossy(&create_room.name).trim_matches(char::from(0)).to_string(); + let mode = match (create_room.battle, create_room.challenge, create_room.single_player) { + (1, 0, 0) => RoomEntityMode::Battle, + (0, 1, 0) => RoomEntityMode::Challenge, + (0, 0, 1) => RoomEntityMode::Single, + _ => RoomEntityMode::Multi, + }; + let episode = create_room.episode.try_into()?; + let difficulty = create_room.difficulty.try_into()?; + let room = clients.with(id, |client| { let mut item_state = item_state.clone(); + let mut entity_gateway = entity_gateway.clone(); Box::pin(async move { item_state.add_character_to_room(room_id, &client.character, new_area_client).await; - let mut room = RoomState::from_create_room(&create_room, map_builder, drop_table_builder, client.character.section_id, event)?; + let room_entity = entity_gateway.create_room(NewRoomEntity { + name: name.clone(), + section_id: client.character.section_id, + mode, + episode, + difficulty, + }).await?; + + let mut room = RoomState::new(room_entity.id, mode, episode, difficulty, + client.character.section_id, name, create_room.password, event, + map_builder, drop_table_builder)?; room.bursting = true; Ok::<_, anyhow::Error>(room) })}).await??; diff --git a/src/ship/quests.rs b/src/ship/quests.rs index 02c1906..e0b8c8c 100644 --- a/src/ship/quests.rs +++ b/src/ship/quests.rs @@ -11,7 +11,7 @@ use ages_prs::{LegacyPrsDecoder, LegacyPrsEncoder}; use byteorder::{LittleEndian, ReadBytesExt}; use libpso::util::array_to_utf16; use crate::ship::map::{MapArea, MapAreaError, MapObject, MapEnemy, enemy_data_from_stream, objects_from_stream}; -use crate::ship::room::Episode; +use crate::ship::room::{Episode, RoomMode}; use crate::ship::map::area::{MapAreaLookup, MapAreaLookupBuilder}; @@ -152,11 +152,14 @@ fn parse_dat(dat: &[u8], episode: &Episode, map_areas: &MapAreaLookup) -> Result } #[derive(Error, Debug)] -#[error("")] pub enum QuestLoadError { + #[error("io error {0}")] IoError(#[from] std::io::Error), + #[error("parse dat error {0}")] ParseDatError(#[from] ParseDatError), + #[error("could not read metadata")] CouldNotReadMetadata, + #[error("could not load config file")] CouldNotLoadConfigFile, } @@ -233,7 +236,7 @@ pub fn load_quest(bin_path: PathBuf, dat_path: PathBuf, quest_path: PathBuf) -> } -pub fn load_quests(mut quest_path: PathBuf) -> Result { +pub fn load_quests_path(mut quest_path: PathBuf) -> Result { let mut f = File::open(quest_path.clone()).map_err(|_| QuestLoadError::CouldNotLoadConfigFile)?; let mut s = String::new(); f.read_to_string(&mut s)?; @@ -242,28 +245,58 @@ pub fn load_quests(mut quest_path: PathBuf) -> Result let ql: BTreeMap = toml::from_str(s.as_str()).map_err(|_| QuestLoadError::CouldNotLoadConfigFile)?; Ok(ql.into_iter().map(|(category, category_details)| { - let quests = category_details.quests - .into_iter() - .filter_map(|quest| { - load_quest(quest.bin.into(), quest.dat.into(), quest_path.to_path_buf()) - .and_then(|quest | { - if used_quest_ids.contains(&quest.id) { - warn!("quest id already exists: {}", quest.id); - return None; - } - used_quest_ids.insert(quest.id); - Some(quest) - }) - }); - (QuestCategory{ - index: category_details.list_order, - name: category, - description: category_details.description, - }, quests.collect()) + ( + QuestCategory { + index: category_details.list_order, + name: category, + description: category_details.description, + }, + category_details.quests + .into_iter() + .filter_map(|quest| { + load_quest(quest.bin.into(), quest.dat.into(), quest_path.to_path_buf()) + .and_then(|quest | { + if used_quest_ids.contains(&quest.id) { + warn!("quest id already exists: {}", quest.id); + return None; + } + used_quest_ids.insert(quest.id); + Some(quest) + }) + }) + .collect() + ) }).collect()) } +pub fn load_standard_quests(mode: RoomMode) -> Result { + match mode { + RoomMode::Single {episode, difficulty } => { + load_quests_path(PathBuf::from_iter(["data", "quests", "bb", &episode.to_string(), "single", "quests.toml"])) + }, + RoomMode::Multi {episode, difficulty } => { + load_quests_path(PathBuf::from_iter(["data", "quests", "bb", &episode.to_string(), "multi", "quests.toml"])) + }, + _ => { + Ok(BTreeMap::new()) + } + } +} + +pub fn load_government_quests(mode: RoomMode) -> Result { + match mode { + RoomMode::Single {episode, difficulty } => { + load_quests_path(PathBuf::from_iter(["data", "quests", "bb", &episode.to_string(), "government", "quests.toml"])) + }, + RoomMode::Multi {episode, difficulty } => { + load_quests_path(PathBuf::from_iter(["data", "quests", "bb", &episode.to_string(), "government", "quests.toml"])) + }, + _ => { + Ok(BTreeMap::new()) + } + } +} diff --git a/src/ship/room.rs b/src/ship/room.rs index 87e5585..69820b1 100644 --- a/src/ship/room.rs +++ b/src/ship/room.rs @@ -11,6 +11,7 @@ use rand::Rng; use crate::ship::map::Maps; use crate::ship::drops::DropTable; use crate::entity::character::SectionID; +use crate::entity::room::{RoomEntityId, RoomEntityMode}; use crate::ship::monster::{load_monster_stats_table, MonsterType, MonsterStats}; use crate::ship::map::area::MapAreaLookup; use crate::ship::quests; @@ -55,7 +56,7 @@ impl Rooms { None => false, } } - + pub async fn with<'a, T, F>(&'a self, room_id: RoomId, func: F) -> Result where T: Send, @@ -92,7 +93,7 @@ impl Rooms { Err(ShipError::InvalidRoom(room_id.0 as u32).into()) } } - + pub async fn get(&self, room_id: RoomId) -> RwLockReadGuard> { self.0 .get(room_id.0) @@ -282,8 +283,15 @@ impl From for QuestCategoryType { fn from(f: usize) -> QuestCategoryType { match f { 0 => QuestCategoryType::Standard, - 1 => QuestCategoryType::Government, - _ => QuestCategoryType::Standard, // TODO: panic? + _ => QuestCategoryType::Government, + } + } +} +impl From for QuestCategoryType { + fn from(f: u32) -> QuestCategoryType { + match f { + 0 => QuestCategoryType::Standard, + _ => QuestCategoryType::Government, } } } @@ -298,6 +306,7 @@ impl QuestCategoryType { } pub struct RoomState { + pub room_id: RoomEntityId, pub mode: RoomMode, pub name: String, pub password: [u16; 16], @@ -309,22 +318,22 @@ pub struct RoomState { pub monster_stats: Box>, pub map_areas: MapAreaLookup, pub quest_group: QuestCategoryType, - pub quests: Vec, - // items on ground + pub standard_quests: quests::QuestList, + pub government_quests: quests::QuestList, // enemy info } impl RoomState { pub fn get_flags_for_room_list(&self) -> u8 { let mut flags = 0u8; - + match self.mode { RoomMode::Single {..} => {flags += 0x04} RoomMode::Battle {..} => {flags += 0x10}, RoomMode::Challenge {..} => {flags += 0x20}, _ => {flags += 0x40}, }; - + if self.password[0] > 0 { flags += 0x02; } @@ -345,85 +354,58 @@ impl RoomState { difficulty + 0x22 } - pub fn set_quest_group(&mut self, group: usize) { - self.quest_group = QuestCategoryType::from(group); - } - - pub fn from_create_room(create_room: &libpso::packet::ship::CreateRoom, - map_builder: Arc Maps + Send + Sync>>, - drop_table_builder: Arc DropTable + Send + Sync>>, - section_id: SectionID, - event: ShipEvent) - -> Result { - if [create_room.battle, create_room.challenge, create_room.single_player].iter().sum::() > 1 { - return Err(RoomCreationError::InvalidMode) - } - - let room_mode = if create_room.battle == 1 { - RoomMode::Battle { - episode: create_room.episode.try_into()?, - difficulty: create_room.difficulty.try_into()?, - } - } - else if create_room.challenge == 1 { - RoomMode::Challenge { - episode: create_room.episode.try_into()?, - } - } - else if create_room.single_player == 1 { - RoomMode::Single { - episode: create_room.episode.try_into()?, - difficulty: create_room.difficulty.try_into()?, - } + pub fn quests(&self) -> &quests::QuestList { + match self.quest_group { + QuestCategoryType::Standard => &self.standard_quests, + QuestCategoryType::Government => &self.government_quests, } - else { // normal multimode - RoomMode::Multi { - episode: create_room.episode.try_into()?, - difficulty: create_room.difficulty.try_into()?, - } - }; - + } - // push the usual set of quests for the selected mode - let mut qpath = PathBuf::from("data/quests/bb"); - qpath.push(room_mode.episode().to_string()); - qpath.push(room_mode.to_string()); - qpath.push("quests.toml"); - let mut room_quests = Vec::new(); - let quest_list = match quests::load_quests(qpath) { - Ok(qlist) => qlist, - Err(_) => return Err(RoomCreationError::CouldNotLoadQuests), + pub fn new (room_id: RoomEntityId, + mode: RoomEntityMode, + episode: Episode, + difficulty: Difficulty, + section_id: SectionID, + name: String, + password: [u16; 16], + event: ShipEvent, + map_builder: Arc Maps + Send + Sync>>, + drop_table_builder: Arc DropTable + Send + Sync>>, + ) -> Result { + let mode = match mode { + RoomEntityMode::Single => RoomMode::Single { + episode, + difficulty, + }, + RoomEntityMode::Multi => RoomMode::Multi { + episode, + difficulty, + }, + RoomEntityMode::Challenge => RoomMode::Challenge { + episode, + }, + RoomEntityMode::Battle => RoomMode::Battle { + episode, + difficulty, + }, }; - room_quests.push(quest_list); - - // if multiplayer also push the government quests - if let RoomMode::Multi {..} = room_mode { - qpath = PathBuf::from("data/quests/bb/"); - qpath.push(room_mode.episode().to_string()); - qpath.push("government/quests.toml"); - - let quest_list = match quests::load_quests(qpath) { - Ok(qlist) => qlist, - Err(_) => return Err(RoomCreationError::CouldNotLoadQuests), - }; - - room_quests.push(quest_list); - } - Ok(RoomState { - monster_stats: Box::new(load_monster_stats_table(&room_mode).map_err(|_| RoomCreationError::CouldNotLoadMonsterStats(room_mode))?), - mode: room_mode, + room_id, + monster_stats: Box::new(load_monster_stats_table(&mode).map_err(|_| RoomCreationError::CouldNotLoadMonsterStats(mode))?), + mode, random_seed: rand::thread_rng().gen(), - name: String::from_utf16_lossy(&create_room.name).trim_matches(char::from(0)).into(), - password: create_room.password, - maps: map_builder(room_mode, event), + name, + password, + maps: map_builder(mode, event), section_id, - drop_table: Box::new(drop_table_builder(room_mode.episode(), room_mode.difficulty(), section_id)), + drop_table: Box::new(drop_table_builder(episode, difficulty, section_id)), bursting: false, - map_areas: MapAreaLookup::new(&room_mode.episode()), + map_areas: MapAreaLookup::new(&episode), quest_group: QuestCategoryType::Standard, - quests: room_quests, + standard_quests: quests::load_standard_quests(mode)?, + government_quests: quests::load_government_quests(mode)?, }) + } } diff --git a/src/ship/ship.rs b/src/ship/ship.rs index 378196f..e4d94c8 100644 --- a/src/ship/ship.rs +++ b/src/ship/ship.rs @@ -757,7 +757,8 @@ impl ServerState for ShipServerState { }, RecvShipPacket::CreateRoom(create_room) => { let block = self.blocks.get_from_client(id, &self.clients).await?; - handler::room::create_room(id, create_room, &mut block.client_location, &self.clients, &mut self.item_state, &block.rooms, self.map_builder.clone(), self.drop_table_builder.clone(), self.event).await? + handler::room::create_room(id, create_room, &mut self.entity_gateway, &mut block.client_location, &self.clients, &mut self.item_state, + &block.rooms, self.map_builder.clone(), self.drop_table_builder.clone(), self.event).await? }, RecvShipPacket::RoomNameRequest(_req) => { let block = self.blocks.get_from_client(id, &self.clients).await?;