diff --git a/assets/common/npc_names.ron b/assets/common/npc_names.ron index 49ceb5c023..aeefb77fee 100644 --- a/assets/common/npc_names.ron +++ b/assets/common/npc_names.ron @@ -969,6 +969,15 @@ ), species: () ), + ship: ( + body: ( + keyword: "ship", + names_0: [ + "Boaty McBoatface", + ], + ), + species: (), + ), biped_small: ( body: ( keyword: "biped_small", diff --git a/assets/server/manifests/ship_manifest.ron b/assets/server/manifests/ship_manifest.ron new file mode 100644 index 0000000000..42555fc051 --- /dev/null +++ b/assets/server/manifests/ship_manifest.ron @@ -0,0 +1,16 @@ +({ + DefaultAirship: ( + bone0: ( + offset: (-20.0, -35.0, 1.0), + central: ("object.Human_Airship"), + ), + bone1: ( + offset: (0.0, 0.0, 0.0), + central: ("propeller-l"), + ), + bone2: ( + offset: (0.0, 0.0, 0.0), + central: ("propeller-r"), + ), + ), +}) diff --git a/assets/server/voxel/Human_Airship.vox b/assets/server/voxel/Human_Airship.vox new file mode 100644 index 0000000000..ab1ea75279 --- /dev/null +++ b/assets/server/voxel/Human_Airship.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6355ef90e28c448e11ad79dc6311388a71b4e46628bb2cf82b6e5fd5f38cd254 +size 88100 diff --git a/assets/server/voxel/airship.vox b/assets/server/voxel/airship.vox new file mode 100644 index 0000000000..06bebaa938 --- /dev/null +++ b/assets/server/voxel/airship.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86f317298900ea98f95c6a33192b25fbbcbd3ce5f105cad58ad3c595a7a7d9ee +size 70176 diff --git a/assets/server/voxel/propeller-l.vox b/assets/server/voxel/propeller-l.vox new file mode 100644 index 0000000000..a193fa89ee --- /dev/null +++ b/assets/server/voxel/propeller-l.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09ef4bad2557abcc5a2b938f21053babc7770ebe2333039aef9b98ba930b7ec7 +size 1584 diff --git a/assets/server/voxel/propeller-r.vox b/assets/server/voxel/propeller-r.vox new file mode 100644 index 0000000000..5b940751e6 --- /dev/null +++ b/assets/server/voxel/propeller-r.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4947977524b88bc5adfa934d9061a3499e94b960abb3bcf0a3e2aca482096dc +size 1584 diff --git a/assets/voxygen/voxel/object/Human_Airship.vox b/assets/voxygen/voxel/object/Human_Airship.vox new file mode 100644 index 0000000000..5d32a3168a --- /dev/null +++ b/assets/voxygen/voxel/object/Human_Airship.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:561dcec86218a94ae85268b3f1863cd3310e24c452dd2ba248e3c37b6aff45e5 +size 78024 diff --git a/assets/voxygen/voxel/object/airship.vox b/assets/voxygen/voxel/object/airship.vox new file mode 120000 index 0000000000..3479493953 --- /dev/null +++ b/assets/voxygen/voxel/object/airship.vox @@ -0,0 +1 @@ +../../../server/voxel/airship.vox \ No newline at end of file diff --git a/assets/voxygen/voxel/object/propeller-l.vox b/assets/voxygen/voxel/object/propeller-l.vox new file mode 120000 index 0000000000..a8105d8b1b --- /dev/null +++ b/assets/voxygen/voxel/object/propeller-l.vox @@ -0,0 +1 @@ +../../../server/voxel/propeller-l.vox \ No newline at end of file diff --git a/assets/voxygen/voxel/object/propeller-r.vox b/assets/voxygen/voxel/object/propeller-r.vox new file mode 120000 index 0000000000..647f3f66d0 --- /dev/null +++ b/assets/voxygen/voxel/object/propeller-r.vox @@ -0,0 +1 @@ +../../../server/voxel/propeller-r.vox \ No newline at end of file diff --git a/common/src/cmd.rs b/common/src/cmd.rs index f6771bdcd3..a74f0cab09 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -36,6 +36,7 @@ impl ChatCommandData { #[derive(Copy, Clone)] pub enum ChatCommand { Adminify, + Airship, Alias, Ban, Build, @@ -89,6 +90,7 @@ pub enum ChatCommand { // Thank you for keeping this sorted alphabetically :-) pub static CHAT_COMMANDS: &[ChatCommand] = &[ ChatCommand::Adminify, + ChatCommand::Airship, ChatCommand::Alias, ChatCommand::Ban, ChatCommand::Build, @@ -222,6 +224,7 @@ impl ChatCommand { "Temporarily gives a player admin permissions or removes them", Admin, ), + ChatCommand::Airship => cmd(vec![], "Spawns an airship", Admin), ChatCommand::Alias => cmd(vec![Any("name", Required)], "Change your alias", NoAdmin), ChatCommand::Ban => cmd( vec![Any("username", Required), Message(Optional)], @@ -449,6 +452,7 @@ impl ChatCommand { pub fn keyword(&self) -> &'static str { match self { ChatCommand::Adminify => "adminify", + ChatCommand::Airship => "airship", ChatCommand::Alias => "alias", ChatCommand::Ban => "ban", ChatCommand::Build => "build", diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 7d6c5a57df..9134e40091 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -168,6 +168,7 @@ impl<'a> From<&'a Body> for Psyche { Body::Golem(_) => 1.0, Body::Theropod(_) => 1.0, Body::Dragon(_) => 1.0, + Body::Ship(_) => 1.0, }, } } diff --git a/common/src/comp/body.rs b/common/src/comp/body.rs index bb331f3a54..1733eec1e1 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -11,6 +11,7 @@ pub mod object; pub mod quadruped_low; pub mod quadruped_medium; pub mod quadruped_small; +pub mod ship; pub mod theropod; use crate::{ @@ -44,6 +45,7 @@ make_case_elim!( Golem(body: golem::Body) = 11, Theropod(body: theropod::Body) = 12, QuadrupedLow(body: quadruped_low::Body) = 13, + Ship(body: ship::Body) = 14, } ); @@ -78,6 +80,7 @@ pub struct AllBodies { pub golem: BodyData>, pub theropod: BodyData>, pub quadruped_low: BodyData>, + pub ship: BodyData, } /// Can only retrieve body metadata by direct index. @@ -124,6 +127,7 @@ impl<'a, BodyMeta, SpeciesMeta> core::ops::Index<&'a Body> for AllBodies &self.golem.body, Body::Theropod(_) => &self.theropod.body, Body::QuadrupedLow(_) => &self.quadruped_low.body, + Body::Ship(_) => &self.ship.body, } } } @@ -218,6 +222,7 @@ impl Body { Body::Golem(_) => 2.5, Body::BipedSmall(_) => 0.75, Body::Object(_) => 0.4, + Body::Ship(_) => 1.0, } } @@ -294,6 +299,7 @@ impl Body { object::Body::Crossbow => 1.7, _ => 1.0, }, + Body::Ship(_) => 1.0, } } @@ -416,6 +422,7 @@ impl Body { quadruped_low::Species::Deadwood => 600, _ => 200, }, + Body::Ship(_) => 10000, } } @@ -508,12 +515,13 @@ impl Body { quadruped_low::Species::Deadwood => 30, _ => 20, }, + Body::Ship(_) => 500, } } pub fn immune_to(&self, buff: BuffKind) -> bool { match buff { - BuffKind::Bleeding => matches!(self, Body::Object(_) | Body::Golem(_)), + BuffKind::Bleeding => matches!(self, Body::Object(_) | Body::Golem(_) | Body::Ship(_)), _ => false, } } @@ -521,7 +529,7 @@ impl Body { /// Returns a multiplier representing increased difficulty not accounted for /// due to AI or not using an actual weapon // TODO: Match on species - pub fn combat_multiplier(&self) -> f32 { if let Body::Object(_) = self { 0.0 } else { 1.0 } } + pub fn combat_multiplier(&self) -> f32 { if let Body::Object(_) | Body::Ship(_) = self { 0.0 } else { 1.0 } } pub fn base_poise(&self) -> u32 { match self { diff --git a/common/src/comp/body/ship.rs b/common/src/comp/body/ship.rs new file mode 100644 index 0000000000..ef2e93e113 --- /dev/null +++ b/common/src/comp/body/ship.rs @@ -0,0 +1,69 @@ +use crate::{ + make_case_elim +}; +use serde::{Deserialize, Serialize}; + +make_case_elim!( + body, + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] + #[repr(u32)] + pub enum Body { + DefaultAirship = 0, + } +); + +impl From for super::Body { + fn from(body: Body) -> Self { super::Body::Ship(body) } +} + +impl Body { + pub fn manifest_id(&self) -> &'static str { + match self { + Body::DefaultAirship => "server.manifests.ship_manifest", + } + } +} + +/// Duplicate of some of the things defined in `voxygen::scene::figure::load` to avoid having to +/// refactor all of that to `common` for using voxels as collider geometry +pub mod figuredata { + use crate::{ + assets::{self, AssetExt, AssetHandle, DotVoxAsset, Ron}, + volumes::dyna::Dyna, + }; + use serde::Deserialize; + use hashbrown::HashMap; + + #[derive(Deserialize)] + pub struct VoxSimple(pub String); + + #[derive(Deserialize)] + pub struct ShipCentralSpec(pub HashMap); + + #[derive(Deserialize)] + pub struct SidedShipCentralVoxSpec { + pub bone0: ShipCentralSubSpec, + pub bone1: ShipCentralSubSpec, + pub bone2: ShipCentralSubSpec, + } + + #[derive(Deserialize)] + pub struct ShipCentralSubSpec { + pub offset: [f32; 3], + pub central: VoxSimple, + } + + /// manual instead of through `make_vox_spec!` so that it can be in `common` + #[derive(Clone)] + pub struct ShipSpec { + pub central: AssetHandle>, + } + + impl assets::Compound for ShipSpec { + fn load(_: &assets::AssetCache, _: &str) -> Result { + Ok(ShipSpec { + central: AssetExt::load("server.manifests.ship_manifest")? + }) + } + } +} diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index e493a527c9..d97e265919 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -48,7 +48,7 @@ pub use self::{ beam::{Beam, BeamSegment}, body::{ biped_large, biped_small, bird_medium, bird_small, dragon, fish_medium, fish_small, golem, - humanoid, object, quadruped_low, quadruped_medium, quadruped_small, theropod, AllBodies, + humanoid, object, quadruped_low, quadruped_medium, quadruped_small, theropod, ship, AllBodies, Body, BodyData, }, buff::{ diff --git a/common/src/comp/phys.rs b/common/src/comp/phys.rs index fdb8d10a05..3e06418792 100644 --- a/common/src/comp/phys.rs +++ b/common/src/comp/phys.rs @@ -55,9 +55,12 @@ impl Component for Mass { type Storage = DerefFlaggedStorage>; } -// Mass -#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +// Collider +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum Collider { + // TODO: pass the map from ids -> voxel data to get_radius and get_z_limits to compute a + // bounding cylinder + Voxel { id: String }, Box { radius: f32, z_min: f32, z_max: f32 }, Point, } @@ -65,6 +68,7 @@ pub enum Collider { impl Collider { pub fn get_radius(&self) -> f32 { match self { + Collider::Voxel { .. } => 0.0, Collider::Box { radius, .. } => *radius, Collider::Point => 0.0, } @@ -72,6 +76,7 @@ impl Collider { pub fn get_z_limits(&self, modifier: f32) -> (f32, f32) { match self { + Collider::Voxel { .. } => (0.0, 0.0), Collider::Box { z_min, z_max, .. } => (*z_min * modifier, *z_max * modifier), Collider::Point => (0.0, 0.0), } diff --git a/common/src/states/utils.rs b/common/src/states/utils.rs index bc43bb7643..ea780a1a38 100644 --- a/common/src/states/utils.rs +++ b/common/src/states/utils.rs @@ -5,7 +5,7 @@ use crate::{ item::{Hands, ItemKind, Tool, ToolKind}, quadruped_low, quadruped_medium, quadruped_small, skills::Skill, - theropod, Body, CharacterAbility, CharacterState, InputKind, InventoryAction, StateUpdate, + theropod, ship, Body, CharacterAbility, CharacterState, InputKind, InventoryAction, StateUpdate, }, consts::{FRIC_GROUND, GRAVITY}, event::{LocalEvent, ServerEvent}, @@ -117,6 +117,7 @@ impl Body { quadruped_low::Species::Basilisk => 120.0, quadruped_low::Species::Deadwood => 140.0, }, + Body::Ship(_) => 30.0, } } @@ -168,13 +169,14 @@ impl Body { quadruped_low::Species::Lavadrake => 4.0, _ => 6.0, }, + Body::Ship(_) => 10.0, } } pub fn can_fly(&self) -> bool { matches!( self, - Body::BirdMedium(_) | Body::Dragon(_) | Body::BirdSmall(_) + Body::BirdMedium(_) | Body::Dragon(_) | Body::BirdSmall(_) | Body::Ship(ship::Body::DefaultAirship) ) } diff --git a/common/src/util/find_dist.rs b/common/src/util/find_dist.rs index cb02c728fe..fc921bc758 100644 --- a/common/src/util/find_dist.rs +++ b/common/src/util/find_dist.rs @@ -39,7 +39,7 @@ impl Cylinder { char_state: Option<&crate::comp::CharacterState>, ) -> Self { let scale = scale.map_or(1.0, |s| s.0); - let radius = collider.map_or(0.5, |c| c.get_radius()) * scale; + let radius = collider.as_ref().map_or(0.5, |c| c.get_radius()) * scale; let z_limit_modifier = char_state .filter(|char_state| char_state.is_dodge()) .map_or(1.0, |_| 0.5) diff --git a/common/sys/src/phys.rs b/common/sys/src/phys.rs index cb0ee3cd51..8545888c7e 100644 --- a/common/sys/src/phys.rs +++ b/common/sys/src/phys.rs @@ -8,12 +8,16 @@ use common::{ resources::DeltaTime, terrain::{Block, TerrainGrid}, uid::Uid, - vol::ReadVol, + vol::{BaseVol, ReadVol}, }; use common_base::{prof_span, span}; use common_ecs::{Job, Origin, ParMode, Phase, PhysicsMetrics, System}; +use hashbrown::HashMap; use rayon::iter::ParallelIterator; -use specs::{Entities, Join, ParJoin, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage}; +use specs::{ + shred::{World, ResourceId}, + Entities, Entity, Join, ParJoin, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage, SystemData, +}; use std::ops::Range; use vek::*; @@ -62,112 +66,78 @@ fn calc_z_limit( #[derive(Default)] pub struct Sys; -impl<'a> System<'a> for Sys { - #[allow(clippy::type_complexity)] - type SystemData = ( - Entities<'a>, - ReadStorage<'a, Uid>, - ReadExpect<'a, TerrainGrid>, - Read<'a, DeltaTime>, - WriteExpect<'a, PhysicsMetrics>, - Read<'a, EventBus>, - ReadStorage<'a, Scale>, - ReadStorage<'a, Sticky>, - ReadStorage<'a, Mass>, - ReadStorage<'a, Collider>, - ReadStorage<'a, Gravity>, - WriteStorage<'a, PhysicsState>, - WriteStorage<'a, Pos>, - WriteStorage<'a, Vel>, - WriteStorage<'a, Ori>, - WriteStorage<'a, PreviousPhysCache>, - ReadStorage<'a, Mounting>, - ReadStorage<'a, Projectile>, - ReadStorage<'a, BeamSegment>, - ReadStorage<'a, Shockwave>, - ReadStorage<'a, CharacterState>, - ); +#[derive(SystemData)] +pub struct PhysicsSystemDataRead<'a> { + entities: Entities<'a>, + uids: ReadStorage<'a, Uid>, + terrain: ReadExpect<'a, TerrainGrid>, + dt: Read<'a, DeltaTime>, + event_bus: Read<'a, EventBus>, + scales: ReadStorage<'a, Scale>, + stickies: ReadStorage<'a, Sticky>, + masses: ReadStorage<'a, Mass>, + colliders: ReadStorage<'a, Collider>, + gravities: ReadStorage<'a, Gravity>, + mountings: ReadStorage<'a, Mounting>, + projectiles: ReadStorage<'a, Projectile>, + beams: ReadStorage<'a, BeamSegment>, + shockwaves: ReadStorage<'a, Shockwave>, + char_states: ReadStorage<'a, CharacterState>, +} - const NAME: &'static str = "phys"; - const ORIGIN: Origin = Origin::Common; - const PHASE: Phase = Phase::Create; +#[derive(SystemData)] +pub struct PhysicsSystemDataWrite<'a> { + physics_metrics: WriteExpect<'a, PhysicsMetrics>, + physics_states: WriteStorage<'a, PhysicsState>, + positions: WriteStorage<'a, Pos>, + velocities: WriteStorage<'a, Vel>, + orientations: WriteStorage<'a, Ori>, + previous_phys_cache: WriteStorage<'a, PreviousPhysCache>, +} - #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 - #[allow(clippy::blocks_in_if_conditions)] // TODO: Pending review in #587 - fn run( - job: &mut Job, - ( - entities, - uids, - terrain, - dt, - mut physics_metrics, - event_bus, - scales, - stickies, - masses, - colliders, - gravities, - mut physics_states, - mut positions, - mut velocities, - mut orientations, - mut previous_phys_cache, - mountings, - projectiles, - beams, - shockwaves, - char_states, - ): Self::SystemData, - ) { - let mut event_emitter = event_bus.emitter(); +#[derive(SystemData)] +pub struct PhysicsSystemData<'a> { + r: PhysicsSystemDataRead<'a>, + w: PhysicsSystemDataWrite<'a>, +} - // Add/reset physics state components +impl<'a> PhysicsSystemData<'a> { + /// Add/reset physics state components + fn reset(&mut self) { span!(guard, "Add/reset physics state components"); for (entity, _, _, _, _) in ( - &entities, - &colliders, - &positions, - &velocities, - &orientations, + &self.r.entities, + &self.r.colliders, + &self.w.positions, + &self.w.velocities, + &self.w.orientations, ) .join() { - let _ = physics_states + let _ = self.w.physics_states .entry(entity) .map(|e| e.or_insert_with(Default::default)); } drop(guard); + } - // 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. + fn maintain_pushback_cache(&mut self) { span!(guard, "Maintain pushback cache"); //Add PreviousPhysCache for all relevant entities for entity in ( - &entities, - &velocities, - &positions, - !&previous_phys_cache, - !&mountings, - !&beams, - !&shockwaves, + &self.r.entities, + &self.w.velocities, + &self.w.positions, + !&self.w.previous_phys_cache, + !&self.r.mountings, + !&self.r.beams, + !&self.r.shockwaves, ) .join() .map(|(e, _, _, _, _, _, _)| e) .collect::>() { - let _ = previous_phys_cache.insert(entity, PreviousPhysCache { + let _ = self.w.previous_phys_cache.insert(entity, PreviousPhysCache { velocity_dt: Vec3::zero(), center: Vec3::zero(), collision_boundary: 0.0, @@ -178,16 +148,16 @@ impl<'a> System<'a> for Sys { //Update PreviousPhysCache for (_, vel, position, mut phys_cache, collider, scale, cs, _, _, _) in ( - &entities, - &velocities, - &positions, - &mut previous_phys_cache, - colliders.maybe(), - scales.maybe(), - char_states.maybe(), - !&mountings, - !&beams, - !&shockwaves, + &self.r.entities, + &self.w.velocities, + &self.w.positions, + &mut self.w.previous_phys_cache, + self.r.colliders.maybe(), + self.r.scales.maybe(), + self.r.char_states.maybe(), + !&self.r.mountings, + !&self.r.beams, + !&self.r.shockwaves, ) .join() { @@ -196,7 +166,7 @@ impl<'a> System<'a> for Sys { let z_limits = (z_limits.0 * scale, z_limits.1 * scale); let half_height = (z_limits.1 - z_limits.0) / 2.0; - phys_cache.velocity_dt = vel.0 * dt.0; + phys_cache.velocity_dt = vel.0 * self.r.dt.0; let entity_center = position.0 + Vec3::new(0.0, z_limits.0 + half_height, 0.0); let flat_radius = collider.map(|c| c.get_radius()).unwrap_or(0.5) * scale; let radius = (flat_radius.powi(2) + half_height.powi(2)).sqrt(); @@ -209,23 +179,26 @@ impl<'a> System<'a> for Sys { phys_cache.scaled_radius = flat_radius; } drop(guard); - + } + fn apply_pushback(&mut self, job: &mut Job) { span!(guard, "Apply pushback"); job.cpu_stats.measure(ParMode::Rayon); + let PhysicsSystemData { r: ref psdr, w: ref mut psdw } = self; + let (positions, previous_phys_cache) = (&psdw.positions, &psdw.previous_phys_cache); let metrics = ( - &entities, - &positions, - &mut velocities, - &previous_phys_cache, - masses.maybe(), - colliders.maybe(), - !&mountings, - stickies.maybe(), - &mut physics_states, + &psdr.entities, + positions, + &mut psdw.velocities, + previous_phys_cache, + psdr.masses.maybe(), + psdr.colliders.maybe(), + !&psdr.mountings, + psdr.stickies.maybe(), + &mut psdw.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 - projectiles.maybe(), - char_states.maybe(), + psdr.projectiles.maybe(), + psdr.char_states.maybe(), ) .par_join() .filter(|(_, _, _, _, _, _, _, sticky, physics, _, _)| { @@ -275,17 +248,17 @@ impl<'a> System<'a> for Sys { _, char_state_other_maybe, ) in ( - &entities, - &uids, - &positions, - &previous_phys_cache, - masses.maybe(), - colliders.maybe(), - !&projectiles, - !&mountings, - !&beams, - !&shockwaves, - char_states.maybe(), + &psdr.entities, + &psdr.uids, + positions, + previous_phys_cache, + psdr.masses.maybe(), + psdr.colliders.maybe(), + !&psdr.projectiles, + !&psdr.mountings, + !&psdr.beams, + !&psdr.shockwaves, + psdr.char_states.maybe(), ) .join() { @@ -358,7 +331,7 @@ impl<'a> System<'a> for Sys { } // Change velocity - vel.0 += vel_delta * dt.0; + vel.0 += vel_delta * psdr.dt.0; // Metrics PhysicsMetrics { @@ -373,435 +346,622 @@ impl<'a> System<'a> for Sys { entity_entity_collisions: old.entity_entity_collisions + new.entity_entity_collisions, }); - physics_metrics.entity_entity_collision_checks = metrics.entity_entity_collision_checks; - physics_metrics.entity_entity_collisions = metrics.entity_entity_collisions; + psdw.physics_metrics.entity_entity_collision_checks = metrics.entity_entity_collision_checks; + psdw.physics_metrics.entity_entity_collisions = metrics.entity_entity_collisions; drop(guard); + } + fn handle_movement_and_terrain(&mut self, job: &mut Job) { + let PhysicsSystemData { r: ref psdr, w: ref mut psdw } = self; // Apply movement inputs span!(guard, "Apply movement and terrain collision"); - let land_on_grounds = ( - &entities, - scales.maybe(), - stickies.maybe(), - &colliders, - &mut positions, - &mut velocities, - &mut orientations, - &mut physics_states, - !&mountings, - ) - .par_join() - .map_init( - || { - prof_span!(guard, "physics e<>t rayon job"); - guard - }, - |_guard, - (entity, _scale, sticky, collider, mut pos, mut vel, _ori, mut physics_state, _), - | { - let mut landed_on_ground = None; - - if sticky.is_some() && physics_state.on_surface().is_some() { - vel.0 = Vec3::zero(); - return landed_on_ground; - } - - // TODO: Use this - //let scale = scale.map(|s| s.0).unwrap_or(1.0); - - let old_vel = *vel; - // Integrate forces - // Friction is assumed to be a constant dependent on location - let friction = FRIC_AIR - .max(if physics_state.on_ground { - FRIC_GROUND - } else { - 0.0 - }) - .max(if physics_state.in_liquid.is_some() { - FRIC_FLUID - } else { - 0.0 - }); - let in_loaded_chunk = terrain - .get_key(terrain.pos_key(pos.0.map(|e| e.floor() as i32))) - .is_some(); - let downward_force = if !in_loaded_chunk { - 0.0 // No gravity in unloaded chunks - } else if physics_state - .in_liquid - .map(|depth| depth > 0.75) - .unwrap_or(false) - { - (1.0 - BOUYANCY) * GRAVITY - } else { - GRAVITY - } * gravities.get(entity).map(|g| g.0).unwrap_or_default(); - vel.0 = integrate_forces(dt.0, vel.0, downward_force, friction); - - // Don't move if we're not in a loaded chunk - let mut pos_delta = if in_loaded_chunk { - // this is an approximation that allows most framerates to - // behave in a similar manner. - let dt_lerp = 0.2; - (vel.0 * dt_lerp + old_vel.0 * (1.0 - dt_lerp)) * dt.0 - } else { - Vec3::zero() - }; - - match *collider { - Collider::Box { - radius, - z_min, - z_max, - } => { - // Scale collider - // TODO: Use scale & actual proportions when pathfinding is good enough to manage irregular entity - // sizes - let radius = radius.min(0.45); // * scale; - let z_min = z_min; // * scale; - let z_max = z_max.clamped(1.2, 1.95); // * scale; - - // Probe distances - let hdist = radius.ceil() as i32; - // Neighbouring blocks iterator - let near_iter = (-hdist..hdist + 1) - .map(move |i| { - (-hdist..hdist + 1).map(move |j| { - (1 - Block::MAX_HEIGHT.ceil() as i32 + z_min.floor() as i32 - ..z_max.ceil() as i32 + 1) - .map(move |k| (i, j, k)) - }) - }) - .flatten() - .flatten(); - - // Function for iterating over the blocks the player at a specific position - // collides with - fn collision_iter<'a>( - pos: Vec3, - terrain: &'a TerrainGrid, - hit: &'a impl Fn(&Block) -> bool, - height: &'a impl Fn(&Block) -> f32, - near_iter: impl Iterator + 'a, - radius: f32, - z_range: Range, - ) -> impl Iterator> + 'a { - near_iter.filter_map(move |(i, j, k)| { - let block_pos = pos.map(|e| e.floor() as i32) + Vec3::new(i, j, k); - - if let Some(block) = terrain.get(block_pos).ok().copied().filter(hit) { - let player_aabb = Aabb { - min: pos + Vec3::new(-radius, -radius, z_range.start), - max: pos + Vec3::new(radius, radius, z_range.end), - }; - let block_aabb = Aabb { - min: block_pos.map(|e| e as f32), - max: block_pos.map(|e| e as f32) - + Vec3::new(1.0, 1.0, height(&block)), - }; - - if player_aabb.collides_with_aabb(block_aabb) { - return Some(block_aabb); - } - } - - None - }) - } - - let z_range = z_min..z_max; - // Function for determining whether the player at a specific position collides - // with blocks with the given criteria - fn collision_with<'a>( - pos: Vec3, - terrain: &'a TerrainGrid, - hit: impl Fn(&Block) -> bool, - near_iter: impl Iterator + 'a, - radius: f32, - z_range: Range, - ) -> bool { - collision_iter(pos, terrain, &|block| block.is_solid() && hit(block), &Block::solid_height, near_iter, radius, z_range).count() - > 0 - } - - let was_on_ground = physics_state.on_ground; - physics_state.on_ground = false; - - let mut on_ground = false; - let mut on_ceiling = false; - let mut attempts = 0; // Don't loop infinitely here - - // Don't jump too far at once - let increments = (pos_delta.map(|e| e.abs()).reduce_partial_max() / 0.3) - .ceil() - .max(1.0); - let old_pos = pos.0; - fn block_true(_: &Block) -> bool { true } - for _ in 0..increments as usize { - pos.0 += pos_delta / increments; - - const MAX_ATTEMPTS: usize = 16; - - // While the player is colliding with the terrain... - while collision_with(pos.0, &terrain, block_true, near_iter.clone(), radius, z_range.clone()) - && attempts < MAX_ATTEMPTS - { - // Calculate the player's AABB - let player_aabb = Aabb { - min: pos.0 + Vec3::new(-radius, -radius, z_min), - max: pos.0 + Vec3::new(radius, radius, z_max), - }; - - // Determine the block that we are colliding with most (based on minimum - // collision axis) - let (_block_pos, block_aabb, block_height) = near_iter - .clone() - // Calculate the block's position in world space - .map(|(i, j, k)| pos.0.map(|e| e.floor() as i32) + Vec3::new(i, j, k)) - // Make sure the block is actually solid - .filter_map(|block_pos| { - if let Some(block) = terrain - .get(block_pos) - .ok() - .filter(|block| block.is_solid()) - { - // Calculate block AABB - Some(( - block_pos, - Aabb { - min: block_pos.map(|e| e as f32), - max: block_pos.map(|e| e as f32) + Vec3::new(1.0, 1.0, block.solid_height()), - }, - block.solid_height(), - )) - } else { - None - } - }) - // Determine whether the block's AABB collides with the player's AABB - .filter(|(_, block_aabb, _)| block_aabb.collides_with_aabb(player_aabb)) - // Find the maximum of the minimum collision axes (this bit is weird, trust me that it works) - .min_by_key(|(_, block_aabb, _)| { - ((block_aabb.center() - player_aabb.center() - Vec3::unit_z() * 0.5) - .map(|e| e.abs()) - .sum() - * 1_000_000.0) as i32 - }) - .expect("Collision detected, but no colliding blocks found!"); - - // Find the intrusion vector of the collision - let dir = player_aabb.collision_vector_with_aabb(block_aabb); - - // Determine an appropriate resolution vector (i.e: the minimum distance - // needed to push out of the block) - let max_axis = dir.map(|e| e.abs()).reduce_partial_min(); - let resolve_dir = -dir.map(|e| { - if e.abs().to_bits() == max_axis.to_bits() { - e - } else { - 0.0 - } - }); - - // When the resolution direction is pointing upwards, we must be on the - // ground - if resolve_dir.z > 0.0 && vel.0.z <= 0.0 { - on_ground = true; - - if !was_on_ground { - landed_on_ground = Some((entity, *vel)); - } - } else if resolve_dir.z < 0.0 && vel.0.z >= 0.0 { - on_ceiling = true; - } - - // When the resolution direction is non-vertical, we must be colliding - // with a wall If the space above is free... - if !collision_with(Vec3::new(pos.0.x, pos.0.y, (pos.0.z + 0.1).ceil()), &terrain, block_true, near_iter.clone(), radius, z_range.clone()) - // ...and we're being pushed out horizontally... - && resolve_dir.z == 0.0 - // ...and the vertical resolution direction is sufficiently great... - && -dir.z > 0.1 - // ...and we're falling/standing OR there is a block *directly* beneath our current origin (note: not hitbox)... - && (vel.0.z <= 0.0 || terrain - .get((pos.0 - Vec3::unit_z() * 0.1).map(|e| e.floor() as i32)) - .map(|block| block.is_solid()) - .unwrap_or(false)) - // ...and there is a collision with a block beneath our current hitbox... - && collision_with( - pos.0 + resolve_dir - Vec3::unit_z() * 1.05, - &terrain, - block_true, - near_iter.clone(), - radius, - z_range.clone(), - ) - { - // ...block-hop! - pos.0.z = (pos.0.z + 0.1).floor() + block_height; - vel.0.z = 0.0; - on_ground = true; - break; - } else { - // Correct the velocity - vel.0 = vel.0.map2(resolve_dir, |e, d| { - if d * e.signum() < 0.0 { 0.0 } else { e } - }); - pos_delta *= resolve_dir.map(|e| if e != 0.0 { 0.0 } else { 1.0 }); - } - - // Resolve the collision normally - pos.0 += resolve_dir; - - attempts += 1; - } - - if attempts == MAX_ATTEMPTS { + let (positions, previous_phys_cache) = (&psdw.positions, &psdw.previous_phys_cache); + let (pos_writes, land_on_grounds) = + ( + &psdr.entities, + psdr.scales.maybe(), + psdr.stickies.maybe(), + &psdr.colliders, + positions, + &mut psdw.velocities, + &psdw.orientations, + &mut psdw.physics_states, + previous_phys_cache, + !&psdr.mountings, + ) + .par_join() + .fold( + || (Vec::new(), Vec::new()), + |(mut pos_writes, mut land_on_grounds), + ( + entity, + scale, + sticky, + collider, + pos, + mut vel, + _ori, + mut physics_state, + previous_cache, + _, + )| { + // defer the writes of positions to allow an inner loop over terrain-like + // entities + let old_pos = *pos; + let mut pos = *pos; + if sticky.is_some() && physics_state.on_surface().is_some() { vel.0 = Vec3::zero(); - pos.0 = old_pos; - break; + return (pos_writes, land_on_grounds); } - } - if on_ceiling { - physics_state.on_ceiling = true; - } - - if on_ground { - physics_state.on_ground = true; - // If the space below us is free, then "snap" to the ground - } else if collision_with( - pos.0 - Vec3::unit_z() * 1.05, - &terrain, - block_true, - near_iter.clone(), - radius, - z_range.clone(), - ) && vel.0.z < 0.0 - && vel.0.z > -1.5 - && was_on_ground - && !collision_with( - pos.0 - Vec3::unit_z() * 0.05, - &terrain, - |block| block.solid_height() >= (pos.0.z - 0.05).rem_euclid(1.0), - near_iter.clone(), - radius, - z_range.clone(), - ) - { - let snap_height = terrain - .get( - Vec3::new(pos.0.x, pos.0.y, pos.0.z - 0.05) - .map(|e| e.floor() as i32), - ) - .ok() - .filter(|block| block.is_solid()) - .map(|block| block.solid_height()) - .unwrap_or(0.0); - pos.0.z = (pos.0.z - 0.05).floor() + snap_height; - physics_state.on_ground = true; - } - - let dirs = [ - Vec3::unit_x(), - Vec3::unit_y(), - -Vec3::unit_x(), - -Vec3::unit_y(), - ]; - - if let (wall_dir, true) = - dirs.iter().fold((Vec3::zero(), false), |(a, hit), dir| { - if collision_with( - pos.0 + *dir * 0.01, - &terrain, - block_true, - near_iter.clone(), - radius, - z_range.clone(), - ) { - (a + dir, true) - } else { - (a, hit) - } - }) - { - physics_state.on_wall = Some(wall_dir); - } else { - physics_state.on_wall = None; - } - - // Figure out if we're in water - physics_state.in_liquid = collision_iter( - pos.0, - &terrain, - &|block| block.is_liquid(), - // The liquid part of a liquid block always extends 1 block high. - &|_block| 1.0, - near_iter.clone(), - radius, - z_min..z_max, - ) - .max_by_key(|block_aabb| (block_aabb.max.z * 100.0) as i32) - .map(|block_aabb| block_aabb.max.z - pos.0.z); - }, - Collider::Point => { - let (dist, block) = terrain.ray(pos.0, pos.0 + pos_delta) - .until(|block: &Block| block.is_filled()) - .ignore_error().cast(); - - pos.0 += pos_delta.try_normalized().unwrap_or(Vec3::zero()) * dist; - - // Can't fail since we do ignore_error above - if block.unwrap().is_some() { - let block_center = pos.0.map(|e| e.floor()) + 0.5; - let block_rpos = (pos.0 - block_center) - .try_normalized() - .unwrap_or(Vec3::zero()); - - // See whether we're on the top/bottom of a block, or the side - if block_rpos.z.abs() - > block_rpos.xy().map(|e| e.abs()).reduce_partial_max() - { - if block_rpos.z > 0.0 { - physics_state.on_ground = true; - } else { - physics_state.on_ceiling = true; - } - vel.0.z = 0.0; + let scale = if let Collider::Voxel { .. } = collider { + scale.map(|s| s.0).unwrap_or(1.0) } else { - physics_state.on_wall = - Some(if block_rpos.x.abs() > block_rpos.y.abs() { - vel.0.x = 0.0; - Vec3::unit_x() * -block_rpos.x.signum() - } else { - vel.0.y = 0.0; - Vec3::unit_y() * -block_rpos.y.signum() - }); + // TODO: Use scale & actual proportions when pathfinding is good + // enough to manage irregular entity sizes + 1.0 + }; + + let old_vel = *vel; + // Integrate forces + // Friction is assumed to be a constant dependent on location + let friction = FRIC_AIR + .max(if physics_state.on_ground { + FRIC_GROUND + } else { + 0.0 + }) + .max(if physics_state.in_liquid.is_some() { + FRIC_FLUID + } else { + 0.0 + }); + let in_loaded_chunk = psdr.terrain + .get_key(psdr.terrain.pos_key(pos.0.map(|e| e.floor() as i32))) + .is_some(); + let downward_force = + if !in_loaded_chunk { + 0.0 // No gravity in unloaded chunks + } else if physics_state + .in_liquid + .map(|depth| depth > 0.75) + .unwrap_or(false) + { + (1.0 - BOUYANCY) * GRAVITY + } else { + GRAVITY + } * psdr.gravities.get(entity).map(|g| g.0).unwrap_or_default(); + vel.0 = integrate_forces(psdr.dt.0, vel.0, downward_force, friction); + + // Don't move if we're not in a loaded chunk + let pos_delta = if in_loaded_chunk { + // this is an approximation that allows most framerates to + // behave in a similar manner. + let dt_lerp = 0.2; + (vel.0 * dt_lerp + old_vel.0 * (1.0 - dt_lerp)) * psdr.dt.0 + } else { + Vec3::zero() + }; + + match &*collider { + Collider::Voxel { .. } => { + // for now, treat entities with voxel colliders as their bounding + // cylinders for the purposes of colliding them with terrain + let radius = collider.get_radius() * scale; + let (z_min, z_max) = collider.get_z_limits(scale); + + let cylinder = (radius, z_min, z_max); + cylinder_voxel_collision( + cylinder, + &*psdr.terrain, + entity, + &mut pos, + pos_delta, + vel, + &mut physics_state, + &mut land_on_grounds, + ); + }, + Collider::Box { + radius, + z_min, + z_max, + } => { + // Scale collider + let radius = radius.min(0.45) * scale; + let z_min = *z_min * scale; + let z_max = z_max.clamped(1.2, 1.95) * scale; + + let cylinder = (radius, z_min, z_max); + cylinder_voxel_collision( + cylinder, + &*psdr.terrain, + entity, + &mut pos, + pos_delta, + vel, + &mut physics_state, + &mut land_on_grounds, + ); + }, + Collider::Point => { + let (dist, block) = psdr.terrain + .ray(pos.0, pos.0 + pos_delta) + .until(|block: &Block| block.is_filled()) + .ignore_error() + .cast(); + + pos.0 += pos_delta.try_normalized().unwrap_or(Vec3::zero()) * dist; + + // Can't fail since we do ignore_error above + if block.unwrap().is_some() { + let block_center = pos.0.map(|e| e.floor()) + 0.5; + let block_rpos = (pos.0 - block_center) + .try_normalized() + .unwrap_or(Vec3::zero()); + + // See whether we're on the top/bottom of a block, or the side + if block_rpos.z.abs() + > block_rpos.xy().map(|e| e.abs()).reduce_partial_max() + { + if block_rpos.z > 0.0 { + physics_state.on_ground = true; + } else { + physics_state.on_ceiling = true; + } + vel.0.z = 0.0; + } else { + physics_state.on_wall = + Some(if block_rpos.x.abs() > block_rpos.y.abs() { + vel.0.x = 0.0; + Vec3::unit_x() * -block_rpos.x.signum() + } else { + vel.0.y = 0.0; + Vec3::unit_y() * -block_rpos.y.signum() + }); + } + } + + physics_state.in_liquid = psdr.terrain + .get(pos.0.map(|e| e.floor() as i32)) + .ok() + .and_then(|vox| vox.is_liquid().then_some(1.0)); + }, } - } - physics_state.in_liquid = terrain.get(pos.0.map(|e| e.floor() as i32)) - .ok() - .and_then(|vox| vox.is_liquid().then_some(1.0)); - }, - } + // Collide with terrain-like entities + for ( + entity_other, + other, + pos_other, + previous_cache_other, + mass_other, + collider_other, + _, + _, + _, + _, + char_state_other_maybe, + ) in ( + &psdr.entities, + &psdr.uids, + positions, + previous_phys_cache, + psdr.masses.maybe(), + &psdr.colliders, + !&psdr.projectiles, + !&psdr.mountings, + !&psdr.beams, + !&psdr.shockwaves, + psdr.char_states.maybe(), + ) + .join() + { + let collision_boundary = previous_cache.collision_boundary + + previous_cache_other.collision_boundary; + if previous_cache + .center + .distance_squared(previous_cache_other.center) + > collision_boundary.powi(2) + || entity == entity_other + { + continue; + } - landed_on_ground - }).fold(Vec::new, |mut lands_on_grounds, landed_on_ground| { - if let Some(land_on_ground) = landed_on_ground { - lands_on_grounds.push(land_on_ground); - } - lands_on_grounds - }).reduce(Vec::new, |mut land_on_grounds_a, mut land_on_grounds_b| { - land_on_grounds_a.append(&mut land_on_grounds_b); - land_on_grounds_a - }); + if let Collider::Voxel { id } = collider_other { + // use bounding cylinder regardless of our collider + // TODO: extract point-terrain collision above to its own function + let radius = collider.get_radius() * scale; + let (z_min, z_max) = collider.get_z_limits(scale); + + let cylinder = (radius, z_min, z_max); + // TODO: load .vox into a Dyna, and use it (appropriately rotated) + // as the terrain + /*cylinder_voxel_collision( + cylinder, + &*psdr.terrain, + entity, + &mut pos, + pos_delta, + vel, + &mut physics_state, + &mut land_on_grounds, + );*/ + } + } + if pos != old_pos { + pos_writes.push((entity, pos)); + } + + (pos_writes, land_on_grounds) + }, + ) + .reduce( + || (Vec::new(), Vec::new()), + |(mut pos_writes_a, mut land_on_grounds_a), + (mut pos_writes_b, mut land_on_grounds_b)| { + pos_writes_a.append(&mut pos_writes_b); + land_on_grounds_a.append(&mut land_on_grounds_b); + (pos_writes_a, land_on_grounds_a) + }, + ); drop(guard); job.cpu_stats.measure(ParMode::Single); + let pos_writes: HashMap = pos_writes.into_iter().collect(); + for (entity, pos) in (&psdr.entities, &mut psdw.positions).join() { + if let Some(new_pos) = pos_writes.get(&entity) { + *pos = *new_pos; + } + } + + let mut event_emitter = psdr.event_bus.emitter(); land_on_grounds.into_iter().for_each(|(entity, vel)| { event_emitter.emit(ServerEvent::LandOnGround { entity, vel: vel.0 }); }); } } + +impl<'a> System<'a> for Sys { + type SystemData = PhysicsSystemData<'a>; + + const NAME: &'static str = "phys"; + const ORIGIN: Origin = Origin::Common; + const PHASE: Phase = Phase::Create; + + #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 + #[allow(clippy::blocks_in_if_conditions)] // TODO: Pending review in #587 + fn run( + job: &mut Job, + mut psd: Self::SystemData, + ) { + psd.reset(); + + // 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. + psd.maintain_pushback_cache(); + psd.apply_pushback(job); + + + psd.handle_movement_and_terrain(job); + } +} + +fn cylinder_voxel_collision<'a, T: BaseVol + ReadVol>( + cylinder: (f32, f32, f32), + terrain: &'a T, + entity: Entity, + pos: &mut Pos, + mut pos_delta: Vec3, + vel: &mut Vel, + physics_state: &mut PhysicsState, + land_on_grounds: &mut Vec<(Entity, Vel)>, +) { + let (radius, z_min, z_max) = cylinder; + + // Probe distances + let hdist = radius.ceil() as i32; + // Neighbouring blocks iterator + let near_iter = (-hdist..hdist + 1) + .map(move |i| { + (-hdist..hdist + 1).map(move |j| { + (1 - Block::MAX_HEIGHT.ceil() as i32 + z_min.floor() as i32 + ..z_max.ceil() as i32 + 1) + .map(move |k| (i, j, k)) + }) + }) + .flatten() + .flatten(); + + // Function for iterating over the blocks the player at a specific position + // collides with + fn collision_iter<'a, T: BaseVol + ReadVol>( + pos: Vec3, + terrain: &'a T, + hit: &'a impl Fn(&Block) -> bool, + height: &'a impl Fn(&Block) -> f32, + near_iter: impl Iterator + 'a, + radius: f32, + z_range: Range, + ) -> impl Iterator> + 'a { + near_iter.filter_map(move |(i, j, k)| { + let block_pos = pos.map(|e| e.floor() as i32) + Vec3::new(i, j, k); + + if let Some(block) = terrain.get(block_pos).ok().copied().filter(hit) { + let player_aabb = Aabb { + min: pos + Vec3::new(-radius, -radius, z_range.start), + max: pos + Vec3::new(radius, radius, z_range.end), + }; + let block_aabb = Aabb { + min: block_pos.map(|e| e as f32), + max: block_pos.map(|e| e as f32) + Vec3::new(1.0, 1.0, height(&block)), + }; + + if player_aabb.collides_with_aabb(block_aabb) { + return Some(block_aabb); + } + } + + None + }) + } + + let z_range = z_min..z_max; + // Function for determining whether the player at a specific position collides + // with blocks with the given criteria + fn collision_with<'a, T: BaseVol + ReadVol>( + pos: Vec3, + terrain: &'a T, + hit: impl Fn(&Block) -> bool, + near_iter: impl Iterator + 'a, + radius: f32, + z_range: Range, + ) -> bool { + collision_iter( + pos, + terrain, + &|block| block.is_solid() && hit(block), + &Block::solid_height, + near_iter, + radius, + z_range, + ) + .count() + > 0 + } + + let was_on_ground = physics_state.on_ground; + physics_state.on_ground = false; + + let mut on_ground = false; + let mut on_ceiling = false; + let mut attempts = 0; // Don't loop infinitely here + + // Don't jump too far at once + let increments = (pos_delta.map(|e| e.abs()).reduce_partial_max() / 0.3) + .ceil() + .max(1.0); + let old_pos = pos.0; + fn block_true(_: &Block) -> bool { true } + for _ in 0..increments as usize { + pos.0 += pos_delta / increments; + + const MAX_ATTEMPTS: usize = 16; + + // While the player is colliding with the terrain... + while collision_with( + pos.0, + &terrain, + block_true, + near_iter.clone(), + radius, + z_range.clone(), + ) && attempts < MAX_ATTEMPTS + { + // Calculate the player's AABB + let player_aabb = Aabb { + min: pos.0 + Vec3::new(-radius, -radius, z_min), + max: pos.0 + Vec3::new(radius, radius, z_max), + }; + + // Determine the block that we are colliding with most (based on minimum + // collision axis) + let (_block_pos, block_aabb, block_height) = near_iter + .clone() + // Calculate the block's position in world space + .map(|(i, j, k)| pos.0.map(|e| e.floor() as i32) + Vec3::new(i, j, k)) + // Make sure the block is actually solid + .filter_map(|block_pos| { + if let Some(block) = terrain + .get(block_pos) + .ok() + .filter(|block| block.is_solid()) + { + // Calculate block AABB + Some(( + block_pos, + Aabb { + min: block_pos.map(|e| e as f32), + max: block_pos.map(|e| e as f32) + Vec3::new(1.0, 1.0, block.solid_height()), + }, + block.solid_height(), + )) + } else { + None + } + }) + // Determine whether the block's AABB collides with the player's AABB + .filter(|(_, block_aabb, _)| block_aabb.collides_with_aabb(player_aabb)) + // Find the maximum of the minimum collision axes (this bit is weird, trust me that it works) + .min_by_key(|(_, block_aabb, _)| { + ((block_aabb.center() - player_aabb.center() - Vec3::unit_z() * 0.5) + .map(|e| e.abs()) + .sum() + * 1_000_000.0) as i32 + }) + .expect("Collision detected, but no colliding blocks found!"); + + // Find the intrusion vector of the collision + let dir = player_aabb.collision_vector_with_aabb(block_aabb); + + // Determine an appropriate resolution vector (i.e: the minimum distance + // needed to push out of the block) + let max_axis = dir.map(|e| e.abs()).reduce_partial_min(); + let resolve_dir = -dir.map(|e| { + if e.abs().to_bits() == max_axis.to_bits() { + e + } else { + 0.0 + } + }); + + // When the resolution direction is pointing upwards, we must be on the + // ground + if resolve_dir.z > 0.0 && vel.0.z <= 0.0 { + on_ground = true; + + if !was_on_ground { + land_on_grounds.push((entity, *vel)); + } + } else if resolve_dir.z < 0.0 && vel.0.z >= 0.0 { + on_ceiling = true; + } + + // When the resolution direction is non-vertical, we must be colliding + // with a wall If the space above is free... + if !collision_with(Vec3::new(pos.0.x, pos.0.y, (pos.0.z + 0.1).ceil()), &terrain, block_true, near_iter.clone(), radius, z_range.clone()) + // ...and we're being pushed out horizontally... + && resolve_dir.z == 0.0 + // ...and the vertical resolution direction is sufficiently great... + && -dir.z > 0.1 + // ...and we're falling/standing OR there is a block *directly* beneath our current origin (note: not hitbox)... + && (vel.0.z <= 0.0 || terrain + .get((pos.0 - Vec3::unit_z() * 0.1).map(|e| e.floor() as i32)) + .map(|block| block.is_solid()) + .unwrap_or(false)) + // ...and there is a collision with a block beneath our current hitbox... + && collision_with( + pos.0 + resolve_dir - Vec3::unit_z() * 1.05, + &terrain, + block_true, + near_iter.clone(), + radius, + z_range.clone(), + ) + { + // ...block-hop! + pos.0.z = (pos.0.z + 0.1).floor() + block_height; + vel.0.z = 0.0; + on_ground = true; + break; + } else { + // Correct the velocity + vel.0 = vel.0.map2( + resolve_dir, + |e, d| { + if d * e.signum() < 0.0 { 0.0 } else { e } + }, + ); + pos_delta *= resolve_dir.map(|e| if e != 0.0 { 0.0 } else { 1.0 }); + } + + // Resolve the collision normally + pos.0 += resolve_dir; + + attempts += 1; + } + + if attempts == MAX_ATTEMPTS { + vel.0 = Vec3::zero(); + pos.0 = old_pos; + break; + } + } + + if on_ceiling { + physics_state.on_ceiling = true; + } + + if on_ground { + physics_state.on_ground = true; + // If the space below us is free, then "snap" to the ground + } else if collision_with( + pos.0 - Vec3::unit_z() * 1.05, + &terrain, + block_true, + near_iter.clone(), + radius, + z_range.clone(), + ) && vel.0.z < 0.0 + && vel.0.z > -1.5 + && was_on_ground + && !collision_with( + pos.0 - Vec3::unit_z() * 0.05, + &terrain, + |block| block.solid_height() >= (pos.0.z - 0.05).rem_euclid(1.0), + near_iter.clone(), + radius, + z_range.clone(), + ) + { + let snap_height = terrain + .get(Vec3::new(pos.0.x, pos.0.y, pos.0.z - 0.05).map(|e| e.floor() as i32)) + .ok() + .filter(|block| block.is_solid()) + .map(|block| block.solid_height()) + .unwrap_or(0.0); + pos.0.z = (pos.0.z - 0.05).floor() + snap_height; + physics_state.on_ground = true; + } + + let dirs = [ + Vec3::unit_x(), + Vec3::unit_y(), + -Vec3::unit_x(), + -Vec3::unit_y(), + ]; + + if let (wall_dir, true) = dirs.iter().fold((Vec3::zero(), false), |(a, hit), dir| { + if collision_with( + pos.0 + *dir * 0.01, + &terrain, + block_true, + near_iter.clone(), + radius, + z_range.clone(), + ) { + (a + dir, true) + } else { + (a, hit) + } + }) { + physics_state.on_wall = Some(wall_dir); + } else { + physics_state.on_wall = None; + } + + // Figure out if we're in water + physics_state.in_liquid = collision_iter( + pos.0, + &*terrain, + &|block| block.is_liquid(), + // The liquid part of a liquid block always extends 1 block high. + &|_block| 1.0, + near_iter.clone(), + radius, + z_min..z_max, + ) + .max_by_key(|block_aabb| (block_aabb.max.z * 100.0) as i32) + .map(|block_aabb| block_aabb.max.z - pos.0.z); +} diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 9043ef5fe5..7e6dc4c454 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -77,6 +77,7 @@ type CommandHandler = fn(&mut Server, EcsEntity, EcsEntity, String, &ChatCommand fn get_handler(cmd: &ChatCommand) -> CommandHandler { match cmd { ChatCommand::Adminify => handle_adminify, + ChatCommand::Airship => handle_spawn_airship, ChatCommand::Alias => handle_alias, ChatCommand::Ban => handle_ban, ChatCommand::Build => handle_build, @@ -984,6 +985,39 @@ fn handle_spawn_training_dummy( } } +fn handle_spawn_airship( + server: &mut Server, + client: EcsEntity, + target: EcsEntity, + _args: String, + _action: &ChatCommand, +) { + match server.state.read_component_copied::(target) { + Some(pos) => { + server + .state + .create_ship(pos, comp::ship::Body::DefaultAirship) + .with(comp::Scale(50.0)) + .with(LightEmitter { + col: Rgb::new(1.0, 0.65, 0.2), + strength: 2.0, + flicker: 1.0, + animated: true, + }) + .build(); + + server.notify_client( + client, + ServerGeneral::server_msg(ChatType::CommandInfo, "Spawned an airship"), + ); + }, + None => server.notify_client( + client, + ServerGeneral::server_msg(ChatType::CommandError, "You have no position!"), + ), + } +} + fn handle_spawn_campfire( server: &mut Server, client: EcsEntity, diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index c1c9f8f450..4005c3131b 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -69,7 +69,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv find_dist::Cylinder::from_components( p.0, scales.get(entity).copied(), - colliders.get(entity).copied(), + colliders.get(entity).cloned(), char_states.get(entity), ) }) diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index a596b31cbb..770b6d28b0 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -41,6 +41,7 @@ pub trait StateExt { ) -> EcsEntityBuilder; /// Build a static object entity fn create_object(&mut self, pos: comp::Pos, object: comp::object::Body) -> EcsEntityBuilder; + fn create_ship(&mut self, pos: comp::Pos, object: comp::ship::Body) -> EcsEntityBuilder; /// Build a projectile fn create_projectile( &mut self, @@ -215,6 +216,18 @@ impl StateExt for State { .with(comp::Gravity(1.0)) } + fn create_ship(&mut self, pos: comp::Pos, object: comp::ship::Body) -> EcsEntityBuilder { + self.ecs_mut() + .create_entity_synced() + .with(pos) + .with(comp::Vel(Vec3::zero())) + .with(comp::Ori::default()) + .with(comp::Mass(50.0)) + .with(comp::Collider::Voxel { id: object.manifest_id().to_string() }) + .with(comp::Body::Ship(object)) + .with(comp::Gravity(1.0)) + } + fn create_projectile( &mut self, pos: comp::Pos, diff --git a/server/src/sys/sentinel.rs b/server/src/sys/sentinel.rs index 105b417a6c..778892724c 100644 --- a/server/src/sys/sentinel.rs +++ b/server/src/sys/sentinel.rs @@ -140,7 +140,7 @@ impl<'a> TrackedComps<'a> { self.mass.get(entity).copied().map(|c| comps.push(c.into())); self.collider .get(entity) - .copied() + .cloned() .map(|c| comps.push(c.into())); self.sticky .get(entity) diff --git a/voxygen/anim/src/lib.rs b/voxygen/anim/src/lib.rs index 4988954a6f..d903d6572f 100644 --- a/voxygen/anim/src/lib.rs +++ b/voxygen/anim/src/lib.rs @@ -51,6 +51,7 @@ pub mod fish_small; pub mod fixture; pub mod golem; pub mod object; +pub mod ship; pub mod quadruped_low; pub mod quadruped_medium; pub mod quadruped_small; diff --git a/voxygen/anim/src/ship/idle.rs b/voxygen/anim/src/ship/idle.rs new file mode 100644 index 0000000000..b96c9fd64e --- /dev/null +++ b/voxygen/anim/src/ship/idle.rs @@ -0,0 +1,34 @@ +use super::{ + super::{vek::*, Animation}, + ShipSkeleton, SkeletonAttr, +}; +use common::comp::item::ToolKind; + +pub struct IdleAnimation; + +impl Animation for IdleAnimation { + type Dependency = (Option, Option, f32); + type Skeleton = ShipSkeleton; + + #[cfg(feature = "use-dyn-lib")] + const UPDATE_FN: &'static [u8] = b"ship_idle\0"; + + #[cfg_attr(feature = "be-dyn-lib", export_name = "ship_idle")] + #[allow(clippy::approx_constant)] // TODO: Pending review in #587 + fn update_skeleton_inner( + skeleton: &Self::Skeleton, + (_active_tool_kind, _second_tool_kind, _global_time): Self::Dependency, + _anim_time: f32, + _rate: &mut f32, + s_a: &SkeletonAttr, + ) -> Self::Skeleton { + let mut next = (*skeleton).clone(); + + next.bone0.position = Vec3::new(s_a.bone0.0, s_a.bone0.1, s_a.bone0.2) / 11.0; + + next.bone1.position = Vec3::new(s_a.bone1.0, s_a.bone1.1, s_a.bone1.2) / 11.0; + + next + } +} + diff --git a/voxygen/anim/src/ship/mod.rs b/voxygen/anim/src/ship/mod.rs new file mode 100644 index 0000000000..2e2783add3 --- /dev/null +++ b/voxygen/anim/src/ship/mod.rs @@ -0,0 +1,71 @@ +pub mod idle; + +// Reexports +pub use self::idle::IdleAnimation; + +use super::{make_bone, vek::*, FigureBoneData, Skeleton}; +use common::comp::{self}; +use core::convert::TryFrom; + +pub type Body = comp::ship::Body; + +skeleton_impls!(struct ShipSkeleton { + + bone0, + + bone1, +}); + +impl Skeleton for ShipSkeleton { + type Attr = SkeletonAttr; + type Body = Body; + + const BONE_COUNT: usize = 2; + #[cfg(feature = "use-dyn-lib")] + const COMPUTE_FN: &'static [u8] = b"ship_compute_mats\0"; + + #[cfg_attr(feature = "be-dyn-lib", export_name = "ship_compute_mats")] + fn compute_matrices_inner( + &self, + base_mat: Mat4, + buf: &mut [FigureBoneData; super::MAX_BONE_COUNT], + ) -> Vec3 { + let bone0_mat = base_mat * Mat4::::from(self.bone0); + + *(<&mut [_; Self::BONE_COUNT]>::try_from(&mut buf[0..Self::BONE_COUNT]).unwrap()) = [ + make_bone(bone0_mat * Mat4::scaling_3d(1.0 / 11.0)), + make_bone(Mat4::::from(self.bone1) * Mat4::scaling_3d(1.0 / 11.0)), /* Decorellated from ori */ + ]; + Vec3::unit_z() * 0.5 + } +} + +pub struct SkeletonAttr { + bone0: (f32, f32, f32), + bone1: (f32, f32, f32), +} + +impl<'a> std::convert::TryFrom<&'a comp::Body> for SkeletonAttr { + type Error = (); + + fn try_from(body: &'a comp::Body) -> Result { + match body { + comp::Body::Ship(body) => Ok(SkeletonAttr::from(body)), + _ => Err(()), + } + } +} + +impl Default for SkeletonAttr { + fn default() -> Self { + Self { + bone0: (0.0, 0.0, 0.0), + bone1: (0.0, 0.0, 0.0), + } + } +} + +impl<'a> From<&'a Body> for SkeletonAttr { + fn from(_: &'a Body) -> Self { + Self::default() + } +} + diff --git a/voxygen/src/render/renderer.rs b/voxygen/src/render/renderer.rs index 6856dbbb4b..eec891e144 100644 --- a/voxygen/src/render/renderer.rs +++ b/voxygen/src/render/renderer.rs @@ -1972,7 +1972,7 @@ fn create_pipelines( &shaders.figure_vert.read().0, &shaders.figure_frag.read().0, &include_ctx, - gfx::state::CullFace::Back, + gfx::state::CullFace::Nothing, )?; // Construct a pipeline for rendering terrain diff --git a/voxygen/src/scene/figure/load.rs b/voxygen/src/scene/figure/load.rs index 6aca80bb7f..a584812462 100644 --- a/voxygen/src/scene/figure/load.rs +++ b/voxygen/src/scene/figure/load.rs @@ -13,6 +13,7 @@ use common::{ humanoid::{self, Body, BodyType, EyeColor, Skin, Species}, item::{ItemDef, ModularComponentKind}, object, + ship::{self, figuredata::{ShipSpec, ShipCentralSubSpec}}, quadruped_low::{self, BodyType as QLBodyType, Species as QLSpecies}, quadruped_medium::{self, BodyType as QMBodyType, Species as QMSpecies}, quadruped_small::{self, BodyType as QSBodyType, Species as QSSpecies}, @@ -22,7 +23,7 @@ use common::{ }; use hashbrown::HashMap; use serde::Deserialize; -use std::sync::Arc; +use std::{fmt, hash::Hash, sync::Arc}; use tracing::{error, warn}; use vek::*; @@ -4102,6 +4103,17 @@ impl QuadrupedLowLateralSpec { #[derive(Deserialize)] struct ObjectCentralSpec(HashMap); +/* +#[derive(Deserialize)] +struct ShipCentralSpec(HashMap); + +#[derive(Deserialize)] +struct SidedShipCentralVoxSpec { + bone0: ObjectCentralSubSpec, + bone1: ObjectCentralSubSpec, + bone2: ObjectCentralSubSpec, +}*/ + #[derive(Deserialize)] struct SidedObjectCentralVoxSpec { bone0: ObjectCentralSubSpec, @@ -4171,3 +4183,99 @@ impl ObjectCentralSpec { (central, Vec3::from(spec.bone1.offset)) } } + +/*make_vox_spec!( + ship::Body, + struct ShipSpec { + central: ShipCentralSpec = "server.manifests.ship_manifest", + }, + |FigureKey { body, .. }, spec| { + [ + Some(spec.central.read().0.mesh_bone( + body, |spec| &spec.bone0, + )), + Some(spec.central.read().0.mesh_bone( + body, |spec| &spec.bone1 + )), + Some(spec.central.read().0.mesh_bone( + body, |spec| &spec.bone2 + )), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ] + }, +); + +impl ShipCentralSpec { + fn mesh_bone &ObjectCentralSubSpec>(&self, obj: &ship::Body, f: F) -> BoneMeshes { + let spec = match self.0.get(&obj) { + Some(spec) => spec, + None => { + error!("No specification exists for {:?}", obj); + return load_mesh("not_found", Vec3::new(-5.0, -5.0, -2.5)); + }, + }; + let bone = f(spec); + let central = graceful_load_segment(&bone.central.0); + + (central, Vec3::from(bone.offset)) + } +}*/ +fn mesh_ship_bone &ShipCentralSubSpec>(map: &HashMap, obj: &K, f: F) -> BoneMeshes { + let spec = match map.get(&obj) { + Some(spec) => spec, + None => { + error!("No specification exists for {:?}", obj); + return load_mesh("not_found", Vec3::new(-5.0, -5.0, -2.5)); + }, + }; + let bone = f(spec); + let central = graceful_load_segment(&bone.central.0); + + (central, Vec3::from(bone.offset)) +} + +impl BodySpec for ship::Body { + type Spec = ShipSpec; + + #[allow(unused_variables)] + fn load_spec() -> Result, assets::Error> { + Self::Spec::load("") + } + + fn bone_meshes( + FigureKey { body, .. }: &FigureKey, + spec: &Self::Spec, + ) -> [Option; anim::MAX_BONE_COUNT] { + let map = &(spec.central.read().0).0; + [ + Some(mesh_ship_bone(map, body, |spec| &spec.bone0,)), + Some(mesh_ship_bone(map, body, |spec| &spec.bone1,)), + Some(mesh_ship_bone(map, body, |spec| &spec.bone2,)), + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + None, + ] + } +} diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index db5146c923..12281f4edf 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -21,7 +21,7 @@ use anim::{ biped_large::BipedLargeSkeleton, biped_small::BipedSmallSkeleton, bird_medium::BirdMediumSkeleton, bird_small::BirdSmallSkeleton, character::CharacterSkeleton, dragon::DragonSkeleton, fish_medium::FishMediumSkeleton, fish_small::FishSmallSkeleton, - golem::GolemSkeleton, object::ObjectSkeleton, quadruped_low::QuadrupedLowSkeleton, + golem::GolemSkeleton, object::ObjectSkeleton, ship::ShipSkeleton, quadruped_low::QuadrupedLowSkeleton, quadruped_medium::QuadrupedMediumSkeleton, quadruped_small::QuadrupedSmallSkeleton, theropod::TheropodSkeleton, Animation, Skeleton, }; @@ -101,6 +101,7 @@ struct FigureMgrStates { biped_small_states: HashMap>, golem_states: HashMap>, object_states: HashMap>, + ship_states: HashMap>, } impl FigureMgrStates { @@ -120,6 +121,7 @@ impl FigureMgrStates { biped_small_states: HashMap::new(), golem_states: HashMap::new(), object_states: HashMap::new(), + ship_states: HashMap::new(), } } @@ -180,6 +182,7 @@ impl FigureMgrStates { .map(DerefMut::deref_mut), Body::Golem(_) => self.golem_states.get_mut(&entity).map(DerefMut::deref_mut), Body::Object(_) => self.object_states.get_mut(&entity).map(DerefMut::deref_mut), + Body::Ship(_) => self.ship_states.get_mut(&entity).map(DerefMut::deref_mut), } } @@ -205,6 +208,7 @@ impl FigureMgrStates { Body::BipedSmall(_) => self.biped_small_states.remove(&entity).map(|e| e.meta), Body::Golem(_) => self.golem_states.remove(&entity).map(|e| e.meta), Body::Object(_) => self.object_states.remove(&entity).map(|e| e.meta), + Body::Ship(_) => self.ship_states.remove(&entity).map(|e| e.meta), } } @@ -224,6 +228,7 @@ impl FigureMgrStates { self.biped_small_states.retain(|k, v| f(k, &mut *v)); self.golem_states.retain(|k, v| f(k, &mut *v)); self.object_states.retain(|k, v| f(k, &mut *v)); + self.ship_states.retain(|k, v| f(k, &mut *v)); } fn count(&self) -> usize { @@ -242,6 +247,7 @@ impl FigureMgrStates { + self.biped_small_states.len() + self.golem_states.len() + self.object_states.len() + + self.ship_states.len() } fn count_visible(&self) -> usize { @@ -314,6 +320,11 @@ impl FigureMgrStates { .iter() .filter(|(_, c)| c.visible()) .count() + + self + .ship_states + .iter() + .filter(|(_, c)| c.visible()) + .count() } } @@ -332,6 +343,7 @@ pub struct FigureMgr { biped_large_model_cache: FigureModelCache, biped_small_model_cache: FigureModelCache, object_model_cache: FigureModelCache, + ship_model_cache: FigureModelCache, golem_model_cache: FigureModelCache, states: FigureMgrStates, } @@ -353,6 +365,7 @@ impl FigureMgr { biped_large_model_cache: FigureModelCache::new(), biped_small_model_cache: FigureModelCache::new(), object_model_cache: FigureModelCache::new(), + ship_model_cache: FigureModelCache::new(), golem_model_cache: FigureModelCache::new(), states: FigureMgrStates::default(), } @@ -384,6 +397,7 @@ impl FigureMgr { self.biped_small_model_cache .clean(&mut self.col_lights, tick); self.object_model_cache.clean(&mut self.col_lights, tick); + self.ship_model_cache.clean(&mut self.col_lights, tick); self.golem_model_cache.clean(&mut self.col_lights, tick); } @@ -4088,6 +4102,79 @@ impl FigureMgr { _ => target_base, }; + state.skeleton = anim::vek::Lerp::lerp(&state.skeleton, &target_bones, dt_lerp); + state.update( + renderer, + pos.0, + ori, + scale, + col, + dt, + state_animation_rate, + model, + lpindex, + true, + is_player, + camera, + &mut update_buf, + terrain, + ); + }, + Body::Ship(body) => { + let (model, skeleton_attr) = self.ship_model_cache.get_or_create_model( + renderer, + &mut self.col_lights, + *body, + inventory, + tick, + player_camera_mode, + player_character_state, + scene_data.runtime, + ); + + let state = + self.states.ship_states.entry(entity).or_insert_with(|| { + FigureState::new(renderer, ShipSkeleton::default()) + }); + + let (character, last_character) = match (character, last_character) { + (Some(c), Some(l)) => (c, l), + _ => (&CharacterState::Idle, &Last { + 0: CharacterState::Idle, + }), + }; + + if !character.same_variant(&last_character.0) { + state.state_time = 0.0; + } + + let target_base = match ( + physics.on_ground, + vel.0.magnitude_squared() > MOVING_THRESHOLD_SQR, // Moving + physics.in_liquid.is_some(), // In water + ) { + // Standing + (true, false, false) => anim::ship::IdleAnimation::update_skeleton( + &ShipSkeleton::default(), + (active_tool_kind, second_tool_kind, time), + state.state_time, + &mut state_animation_rate, + skeleton_attr, + ), + _ => anim::ship::IdleAnimation::update_skeleton( + &ShipSkeleton::default(), + (active_tool_kind, second_tool_kind, time), + state.state_time, + &mut state_animation_rate, + skeleton_attr, + ), + }; + + let target_bones = match &character { + // TODO! + _ => target_base, + }; + state.skeleton = anim::vek::Lerp::lerp(&state.skeleton, &target_bones, dt_lerp); state.update( renderer, @@ -4313,6 +4400,7 @@ impl FigureMgr { biped_large_model_cache, biped_small_model_cache, object_model_cache, + ship_model_cache, golem_model_cache, states: FigureMgrStates { @@ -4330,6 +4418,7 @@ impl FigureMgr { biped_small_states, golem_states, object_states, + ship_states, }, } = self; let col_lights = &*col_lights_; @@ -4572,6 +4661,23 @@ impl FigureMgr { ), ) }), + Body::Ship(body) => ship_states + .get(&entity) + .filter(|state| filter_state(&*state)) + .map(move |state| { + ( + state.locals(), + state.bone_consts(), + ship_model_cache.get_model( + col_lights, + *body, + inventory, + tick, + player_camera_mode, + character_state, + ), + ) + }), } { let model_entry = model_entry?; diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 822b3d6156..8bcbffd4bf 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -1562,7 +1562,7 @@ fn under_cursor( let player_cylinder = Cylinder::from_components( player_pos, scales.get(player_entity).copied(), - colliders.get(player_entity).copied(), + colliders.get(player_entity).cloned(), char_states.get(player_entity), ); let terrain = client.state().terrain(); @@ -1643,7 +1643,7 @@ fn under_cursor( let target_cylinder = Cylinder::from_components( p, scales.get(*e).copied(), - colliders.get(*e).copied(), + colliders.get(*e).cloned(), char_states.get(*e), ); @@ -1706,7 +1706,7 @@ fn select_interactable( let player_cylinder = Cylinder::from_components( player_pos, scales.get(player_entity).copied(), - colliders.get(player_entity).copied(), + colliders.get(player_entity).cloned(), char_states.get(player_entity), ); @@ -1720,7 +1720,7 @@ fn select_interactable( .join() .filter(|(e, _, _, _, _)| *e != player_entity) .map(|(e, p, s, c, cs)| { - let cylinder = Cylinder::from_components(p.0, s.copied(), c.copied(), cs); + let cylinder = Cylinder::from_components(p.0, s.copied(), c.cloned(), cs); (e, cylinder) }) // Roughly filter out entities farther than interaction distance