diff --git a/Cargo.lock b/Cargo.lock index cbf9c2c434..271557a365 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6718,6 +6718,7 @@ dependencies = [ "sha2", "slab", "slotmap 1.0.6", + "smallvec", "specs", "spin_sleep", "structopt", @@ -6861,7 +6862,7 @@ dependencies = [ "crossbeam-channel", "futures-core", "futures-util", - "hashbrown 0.9.1", + "hashbrown 0.12.3", "lazy_static", "lz-fear", "prometheus", diff --git a/common/Cargo.toml b/common/Cargo.toml index 5a8dcf4f92..cd0b5be14c 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -71,9 +71,9 @@ petgraph = { version = "0.6", optional = true } kiddo = { version = "0.1", optional = true } # Data structures -dashmap = { version = "5.4.0", features = ["rayon"] } hashbrown = { version = "0.12", features = ["rayon", "serde", "nightly"] } slotmap = { version = "1.0", features = ["serde"] } +smallvec = { version = "1.9.0", features = ["union", "const_generics", "const_new", "specialization", "may_dangle"] } indexmap = { version = "1.3.0", features = ["rayon"] } slab = "0.4.2" diff --git a/common/src/cached_spatial_grid.rs b/common/src/cached_spatial_grid.rs index 2e3c8689cd..c2fd9d0361 100644 --- a/common/src/cached_spatial_grid.rs +++ b/common/src/cached_spatial_grid.rs @@ -14,7 +14,7 @@ impl Default for CachedSpatialGrid { let lg2_large_cell_size = 6; // 64 let radius_cutoff = 8; - let spatial_grid = SpatialGrid::new(lg2_cell_size, lg2_large_cell_size, radius_cutoff).into_read_only(); + let spatial_grid = SpatialGrid::new(lg2_cell_size, lg2_large_cell_size, radius_cutoff, (0, 0)).into_read_only(); Self(spatial_grid) } diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 4da189e746..6513d3af98 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -104,7 +104,7 @@ pub use self::{ player::DisconnectReason, player::{AliasError, Player, MAX_ALIAS_LEN}, poise::{Poise, PoiseChange, PoiseState}, - projectile::{Projectile, ProjectileConstructor}, + projectile::{Projectile, ProjectileConstructor, ProjectileOwned}, shockwave::{Shockwave, ShockwaveHitEntities}, skillset::{ skills::{self, Skill}, diff --git a/common/src/comp/projectile.rs b/common/src/comp/projectile.rs index 607a673d0d..3f383154b5 100644 --- a/common/src/comp/projectile.rs +++ b/common/src/comp/projectile.rs @@ -23,11 +23,6 @@ pub enum Effect { #[derive(Clone, Debug)] pub struct Projectile { - // TODO: use SmallVec for these effects - pub hit_solid: Vec, - pub hit_entity: Vec, - /// Time left until the projectile will despawn - pub time_left: Duration, pub owner: Option, /// Whether projectile collides with entities in the same group as its /// owner @@ -38,10 +33,26 @@ pub struct Projectile { pub is_point: bool, } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct ProjectileOwned { + /// TODO: use SmallVec for these effects + pub hit_solid: Vec, + pub hit_entity: Vec, + /// Time left until the projectile will despawn + /// + /// TODO: Remove this, we can calculate this from an initial time which we + /// can store in the regular component. + pub time_left: Duration, +} + impl Component for Projectile { type Storage = specs::DenseVecStorage; } +impl Component for ProjectileOwned { + type Storage = specs::DenseVecStorage; +} + #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] pub enum ProjectileConstructor { Arrow { @@ -103,7 +114,7 @@ impl ProjectileConstructor { crit_chance: f32, crit_mult: f32, buff_strength: f32, - ) -> Projectile { + ) -> (ProjectileOwned, Projectile) { let instance = rand::random(); use ProjectileConstructor::*; match self { @@ -145,15 +156,19 @@ impl ProjectileConstructor { .with_effect(knockback) .with_combo_increment(); - Projectile { - hit_solid: vec![Effect::Stick, Effect::Bonk], - hit_entity: vec![Effect::Attack(attack), Effect::Vanish], - time_left: Duration::from_secs(15), - owner, - ignore_group: true, - is_sticky: true, - is_point: true, - } + ( + ProjectileOwned { + hit_solid: vec![Effect::Stick, Effect::Bonk], + hit_entity: vec![Effect::Attack(attack), Effect::Vanish], + time_left: Duration::from_secs(15), + }, + Projectile { + owner, + ignore_group: true, + is_sticky: true, + is_point: true, + }, + ) }, Fireball { damage, @@ -193,15 +208,19 @@ impl ProjectileConstructor { reagent: Some(Reagent::Red), min_falloff, }; - Projectile { - hit_solid: vec![Effect::Explode(explosion.clone()), Effect::Vanish], - hit_entity: vec![Effect::Explode(explosion), Effect::Vanish], - time_left: Duration::from_secs(10), - owner, - ignore_group: true, - is_sticky: true, - is_point: true, - } + ( + ProjectileOwned { + hit_solid: vec![Effect::Explode(explosion.clone()), Effect::Vanish], + hit_entity: vec![Effect::Explode(explosion), Effect::Vanish], + time_left: Duration::from_secs(10), + }, + Projectile { + owner, + ignore_group: true, + is_sticky: true, + is_point: true, + }, + ) }, Frostball { damage, @@ -227,15 +246,19 @@ impl ProjectileConstructor { reagent: Some(Reagent::White), min_falloff, }; - Projectile { - hit_solid: vec![Effect::Explode(explosion.clone()), Effect::Vanish], - hit_entity: vec![Effect::Explode(explosion), Effect::Vanish], - time_left: Duration::from_secs(10), - owner, - ignore_group: true, - is_sticky: true, - is_point: true, - } + ( + ProjectileOwned { + hit_solid: vec![Effect::Explode(explosion.clone()), Effect::Vanish], + hit_entity: vec![Effect::Explode(explosion), Effect::Vanish], + time_left: Duration::from_secs(10), + }, + Projectile { + owner, + ignore_group: true, + is_sticky: true, + is_point: true, + }, + ) }, Poisonball { damage, @@ -274,15 +297,19 @@ impl ProjectileConstructor { reagent: Some(Reagent::Purple), min_falloff, }; - Projectile { - hit_solid: vec![Effect::Explode(explosion.clone()), Effect::Vanish], - hit_entity: vec![Effect::Explode(explosion), Effect::Vanish], - time_left: Duration::from_secs(10), - owner, - ignore_group: true, - is_sticky: true, - is_point: true, - } + ( + ProjectileOwned { + hit_solid: vec![Effect::Explode(explosion.clone()), Effect::Vanish], + hit_entity: vec![Effect::Explode(explosion), Effect::Vanish], + time_left: Duration::from_secs(10), + }, + Projectile { + owner, + ignore_group: true, + is_sticky: true, + is_point: true, + }, + ) }, NecroticSphere { damage, @@ -308,25 +335,33 @@ impl ProjectileConstructor { reagent: Some(Reagent::Purple), min_falloff, }; - Projectile { - hit_solid: vec![Effect::Explode(explosion.clone()), Effect::Vanish], - hit_entity: vec![Effect::Explode(explosion), Effect::Vanish], + ( + ProjectileOwned { + hit_solid: vec![Effect::Explode(explosion.clone()), Effect::Vanish], + hit_entity: vec![Effect::Explode(explosion), Effect::Vanish], + time_left: Duration::from_secs(10), + }, + Projectile { + owner, + ignore_group: true, + is_sticky: true, + is_point: true, + }, + ) + }, + Possess => ( + ProjectileOwned { + hit_solid: vec![Effect::Stick], + hit_entity: vec![Effect::Stick, Effect::Possess], time_left: Duration::from_secs(10), + }, + Projectile { owner, - ignore_group: true, + ignore_group: false, is_sticky: true, is_point: true, - } - }, - Possess => Projectile { - hit_solid: vec![Effect::Stick], - hit_entity: vec![Effect::Stick, Effect::Possess], - time_left: Duration::from_secs(10), - owner, - ignore_group: false, - is_sticky: true, - is_point: true, - }, + }, + ), ClayRocket { damage, radius, @@ -363,15 +398,19 @@ impl ProjectileConstructor { reagent: Some(Reagent::Red), min_falloff, }; - Projectile { - hit_solid: vec![Effect::Explode(explosion.clone()), Effect::Vanish], - hit_entity: vec![Effect::Explode(explosion), Effect::Vanish], - time_left: Duration::from_secs(10), - owner, - ignore_group: true, - is_sticky: true, - is_point: true, - } + ( + ProjectileOwned { + hit_solid: vec![Effect::Explode(explosion.clone()), Effect::Vanish], + hit_entity: vec![Effect::Explode(explosion), Effect::Vanish], + time_left: Duration::from_secs(10), + }, + Projectile { + owner, + ignore_group: true, + is_sticky: true, + is_point: true, + }, + ) }, Snowball { damage, @@ -396,15 +435,19 @@ impl ProjectileConstructor { reagent: Some(Reagent::White), min_falloff, }; - Projectile { - hit_solid: vec![], - hit_entity: vec![Effect::Explode(explosion), Effect::Vanish], - time_left: Duration::from_secs(120), - owner, - ignore_group: true, - is_sticky: false, - is_point: false, - } + ( + ProjectileOwned { + hit_solid: vec![], + hit_entity: vec![Effect::Explode(explosion), Effect::Vanish], + time_left: Duration::from_secs(120), + }, + Projectile { + owner, + ignore_group: true, + is_sticky: false, + is_point: false, + }, + ) }, ExplodingPumpkin { damage, @@ -453,15 +496,19 @@ impl ProjectileConstructor { reagent: Some(Reagent::Red), min_falloff, }; - Projectile { - hit_solid: vec![Effect::Explode(explosion.clone()), Effect::Vanish], - hit_entity: vec![Effect::Explode(explosion), Effect::Vanish], - time_left: Duration::from_secs(10), - owner, - ignore_group: true, - is_sticky: true, - is_point: true, - } + ( + ProjectileOwned { + hit_solid: vec![Effect::Explode(explosion.clone()), Effect::Vanish], + hit_entity: vec![Effect::Explode(explosion), Effect::Vanish], + time_left: Duration::from_secs(10), + }, + Projectile { + owner, + ignore_group: true, + is_sticky: true, + is_point: true, + }, + ) }, DagonBomb { damage, @@ -510,15 +557,19 @@ impl ProjectileConstructor { reagent: Some(Reagent::Blue), min_falloff, }; - Projectile { - hit_solid: vec![Effect::Explode(explosion.clone()), Effect::Vanish], - hit_entity: vec![Effect::Explode(explosion), Effect::Vanish], - time_left: Duration::from_secs(10), - owner, - ignore_group: true, - is_sticky: true, - is_point: true, - } + ( + ProjectileOwned { + hit_solid: vec![Effect::Explode(explosion.clone()), Effect::Vanish], + hit_entity: vec![Effect::Explode(explosion), Effect::Vanish], + time_left: Duration::from_secs(10), + }, + Projectile { + owner, + ignore_group: true, + is_sticky: true, + is_point: true, + }, + ) }, } } diff --git a/common/src/event.rs b/common/src/event.rs index cc76203441..23a3d808fa 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -78,7 +78,7 @@ pub enum ServerEvent { dir: Dir, body: comp::Body, light: Option, - projectile: comp::Projectile, + projectile: (comp::ProjectileOwned, comp::Projectile), speed: f32, object: Option, }, @@ -148,7 +148,7 @@ pub enum ServerEvent { anchor: Option, loot: LootSpec, rtsim_entity: Option, - projectile: Option, + projectile: Option<(comp::ProjectileOwned, comp::Projectile)>, }, CreateShip { pos: Pos, diff --git a/common/src/states/basic_summon.rs b/common/src/states/basic_summon.rs index 77a2553f9c..87f4b3f142 100644 --- a/common/src/states/basic_summon.rs +++ b/common/src/states/basic_summon.rs @@ -4,7 +4,7 @@ use crate::{ character_state::OutputEvents, inventory::loadout_builder::{self, LoadoutBuilder}, skillset::skills, - Behavior, BehaviorCapability, CharacterState, Projectile, StateUpdate, + Behavior, BehaviorCapability, CharacterState, Projectile, ProjectileOwned, StateUpdate, }, event::{LocalEvent, ServerEvent}, outcome::Outcome, @@ -162,15 +162,17 @@ impl CharacterBehavior for Data { .0; // If a duration is specified, create a projectile component for the npc - let projectile = self.static_data.duration.map(|duration| Projectile { + let projectile = self.static_data.duration.map(|duration| (ProjectileOwned { hit_solid: Vec::new(), hit_entity: Vec::new(), time_left: duration, + }, + Projectile { owner: Some(*data.uid), ignore_group: true, is_sticky: false, is_point: false, - }); + })); // Send server event to create npc output_events.emit_server(ServerEvent::CreateNpc { diff --git a/common/src/util/spatial_grid.rs b/common/src/util/spatial_grid.rs index 4b78fbf08b..fcad787ca2 100644 --- a/common/src/util/spatial_grid.rs +++ b/common/src/util/spatial_grid.rs @@ -1,8 +1,13 @@ -use core::sync::atomic::{AtomicU32, Ordering}; +use core::sync::atomic::AtomicU32; +use smallvec::SmallVec; use vek::*; -pub type MapMut = dashmap::DashMap, Vec>; -pub type MapRef = dashmap::ReadOnlyView, Vec>; +// NOTE: (Vec2, [specs::Entity; 6]) should fit in a cacheline, reducing false sharing if we +// ever decide to directly update the SmallVecs. +type EntityVec = SmallVec<[specs::Entity; 6]>; + +pub type MapMut = /*dashmap::DashMap*/hashbrown::HashMap, EntityVec>; +pub type MapRef = /*dashmap::ReadOnlyView*/hashbrown::HashMap, EntityVec>; #[derive(Debug)] pub struct SpatialGridInner { @@ -32,10 +37,10 @@ pub type SpatialGrid = SpatialGridInner; pub type SpatialGridRef = SpatialGridInner; impl SpatialGrid { - pub fn new(lg2_cell_size: usize, lg2_large_cell_size: usize, radius_cutoff: u32) -> Self { + pub fn new(lg2_cell_size: usize, lg2_large_cell_size: usize, radius_cutoff: u32, (capacity, large_capacity): (usize, usize)) -> Self { Self { - grid: Default::default(), - large_grid: Default::default(), + grid: MapMut::with_capacity(capacity), + large_grid: MapMut::with_capacity(large_capacity), lg2_cell_size, lg2_large_cell_size, radius_cutoff, @@ -43,8 +48,12 @@ impl SpatialGrid { } } + pub fn len(&self) -> (usize, usize) { + (self.grid.len(), self.large_grid.len()) + } + /// Add an entity at the provided 2d pos into the spatial grid - pub fn insert(&self, pos: Vec2, radius: u32, entity: specs::Entity) { + pub fn insert(&mut self, pos: Vec2, radius: u32, entity: specs::Entity) { if radius <= self.radius_cutoff { let cell = pos.map(|e| e >> self.lg2_cell_size); self.grid.entry(cell).or_default().push(entity); @@ -59,14 +68,16 @@ impl SpatialGrid { // // TODO: Verify that intrinsics lower intelligently to a priority update on CPUs (since // the intrinsic seems targeted at GPUs). - self.largest_large_radius.fetch_max(radius, Ordering::Relaxed); + /* self.largest_large_radius.fetch_max(radius, Ordering::Relaxed); */ + let largest_radius = self.largest_large_radius.get_mut(); + *largest_radius = (*largest_radius).max(radius); } } pub fn into_read_only(self) -> SpatialGridRef { SpatialGridInner { - grid: self.grid.into_read_only(), - large_grid: self.large_grid.into_read_only(), + grid: self.grid/*.into_read_only()*/, + large_grid: self.large_grid/*.into_read_only()*/, lg2_cell_size: self.lg2_cell_size, lg2_large_cell_size: self.lg2_large_cell_size, radius_cutoff: self.radius_cutoff, @@ -136,8 +147,8 @@ impl SpatialGridRef { pub fn into_inner(self) -> SpatialGrid { SpatialGridInner { - grid: self.grid.into_inner(), - large_grid: self.large_grid.into_inner(), + grid: self.grid/*.into_inner()*/, + large_grid: self.large_grid/*.into_inner()*/, lg2_cell_size: self.lg2_cell_size, lg2_large_cell_size: self.lg2_large_cell_size, radius_cutoff: self.radius_cutoff, diff --git a/common/state/src/state.rs b/common/state/src/state.rs index de1d7c7d3b..3710555517 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -214,6 +214,7 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); + ecs.register::(); ecs.register::(); ecs.register::(); ecs.register::(); diff --git a/common/systems/src/buff.rs b/common/systems/src/buff.rs index fb70b7e122..cc246e0869 100644 --- a/common/systems/src/buff.rs +++ b/common/systems/src/buff.rs @@ -20,10 +20,9 @@ use common::{ use common_base::prof_span; use common_ecs::{Job, Origin, ParMode, Phase, System}; use hashbrown::HashMap; -use rayon::iter::ParallelIterator; use specs::{ - saveload::MarkerAllocator, shred::ResourceId, Entities, Entity, Join, ParJoin, Read, - ReadExpect, ReadStorage, SystemData, World, WriteStorage, + saveload::MarkerAllocator, shred::ResourceId, Entities, Entity, Join, Read, ReadExpect, + ReadStorage, SystemData, World, WriteStorage, }; use std::time::Duration; @@ -71,59 +70,30 @@ impl<'a> System<'a> for Sys { // removes burning, but campfires don't have healths/stats/energies/buffs, so // this needs a separate loop. job.cpu_stats.measure(ParMode::Rayon); - let to_put_out_campfires = ( + let light_emitters_mask = light_emitters.mask().clone(); + prof_span!(guard_, "buff campfire deactivate"); + ( &read_data.entities, - &bodies, + &mut bodies, &read_data.physics_states, - &light_emitters, //to improve iteration speed + light_emitters_mask, //to improve iteration speed ) - .par_join() - .map_init( - || { - prof_span!(guard, "buff campfire deactivate"); - guard - }, - |_guard, (entity, body, physics_state, _)| { - if matches!(*body, Body::Object(object::Body::CampfireLit)) - && matches!( - physics_state.in_fluid, - Some(Fluid::Liquid { - kind: LiquidKind::Water, - .. - }) - ) - { - Some(entity) - } else { - None - } - }, - ) - .fold(Vec::new, |mut to_put_out_campfires, put_out_campfire| { - put_out_campfire.map(|put| to_put_out_campfires.push(put)); - to_put_out_campfires + .join() + .filter(|(_, body, physics_state, _)| { + matches!(&**body, Body::Object(object::Body::CampfireLit)) + && matches!( + physics_state.in_fluid, + Some(Fluid::Liquid { + kind: LiquidKind::Water, + .. + }) + ) }) - .reduce( - Vec::new, - |mut to_put_out_campfires_a, mut to_put_out_campfires_b| { - to_put_out_campfires_a.append(&mut to_put_out_campfires_b); - to_put_out_campfires_a - }, - ); - job.cpu_stats.measure(ParMode::Single); - { - prof_span!(_guard, "write deferred campfire deletion"); - // Assume that to_put_out_campfires is near to zero always, so this access isn't - // slower than parallel checking above - for e in to_put_out_campfires { - { - bodies - .get_mut(e) - .map(|mut body| *body = Body::Object(object::Body::Campfire)); - light_emitters.remove(e); - } - } - } + .for_each(|(e, mut body, _, _)| { + *body = Body::Object(object::Body::Campfire); + light_emitters.remove(e); + }); + drop(guard_); for (entity, mut buff_comp, mut stat, health, energy, physics_state) in ( &read_data.entities, diff --git a/common/systems/src/lib.rs b/common/systems/src/lib.rs index a0e18fcf5f..536289544b 100644 --- a/common/systems/src/lib.rs +++ b/common/systems/src/lib.rs @@ -1,4 +1,4 @@ -#![feature(btree_drain_filter)] +#![feature(let_chains, btree_drain_filter)] #![allow(clippy::option_map_unit_fn)] mod aura; diff --git a/common/systems/src/phys.rs b/common/systems/src/phys.rs index 75684ae815..dbec8a23fa 100644 --- a/common/systems/src/phys.rs +++ b/common/systems/src/phys.rs @@ -12,6 +12,7 @@ use common::{ mounting::Rider, outcome::Outcome, resources::DeltaTime, + slowjob::SlowJobPool, states, terrain::{Block, BlockKind, TerrainGrid}, uid::Uid, @@ -107,6 +108,7 @@ pub struct PhysicsRead<'a> { entities: Entities<'a>, uids: ReadStorage<'a, Uid>, terrain: ReadExpect<'a, TerrainGrid>, + slowjob: ReadExpect<'a, SlowJobPool>, dt: Read<'a, DeltaTime>, event_bus: Read<'a, EventBus>, scales: ReadStorage<'a, Scale>, @@ -121,6 +123,7 @@ pub struct PhysicsRead<'a> { character_states: ReadStorage<'a, CharacterState>, densities: ReadStorage<'a, Density>, stats: ReadStorage<'a, Stats>, + outcomes: Read<'a, EventBus>, } #[derive(SystemData)] @@ -133,7 +136,6 @@ pub struct PhysicsWrite<'a> { pos_vel_ori_defers: WriteStorage<'a, PosVelOriDefer>, orientations: WriteStorage<'a, Ori>, previous_phys_cache: WriteStorage<'a, PreviousPhysCache>, - outcomes: Read<'a, EventBus>, } #[derive(SystemData)] @@ -142,68 +144,75 @@ pub struct PhysicsData<'a> { write: PhysicsWrite<'a>, } -impl<'a> PhysicsData<'a> { +impl<'a> PhysicsRead<'a> { /// Add/reset physics state components - fn reset(&mut self) { + fn reset( + &self, + positions: &WriteStorage<'a, Pos>, + velocities: &WriteStorage<'a, Vel>, + orientations: &WriteStorage<'a, Ori>, + physics_states: &mut WriteStorage<'a, PhysicsState>, + ) { span!(_guard, "Add/reset physics state components"); ( - &self.read.entities, - self.read.colliders.mask(), - self.write.positions.mask(), - self.write.velocities.mask(), - self.write.orientations.mask(), + &self.entities, + self.colliders.mask(), + positions.mask(), + velocities.mask(), + orientations.mask(), ) .join() .for_each(|(entity, _, _, _, _)| { - let _ = self - .write - .physics_states + let _ = physics_states .entry(entity) .map(|e| e.or_insert_with(Default::default)); }); } - fn maintain_pushback_cache(&mut self) { + fn maintain_pushback_cache( + &self, + positions: &WriteStorage<'a, Pos>, + velocities: &WriteStorage<'a, Vel>, + orientations: &WriteStorage<'a, Ori>, + previous_phys_cache: &mut WriteStorage<'a, PreviousPhysCache>, + ) { span!(_guard, "Maintain pushback cache"); // Add PreviousPhysCache for all relevant entities ( - &self.read.entities, - self.read.colliders.mask(), - self.write.velocities.mask(), - self.write.positions.mask(), - !self.write.previous_phys_cache.mask(), + &self.entities, + self.colliders.mask(), + velocities.mask(), + positions.mask(), + !previous_phys_cache.mask(), ) .join() .map(|(e, _, _, _, _)| e) .collect::>() .into_iter() .for_each(|entity| { - let _ = self - .write - .previous_phys_cache - .insert(entity, PreviousPhysCache { - velocity_dt: Vec3::zero(), - center: Vec3::zero(), - collision_boundary: 0.0, - scale: 0.0, - scaled_radius: 0.0, - neighborhood_radius: 0.0, - origins: None, - pos: None, - ori: Quaternion::identity(), - }); + let _ = previous_phys_cache.insert(entity, PreviousPhysCache { + velocity_dt: Vec3::zero(), + center: Vec3::zero(), + collision_boundary: 0.0, + scale: 0.0, + scaled_radius: 0.0, + neighborhood_radius: 0.0, + origins: None, + pos: None, + ori: Quaternion::identity(), + }); }); // Update PreviousPhysCache ( /* &self.read.entities, */ - &self.write.velocities, - &self.write.positions, - &self.write.orientations, - &mut self.write.previous_phys_cache, - &self.read.colliders, - self.read.scales.maybe(), - self.read.char_states.maybe(), + velocities, + positions, + orientations, + previous_phys_cache, + &self.colliders, + self.scales.maybe(), + self.char_states.maybe(), ) .par_join() .for_each( @@ -214,7 +223,7 @@ impl<'a> PhysicsData<'a> { let (z_min, z_max) = (z_min * scale, z_max * scale); let half_height = (z_max - z_min) / 2.0; - phys_cache.velocity_dt = vel.0 * self.read.dt.0; + phys_cache.velocity_dt = vel.0 * self.dt.0; let entity_center = position.0 + Vec3::new(0.0, 0.0, z_min + half_height); let flat_radius = collider.bounding_radius() * scale; let radius = (flat_radius.powi(2) + half_height.powi(2)).sqrt(); @@ -279,12 +288,14 @@ impl<'a> PhysicsData<'a> { ); } - fn construct_spatial_grid(&mut self) -> SpatialGrid { + fn construct_spatial_grid( + &self, + positions: &WriteStorage<'a, Pos>, + velocities: &WriteStorage<'a, Vel>, + capacity: (usize, usize), + /* previous_phys_cache: &WriteStorage<'a, PreviousPhysCache>, */ + ) -> SpatialGrid { span!(_guard, "Construct spatial grid"); - let PhysicsData { - ref read, - ref write, - } = self; // NOTE: i32 places certain constraints on how far out collision works // NOTE: uses the radius of the entity and their current position rather than // the radius of their bounding sphere for the current frame of movement @@ -298,19 +309,24 @@ impl<'a> PhysicsData<'a> { let lg2_cell_size = 5; let lg2_large_cell_size = 6; let radius_cutoff = 8; - let spatial_grid = SpatialGrid::new(lg2_cell_size, lg2_large_cell_size, radius_cutoff); + let mut spatial_grid = + SpatialGrid::new(lg2_cell_size, lg2_large_cell_size, radius_cutoff, capacity); ( - &read.entities, - &write.positions, - &write.previous_phys_cache, - write.velocities.mask(), - !read.projectiles.mask(), /* Not needed because they are skipped in the inner loop + &self.entities, + positions, + /* previous_phys_cache, */ + velocities.mask(), + &self.colliders, + self.scales.maybe(), + !self.projectiles.mask(), /* Not needed because they are skipped in the inner loop * below */ ) - .par_join() - .for_each(|(entity, pos, phys_cache, _, _)| { + ./*par_join*/join() + .for_each(|(entity, pos, /*phys_cache, */_, collider, scale, _)| { + let scale = scale.map(|s| s.0).unwrap_or(1.0); + let scaled_radius = collider.bounding_radius() * scale; // Note: to not get too fine grained we use a 2D grid for now - let radius_2d = phys_cache.scaled_radius.ceil() as u32; + let radius_2d = /*phys_cache.*/scaled_radius.ceil() as u32; let pos_2d = pos.0.xy().map(|e| e as i32); const POS_TRUNCATION_ERROR: u32 = 1; spatial_grid.insert(pos_2d, radius_2d + POS_TRUNCATION_ERROR, entity); @@ -319,30 +335,34 @@ impl<'a> PhysicsData<'a> { spatial_grid } - fn apply_pushback(&mut self, job: &mut Job, spatial_grid: &SpatialGridRef) { + fn apply_pushback( + &self, + job: &mut Job, + spatial_grid: &SpatialGridRef, + physics_metrics: &mut WriteExpect<'a, PhysicsMetrics>, + physics_states: &mut WriteStorage<'a, PhysicsState>, + positions: &WriteStorage<'a, Pos>, + velocities: &mut WriteStorage<'a, Vel>, + previous_phys_cache: &WriteStorage<'a, PreviousPhysCache>, + ) { span!(_guard, "Apply pushback"); job.cpu_stats.measure(ParMode::Rayon); - let PhysicsData { - ref read, - ref mut write, - } = self; - let (positions, previous_phys_cache) = (&write.positions, &write.previous_phys_cache); let metrics = ( - &read.entities, + &self.entities, positions, - &mut write.velocities, + velocities, previous_phys_cache, - &read.masses, - &read.colliders, - read.is_ridings.maybe(), - read.stickies.maybe(), - read.immovables.maybe(), - &mut write.physics_states, + &self.masses, + &self.colliders, + self.is_ridings.maybe(), + self.stickies.maybe(), + self.immovables.maybe(), + physics_states, // TODO: if we need to avoid collisions for other things consider // moving whether it should interact into the collider component // or into a separate component. - read.projectiles.maybe(), - read.char_states.maybe(), + self.projectiles.maybe(), + self.char_states.maybe(), ) .par_join() .map_init( @@ -397,11 +417,11 @@ impl<'a> PhysicsData<'a> { spatial_grid .in_circle_aabr(query_center, query_radius) .filter_map(|entity| { - let uid = read.uids.get(entity)?; + let uid = self.uids.get(entity)?; let pos = positions.get(entity)?; let previous_cache = previous_phys_cache.get(entity)?; - let mass = read.masses.get(entity)?; - let collider = read.colliders.get(entity)?; + let mass = self.masses.get(entity)?; + let collider = self.colliders.get(entity)?; Some(( entity, @@ -410,8 +430,8 @@ impl<'a> PhysicsData<'a> { previous_cache, mass, collider, - read.char_states.get(entity), - read.is_ridings.get(entity), + self.char_states.get(entity), + self.is_ridings.get(entity), )) }) .for_each( @@ -498,7 +518,7 @@ impl<'a> PhysicsData<'a> { ); // Change velocity - vel.0 += vel_delta * read.dt.0; + vel.0 += vel_delta * self.dt.0; // Metrics PhysicsMetrics { @@ -513,18 +533,17 @@ impl<'a> PhysicsData<'a> { entity_entity_collisions: old.entity_entity_collisions + new.entity_entity_collisions, }); - write.physics_metrics.entity_entity_collision_checks = - metrics.entity_entity_collision_checks; - write.physics_metrics.entity_entity_collisions = metrics.entity_entity_collisions; + physics_metrics.entity_entity_collision_checks = metrics.entity_entity_collision_checks; + physics_metrics.entity_entity_collisions = metrics.entity_entity_collisions; } - fn construct_voxel_collider_spatial_grid(&mut self) -> SpatialGrid { + fn construct_voxel_collider_spatial_grid( + &self, + positions: &WriteStorage<'a, Pos>, + orientations: &WriteStorage<'a, Ori>, + capacity: (usize, usize), + ) -> SpatialGrid { span!(_guard, "Construct voxel collider spatial grid"); - let PhysicsData { - ref read, - ref write, - } = self; - let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read(); // NOTE: i32 places certain constraints on how far out collision works @@ -536,15 +555,16 @@ impl<'a> PhysicsData<'a> { let lg2_cell_size = 7; // 128 let lg2_large_cell_size = 8; // 256 let radius_cutoff = 64; - let spatial_grid = SpatialGrid::new(lg2_cell_size, lg2_large_cell_size, radius_cutoff); + let mut spatial_grid = + SpatialGrid::new(lg2_cell_size, lg2_large_cell_size, radius_cutoff, capacity); // TODO: give voxel colliders their own component type ( - &read.entities, - &write.positions, - &read.colliders, - &write.orientations, + &self.entities, + positions, + &self.colliders, + orientations, ) - .par_join() + ./*par_join*/join() .for_each(|(entity, pos, collider, ori)| { let vol = match collider { Collider::Voxel { id } => voxel_colliders_manifest.colliders.get(id), @@ -563,7 +583,9 @@ impl<'a> PhysicsData<'a> { spatial_grid } +} +impl<'a> PhysicsData<'a> { fn handle_movement_and_terrain( &mut self, job: &mut Job, @@ -707,7 +729,7 @@ impl<'a> PhysicsData<'a> { &mut write.previous_phys_cache, read.colliders.mask(), ) - .par_join() + .join() .for_each(|(pos, ori, previous_phys_cache, _)| { // Note: updating ori with the rest of the cache values above was attempted but // it did not work (investigate root cause?) @@ -746,6 +768,7 @@ impl<'a> PhysicsData<'a> { &mut write.pos_vel_ori_defers, previous_phys_cache, !&read.is_ridings, + read.projectiles.maybe(), ) .par_join() .filter(|tuple| tuple.3.is_voxel() == terrain_like_entities) @@ -769,6 +792,7 @@ impl<'a> PhysicsData<'a> { pos_vel_ori_defer, previous_cache, _, + projectile, )| { let mut land_on_ground = None; let mut outcomes = Vec::new(); @@ -931,11 +955,9 @@ impl<'a> PhysicsData<'a> { // TODO: Not all projectiles should count as sticky! if sticky.is_some() { - if let Some((projectile, body)) = read - .projectiles - .get(entity) + if let Some((projectile, body)) = projectile .filter(|_| vel.0.magnitude_squared() > 1.0 && block.is_some()) - .zip(read.bodies.get(entity).copied()) + .zip(body.copied()) { outcomes.push(Outcome::ProjectileHit { pos: pos.0 + pos_delta * dist, @@ -1212,6 +1234,11 @@ impl<'a> PhysicsData<'a> { } if vel != old_vel { pos_vel_ori_defer.vel = Some(vel); + // Moving this logic here instead of the projectile system allows it to + // avoid writing to ori. + if projectile.is_some() && let Some(dir) = Ori::from_unnormalized_vec(vel.0) { + ori = dir; + } } else { pos_vel_ori_defer.vel = None; } @@ -1244,7 +1271,7 @@ impl<'a> PhysicsData<'a> { drop(guard); job.cpu_stats.measure(ParMode::Single); - write.outcomes.emitter().emit_many(outcomes); + read.outcomes.emitter().emit_many(outcomes); prof_span!(guard, "write deferred pos and vel"); ( @@ -1255,7 +1282,7 @@ impl<'a> PhysicsData<'a> { &mut write.pos_vel_ori_defers, &read.colliders, ) - .par_join() + .join() .filter(|tuple| tuple./*5*/4.is_voxel() == terrain_like_entities) .for_each(|(/* _, */ pos, vel, ori, pos_vel_ori_defer, _)| { if let Some(new_pos) = pos_vel_ori_defer.pos.take() { @@ -1276,26 +1303,20 @@ impl<'a> PhysicsData<'a> { }); } - fn update_cached_spatial_grid(&mut self) { + fn update_cached_spatial_grid(&mut self, mut spatial_grid: SpatialGrid) { span!(_guard, "Update cached spatial grid"); let PhysicsData { ref read, ref mut write, } = self; - // Borrow checker dance, since transferring away from a read only view requires - // &mut self. - let mut spatial_grid = core::mem::take(&mut *write.cached_spatial_grid) - .0 - .into_inner(); - spatial_grid.clear(); ( &read.entities, &write.positions, read.scales.maybe(), read.colliders.maybe(), ) - .par_join() + .join() .for_each(|(entity, pos, scale, collider)| { let scale = scale.map(|s| s.0).unwrap_or(1.0); let radius_2d = @@ -1317,33 +1338,112 @@ impl<'a> System<'a> for Sys { const PHASE: Phase = Phase::Create; fn run(job: &mut Job, mut physics_data: Self::SystemData) { - physics_data.reset(); + // Borrow checker dance, since transferring away from a read only view requires + // &mut self. + let mut cached_spatial_grid = core::mem::take(&mut *physics_data.write.cached_spatial_grid) + .0 + .into_inner(); + // Initialize grids to twice the cached grid's capacity, since entity + // distribution will rarely change much from tick to tick. + let grid_capacity = cached_spatial_grid.len(); + let grid_capacity = ( + grid_capacity.0.saturating_mul(2), + grid_capacity.1.saturating_mul(2), + ); + rayon::join( + || { + let PhysicsData { + ref read, + ref mut write, + } = physics_data; - // Apply pushback - // - // Note: We now do this first because we project velocity ahead. This is slighty - // imperfect and implies that we might get edge-cases where entities - // standing right next to the edge of a wall may get hit by projectiles - // fired into the wall very close to them. However, this sort of thing is - // already possible with poorly-defined hitboxes anyway so it's not too - // much of a concern. - // - // If this situation becomes a problem, this code should be integrated with the - // terrain collision code below, although that's not trivial to do since - // it means the step needs to take into account the speeds of both - // entities. - physics_data.maintain_pushback_cache(); + let (spatial_grid, voxel_collider_spatial_grid) = rayon::join( + || { + let (spatial_grid, ()) = rayon::join( + || { + read.construct_spatial_grid( + &write.positions, + &write.velocities, + grid_capacity, + ) + .into_read_only() + }, + || { + rayon::join( + || { + read.reset( + &write.positions, + &write.velocities, + &write.orientations, + &mut write.physics_states, + ); + }, + || { + read.maintain_pushback_cache( + &write.positions, + &write.velocities, + &write.orientations, + &mut write.previous_phys_cache, + ); + }, + ); + }, + ); - let spatial_grid = physics_data.construct_spatial_grid().into_read_only(); - physics_data.apply_pushback(job, &spatial_grid); + // Apply pushback + // + // Note: We now do this first because we project velocity ahead. This is + // slighty imperfect and implies that we might get + // edge-cases where entities standing right next to + // the edge of a wall may get hit by projectiles + // fired into the wall very close to them. However, this sort of thing is + // already possible with poorly-defined hitboxes anyway so it's not too + // much of a concern. + // + // If this situation becomes a problem, this code should be integrated with + // the terrain collision code below, although that's + // not trivial to do since it means the step needs + // to take into account the speeds of both entities. + read.apply_pushback( + job, + &spatial_grid, + &mut write.physics_metrics, + &mut write.physics_states, + &write.positions, + &mut write.velocities, + &write.previous_phys_cache, + ); + spatial_grid + }, + || { + read.construct_voxel_collider_spatial_grid( + &write.positions, + &write.orientations, + // Almost certainly overkill since most chunks won't contain a + // collider, but probably not worth altering. + grid_capacity, + ) + .into_read_only() + }, + ); - let voxel_collider_spatial_grid = physics_data - .construct_voxel_collider_spatial_grid() - .into_read_only(); - physics_data.handle_movement_and_terrain(job, &voxel_collider_spatial_grid); + physics_data.handle_movement_and_terrain(job, &voxel_collider_spatial_grid); + + physics_data.read.slowjob.spawn("CHUNK_DROP", move || { + drop(spatial_grid); + drop(voxel_collider_spatial_grid); + }); + }, + || { + cached_spatial_grid.clear(); + }, + ); // Spatial grid used by other systems - physics_data.update_cached_spatial_grid(); + // + // TODO: Consider inserting only the difference so we can update in parallel, + // rather than clearing and reinserting everything. + physics_data.update_cached_spatial_grid(cached_spatial_grid); } } diff --git a/common/systems/src/projectile.rs b/common/systems/src/projectile.rs index 0865d40dd7..503ccab63c 100644 --- a/common/systems/src/projectile.rs +++ b/common/systems/src/projectile.rs @@ -3,20 +3,21 @@ use common::{ comp::{ agent::{Sound, SoundKind}, projectile, Alignment, Body, CharacterState, Combo, Energy, Group, Health, Inventory, Ori, - PhysicsState, Player, Pos, Projectile, Stats, Vel, + PhysicsState, Player, Pos, Projectile, ProjectileOwned, Stats, Vel, }, event::{Emitter, EventBus, ServerEvent}, outcome::Outcome, resources::{DeltaTime, Time}, uid::{Uid, UidAllocator}, - util::Dir, GroupTarget, }; +use common_base::prof_span; use common_ecs::{Job, Origin, Phase, System}; use rand::{thread_rng, Rng}; +use rayon::iter::ParallelIterator; use specs::{ - saveload::MarkerAllocator, shred::ResourceId, Entities, Entity as EcsEntity, Join, Read, - ReadStorage, SystemData, World, WriteStorage, + saveload::MarkerAllocator, shred::ResourceId, Entities, Entity as EcsEntity, Join, ParJoin, + Read, ReadStorage, SystemData, World, WriteStorage, }; use std::time::Duration; use vek::*; @@ -25,6 +26,8 @@ use vek::*; pub struct ReadData<'a> { time: Read<'a, Time>, entities: Entities<'a>, + projectiles: ReadStorage<'a, Projectile>, + orientations: ReadStorage<'a, Ori>, players: ReadStorage<'a, Player>, dt: Read<'a, DeltaTime>, uid_allocator: Read<'a, UidAllocator>, @@ -50,8 +53,7 @@ pub struct Sys; impl<'a> System<'a> for Sys { type SystemData = ( ReadData<'a>, - WriteStorage<'a, Ori>, - WriteStorage<'a, Projectile>, + WriteStorage<'a, ProjectileOwned>, Read<'a, EventBus>, ); @@ -59,23 +61,26 @@ impl<'a> System<'a> for Sys { const ORIGIN: Origin = Origin::Common; const PHASE: Phase = Phase::Create; - fn run( - _job: &mut Job, - (read_data, mut orientations, mut projectiles, outcomes): Self::SystemData, - ) { - let mut server_emitter = read_data.server_bus.emitter(); - let mut outcomes_emitter = outcomes.emitter(); - + fn run(_job: &mut Job, (read_data, mut projectiles, outcomes): Self::SystemData) { // Attacks - 'projectile_loop: for (entity, pos, physics, vel, mut projectile) in ( + ( &read_data.entities, &read_data.positions, &read_data.physics_states, &read_data.velocities, + &read_data.projectiles, + // TODO: Investigate whether the `maybe` are actually necessary here. + (&read_data.orientations).maybe(), + (&read_data.bodies).maybe(), &mut projectiles, ) - .join() - { + .par_join() + .for_each_init( + || { + prof_span!(guard, "projectile rayon job"); + (read_data.server_bus.emitter(), outcomes.emitter(), guard) + }, + |(server_emitter, outcomes_emitter, _guard), (entity, pos, physics, vel, projectile, ori, body, projectile_write)| { let projectile_owner = projectile .owner .and_then(|uid| read_data.uid_allocator.retrieve_entity_internal(uid.into())); @@ -117,19 +122,21 @@ impl<'a> System<'a> for Sys { continue; } - let projectile = &mut *projectile; + let projectile_write = &mut *projectile_write; let entity_of = |uid: Uid| read_data.uid_allocator.retrieve_entity_internal(uid.into()); - for effect in projectile.hit_entity.drain(..) { + for effect in projectile_write.hit_entity.drain(..) { let owner = projectile.owner.and_then(entity_of); let projectile_info = ProjectileInfo { entity, effect, owner_uid: projectile.owner, owner, - ori: orientations.get(entity), + ori, pos, + vel, + body, }; let target = entity_of(other); @@ -137,7 +144,7 @@ impl<'a> System<'a> for Sys { uid: other, entity: target, target_group, - ori: target.and_then(|target| orientations.get(target)), + ori: target.and_then(|target| read_data.orientations.get(target)), }; dispatch_hit( @@ -145,19 +152,19 @@ impl<'a> System<'a> for Sys { projectile_target_info, &read_data, &mut projectile_vanished, - &mut outcomes_emitter, - &mut server_emitter, + outcomes_emitter, + server_emitter, ); } if projectile_vanished { - continue 'projectile_loop; + return; } } if physics.on_surface().is_some() { - let projectile = &mut *projectile; - for effect in projectile.hit_solid.drain(..) { + let projectile_write = &mut *projectile_write; + for effect in projectile_write.hit_solid.drain(..) { match effect { projectile::Effect::Explode(e) => { // We offset position a little back on the way, @@ -167,8 +174,7 @@ impl<'a> System<'a> for Sys { // TODO: orientation of fallen projectile is // fragile heuristic for direction, find more // robust method. - let projectile_direction = orientations - .get(entity) + let projectile_direction = ori .map_or_else(Vec3::zero, |ori| ori.look_vec()); let offset = -0.2 * projectile_direction; server_emitter.emit(ServerEvent::Explosion { @@ -193,22 +199,18 @@ impl<'a> System<'a> for Sys { } if projectile_vanished { - continue 'projectile_loop; - } - } else if let Some(ori) = orientations.get_mut(entity) { - if let Some(dir) = Dir::from_unnormalized(vel.0) { - *ori = dir.into(); + return; } } - if projectile.time_left == Duration::default() { + if projectile_write.time_left == Duration::default() { server_emitter.emit(ServerEvent::Delete(entity)); } - projectile.time_left = projectile + projectile_write.time_left = projectile_write .time_left .checked_sub(Duration::from_secs_f32(read_data.dt.0)) .unwrap_or_default(); - } + }); } } @@ -218,7 +220,9 @@ struct ProjectileInfo<'a> { owner_uid: Option, owner: Option, ori: Option<&'a Ori>, + body: Option<&'a Body>, pos: &'a Pos, + vel: &'a Vel, } struct ProjectileTargetInfo<'a> { @@ -258,7 +262,6 @@ fn dispatch_hit( }; let owner = projectile_info.owner; - let projectile_entity = projectile_info.entity; let attacker_info = owner @@ -285,14 +288,11 @@ fn dispatch_hit( }; // TODO: Is it possible to have projectile without body?? - if let Some(&body) = read_data.bodies.get(projectile_entity) { + if let Some(&body) = projectile_info.body { outcomes_emitter.emit(Outcome::ProjectileHit { pos: target_pos, body, - vel: read_data - .velocities - .get(projectile_entity) - .map_or(Vec3::zero(), |v| v.0), + vel: projectile_info.vel.0, source: projectile_info.owner_uid, target: read_data.uids.get(target).copied(), }); diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index a9c7ada3d1..dedeab3535 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -8,8 +8,8 @@ use common::{ beam, buff::{BuffCategory, BuffData, BuffKind, BuffSource}, shockwave, Agent, Alignment, Anchor, Body, Health, Inventory, ItemDrop, LightEmitter, - Object, Ori, PidController, Poise, Pos, Projectile, Scale, SkillSet, Stats, Vel, - WaypointArea, + Object, Ori, PidController, Poise, Pos, Projectile, ProjectileOwned, Scale, SkillSet, + Stats, Vel, WaypointArea, }, event::{EventBus, UpdateCharacterMetadata}, lottery::LootSpec, @@ -93,7 +93,7 @@ pub fn handle_create_npc( loot: LootSpec, home_chunk: Option, rtsim_entity: Option, - projectile: Option, + projectile: Option<(ProjectileOwned, Projectile)>, ) { let entity = server .state @@ -125,8 +125,8 @@ pub fn handle_create_npc( entity }; - let entity = if let Some(projectile) = projectile { - entity.with(projectile) + let entity = if let Some((projectile_owned, projectile)) = projectile { + entity.with(projectile_owned).with(projectile) } else { entity }; @@ -207,7 +207,7 @@ pub fn handle_shoot( dir: Dir, body: Body, light: Option, - projectile: Projectile, + projectile: (ProjectileOwned, Projectile), speed: f32, object: Option, ) { diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index b4d5891139..e7663d9eca 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -72,7 +72,7 @@ pub trait StateExt { pos: comp::Pos, vel: comp::Vel, body: comp::Body, - projectile: comp::Projectile, + projectile: (comp::ProjectileOwned, comp::Projectile), ) -> EcsEntityBuilder; /// Build a shockwave entity fn create_shockwave( @@ -343,7 +343,7 @@ impl StateExt for State { pos: comp::Pos, vel: comp::Vel, body: comp::Body, - projectile: comp::Projectile, + (projectile_owned, projectile): (comp::ProjectileOwned, comp::Projectile), ) -> EcsEntityBuilder { let mut projectile_base = self .ecs_mut() @@ -363,7 +363,10 @@ impl StateExt for State { projectile_base = projectile_base.with(body.collider()) } - projectile_base.with(projectile).with(body) + projectile_base + .with(projectile_owned) + .with(projectile) + .with(body) } fn create_shockwave( diff --git a/server/src/sys/object.rs b/server/src/sys/object.rs index 7e3754e20d..a81de4f4df 100644 --- a/server/src/sys/object.rs +++ b/server/src/sys/object.rs @@ -71,7 +71,7 @@ impl<'a> System<'a> for Sys { const ENABLE_RECURSIVE_FIREWORKS: bool = true; if ENABLE_RECURSIVE_FIREWORKS { use common::{ - comp::{object, Body, LightEmitter, Projectile}, + comp::{object, Body, LightEmitter, Projectile, ProjectileOwned}, util::Dir, }; use rand::Rng; @@ -121,15 +121,19 @@ impl<'a> System<'a> for Sys { strength: 2.0, col: Rgb::new(1.0, 1.0, 0.0), }), - projectile: Projectile { - hit_solid: Vec::new(), - hit_entity: Vec::new(), - time_left: Duration::from_secs(60), - owner: *owner, - ignore_group: true, - is_sticky: true, - is_point: true, - }, + projectile: ( + ProjectileOwned { + hit_solid: Vec::new(), + hit_entity: Vec::new(), + time_left: Duration::from_secs(60), + }, + Projectile { + owner: *owner, + ignore_group: true, + is_sticky: true, + is_point: true, + }, + ), speed, object: Some(Object::Firework { owner: *owner,