From feecd6ea2bd372c8566efb04423b292117578a38 Mon Sep 17 00:00:00 2001 From: crabman Date: Tue, 5 Mar 2024 06:35:55 +0000 Subject: [PATCH] Dynamic merging of dropped items --- CHANGELOG.md | 1 + common/net/src/synced_components.rs | 4 +- common/src/comp/inventory/item/mod.rs | 277 ++++++++++++++++++---- common/src/comp/inventory/item/modular.rs | 2 +- common/src/comp/mod.rs | 2 +- common/src/event.rs | 4 +- common/src/lib.rs | 3 +- common/state/src/state.rs | 2 +- server/src/cmd.rs | 4 +- server/src/events/entity_manipulation.rs | 15 +- server/src/events/interaction.rs | 5 +- server/src/events/inventory_manip.rs | 102 +++++--- server/src/state_ext.rs | 68 +++--- server/src/sys/item.rs | 158 ++++++++++++ server/src/sys/mod.rs | 2 + voxygen/src/hud/mod.rs | 12 +- voxygen/src/scene/figure/mod.rs | 22 +- voxygen/src/session/interactable.rs | 2 +- voxygen/src/session/mod.rs | 2 +- voxygen/src/session/target.rs | 2 +- 20 files changed, 540 insertions(+), 149 deletions(-) create mode 100644 server/src/sys/item.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 34307ab0de..bf5d302f47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,6 +96,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Weapons block are based on poise - Wooden Shield recipe - Overhauled the visuals of several cave biomes +- Dropped items now merge dynamically (including non-stackables) ### Removed - Medium and large potions from all loot tables diff --git a/common/net/src/synced_components.rs b/common/net/src/synced_components.rs index 4819cb1880..7e21a5f1c0 100755 --- a/common/net/src/synced_components.rs +++ b/common/net/src/synced_components.rs @@ -29,7 +29,7 @@ macro_rules! synced_components { poise: Poise, light_emitter: LightEmitter, loot_owner: LootOwner, - item: Item, + item: PickupItem, scale: Scale, group: Group, is_mount: IsMount, @@ -166,7 +166,7 @@ impl NetSync for LootOwner { const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity; } -impl NetSync for Item { +impl NetSync for PickupItem { const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity; } diff --git a/common/src/comp/inventory/item/mod.rs b/common/src/comp/inventory/item/mod.rs index 49b237bc96..4e7c3a47c3 100644 --- a/common/src/comp/inventory/item/mod.rs +++ b/common/src/comp/inventory/item/mod.rs @@ -12,6 +12,7 @@ use crate::{ comp::inventory::InvSlot, effect::Effect, recipe::RecipeInput, + resources::ProgramTime, terrain::Block, }; use common_i18n::Content; @@ -474,6 +475,27 @@ pub struct Item { durability_lost: Option, } +// An item that is dropped into the world an can be picked up. It can stack with +// other items of the same type regardless of the stack limit, when picked up +// the last item from the list is popped +// +// NOTE: Never call PickupItem::clone, it is only used for network +// synchronization +// +// Invariants: +// - Any item that is not the last one must have an amount equal to its +// `max_amount()` +// - All items must be equal and have a zero amount of slots +// - The Item list must not be empty +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PickupItem { + items: Vec, + /// This [`ProgramTime`] only makes sense on the server + created_at: ProgramTime, + /// This [`ProgramTime`] only makes sense on the server + next_merge_check: ProgramTime, +} + use std::hash::{Hash, Hasher}; // Used to find inventory item corresponding to hotbar slot @@ -824,14 +846,13 @@ impl ItemDef { /// please don't rely on this for anything! impl PartialEq for Item { fn eq(&self, other: &Self) -> bool { - if let (ItemBase::Simple(self_def), ItemBase::Simple(other_def)) = - (&self.item_base, &other.item_base) - { - self_def.item_definition_id == other_def.item_definition_id - && self.components == other.components - } else { - false - } + (match (&self.item_base, &other.item_base) { + (ItemBase::Simple(our_def), ItemBase::Simple(other_def)) => { + our_def.item_definition_id == other_def.item_definition_id + }, + (ItemBase::Modular(our_base), ItemBase::Modular(other_base)) => our_base == other_base, + _ => false, + }) && self.components() == other.components() } } @@ -1270,37 +1291,6 @@ impl Item { } } - /// Return `true` if `other` can be merged into this item. This is generally - /// only possible if the item has a compatible item ID and is stackable, - /// along with any other similarity checks. - pub fn can_merge(&self, other: &Item) -> bool { - if self.is_stackable() - && let ItemBase::Simple(other_item_def) = &other.item_base - && self.is_same_item_def(other_item_def) - && u32::from(self.amount) - .checked_add(other.amount()) - .filter(|&amount| amount <= self.max_amount()) - .is_some() - { - true - } else { - false - } - } - - /// Try to merge `other` into this item. This is generally only possible if - /// the item has a compatible item ID and is stackable, along with any - /// other similarity checks. - pub fn try_merge(&mut self, other: Item) -> Result<(), Item> { - if self.can_merge(&other) { - self.increase_amount(other.amount()) - .expect("`can_merge` succeeded but `increase_amount` did not"); - Ok(()) - } else { - Err(other) - } - } - pub fn num_slots(&self) -> u16 { self.item_base.num_slots() } /// NOTE: invariant that amount() ≤ max_amount(), 1 ≤ max_amount(), @@ -1476,6 +1466,173 @@ impl Item { msm, ) } + + /// Checks if this item and another are suitable for grouping into the same + /// [`PickItem`]. + /// + /// Also see [`Item::try_merge`]. + pub fn can_merge(&self, other: &Self) -> bool { + if self.amount() > self.max_amount() || other.amount() > other.max_amount() { + error!("An item amount is over max_amount!"); + return false; + } + + (self == other) + && self.slots().iter().all(Option::is_none) + && other.slots().iter().all(Option::is_none) + && self.durability_lost() == other.durability_lost() + } + + /// Checks if this item and another are suitable for grouping into the same + /// [`PickItem`] and combines stackable items if possible. + /// + /// If the sum of both amounts is larger than their max amount, a remainder + /// item is returned as `Ok(Some(remainder))`. A remainder item will + /// always be produced for non-stackable items. + /// + /// If the items are not suitable for grouping `Err(other)` will be + /// returned. + pub fn try_merge(&mut self, mut other: Self) -> Result, Self> { + if self.can_merge(&other) { + let max_amount = self.max_amount(); + debug_assert_eq!( + max_amount, + other.max_amount(), + "Mergeable items must have the same max_amount()" + ); + + // Additional amount `self` can hold + // For non-stackable items this is always zero + let to_fill_self = max_amount + .checked_sub(self.amount()) + .expect("can_merge should ensure that amount() <= max_amount()"); + + if let Some(remainder) = other.amount().checked_sub(to_fill_self).filter(|r| *r > 0) { + self.set_amount(max_amount) + .expect("max_amount() is always a valid amount."); + other.set_amount(remainder).expect( + "We know remainder is more than 0 and less than or equal to max_amount()", + ); + Ok(Some(other)) + } else { + // If there would be no remainder, add the amounts! + self.increase_amount(other.amount()) + .expect("We know that we can at least add other.amount() to this item"); + drop(other); + Ok(None) + } + } else { + Err(other) + } + } +} + +impl PickupItem { + pub fn new(item: Item, time: ProgramTime) -> Self { + Self { + items: vec![item], + created_at: time, + next_merge_check: time, + } + } + + /// Get a reference to the last item in this stack + /// + /// The amount of this item should *not* be used. + pub fn item(&self) -> &Item { + self.items + .last() + .expect("PickupItem without at least one item is an invariant") + } + + pub fn created(&self) -> ProgramTime { self.created_at } + + pub fn next_merge_check(&self) -> ProgramTime { self.next_merge_check } + + pub fn next_merge_check_mut(&mut self) -> &mut ProgramTime { &mut self.next_merge_check } + + // Get the total amount of items in here + pub fn amount(&self) -> u32 { self.items.iter().map(Item::amount).sum() } + + /// Remove any debug items if this is a container, used before dropping an + /// item from an inventory + pub fn remove_debug_items(&mut self) { + for item in self.items.iter_mut() { + item.slots_mut().iter_mut().for_each(|container_slot| { + container_slot + .take_if(|contained_item| matches!(contained_item.quality(), Quality::Debug)); + }); + } + } + + pub fn can_merge(&self, other: &PickupItem) -> bool { + let self_item = self.item(); + let other_item = other.item(); + + self_item.can_merge(other_item) + } + + // Attempt to merge another PickupItem into this one, can only fail if + // `can_merge` returns false + pub fn try_merge(&mut self, mut other: PickupItem) -> Result<(), PickupItem> { + if self.can_merge(&other) { + // Pop the last item from `self` and `other` to merge them, as only the last + // items can have an amount != max_amount() + let mut self_last = self + .items + .pop() + .expect("PickupItem without at least one item is an invariant"); + let other_last = other + .items + .pop() + .expect("PickupItem without at least one item is an invariant"); + + // Merge other_last into self_last + let merged = self_last + .try_merge(other_last) + .expect("We know these items can be merged"); + + debug_assert!( + other + .items + .iter() + .chain(self.items.iter()) + .all(|item| item.amount() == item.max_amount()), + "All items before the last in `PickupItem` should have a full amount" + ); + + // We know all items except the last have a full amount, so we can safely append + // them here + self.items.append(&mut other.items); + + debug_assert!( + merged.is_none() || self_last.amount() == self_last.max_amount(), + "Merged can only be `Some` if the origin was set to `max_amount()`" + ); + + // Push the potentially not fully-stacked item at the end + self.items.push(self_last); + + // Push the remainder, merged is only `Some` if self_last was set to + // `max_amount()` + if let Some(remainder) = merged { + self.items.push(remainder); + } + + Ok(()) + } else { + Err(other) + } + } + + pub fn pick_up(mut self) -> (Item, Option) { + ( + self.items + .pop() + .expect("PickupItem without at least one item is an invariant"), + (!self.items.is_empty()).then_some(self), + ) + } } pub fn flatten_counted_items<'a>( @@ -1603,8 +1760,42 @@ impl ItemDesc for ItemDef { fn stats_durability_multiplier(&self) -> DurabilityMultiplier { DurabilityMultiplier(1.0) } } -impl Component for Item { - type Storage = DerefFlaggedStorage>; +impl ItemDesc for PickupItem { + fn description(&self) -> &str { + #[allow(deprecated)] + self.item().description() + } + + fn name(&self) -> Cow { + #[allow(deprecated)] + self.item().name() + } + + fn kind(&self) -> Cow { self.item().kind() } + + fn amount(&self) -> NonZeroU32 { + NonZeroU32::new(self.amount()).expect("Item having amount of 0 is invariant") + } + + fn quality(&self) -> Quality { self.item().quality() } + + fn num_slots(&self) -> u16 { self.item().num_slots() } + + fn item_definition_id(&self) -> ItemDefinitionId<'_> { self.item().item_definition_id() } + + fn tags(&self) -> Vec { self.item().tags() } + + fn is_modular(&self) -> bool { self.item().is_modular() } + + fn components(&self) -> &[Item] { self.item().components() } + + fn has_durability(&self) -> bool { self.item().has_durability() } + + fn durability_lost(&self) -> Option { self.item().durability_lost() } + + fn stats_durability_multiplier(&self) -> DurabilityMultiplier { + self.item().stats_durability_multiplier() + } } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -1614,6 +1805,10 @@ impl Component for ItemDrops { type Storage = DenseVecStorage; } +impl Component for PickupItem { + type Storage = DerefFlaggedStorage>; +} + #[derive(Copy, Clone, Debug)] pub struct DurabilityMultiplier(pub f32); diff --git a/common/src/comp/inventory/item/modular.rs b/common/src/comp/inventory/item/modular.rs index f7ab0fad51..62703b4eca 100644 --- a/common/src/comp/inventory/item/modular.rs +++ b/common/src/comp/inventory/item/modular.rs @@ -56,7 +56,7 @@ impl Asset for MaterialStatManifest { const EXTENSION: &'static str = "ron"; } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum ModularBase { Tool, } diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 10a03646fc..d2bf6d5216 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -78,7 +78,7 @@ pub use self::{ self, item_key::ItemKey, tool::{self, AbilityItem}, - Item, ItemConfig, ItemDrops, + Item, ItemConfig, ItemDrops, PickupItem, }, slot, CollectFailedReason, Inventory, InventoryUpdate, InventoryUpdateEvent, }, diff --git a/common/src/event.rs b/common/src/event.rs index 2869fb02b0..d0bc7a3e52 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -165,7 +165,7 @@ pub struct CreateItemDropEvent { pub pos: Pos, pub vel: Vel, pub ori: Ori, - pub item: comp::Item, + pub item: comp::PickupItem, pub loot_owner: Option, } pub struct CreateObjectEvent { @@ -173,7 +173,7 @@ pub struct CreateObjectEvent { pub vel: Vel, pub body: comp::object::Body, pub object: Option, - pub item: Option, + pub item: Option, pub light_emitter: Option, pub stats: Option, } diff --git a/common/src/lib.rs b/common/src/lib.rs index 76d9b5bd78..1e2eeb50b5 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -13,7 +13,8 @@ extend_one, arbitrary_self_types, int_roundings, - hash_extract_if + hash_extract_if, + option_take_if )] pub use common_assets as assets; diff --git a/common/state/src/state.rs b/common/state/src/state.rs index bd894e0758..2111e3f768 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -234,7 +234,7 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); - ecs.register::(); + ecs.register::(); ecs.register::(); ecs.register::>(); ecs.register::>(); diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 5d49f8b680..11dc39b095 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -49,7 +49,7 @@ use common::{ npc::{self, get_npc_name}, outcome::Outcome, parse_cmd_args, - resources::{BattleMode, PlayerPhysicsSettings, Secs, Time, TimeOfDay, TimeScale}, + resources::{BattleMode, PlayerPhysicsSettings, ProgramTime, Secs, Time, TimeOfDay, TimeScale}, rtsim::{Actor, Role}, terrain::{Block, BlockKind, CoordinateConversions, SpriteKind, TerrainChunkSize}, tether::Tethered, @@ -494,7 +494,7 @@ fn handle_drop_all( )), comp::Ori::default(), comp::Vel(vel), - item, + comp::PickupItem::new(item, ProgramTime(server.state.get_program_time())), None, ); } diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 90928450fc..5a68e12037 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -21,8 +21,9 @@ use common::{ inventory::item::{AbilityMap, MaterialStatManifest}, item::flatten_counted_items, loot_owner::LootOwnerKind, - Alignment, Auras, Body, CharacterState, Energy, Group, Health, Inventory, Object, Player, - Poise, Pos, Presence, PresenceKind, SkillSet, Stats, BASE_ABILITY_LIMIT, + Alignment, Auras, Body, CharacterState, Energy, Group, Health, Inventory, Object, + PickupItem, Player, Poise, Pos, Presence, PresenceKind, SkillSet, Stats, + BASE_ABILITY_LIMIT, }, consts::TELEPORTER_RADIUS, event::{ @@ -40,7 +41,7 @@ use common::{ lottery::distribute_many, mounting::{Rider, VolumeRider}, outcome::{HealthChangeInfo, Outcome}, - resources::{Secs, Time}, + resources::{ProgramTime, Secs, Time}, rtsim::{Actor, RtSimEntity}, spiral::Spiral2d, states::utils::StageSection, @@ -286,6 +287,7 @@ pub struct DestroyEventData<'a> { msm: ReadExpect<'a, MaterialStatManifest>, ability_map: ReadExpect<'a, AbilityMap>, time: Read<'a, Time>, + program_time: ReadExpect<'a, ProgramTime>, world: ReadExpect<'a, Arc>, index: ReadExpect<'a, world::IndexOwned>, areas_container: Read<'a, AreasContainer>, @@ -641,7 +643,7 @@ impl ServerEvent for DestroyEvent { vel: vel.copied().unwrap_or(comp::Vel(Vec3::zero())), // TODO: Random ori: comp::Ori::from(Dir::random_2d(&mut rng)), - item, + item: PickupItem::new(item, *data.program_time), loot_owner: if let Some(loot_owner) = loot_owner { debug!( "Assigned UID {loot_owner:?} as the winner for the loot \ @@ -1432,12 +1434,13 @@ impl ServerEvent for BonkEvent { type SystemData<'a> = ( Write<'a, BlockChange>, ReadExpect<'a, TerrainGrid>, + ReadExpect<'a, ProgramTime>, Read<'a, EventBus>, ); fn handle( events: impl ExactSizeIterator, - (mut block_change, terrain, create_object_events): Self::SystemData<'_>, + (mut block_change, terrain, program_time, create_object_events): Self::SystemData<'_>, ) { let mut create_object_emitter = create_object_events.emitter(); for ev in events { @@ -1474,7 +1477,7 @@ impl ServerEvent for BonkEvent { }, _ => None, }, - item: Some(item), + item: Some(comp::PickupItem::new(item, *program_time)), light_emitter: None, stats: None, }); diff --git a/server/src/events/interaction.rs b/server/src/events/interaction.rs index e24937a18c..0aea885aec 100755 --- a/server/src/events/interaction.rs +++ b/server/src/events/interaction.rs @@ -20,6 +20,7 @@ use common::{ link::Is, mounting::Mount, outcome::Outcome, + resources::ProgramTime, terrain::{Block, SpriteKind, TerrainGrid}, uid::Uid, util::Dir, @@ -194,6 +195,7 @@ impl ServerEvent for MineBlockEvent { ReadExpect<'a, AbilityMap>, ReadExpect<'a, EventBus>, ReadExpect<'a, EventBus>, + ReadExpect<'a, ProgramTime>, WriteStorage<'a, comp::SkillSet>, ReadStorage<'a, Uid>, ); @@ -207,6 +209,7 @@ impl ServerEvent for MineBlockEvent { ability_map, create_item_drop_events, outcomes, + program_time, mut skill_sets, uids, ): Self::SystemData<'_>, @@ -296,7 +299,7 @@ impl ServerEvent for MineBlockEvent { pos: comp::Pos(ev.pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0)), vel: comp::Vel(Vec3::zero()), ori: comp::Ori::from(Dir::random_2d(&mut rng)), - item, + item: comp::PickupItem::new(item, *program_time), loot_owner, }); } diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index 3ef42a5709..71cfce65b5 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -14,7 +14,7 @@ use common::{ item::{self, flatten_counted_items, tool::AbilityMap, MaterialStatManifest}, loot_owner::LootOwnerKind, slot::{self, Slot}, - InventoryUpdate, LootOwner, + InventoryUpdate, LootOwner, PickupItem, }, consts::MAX_PICKUP_RANGE, event::{ @@ -26,7 +26,7 @@ use common::{ recipe::{ self, default_component_recipe_book, default_recipe_book, default_repair_recipe_book, }, - resources::Time, + resources::{ProgramTime, Time}, terrain::{Block, SpriteKind}, trade::Trades, uid::{IdMaps, Uid}, @@ -82,10 +82,11 @@ pub struct InventoryManipData<'a> { terrain: ReadExpect<'a, common::terrain::TerrainGrid>, id_maps: Read<'a, IdMaps>, time: Read<'a, Time>, + program_time: ReadExpect<'a, ProgramTime>, ability_map: ReadExpect<'a, AbilityMap>, msm: ReadExpect<'a, MaterialStatManifest>, inventories: WriteStorage<'a, comp::Inventory>, - items: WriteStorage<'a, comp::Item>, + items: WriteStorage<'a, comp::PickupItem>, inventory_updates: WriteStorage<'a, comp::InventoryUpdate>, light_emitters: WriteStorage<'a, comp::LightEmitter>, positions: ReadStorage<'a, comp::Pos>, @@ -234,6 +235,12 @@ impl ServerEvent for InventoryManipEvent { continue; }; + const ITEM_ENTITY_EXPECT_MESSAGE: &str = "We know item_entity still exist \ + since we just successfully removed \ + its PickupItem component."; + + let (item, reinsert_item) = item.pick_up(); + // NOTE: We dup the item for message purposes. let item_msg = item.duplicate(&data.ability_map, &data.msm); @@ -244,13 +251,25 @@ impl ServerEvent for InventoryManipEvent { inventory.pickup_item(returned_item) }) { Err(returned_item) => { + // If we had a `reinsert_item`, merge returned_item into it + let returned_item = if let Some(mut reinsert_item) = reinsert_item { + reinsert_item + .try_merge(PickupItem::new(returned_item, *data.program_time)) + .expect( + "We know this item must be mergeable since it is a \ + duplicate", + ); + reinsert_item + } else { + PickupItem::new(returned_item, *data.program_time) + }; + // Inventory was full, so we need to put back the item (note that we // know there was no old item component for // this entity). - data.items.insert(item_entity, returned_item).expect( - "We know item_entity exists since we just successfully removed \ - its Item component.", - ); + data.items + .insert(item_entity, returned_item) + .expect(ITEM_ENTITY_EXPECT_MESSAGE); comp::InventoryUpdate::new(InventoryUpdateEvent::EntityCollectFailed { entity: pickup_uid, reason: CollectFailedReason::InventoryFull, @@ -259,7 +278,14 @@ impl ServerEvent for InventoryManipEvent { Ok(_) => { // We succeeded in picking up the item, so we may now delete its old // entity entirely. - emitters.emit(DeleteEvent(item_entity)); + if let Some(reinsert_item) = reinsert_item { + data.items + .insert(item_entity, reinsert_item) + .expect(ITEM_ENTITY_EXPECT_MESSAGE); + } else { + emitters.emit(DeleteEvent(item_entity)); + } + if let Some(group_id) = data.groups.get(entity) { announce_loot_to_group( group_id, @@ -415,7 +441,7 @@ impl ServerEvent for InventoryManipEvent { ), vel: comp::Vel(Vec3::zero()), ori: data.orientations.get(entity).copied().unwrap_or_default(), - item, + item: PickupItem::new(item, *data.program_time), loot_owner: Some(LootOwner::new(LootOwnerKind::Player(*uid), false)), }); } @@ -445,14 +471,14 @@ impl ServerEvent for InventoryManipEvent { } if let Some(pos) = data.positions.get(entity) { dropped_items.extend( - inventory.equip(slot, *data.time).into_iter().map(|x| { + inventory.equip(slot, *data.time).into_iter().map(|item| { ( *pos, data.orientations .get(entity) .copied() .unwrap_or_default(), - x, + PickupItem::new(item, *data.program_time), *uid, ) }), @@ -567,14 +593,14 @@ impl ServerEvent for InventoryManipEvent { if let Ok(Some(leftover_items)) = inventory.unequip(slot, *data.time) { - dropped_items.extend(leftover_items.into_iter().map(|x| { + dropped_items.extend(leftover_items.into_iter().map(|item| { ( *pos, data.orientations .get(entity) .copied() .unwrap_or_default(), - x, + PickupItem::new(item, *data.program_time), *uid, ) })); @@ -671,11 +697,11 @@ impl ServerEvent for InventoryManipEvent { // If the stacks weren't mergable carry out a swap. if !merged_stacks { dropped_items.extend(inventory.swap(a, b, *data.time).into_iter().map( - |x| { + |item| { ( *pos, data.orientations.get(entity).copied().unwrap_or_default(), - x, + PickupItem::new(item, *data.program_time), *uid, ) }, @@ -743,7 +769,7 @@ impl ServerEvent for InventoryManipEvent { dropped_items.push(( *pos, data.orientations.get(entity).copied().unwrap_or_default(), - item, + PickupItem::new(item, *data.program_time), *uid, )); } @@ -771,7 +797,7 @@ impl ServerEvent for InventoryManipEvent { dropped_items.push(( *pos, data.orientations.get(entity).copied().unwrap_or_default(), - item, + PickupItem::new(item, *data.program_time), *uid, )); } @@ -949,18 +975,35 @@ impl ServerEvent for InventoryManipEvent { // Attempt to insert items into inventory, dropping them if there is not enough // space let items_were_crafted = if let Some(crafted_items) = crafted_items { + let mut dropped: Vec = Vec::new(); for item in crafted_items { if let Err(item) = inventory.push(item) { - if let Some(pos) = data.positions.get(entity) { - dropped_items.push(( - *pos, - data.orientations.get(entity).copied().unwrap_or_default(), - item.duplicate(&data.ability_map, &data.msm), - *uid, - )); + let item = PickupItem::new(item, *data.program_time); + if let Some(can_merge) = + dropped.iter_mut().find(|other| other.can_merge(&item)) + { + can_merge + .try_merge(item) + .expect("We know these items can be merged"); + } else { + dropped.push(item); } } } + + if !dropped.is_empty() + && let Some(pos) = data.positions.get(entity) + { + for item in dropped { + dropped_items.push(( + *pos, + data.orientations.get(entity).copied().unwrap_or_default(), + item, + *uid, + )); + } + } + true } else { false @@ -990,16 +1033,9 @@ impl ServerEvent for InventoryManipEvent { // Drop items, Debug items should simply disappear when dropped for (pos, ori, mut item, owner) in dropped_items .into_iter() - .filter(|(_, _, i, _)| !matches!(i.quality(), item::Quality::Debug)) + .filter(|(_, _, i, _)| !matches!(i.item().quality(), item::Quality::Debug)) { - // If item is a container check inside of it for Debug items and remove them - item.slots_mut().iter_mut().for_each(|x| { - if let Some(contained_item) = &x { - if matches!(contained_item.quality(), item::Quality::Debug) { - std::mem::take(x); - } - } - }); + item.remove_debug_items(); emitters.emit(CreateItemDropEvent { pos, diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index cc298868cb..14eeb204f4 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -22,7 +22,7 @@ use common::{ misc::PortalData, object, skills::{GeneralSkill, Skill}, - ChatType, Content, Group, Inventory, Item, LootOwner, Object, Player, Poise, Presence, + ChatType, Content, Group, Inventory, LootOwner, Object, Player, Poise, Presence, PresenceKind, BASE_ABILITY_LIMIT, }, effect::Effect, @@ -74,7 +74,7 @@ pub trait StateExt { pos: comp::Pos, ori: comp::Ori, vel: comp::Vel, - item: Item, + item: comp::PickupItem, loot_owner: Option, ) -> Option; fn create_ship comp::Collider>( @@ -342,54 +342,44 @@ impl StateExt for State { pos: comp::Pos, ori: comp::Ori, vel: comp::Vel, - item: Item, + world_item: comp::PickupItem, loot_owner: Option, ) -> Option { + // Attempt merging with any nearby entities if possible { - const MAX_MERGE_DIST: f32 = 1.5; + use crate::sys::item::get_nearby_mergeable_items; - // First, try to identify possible candidates for item merging - // We limit our search to just a few blocks and we prioritise merging with the - // closest let positions = self.ecs().read_storage::(); let loot_owners = self.ecs().read_storage::(); - let mut items = self.ecs().write_storage::(); - let mut nearby_items = self - .ecs() - .read_resource::() - .0 - .in_circle_aabr(pos.0.xy(), MAX_MERGE_DIST) - .filter(|entity| items.contains(*entity)) - .filter_map(|entity| { - Some((entity, positions.get(entity)?.0.distance_squared(pos.0))) - }) - .filter(|(_, dist_sqrd)| *dist_sqrd < MAX_MERGE_DIST.powi(2)) - .collect::>(); - nearby_items.sort_by_key(|(_, dist_sqrd)| (dist_sqrd * 1000.0) as i32); - for (nearby, _) in nearby_items { - // Only merge if the loot owner is the same - if loot_owners.get(nearby).map(|lo| lo.owner()) == loot_owner.map(|lo| lo.owner()) - && items - .get(nearby) - .map_or(false, |nearby_item| nearby_item.can_merge(&item)) - { - // Merging can occur! Perform the merge: - items - .get_mut(nearby) - .expect("we know that the item exists") - .try_merge(item) - .expect("`try_merge` should succeed because `can_merge` returned `true`"); - return None; - } + let mut items = self.ecs().write_storage::(); + let entities = self.ecs().entities(); + let spatial_grid = self.ecs().read_resource(); + + let nearby_items = get_nearby_mergeable_items( + &world_item, + &pos, + loot_owner.as_ref(), + (&entities, &items, &positions, &loot_owners, &spatial_grid), + ); + + // Merge the nearest item if possible, skip to creating a drop otherwise + if let Some((mergeable_item, _)) = + nearby_items.min_by_key(|(_, dist)| (dist * 1000.0) as i32) + { + items + .get_mut(mergeable_item) + .expect("we know that the item exists") + .try_merge(world_item) + .expect("`try_merge` should succeed because `can_merge` returned `true`"); + return None; } - // Only if merging items fails do we give up and create a new item } let spawned_at = *self.ecs().read_resource::