diff --git a/src/entity/item/mod.rs b/src/entity/item/mod.rs index a1f78bd..eabddb5 100644 --- a/src/entity/item/mod.rs +++ b/src/entity/item/mod.rs @@ -241,6 +241,13 @@ impl InventoryItemEntity { _ => None, } } + + pub fn stacked(&self) -> Option<&Vec> { + match self { + InventoryItemEntity::Stacked(i) => Some(i), + _ => None, + } + } } #[derive(Clone, Debug, Default)] diff --git a/src/ship/items/actions.rs b/src/ship/items/actions.rs index 466c4d3..acacbfd 100644 --- a/src/ship/items/actions.rs +++ b/src/ship/items/actions.rs @@ -1,10 +1,14 @@ // TODO: replace various u32s and usizes denoting item amounts for ItemAmount(u32) for consistency use crate::ship::items::ClientItemId; use crate::entity::item::{Meseta, ItemNote}; +use async_std::sync::Arc; use std::future::Future; use std::pin::Pin; +use std::iter::IntoIterator; +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::ship::items::state::{ItemStateProxy, ItemStateError, AddItemResult, StackedItemDetail, IndividualItemDetail}; @@ -17,6 +21,8 @@ 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; type BoxFuture = Pin + Send>>; @@ -462,13 +468,14 @@ where pub(super) fn use_consumed_item( - character: CharacterEntity, + character: &CharacterEntity, ) -> impl Fn((ItemStateProxy, TR), InventoryItem) -> BoxFuture), ItemStateError>> where EG: EntityGateway + Clone + 'static, TR: EntityGatewayTransaction + 'static, { + let character = character.clone(); move |(mut item_state, transaction), inventory_item| { let mut character = character.clone(); Box::pin(async move { @@ -690,22 +697,22 @@ where #[async_recursion::async_recursion] -async fn foreach_inner<'a, EG, TR, O, T, F>( +async fn foreach_inner<'a, EG, TR, O, T, F, I>( state: (ItemStateProxy, TR), - mut input: Vec, - func: F, + mut input: I, + func: Arc, ) -> Result<((ItemStateProxy, TR), Vec), ItemStateError> where 'a: 'async_recursion, EG: EntityGateway, TR: EntityGatewayTransaction + 'static, O: Send, - T: Clone + Send, + T: Send, F: Fn((ItemStateProxy, TR), T) -> BoxFuture> + Send + Sync, - F: Clone, + I: Iterator + Send + Sync + 'static, { - let item = match input.pop() { + let item = match input.next() { Some(item) => item, None => return Ok((state, Vec::new())) }; @@ -718,9 +725,9 @@ where Ok((state, output)) } -pub(super) fn foreach( +pub(super) fn foreach( func: F -) -> impl Fn((ItemStateProxy, TR), Vec) +) -> impl Fn((ItemStateProxy, TR), I) -> BoxFuture), ItemStateError>> where EG: EntityGateway, @@ -729,11 +736,14 @@ where T: Send + Clone + 'static + std::fmt::Debug, F: Fn((ItemStateProxy, TR), T) -> BoxFuture> + Send + Sync + 'static, - F: Clone, - T: Clone + Send + Sync, + T: Send + Sync, + I: IntoIterator + Send + Sync + 'static, + I::IntoIter: Send + Sync, { + let func = Arc::new(func); move |(item_state, transaction), items| { let func = func.clone(); + let items = items.into_iter(); Box::pin(async move { let (state, result) = foreach_inner((item_state, transaction), items, func).await?; Ok((state, result)) @@ -758,6 +768,35 @@ where } } +pub(super) fn fork( + func1: F1, + func2: F2, +) -> impl Fn((ItemStateProxy, TR), T) + -> BoxFuture> +where + EG: EntityGateway, + TR: EntityGatewayTransaction + 'static, + F1: Fn((ItemStateProxy, TR), T) -> BoxFuture> + Send + Sync + 'static, + F2: Fn((ItemStateProxy, TR), T) -> BoxFuture> + Send + Sync + 'static, + T: Send + Sync + Clone + 'static, + O1: Send, + O2: Send, +{ + let func1 = Arc::new(func1); + let func2 = Arc::new(func2); + move |(item_state, transaction), input| { + let input = input.clone(); + let func1 = func1.clone(); + let func2 = func2.clone(); + Box::pin(async move { + let ((item_state, transaction), result1) = func1((item_state, transaction), input.clone()).await?; + let ((item_state, transaction), result2) = func2((item_state, transaction), input).await?; + + Ok(((item_state, transaction), (result1, result2))) + }) + } +} + pub(super) fn add_item_to_inventory( character: CharacterEntity, ) -> impl Fn((ItemStateProxy, TR), InventoryItem) @@ -994,3 +1033,86 @@ where }) } } + + +pub(super) fn apply_item_action_packets( + character_id: CharacterEntityId, + area_client: AreaClient, +) -> impl Fn((ItemStateProxy, TR), ApplyItemAction) + -> BoxFuture), ItemStateError>> +where + EG: EntityGateway, + TR: EntityGatewayTransaction + 'static, +{ + move |(mut item_state, mut transaction), apply_item_action| { + Box::pin(async move { + let pkts = if let ApplyItemAction::CreateItem(item_detail) = apply_item_action { + let new_item = transaction.gateway().create_item(NewItemEntity { + item: item_detail.clone(), + }).await?; + + let item_id = item_state.new_item_id().await?; + + let (inventory_item_detail, create_item) = if item_detail.is_stackable() { + let tool = item_detail.as_tool().ok_or_else(|| ItemStateError::NotATool(ClientItemId(0xFFFFFFFF)))?; + + let create_item = builder::message::create_stacked_item(area_client, item_id, &tool, 1).map_err(|_err| ItemStateError::Dummy)?; + let item_detail = StackedItemDetail { + entity_ids: vec![new_item.id], + tool + }; + (InventoryItemDetail::Stacked(item_detail), create_item) + } + else { + let item_detail = IndividualItemDetail { + entity_id: new_item.id, + item: item_detail, + }; + let create_item = builder::message::create_individual_item(area_client, item_id, &item_detail).map_err(|_err| ItemStateError::Dummy)?; + (InventoryItemDetail::Individual(item_detail), create_item) + }; + + let inventory_item = InventoryItem { + item_id, + item: inventory_item_detail, + }; + + let mut inventory = item_state.inventory(&character_id).await?; + inventory.add_item(inventory_item)?; + transaction.gateway().set_character_inventory(&character_id, &inventory.as_inventory_entity(&character_id)).await?; + item_state.set_inventory(inventory).await; + + vec![SendShipPacket::Message(Message::new(GameMessage::CreateItem(create_item)))] + } + else { + Vec::new() + }; + + Ok(((item_state, transaction), pkts)) + }) + } +} + +pub(super) fn apply_item_action_character( + character: &CharacterEntity +) -> impl Fn((ItemStateProxy, TR), Vec) + -> BoxFuture> +where + EG: EntityGateway, + TR: EntityGatewayTransaction + 'static, +{ + let character = character.clone(); + move |(item_state, transaction), apply_item_actions| { + let mut character = character.clone(); + Box::pin(async move { + for action in apply_item_actions { + match action { + ApplyItemAction::UpdateCharacter(new_character) => character = *new_character, + _ => {} + } + } + + Ok(((item_state, transaction), character)) + }) + } +} diff --git a/src/ship/items/apply_item.rs b/src/ship/items/apply_item.rs index 6d4f840..edba120 100644 --- a/src/ship/items/apply_item.rs +++ b/src/ship/items/apply_item.rs @@ -2,10 +2,12 @@ use std::convert::TryInto; use futures::future::{join_all, BoxFuture, LocalBoxFuture}; use futures::stream::{FuturesOrdered, StreamExt}; use thiserror::Error; +use rand::{Rng, SeedableRng}; +use rand::distributions::{WeightedIndex, Distribution}; use crate::entity::gateway::{EntityGateway, GatewayError}; use crate::entity::character::CharacterEntity; -use crate::entity::item::mag::{MagCell, MagCellError}; -use crate::entity::item::tool::ToolType; +use crate::entity::item::mag::{MagType, MagCell, MagCellError}; +use crate::entity::item::tool::{Tool, ToolType}; use crate::entity::item::{ItemDetail, ItemEntityId}; use crate::ship::items::state::{ItemStateProxy, ItemStateError}; use crate::ship::items::inventory::{InventoryItem, InventoryItemDetail}; @@ -29,9 +31,12 @@ pub enum ApplyItemError { MagCellError(#[from] MagCellError), } +#[derive(Debug, Clone)] pub enum ApplyItemAction { - UpdateCharacter(CharacterEntity), - CreateItem(()), + UpdateCharacter(Box), + CreateItem(ItemDetail), + //TransformItem, + //RemoveItem, } impl From for ApplyItemError { @@ -43,43 +48,43 @@ impl From for ApplyItemError { async fn power_material(entity_gateway: &mut EG, character: &mut CharacterEntity) -> Result, ApplyItemError> { character.materials.power += 1; entity_gateway.save_character(character).await?; - Ok(vec![ApplyItemAction::UpdateCharacter(character.clone())]) + Ok(vec![ApplyItemAction::UpdateCharacter(Box::new(character.clone()))]) } async fn mind_material(entity_gateway: &mut EG, character: &mut CharacterEntity) -> Result, ApplyItemError> { character.materials.mind += 1; entity_gateway.save_character(character).await.unwrap(); - Ok(vec![ApplyItemAction::UpdateCharacter(character.clone())]) + Ok(vec![ApplyItemAction::UpdateCharacter(Box::new(character.clone()))]) } async fn evade_material(entity_gateway: &mut EG, character: &mut CharacterEntity) -> Result, ApplyItemError> { character.materials.evade += 1; entity_gateway.save_character(character).await.unwrap(); - Ok(vec![ApplyItemAction::UpdateCharacter(character.clone())]) + Ok(vec![ApplyItemAction::UpdateCharacter(Box::new(character.clone()))]) } async fn def_material(entity_gateway: &mut EG, character: &mut CharacterEntity) -> Result, ApplyItemError> { character.materials.def += 1; entity_gateway.save_character(character).await.unwrap(); - Ok(vec![ApplyItemAction::UpdateCharacter(character.clone())]) + Ok(vec![ApplyItemAction::UpdateCharacter(Box::new(character.clone()))]) } async fn luck_material(entity_gateway: &mut EG, character: &mut CharacterEntity) -> Result, ApplyItemError> { character.materials.luck += 1; entity_gateway.save_character(character).await.unwrap(); - Ok(vec![ApplyItemAction::UpdateCharacter(character.clone())]) + Ok(vec![ApplyItemAction::UpdateCharacter(Box::new(character.clone()))]) } async fn hp_material(entity_gateway: &mut EG, character: &mut CharacterEntity) -> Result, ApplyItemError> { character.materials.hp += 1; entity_gateway.save_character(character).await.unwrap(); - Ok(vec![ApplyItemAction::UpdateCharacter(character.clone())]) + Ok(vec![ApplyItemAction::UpdateCharacter(Box::new(character.clone()))]) } async fn tp_material(entity_gateway: &mut EG, character: &mut CharacterEntity) -> Result, ApplyItemError> { character.materials.tp += 1; entity_gateway.save_character(character).await.unwrap(); - Ok(vec![ApplyItemAction::UpdateCharacter(character.clone())]) + Ok(vec![ApplyItemAction::UpdateCharacter(Box::new(character.clone()))]) } /* @@ -224,13 +229,23 @@ pub async fn liberta_kit(entity_gateway: &mut EG, used_cell: } */ -async fn jack_o_lantern<'a, EG>(item_state: &mut ItemStateProxy, - entity_gateway: &mut EG -) -> Result, ApplyItemError> -where - EG: EntityGateway + ?Sized, + +fn jack_o_lantern() -> Result, ApplyItemError> { - Ok(Vec::new()) + let mag_rate = WeightedIndex::new(&[13, 13, 13, 13, 12, 12, 12, 12]).unwrap(); + let mag_type = match mag_rate.sample(&mut rand_chacha::ChaChaRng::from_entropy()) { + 0 => ToolType::CellOfMag502, + 1 => ToolType::CellOfMag213, + 2 => ToolType::HeartOfChuChu, + 3 => ToolType::HeartOfKapuKapu, + 4 => ToolType::PartsOfRobochao, + 5 => ToolType::HeartOfOpaOpa, + 6 => ToolType::HeartOfPian, + 7 => ToolType::HeartOfChao, + _ => unreachable!(), + }; + + Ok(vec![ApplyItemAction::CreateItem(ItemDetail::Tool(Tool {tool: mag_type}))]) } async fn apply_tool<'a, EG>(item_state: &mut ItemStateProxy, @@ -283,7 +298,7 @@ where | ToolType::LibertaKit => { mag_cell(item_state, entity_gateway, character, entity_id, tool.try_into()?).await } - ToolType::JackOLantern => jack_o_lantern(item_state, entity_gateway).await, + ToolType::JackOLantern => jack_o_lantern(), // TODO: rest of these _ => Err(ApplyItemError::InvalidItem) } diff --git a/src/ship/items/inventory.rs b/src/ship/items/inventory.rs index 4024fc8..e10a808 100644 --- a/src/ship/items/inventory.rs +++ b/src/ship/items/inventory.rs @@ -179,7 +179,7 @@ pub enum InventoryError { MesetaFull, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct InventoryState { pub character_id: CharacterEntityId, pub item_id_counter: u32, diff --git a/src/ship/items/state.rs b/src/ship/items/state.rs index 9cea40f..6473f38 100644 --- a/src/ship/items/state.rs +++ b/src/ship/items/state.rs @@ -124,7 +124,7 @@ pub enum AddItemResult { } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct ItemState { character_inventory: Arc>>>, character_bank: Arc>>>, diff --git a/src/ship/items/tasks.rs b/src/ship/items/tasks.rs index 1861bfc..01671d2 100644 --- a/src/ship/items/tasks.rs +++ b/src/ship/items/tasks.rs @@ -272,22 +272,28 @@ pub async fn use_item<'a, EG> ( item_state: &'a mut ItemState, entity_gateway: &mut EG, character: &mut CharacterEntity, + area_client: AreaClient, item_id: &ClientItemId, amount: u32, -) -> Result<(), ItemStateError> +) -> Result, ItemStateError> where EG: EntityGateway + 'static, { entity_gateway.with_transaction(|transaction| async move { let item_state_proxy = ItemStateProxy::new(item_state.clone()); - let ((item_state_proxy, transaction), apply_item_actions) = ItemStateAction::default() + let ((item_state_proxy, transaction), (pkts, new_character)) = ItemStateAction::default() .act(actions::take_item_from_inventory(character.id, *item_id, amount)) - .act(actions::use_consumed_item(character.clone())) + .act(actions::use_consumed_item(&character)) + .act(actions::fork( + actions::foreach(actions::apply_item_action_packets(character.id, area_client)), + actions::apply_item_action_character(character) + )) .commit((item_state_proxy, transaction)) .await?; item_state_proxy.commit().await; + *character = new_character; - Ok((transaction, ())) + Ok((transaction, pkts.into_iter().flatten().collect())) }).await } diff --git a/src/ship/packet/handler/message.rs b/src/ship/packet/handler/message.rs index df8b7d0..8256a91 100644 --- a/src/ship/packet/handler/message.rs +++ b/src/ship/packet/handler/message.rs @@ -330,20 +330,32 @@ where pub async fn player_uses_item(id: ClientId, player_use_tool: PlayerUseItem, entity_gateway: &mut EG, - _client_location: &ClientLocation, + client_location: &ClientLocation, clients: &Clients, item_state: &mut ItemState) -> Result, ShipError> where EG: EntityGateway + Clone + 'static, { - clients.with_mut(id, |client| { + let neighbors = client_location.get_all_clients_by_client(id).await?.into_iter(); + let area_client = client_location.get_local_client(id).await?; + + Ok(clients.with_mut(id, |client| { let mut entity_gateway = entity_gateway.clone(); let mut item_state = item_state.clone(); Box::pin(async move { - use_item(&mut item_state, &mut entity_gateway, &mut client.character, &ClientItemId(player_use_tool.item_id), 1).await - })}).await??; - Ok(Vec::new()) + use_item(&mut item_state, &mut entity_gateway, &mut client.character, area_client, &ClientItemId(player_use_tool.item_id), 1).await + })}).await?? + .into_iter() + .flat_map(move |pkt| { + let player_use_tool = player_use_tool.clone(); + neighbors.clone().map(move |client| { + vec![(client.client, SendShipPacket::Message(Message::new(GameMessage::PlayerUseItem(player_use_tool.clone())))), (client.client, pkt.clone())] + }) + }) + .flatten() + .collect::>() + ) } pub async fn player_used_medical_center(id: ClientId, diff --git a/tests/test_item_use.rs b/tests/test_item_use.rs index 574f948..2289822 100644 --- a/tests/test_item_use.rs +++ b/tests/test_item_use.rs @@ -251,6 +251,59 @@ async fn test_use_materials() { assert!(char.materials.luck == 2); } +#[async_std::test] +async fn test_jackolantern() { + let mut entity_gateway = InMemoryGateway::default(); + let (_user1, char1) = new_user_character(&mut entity_gateway, "a1", "a", 1).await; + + let p1_inv = vec![ + item::InventoryItemEntity::Stacked( + vec![ + entity_gateway.create_item( + item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::JackOLantern, + } + ), + }).await.unwrap(), + entity_gateway.create_item(item::NewItemEntity { + item: item::ItemDetail::Tool( + item::tool::Tool { + tool: item::tool::ToolType::JackOLantern, + } + ), + }).await.unwrap(), + ])]; + entity_gateway.set_character_inventory(&char1.id, &item::InventoryEntity::new(p1_inv)).await.unwrap(); + + let mut ship = Box::new(ShipServerState::builder() + .gateway(entity_gateway.clone()) + .build()); + log_in_char(&mut ship, ClientId(1), "a1", "a").await; + join_lobby(&mut ship, ClientId(1)).await; + create_room(&mut ship, ClientId(1), "room", "").await; + + ship.handle(ClientId(1), RecvShipPacket::Message(Message::new(GameMessage::PlayerUseItem(PlayerUseItem { + client: 0, + target: 0, + item_id: 0x10000, + })))).await.unwrap(); + + ship.handle(ClientId(1), RecvShipPacket::Message(Message::new(GameMessage::PlayerUseItem(PlayerUseItem { + client: 0, + target: 0, + item_id: 0x10000, + })))).await.unwrap(); + + let inventory_items = entity_gateway.get_character_inventory(&char1.id).await.unwrap(); + for item in inventory_items.items { + for sitem in item.stacked().unwrap() { + assert!(sitem.item.clone().as_tool().unwrap().tool.is_mag_cell()); + } + } +} + // TODO: tests for ALL ITEMS WOW /* diff --git a/tests/test_trade.rs b/tests/test_trade.rs index 9175716..61bff99 100644 --- a/tests/test_trade.rs +++ b/tests/test_trade.rs @@ -1792,7 +1792,7 @@ async fn test_trade_multiple_individual() { msg: GameMessage::CreateItem(CreateItem { client: 1, item_data: [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // saber - item_id: 0x810003, + item_id: 0x810004, .. }), .. @@ -1801,7 +1801,7 @@ async fn test_trade_multiple_individual() { msg: GameMessage::CreateItem(CreateItem { client: 1, item_data: [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // saber - item_id: 0x810003, + item_id: 0x810004, .. }), .. @@ -1810,7 +1810,7 @@ async fn test_trade_multiple_individual() { msg: GameMessage::CreateItem(CreateItem { client: 1, item_data: [0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0], // saber - item_id: 0x810004, + item_id: 0x810003, .. }), .. @@ -1819,7 +1819,7 @@ async fn test_trade_multiple_individual() { msg: GameMessage::CreateItem(CreateItem { client: 1, item_data: [0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0], // saber - item_id: 0x810004, + item_id: 0x810003, .. }), .. @@ -1828,7 +1828,7 @@ async fn test_trade_multiple_individual() { msg: GameMessage::CreateItem(CreateItem { client: 0, item_data: [0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // handgun - item_id: 0x810001, + item_id: 0x810002, .. }), .. @@ -1837,7 +1837,7 @@ async fn test_trade_multiple_individual() { msg: GameMessage::CreateItem(CreateItem { client: 0, item_data: [0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], // handgun - item_id: 0x810001, + item_id: 0x810002, .. }), .. @@ -1846,7 +1846,7 @@ async fn test_trade_multiple_individual() { msg: GameMessage::CreateItem(CreateItem { client: 0, item_data: [0, 6, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], // handgun - item_id: 0x810002, + item_id: 0x810001, .. }), .. @@ -1855,7 +1855,7 @@ async fn test_trade_multiple_individual() { msg: GameMessage::CreateItem(CreateItem { client: 0, item_data: [0, 6, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0], // handgun - item_id: 0x810002, + item_id: 0x810001, .. }), .. @@ -2063,7 +2063,7 @@ async fn test_trade_multiple_stacked() { assert!(matches!(ack[4], (ClientId(1), SendShipPacket::Message(Message { msg: GameMessage::CreateItem(CreateItem { client: 1, - item_id: 0x810003, + item_id: 0x810004, .. }), .. @@ -2071,7 +2071,7 @@ async fn test_trade_multiple_stacked() { assert!(matches!(ack[5], (ClientId(2), SendShipPacket::Message(Message { msg: GameMessage::CreateItem(CreateItem { client: 1, - item_id: 0x810003, + item_id: 0x810004, .. }), .. @@ -2079,7 +2079,7 @@ async fn test_trade_multiple_stacked() { assert!(matches!(ack[6], (ClientId(1), SendShipPacket::Message(Message { msg: GameMessage::CreateItem(CreateItem { client: 1, - item_id: 0x810004, + item_id: 0x810003, .. }), .. @@ -2087,7 +2087,7 @@ async fn test_trade_multiple_stacked() { assert!(matches!(ack[7], (ClientId(2), SendShipPacket::Message(Message { msg: GameMessage::CreateItem(CreateItem { client: 1, - item_id: 0x810004, + item_id: 0x810003, .. }), .. @@ -2095,7 +2095,7 @@ async fn test_trade_multiple_stacked() { assert!(matches!(ack[8], (ClientId(1), SendShipPacket::Message(Message { msg: GameMessage::CreateItem(CreateItem { client: 0, - item_id: 0x810001, + item_id: 0x810002, .. }), .. @@ -2103,7 +2103,7 @@ async fn test_trade_multiple_stacked() { assert!(matches!(ack[9], (ClientId(2), SendShipPacket::Message(Message { msg: GameMessage::CreateItem(CreateItem { client: 0, - item_id: 0x810001, + item_id: 0x810002, .. }), .. @@ -2111,7 +2111,7 @@ async fn test_trade_multiple_stacked() { assert!(matches!(ack[10], (ClientId(1), SendShipPacket::Message(Message { msg: GameMessage::CreateItem(CreateItem { client: 0, - item_id: 0x810002, + item_id: 0x810001, .. }), .. @@ -2119,7 +2119,7 @@ async fn test_trade_multiple_stacked() { assert!(matches!(ack[11], (ClientId(2), SendShipPacket::Message(Message { msg: GameMessage::CreateItem(CreateItem { client: 0, - item_id: 0x810002, + item_id: 0x810001, .. }), ..