From ee971e4056d6aebdd563c3b39f78a6db3663a61f Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 4 May 2023 22:12:25 +0100 Subject: [PATCH 1/4] Added item merging --- common/src/comp/inventory/item/mod.rs | 15 + common/src/comp/phys.rs | 2 +- server/src/cmd.rs | 14 +- server/src/events/entity_manipulation.rs | 45 +- server/src/events/interaction.rs | 18 +- server/src/events/inventory_manip.rs | 45 +- server/src/state_ext.rs | 81 +++- .../src/audio/sfx/event_mapper/block/mod.rs | 383 +++++++++--------- 8 files changed, 334 insertions(+), 269 deletions(-) diff --git a/common/src/comp/inventory/item/mod.rs b/common/src/comp/inventory/item/mod.rs index a07575aad4..e02a7d6b0d 100644 --- a/common/src/comp/inventory/item/mod.rs +++ b/common/src/comp/inventory/item/mod.rs @@ -1158,6 +1158,21 @@ impl Item { } } + /// 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.is_stackable() + && let ItemBase::Simple(other_item_def) = &other.item_base + && self.is_same_item_def(other_item_def) + { + self.increase_amount(other.amount()).map_err(|_| other)?; + Ok(()) + } else { + Err(other) + } + } + pub fn num_slots(&self) -> u16 { self.item_base.num_slots() } /// NOTE: invariant that amount() ≤ max_amount(), 1 ≤ max_amount(), diff --git a/common/src/comp/phys.rs b/common/src/comp/phys.rs index d8c688cf1c..ebe0dc18eb 100644 --- a/common/src/comp/phys.rs +++ b/common/src/comp/phys.rs @@ -12,7 +12,7 @@ use std::sync::Arc; use vek::*; /// Position -#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Pos(pub Vec3); impl Component for Pos { diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 8e51f654c8..b568b464a1 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -503,16 +503,16 @@ fn handle_drop_all( for item in item_to_place { let vel = Vec3::new(rng.gen_range(-0.1..0.1), rng.gen_range(-0.1..0.1), 0.5); - server - .state - .create_item_drop(Default::default(), item) - .with(comp::Pos(Vec3::new( + server.state.create_item_drop( + comp::Pos(Vec3::new( pos.0.x + rng.gen_range(5.0..10.0), pos.0.y + rng.gen_range(5.0..10.0), pos.0.z + 5.0, - ))) - .with(comp::Vel(vel)) - .build(); + )), + comp::Vel(vel), + item, + None, + ); } Ok(()) diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index a2346e0741..72b961b315 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -473,20 +473,17 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt let mut spawn_item = |item, loot_owner| { let offset = item_offset_spiral.next().unwrap_or_default(); - let item_drop_entity = state - .create_item_drop(Pos(pos.0 + Vec3::unit_z() * 0.25 + offset), item) - .maybe_with(vel) - .build(); - if let Some(loot_owner) = loot_owner { - debug!("Assigned UID {loot_owner:?} as the winner for the loot drop"); - if let Err(err) = state - .ecs() - .write_storage::() - .insert(item_drop_entity, LootOwner::new(loot_owner)) - { - error!("Failed to set loot owner on item drop: {err}"); - }; - } + state.create_item_drop( + Pos(pos.0 + Vec3::unit_z() * 0.25 + offset), + vel.unwrap_or(comp::Vel(Vec3::zero())), + item, + if let Some(loot_owner) = loot_owner { + debug!("Assigned UID {loot_owner:?} as the winner for the loot drop"); + Some(LootOwner::new(loot_owner)) + } else { + None + }, + ); }; let msm = &MaterialStatManifest::load().read(); @@ -1103,15 +1100,17 @@ pub fn handle_bonk(server: &mut Server, pos: Vec3, owner: Option, targ for item in flatten_counted_items(&items, ability_map, msm) { server .state - .create_object(Default::default(), match block.get_sprite() { - // Create different containers depending on the original sprite - Some(SpriteKind::Apple) => comp::object::Body::Apple, - Some(SpriteKind::Beehive) => comp::object::Body::Hive, - Some(SpriteKind::Coconut) => comp::object::Body::Coconut, - Some(SpriteKind::Bomb) => comp::object::Body::Bomb, - _ => comp::object::Body::Pouch, - }) - .with(Pos(pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0))) + .create_object( + Pos(pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0)), + match block.get_sprite() { + // Create different containers depending on the original sprite + Some(SpriteKind::Apple) => comp::object::Body::Apple, + Some(SpriteKind::Beehive) => comp::object::Body::Hive, + Some(SpriteKind::Coconut) => comp::object::Body::Coconut, + Some(SpriteKind::Bomb) => comp::object::Body::Bomb, + _ => comp::object::Body::Pouch, + }, + ) .with(item) .maybe_with(match block.get_sprite() { Some(SpriteKind::Bomb) => Some(comp::Object::Bomb { owner }), diff --git a/server/src/events/interaction.rs b/server/src/events/interaction.rs index 0345d72c7e..139d9b665e 100644 --- a/server/src/events/interaction.rs +++ b/server/src/events/interaction.rs @@ -1,4 +1,4 @@ -use specs::{world::WorldExt, Builder, Entity as EcsEntity, Join}; +use specs::{world::WorldExt, Entity as EcsEntity, Join}; use vek::*; use common::{ @@ -253,15 +253,13 @@ pub fn handle_mine_block( } } for item in items { - let item_drop = state - .create_item_drop(Default::default(), item) - .with(Pos(pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0))); - if let Some(uid) = maybe_uid { - item_drop.with(LootOwner::new(LootOwnerKind::Player(uid))) - } else { - item_drop - } - .build(); + let loot_owner = maybe_uid.map(LootOwnerKind::Player).map(LootOwner::new); + state.create_item_drop( + Pos(pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0)), + comp::Vel(Vec3::zero()), + item, + loot_owner, + ); } } diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index b61ff44b8c..bb62a7ba03 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -377,17 +377,18 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv drop(inventory_updates); for item in drop_items { - state - .create_item_drop(Default::default(), item) - .with(comp::Pos( + state.create_item_drop( + comp::Pos( Vec3::new( sprite_pos.x as f32, sprite_pos.y as f32, sprite_pos.z as f32, ) + Vec3::one().with_z(0.0) * 0.5, - )) - .with(comp::Vel(Vec3::zero())) - .build(); + ), + comp::Vel(Vec3::zero()), + item, + None, + ); } }, comp::InventoryManip::Use(slot) => { @@ -890,15 +891,15 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv let items_were_crafted = if let Some(crafted_items) = crafted_items { for item in crafted_items { if let Err(item) = inventory.push(item) { - dropped_items.push(( - state - .read_component_copied::(entity) - .unwrap_or_default(), - state - .read_component_copied::(entity) - .unwrap_or_default(), - item.duplicate(ability_map, &msm), - )); + if let Some(pos) = state.read_component_copied::(entity) { + dropped_items.push(( + pos, + state + .read_component_copied::(entity) + .unwrap_or_default(), + item.duplicate(ability_map, &msm), + )); + } } } true @@ -940,11 +941,12 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv } }); - state - .create_item_drop(Default::default(), item) - .with(comp::Pos(pos.0 + *ori.look_dir() + Vec3::unit_z())) - .with(comp::Vel(Vec3::zero())) - .build(); + state.create_item_drop( + comp::Pos(pos.0 + *ori.look_dir() + Vec3::unit_z()), + comp::Vel(Vec3::zero()), + item, + None, + ); } let mut rng = rand::thread_rng(); @@ -963,12 +965,11 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv let uid = state.read_component_copied::(entity); let mut new_entity = state - .create_object(Default::default(), match kind { + .create_object(comp::Pos(pos.0 + Vec3::unit_z() * 0.25), match kind { item::Throwable::Bomb => comp::object::Body::Bomb, item::Throwable::Firework(reagent) => comp::object::Body::for_firework(reagent), item::Throwable::TrainingDummy => comp::object::Body::TrainingDummy, }) - .with(comp::Pos(pos.0 + Vec3::unit_z() * 0.25)) .with(comp::Vel(vel)); match kind { diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index bbf5a7d280..7131911472 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -19,7 +19,7 @@ use common::{ self, item::{ItemKind, MaterialStatManifest}, skills::{GeneralSkill, Skill}, - ChatType, Group, Inventory, Item, Player, Poise, Presence, PresenceKind, + ChatType, Group, Inventory, Item, LootOwner, Player, Poise, Presence, PresenceKind, }, effect::Effect, link::{Link, LinkHandle}, @@ -60,7 +60,15 @@ pub trait StateExt { ) -> EcsEntityBuilder; /// Build a static object entity fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder; - fn create_item_drop(&mut self, pos: comp::Pos, item: Item) -> EcsEntityBuilder; + /// Create an item drop or merge the item with an existing drop, if a + /// suitable candidate exists. + fn create_item_drop( + &mut self, + pos: comp::Pos, + vel: comp::Vel, + item: Item, + loot_owner: Option, + ) -> Option; fn create_ship comp::Collider>( &mut self, pos: comp::Pos, @@ -311,7 +319,48 @@ impl StateExt for State { .with(body) } - fn create_item_drop(&mut self, pos: comp::Pos, item: Item) -> EcsEntityBuilder { + fn create_item_drop( + &mut self, + pos: comp::Pos, + vel: comp::Vel, + mut item: Item, + loot_owner: Option, + ) -> Option { + { + const MAX_MERGE_DIST: f32 = 1.5; + + // 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.get(*entity).is_some()) + .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()) { + if let Some(mut nearby_item) = items.get_mut(nearby) { + match nearby_item.try_merge(item) { + Ok(()) => return None, // Merging was successful! + Err(rejected_item) => item = rejected_item, + } + } + } + } + // Only if merging items fails do we give up and create a new item + } + let item_drop = comp::item_drop::Body::from(&item); let body = comp::Body::ItemDrop(item_drop); let light_emitter = match &*item.kind() { @@ -323,17 +372,21 @@ impl StateExt for State { }), _ => None, }; - self.ecs_mut() - .create_entity_synced() - .with(item) - .with(pos) - .with(comp::Vel(Vec3::zero())) - .with(item_drop.orientation(&mut thread_rng())) - .with(item_drop.mass()) - .with(item_drop.density()) - .with(body.collider()) - .with(body) - .maybe_with(light_emitter) + Some( + self.ecs_mut() + .create_entity_synced() + .with(item) + .with(pos) + .with(vel) + .with(item_drop.orientation(&mut thread_rng())) + .with(item_drop.mass()) + .with(item_drop.density()) + .with(body.collider()) + .with(body) + .maybe_with(loot_owner) + .maybe_with(light_emitter) + .build(), + ) } fn create_ship comp::Collider>( diff --git a/voxygen/src/audio/sfx/event_mapper/block/mod.rs b/voxygen/src/audio/sfx/event_mapper/block/mod.rs index d5c7efc669..706b44c79d 100644 --- a/voxygen/src/audio/sfx/event_mapper/block/mod.rs +++ b/voxygen/src/audio/sfx/event_mapper/block/mod.rs @@ -56,210 +56,209 @@ impl EventMapper for BlockEventMapper { let cam_pos = camera.dependents().cam_pos + focus_off; // Get the player position and chunk - let player_pos = state - .read_component_copied::(player_entity) - .unwrap_or_default(); - let player_chunk = player_pos.0.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| { - (e.floor() as i32).div_euclid(sz as i32) - }); + if let Some(player_pos) = state.read_component_copied::(player_entity) { + let player_chunk = player_pos.0.xy().map2(TerrainChunk::RECT_SIZE, |e, sz| { + (e.floor() as i32).div_euclid(sz as i32) + }); - // For determining if underground/crickets should chirp - let (terrain_alt, temp) = match client.current_chunk() { - Some(chunk) => (chunk.meta().alt(), chunk.meta().temp()), - None => (0.0, 0.0), - }; + // For determining if underground/crickets should chirp + let (terrain_alt, temp) = match client.current_chunk() { + Some(chunk) => (chunk.meta().alt(), chunk.meta().temp()), + None => (0.0, 0.0), + }; - struct BlockSounds<'a> { - // The function to select the blocks of interest that we should emit from - blocks: fn(&'a BlocksOfInterest) -> &'a [Vec3], - // The range, in chunks, that the particles should be generated in from the player - range: usize, - // The sound of the generated particle - sfx: SfxEvent, - // The volume of the sfx - volume: f32, - // Condition that must be true to play - cond: fn(&State) -> bool, - } - - let sounds: &[BlockSounds] = &[ - BlockSounds { - blocks: |boi| &boi.leaves, - range: 1, - sfx: SfxEvent::Birdcall, - volume: 1.0, - cond: |st| st.get_day_period().is_light(), - }, - BlockSounds { - blocks: |boi| &boi.leaves, - range: 1, - sfx: SfxEvent::Owl, - volume: 1.0, - cond: |st| st.get_day_period().is_dark(), - }, - BlockSounds { - blocks: |boi| &boi.slow_river, - range: 1, - sfx: SfxEvent::RunningWaterSlow, - volume: 1.2, - cond: |_| true, - }, - BlockSounds { - blocks: |boi| &boi.fast_river, - range: 1, - sfx: SfxEvent::RunningWaterFast, - volume: 1.5, - cond: |_| true, - }, - //BlockSounds { - // blocks: |boi| &boi.embers, - // range: 1, - // sfx: SfxEvent::Embers, - // volume: 0.15, - // //volume: 0.05, - // cond: |_| true, - // //cond: |st| st.get_day_period().is_dark(), - //}, - BlockSounds { - blocks: |boi| &boi.frogs, - range: 1, - sfx: SfxEvent::Frog, - volume: 0.8, - cond: |st| st.get_day_period().is_dark(), - }, - //BlockSounds { - // blocks: |boi| &boi.flowers, - // range: 4, - // sfx: SfxEvent::LevelUp, - // volume: 1.0, - // cond: |st| st.get_day_period().is_dark(), - //}, - BlockSounds { - blocks: |boi| &boi.cricket1, - range: 1, - sfx: SfxEvent::Cricket1, - volume: 0.33, - cond: |st| st.get_day_period().is_dark(), - }, - BlockSounds { - blocks: |boi| &boi.cricket2, - range: 1, - sfx: SfxEvent::Cricket2, - volume: 0.33, - cond: |st| st.get_day_period().is_dark(), - }, - BlockSounds { - blocks: |boi| &boi.cricket3, - range: 1, - sfx: SfxEvent::Cricket3, - volume: 0.33, - cond: |st| st.get_day_period().is_dark(), - }, - BlockSounds { - blocks: |boi| &boi.beehives, - range: 1, - sfx: SfxEvent::Bees, - volume: 0.5, - cond: |st| st.get_day_period().is_light(), - }, - ]; - - // Iterate through each kind of block of interest - for sounds in sounds.iter() { - // If the timing condition is false, continue - // or if the player is far enough underground, continue - // TODO Address bird hack properly. See TODO on line 190 - if !(sounds.cond)(state) - || player_pos.0.z < (terrain_alt - 30.0) - || (sounds.sfx == SfxEvent::Birdcall && thread_rng().gen_bool(0.995)) - || (sounds.sfx == SfxEvent::Owl && thread_rng().gen_bool(0.998)) - || (sounds.sfx == SfxEvent::Frog && thread_rng().gen_bool(0.95)) - //Crickets will not chirp below 5 Celsius - || (sounds.sfx == SfxEvent::Cricket1 && (temp < -0.33)) - || (sounds.sfx == SfxEvent::Cricket2 && (temp < -0.33)) - || (sounds.sfx == SfxEvent::Cricket3 && (temp < -0.33)) - { - continue; + struct BlockSounds<'a> { + // The function to select the blocks of interest that we should emit from + blocks: fn(&'a BlocksOfInterest) -> &'a [Vec3], + // The range, in chunks, that the particles should be generated in from the player + range: usize, + // The sound of the generated particle + sfx: SfxEvent, + // The volume of the sfx + volume: f32, + // Condition that must be true to play + cond: fn(&State) -> bool, } - // For chunks surrounding the player position - for offset in Spiral2d::new().take((sounds.range * 2 + 1).pow(2)) { - let chunk_pos = player_chunk + offset; + let sounds: &[BlockSounds] = &[ + BlockSounds { + blocks: |boi| &boi.leaves, + range: 1, + sfx: SfxEvent::Birdcall, + volume: 1.0, + cond: |st| st.get_day_period().is_light(), + }, + BlockSounds { + blocks: |boi| &boi.leaves, + range: 1, + sfx: SfxEvent::Owl, + volume: 1.0, + cond: |st| st.get_day_period().is_dark(), + }, + BlockSounds { + blocks: |boi| &boi.slow_river, + range: 1, + sfx: SfxEvent::RunningWaterSlow, + volume: 1.2, + cond: |_| true, + }, + BlockSounds { + blocks: |boi| &boi.fast_river, + range: 1, + sfx: SfxEvent::RunningWaterFast, + volume: 1.5, + cond: |_| true, + }, + //BlockSounds { + // blocks: |boi| &boi.embers, + // range: 1, + // sfx: SfxEvent::Embers, + // volume: 0.15, + // //volume: 0.05, + // cond: |_| true, + // //cond: |st| st.get_day_period().is_dark(), + //}, + BlockSounds { + blocks: |boi| &boi.frogs, + range: 1, + sfx: SfxEvent::Frog, + volume: 0.8, + cond: |st| st.get_day_period().is_dark(), + }, + //BlockSounds { + // blocks: |boi| &boi.flowers, + // range: 4, + // sfx: SfxEvent::LevelUp, + // volume: 1.0, + // cond: |st| st.get_day_period().is_dark(), + //}, + BlockSounds { + blocks: |boi| &boi.cricket1, + range: 1, + sfx: SfxEvent::Cricket1, + volume: 0.33, + cond: |st| st.get_day_period().is_dark(), + }, + BlockSounds { + blocks: |boi| &boi.cricket2, + range: 1, + sfx: SfxEvent::Cricket2, + volume: 0.33, + cond: |st| st.get_day_period().is_dark(), + }, + BlockSounds { + blocks: |boi| &boi.cricket3, + range: 1, + sfx: SfxEvent::Cricket3, + volume: 0.33, + cond: |st| st.get_day_period().is_dark(), + }, + BlockSounds { + blocks: |boi| &boi.beehives, + range: 1, + sfx: SfxEvent::Bees, + volume: 0.5, + cond: |st| st.get_day_period().is_light(), + }, + ]; - // Get all the blocks of interest in this chunk - terrain.get(chunk_pos).map(|chunk_data| { - // Get the positions of the blocks of type sounds - let blocks = (sounds.blocks)(&chunk_data.blocks_of_interest); + // Iterate through each kind of block of interest + for sounds in sounds.iter() { + // If the timing condition is false, continue + // or if the player is far enough underground, continue + // TODO Address bird hack properly. See TODO on line 190 + if !(sounds.cond)(state) + || player_pos.0.z < (terrain_alt - 30.0) + || (sounds.sfx == SfxEvent::Birdcall && thread_rng().gen_bool(0.995)) + || (sounds.sfx == SfxEvent::Owl && thread_rng().gen_bool(0.998)) + || (sounds.sfx == SfxEvent::Frog && thread_rng().gen_bool(0.95)) + //Crickets will not chirp below 5 Celsius + || (sounds.sfx == SfxEvent::Cricket1 && (temp < -0.33)) + || (sounds.sfx == SfxEvent::Cricket2 && (temp < -0.33)) + || (sounds.sfx == SfxEvent::Cricket3 && (temp < -0.33)) + { + continue; + } - let absolute_pos: Vec3 = - Vec3::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32)); + // For chunks surrounding the player position + for offset in Spiral2d::new().take((sounds.range * 2 + 1).pow(2)) { + let chunk_pos = player_chunk + offset; - // Replace all RunningWater blocks with just one random one per tick - let blocks = if sounds.sfx == SfxEvent::RunningWaterSlow - || sounds.sfx == SfxEvent::RunningWaterFast - { - blocks - .choose(&mut thread_rng()) - .map(std::slice::from_ref) - .unwrap_or(&[]) - } else { - blocks - }; + // Get all the blocks of interest in this chunk + terrain.get(chunk_pos).map(|chunk_data| { + // Get the positions of the blocks of type sounds + let blocks = (sounds.blocks)(&chunk_data.blocks_of_interest); - // Iterate through each individual block - for block in blocks { - // TODO Address this hack properly, potentially by making a new - // block of interest type which picks fewer leaf blocks - // Hack to reduce the number of bird, frog, and water sounds - if ((sounds.sfx == SfxEvent::Birdcall || sounds.sfx == SfxEvent::Owl) - && thread_rng().gen_bool(0.9995)) - || (sounds.sfx == SfxEvent::Frog && thread_rng().gen_bool(0.75)) - || (sounds.sfx == SfxEvent::RunningWaterSlow - && thread_rng().gen_bool(0.5)) + let absolute_pos: Vec3 = + Vec3::from(chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32)); + + // Replace all RunningWater blocks with just one random one per tick + let blocks = if sounds.sfx == SfxEvent::RunningWaterSlow + || sounds.sfx == SfxEvent::RunningWaterFast { - continue; - } - let block_pos: Vec3 = absolute_pos + block; - let internal_state = self.history.entry(block_pos).or_default(); + blocks + .choose(&mut thread_rng()) + .map(std::slice::from_ref) + .unwrap_or(&[]) + } else { + blocks + }; - let block_pos = block_pos.map(|x| x as f32); - - if Self::should_emit( - internal_state, - triggers.get_key_value(&sounds.sfx), - temp, - ) { - // If the camera is within SFX distance - if (block_pos.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR { - let underwater = state - .terrain() - .get(cam_pos.map(|e| e.floor() as i32)) - .map(|b| b.is_liquid()) - .unwrap_or(false); - - let sfx_trigger_item = triggers.get_key_value(&sounds.sfx); - if sounds.sfx == SfxEvent::RunningWaterFast { - audio.emit_filtered_sfx( - sfx_trigger_item, - block_pos, - Some(sounds.volume), - Some(8000), - underwater, - ); - } else { - audio.emit_sfx( - sfx_trigger_item, - block_pos, - Some(sounds.volume), - underwater, - ); - } + // Iterate through each individual block + for block in blocks { + // TODO Address this hack properly, potentially by making a new + // block of interest type which picks fewer leaf blocks + // Hack to reduce the number of bird, frog, and water sounds + if ((sounds.sfx == SfxEvent::Birdcall || sounds.sfx == SfxEvent::Owl) + && thread_rng().gen_bool(0.9995)) + || (sounds.sfx == SfxEvent::Frog && thread_rng().gen_bool(0.75)) + || (sounds.sfx == SfxEvent::RunningWaterSlow + && thread_rng().gen_bool(0.5)) + { + continue; + } + let block_pos: Vec3 = absolute_pos + block; + let internal_state = self.history.entry(block_pos).or_default(); + + let block_pos = block_pos.map(|x| x as f32); + + if Self::should_emit( + internal_state, + triggers.get_key_value(&sounds.sfx), + temp, + ) { + // If the camera is within SFX distance + if (block_pos.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR { + let underwater = state + .terrain() + .get(cam_pos.map(|e| e.floor() as i32)) + .map(|b| b.is_liquid()) + .unwrap_or(false); + + let sfx_trigger_item = triggers.get_key_value(&sounds.sfx); + if sounds.sfx == SfxEvent::RunningWaterFast { + audio.emit_filtered_sfx( + sfx_trigger_item, + block_pos, + Some(sounds.volume), + Some(8000), + underwater, + ); + } else { + audio.emit_sfx( + sfx_trigger_item, + block_pos, + Some(sounds.volume), + underwater, + ); + } + } + internal_state.time = Instant::now(); + internal_state.event = sounds.sfx.clone(); } - internal_state.time = Instant::now(); - internal_state.event = sounds.sfx.clone(); } - } - }); + }); + } } } } From 693684d1c90611a23f5196f00fdb3dcf9dc9226e Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 4 May 2023 22:13:14 +0100 Subject: [PATCH 2/4] Updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e13e50203..ecb1d05ee2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Loot that drops multiple items is now distributed fairly between damage contributors. - Added accessibility settings tab. - Setting to enable subtitles describing sfx. +- Item drops that are spatially close and compatible will now merge with one-another to reduce performance problems. ### Changed From 8d9625d6ee916e6df4585c43e9054aa944cc01e1 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Thu, 4 May 2023 23:18:40 +0100 Subject: [PATCH 3/4] Addressed comments --- common/src/comp/inventory/item/mod.rs | 26 +++++++++++++++++++++----- server/src/state_ext.rs | 22 +++++++++++++++------- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/common/src/comp/inventory/item/mod.rs b/common/src/comp/inventory/item/mod.rs index e02a7d6b0d..06c9a49a21 100644 --- a/common/src/comp/inventory/item/mod.rs +++ b/common/src/comp/inventory/item/mod.rs @@ -1158,15 +1158,31 @@ 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.is_stackable() - && let ItemBase::Simple(other_item_def) = &other.item_base - && self.is_same_item_def(other_item_def) - { - self.increase_amount(other.amount()).map_err(|_| other)?; + if self.can_merge(&other) { + self.increase_amount(other.amount()) + .expect("`can_merge` succeeded but `increase_amount` did not"); Ok(()) } else { Err(other) diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 7131911472..272b193fe3 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -323,7 +323,7 @@ impl StateExt for State { &mut self, pos: comp::Pos, vel: comp::Vel, - mut item: Item, + item: Item, loot_owner: Option, ) -> Option { { @@ -340,7 +340,7 @@ impl StateExt for State { .read_resource::() .0 .in_circle_aabr(pos.0.xy(), MAX_MERGE_DIST) - .filter(|entity| items.get(*entity).is_some()) + .filter(|entity| items.contains(*entity)) .filter_map(|entity| { Some((entity, positions.get(entity)?.0.distance_squared(pos.0))) }) @@ -350,11 +350,19 @@ impl StateExt for State { 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()) { - if let Some(mut nearby_item) = items.get_mut(nearby) { - match nearby_item.try_merge(item) { - Ok(()) => return None, // Merging was successful! - Err(rejected_item) => item = rejected_item, - } + if 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; } } } From 73c84dfcc254aeb527ef784a3d5926cb8c4dca08 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Fri, 5 May 2023 00:04:13 +0100 Subject: [PATCH 4/4] Clippy --- server/src/state_ext.rs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 272b193fe3..0ea3374acf 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -349,21 +349,18 @@ impl StateExt for State { 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()) { - if items + 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; - } + { + // 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; } } // Only if merging items fails do we give up and create a new item