From 9aa757cd09239f80c9a539f2f65cbe239dccd3f6 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Fri, 26 May 2023 21:02:32 +0100 Subject: [PATCH] Added basic tethering --- common/src/cmd.rs | 6 ++ common/src/comp/body.rs | 2 + common/src/lib.rs | 1 + common/src/mounting.rs | 8 +- common/src/tether.rs | 140 +++++++++++++++++++++++++++++++++++ common/state/src/state.rs | 3 + common/systems/src/lib.rs | 2 + common/systems/src/tether.rs | 106 ++++++++++++++++++++++++++ server/src/cmd.rs | 80 ++++++++++++++++---- 9 files changed, 332 insertions(+), 16 deletions(-) create mode 100644 common/src/tether.rs create mode 100644 common/systems/src/tether.rs diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 9b412f44f8..d0a6c4de0f 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -689,6 +689,11 @@ impl ServerChatCommand { Optional, ), Float("destination_degrees_ccw_of_east", 90.0, Optional), + Boolean( + "Whether the ship should be tethered to the target (or its mount)", + "false".to_string(), + Optional, + ), ], "Spawns a ship", Some(Admin), @@ -723,6 +728,7 @@ impl ServerChatCommand { Integer("amount", 1, Optional), Boolean("ai", "true".to_string(), Optional), Float("scale", 1.0, Optional), + Boolean("tethered", "false".to_string(), Optional), ], "Spawn a test entity", Some(Admin), diff --git a/common/src/comp/body.rs b/common/src/comp/body.rs index fd38d3f9e4..675250634a 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -1214,6 +1214,8 @@ impl Body { .into() } + pub fn tether_offset(&self) -> Vec3 { Vec3::new(0.0, self.dimensions().y * 0.5, 0.0) } + pub fn localize(&self) -> Content { match self { Self::BipedLarge(biped_large) => biped_large.localize(), diff --git a/common/src/lib.rs b/common/src/lib.rs index 0b558498c5..76d9b5bd78 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -57,6 +57,7 @@ pub mod spiral; pub mod states; pub mod store; pub mod terrain; +pub mod tether; pub mod time; pub mod trade; pub mod util; diff --git a/common/src/mounting.rs b/common/src/mounting.rs index 002b4735e2..e86d5162c2 100644 --- a/common/src/mounting.rs +++ b/common/src/mounting.rs @@ -2,7 +2,8 @@ use crate::{ comp::{self, pet::is_mountable, ship::figuredata::VOXEL_COLLIDER_MANIFEST}, link::{Is, Link, LinkHandle, Role}, terrain::{Block, TerrainGrid}, - uid::{IdMaps, Uid}, + tether, + uid::{Uid, UidAllocator}, vol::ReadVol, }; use hashbrown::HashSet; @@ -45,6 +46,7 @@ impl Link for Mounting { WriteStorage<'a, Is>, WriteStorage<'a, Is>, ReadStorage<'a, Is>, + ReadStorage<'a, Is>, ); type DeleteData<'a> = ( Read<'a, IdMaps>, @@ -67,7 +69,7 @@ impl Link for Mounting { fn create( this: &LinkHandle, - (id_maps, is_mounts, is_riders, is_volume_rider): &mut Self::CreateData<'_>, + (id_maps, is_mounts, is_riders, is_volume_rider, is_followers): &mut Self::CreateData<'_>, ) -> Result<(), Self::Error> { let entity = |uid: Uid| id_maps.uid_entity(uid); @@ -79,7 +81,7 @@ impl Link for Mounting { // relationship if !is_mounts.contains(mount) && !is_riders.contains(rider) - && !is_riders.contains(rider) + && !is_followers.contains(rider) // TODO: Does this definitely prevent mount cycles? && (!is_mounts.contains(rider) || !is_riders.contains(mount)) && !is_volume_rider.contains(rider) diff --git a/common/src/tether.rs b/common/src/tether.rs new file mode 100644 index 0000000000..94d86f1fe9 --- /dev/null +++ b/common/src/tether.rs @@ -0,0 +1,140 @@ +use crate::{ + comp, + link::{Is, Link, LinkHandle, Role}, + mounting::{Mount, Rider, VolumeRider}, + uid::{Uid, UidAllocator}, +}; +use serde::{Deserialize, Serialize}; +use specs::{ + saveload::MarkerAllocator, storage::GenericWriteStorage, Component, DenseVecStorage, Entities, + Entity, Read, ReadExpect, ReadStorage, Write, WriteStorage, +}; +use vek::*; + +#[derive(Serialize, Deserialize, Debug)] +pub struct Leader; + +impl Role for Leader { + type Link = Tethered; +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Follower; + +impl Role for Follower { + type Link = Tethered; +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Tethered { + pub leader: Uid, + pub follower: Uid, + pub tether_length: f32, +} + +#[derive(Debug)] +pub enum TetherError { + NoSuchEntity, + NotTetherable, +} + +impl Link for Tethered { + type CreateData<'a> = ( + Read<'a, UidAllocator>, + WriteStorage<'a, Is>, + WriteStorage<'a, Is>, + ReadStorage<'a, Is>, + ReadStorage<'a, Is>, + ReadStorage<'a, Is>, + ); + type DeleteData<'a> = ( + Read<'a, UidAllocator>, + WriteStorage<'a, Is>, + WriteStorage<'a, Is>, + WriteStorage<'a, comp::Pos>, + WriteStorage<'a, comp::ForceUpdate>, + ); + type Error = TetherError; + type PersistData<'a> = ( + Read<'a, UidAllocator>, + Entities<'a>, + ReadStorage<'a, comp::Health>, + ReadStorage<'a, comp::Body>, + ReadStorage<'a, Is>, + ReadStorage<'a, Is>, + ReadStorage<'a, comp::CharacterState>, + ); + + fn create( + this: &LinkHandle, + ( + uid_allocator, + is_leaders, + is_followers, + is_riders, + is_mounts, + is_volume_rider, + ): &mut Self::CreateData<'_>, + ) -> Result<(), Self::Error> { + let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into()); + + if this.leader == this.follower { + // Forbid self-tethering + Err(TetherError::NotTetherable) + } else if let Some((leader, follower)) = entity(this.leader).zip(entity(this.follower)) { + // Ensure that neither leader or follower are already part of a conflicting + // relationship + if !is_riders.contains(follower) + && !is_volume_rider.contains(follower) + && !is_followers.contains(follower) + // TODO: Does this definitely prevent tether cycles? + && (!is_leaders.contains(follower) || !is_followers.contains(leader)) + { + let _ = is_leaders.insert(leader, this.make_role()); + let _ = is_followers.insert(follower, this.make_role()); + Ok(()) + } else { + Err(TetherError::NotTetherable) + } + } else { + Err(TetherError::NoSuchEntity) + } + } + + fn persist( + this: &LinkHandle, + (uid_allocator, entities, healths, bodies, is_leaders, is_followers, character_states): &mut Self::PersistData<'_>, + ) -> bool { + let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into()); + + if let Some((leader, follower)) = entity(this.leader).zip(entity(this.follower)) { + let is_alive = |entity| { + entities.is_alive(entity) && healths.get(entity).map_or(true, |h| !h.is_dead) + }; + + // Ensure that both entities are alive and that they continue to be linked + is_alive(leader) + && is_alive(follower) + && is_leaders.get(leader).is_some() + && is_followers.get(follower).is_some() + } else { + false + } + } + + fn delete( + this: &LinkHandle, + (uid_allocator, is_leaders, is_followers, positions, force_update): &mut Self::DeleteData< + '_, + >, + ) { + let entity = |uid: Uid| uid_allocator.retrieve_entity_internal(uid.into()); + + let leader = entity(this.leader); + let follower = entity(this.follower); + + // Delete link components + leader.map(|leader| is_leaders.remove(leader)); + follower.map(|follower| is_followers.remove(follower)); + } +} diff --git a/common/state/src/state.rs b/common/state/src/state.rs index 41afed0237..377b6cbc90 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -19,6 +19,7 @@ use common::{ shared_server_config::ServerConstants, slowjob::SlowJobPool, terrain::{Block, MapSizeLg, TerrainChunk, TerrainGrid}, + tether, time::DayPeriod, trade::Trades, vol::{ReadVol, WriteVol}, @@ -202,6 +203,8 @@ impl State { ecs.register::>(); ecs.register::>(); ecs.register::>(); + ecs.register::>(); + ecs.register::>(); ecs.register::(); ecs.register::(); ecs.register::(); diff --git a/common/systems/src/lib.rs b/common/systems/src/lib.rs index e9c30bc0d1..ac2b4d9394 100644 --- a/common/systems/src/lib.rs +++ b/common/systems/src/lib.rs @@ -13,6 +13,7 @@ pub mod phys; pub mod projectile; mod shockwave; mod stats; +mod tether; // External use common_ecs::{dispatch, System}; @@ -21,6 +22,7 @@ use specs::DispatcherBuilder; pub fn add_local_systems(dispatch_builder: &mut DispatcherBuilder) { //TODO: don't run interpolation on server dispatch::(dispatch_builder, &[]); + dispatch::(dispatch_builder, &[]); dispatch::(dispatch_builder, &[]); dispatch::(dispatch_builder, &[&mount::Sys::sys_name()]); dispatch::(dispatch_builder, &[&controller::Sys::sys_name()]); diff --git a/common/systems/src/tether.rs b/common/systems/src/tether.rs new file mode 100644 index 0000000000..9302acbf2a --- /dev/null +++ b/common/systems/src/tether.rs @@ -0,0 +1,106 @@ +use common::{ + comp::{Body, Collider, InputKind, Mass, Ori, Pos, Scale, Vel}, + link::Is, + resources::DeltaTime, + tether::{Follower, Leader}, + uid::UidAllocator, + util::Dir, +}; +use common_ecs::{Job, Origin, Phase, System}; +use specs::{ + saveload::{Marker, MarkerAllocator}, + Entities, Join, Read, ReadExpect, ReadStorage, WriteStorage, +}; +use tracing::error; +use vek::*; + +/// This system is responsible for controlling mounts +#[derive(Default)] +pub struct Sys; +impl<'a> System<'a> for Sys { + type SystemData = ( + Read<'a, UidAllocator>, + Entities<'a>, + Read<'a, DeltaTime>, + ReadStorage<'a, Is>, + ReadStorage<'a, Is>, + WriteStorage<'a, Pos>, + WriteStorage<'a, Vel>, + WriteStorage<'a, Ori>, + ReadStorage<'a, Body>, + ReadStorage<'a, Scale>, + ReadStorage<'a, Collider>, + ReadStorage<'a, Mass>, + ); + + const NAME: &'static str = "tether"; + const ORIGIN: Origin = Origin::Common; + const PHASE: Phase = Phase::Create; + + fn run( + _job: &mut Job, + ( + uid_allocator, + entities, + dt, + is_leaders, + is_followers, + mut positions, + mut velocities, + mut orientations, + bodies, + scales, + colliders, + masses, + ): Self::SystemData, + ) { + for (follower, is_follower, follower_body) in + (&entities, &is_followers, bodies.maybe()).join() + { + let Some(leader) = uid_allocator + .retrieve_entity_internal(is_follower.leader.id()) + else { continue }; + + let (Some(leader_pos), Some(follower_pos)) = ( + positions.get(leader).copied(), + positions.get(follower).copied(), + ) else { continue }; + + let (Some(leader_mass), Some(follower_mass)) = ( + masses.get(leader).copied(), + masses.get(follower).copied(), + ) else { continue }; + + if velocities.contains(follower) && velocities.contains(leader) { + let tether_offset = orientations + .get(follower) + .map(|ori| { + ori.to_quat() * follower_body.map(|b| b.tether_offset()).unwrap_or_default() + }) + .unwrap_or_default(); + let tether_pos = follower_pos.0 + tether_offset; + let pull_factor = + (leader_pos.0.distance(tether_pos) - is_follower.tether_length).clamp(0.0, 1.0); + let strength = pull_factor * 30000.0; + let pull_dir = (leader_pos.0 - follower_pos.0) + .try_normalized() + .unwrap_or_default(); + let impulse = pull_dir * strength * dt.0; + + // Can't fail + velocities.get_mut(follower).unwrap().0 += impulse / follower_mass.0; + velocities.get_mut(leader).unwrap().0 -= impulse / leader_mass.0; + + if let Some(follower_ori) = orientations.get_mut(follower) { + let turn_strength = pull_factor + * follower_body + .map(|b| b.tether_offset().magnitude()) + .unwrap_or(0.0) + * 4.0; + let target_ori = follower_ori.yawed_towards(Dir::new(pull_dir)); + *follower_ori = follower_ori.slerped_towards(target_ori, turn_strength * dt.0); + } + } + } + } +} diff --git a/server/src/cmd.rs b/server/src/cmd.rs index d9dc3575e2..842714a616 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -46,7 +46,8 @@ use common::{ resources::{BattleMode, PlayerPhysicsSettings, Secs, Time, TimeOfDay, TimeScale}, rtsim::{Actor, Role}, terrain::{Block, BlockKind, CoordinateConversions, SpriteKind, TerrainChunkSize}, - uid::Uid, + tether::Tethered, + uid::{IdMaps, Uid}, vol::ReadVol, weather, Damage, DamageKind, DamageSource, Explosion, LoadoutBuilder, RadiusEffect, }; @@ -1558,8 +1559,15 @@ fn handle_spawn( args: Vec, action: &ServerChatCommand, ) -> CmdResult<()> { - match parse_cmd_args!(args, String, npc::NpcBody, u32, bool, f32) { - (Some(opt_align), Some(npc::NpcBody(id, mut body)), opt_amount, opt_ai, opt_scale) => { + match parse_cmd_args!(args, String, npc::NpcBody, u32, bool, f32, bool) { + ( + Some(opt_align), + Some(npc::NpcBody(id, mut body)), + opt_amount, + opt_ai, + opt_scale, + opt_tethered, + ) => { let uid = uid(server, target, "target")?; let alignment = parse_alignment(uid, &opt_align)?; let amount = opt_amount.filter(|x| *x > 0).unwrap_or(1).min(50); @@ -1608,6 +1616,28 @@ fn handle_spawn( let new_entity = entity_base.build(); + if opt_tethered == Some(true) { + let tether_leader = server + .state + .read_component_cloned::>(target) + .map(|is_rider| is_rider.mount) + .or_else(|| server.state.ecs().uid_from_entity(target)); + let tether_follower = server.state.ecs().uid_from_entity(new_entity); + + if let (Some(leader), Some(follower)) = (tether_leader, tether_follower) { + server + .state + .link(Tethered { + leader, + follower, + tether_length: 4.0, + }) + .map_err(|_| "Failed to tether entities")?; + } else { + return Err("Tether members don't have Uids.".into()); + } + } + // Add to group system if a pet if matches!(alignment, comp::Alignment::Owned { .. }) { let server_eventbus = @@ -1748,7 +1778,7 @@ fn handle_spawn_ship( args: Vec, _action: &ServerChatCommand, ) -> CmdResult<()> { - let (body_name, angle) = parse_cmd_args!(args, String, f32); + let (body_name, angle, tethered) = parse_cmd_args!(args, String, f32, bool); let mut pos = position(server, target, "target")?; pos.0.z += 2.0; const DESTINATION_RADIUS: f32 = 2000.0; @@ -1767,16 +1797,40 @@ fn handle_spawn_ship( let mut builder = server .state .create_ship(pos, ori, ship, |ship| ship.make_collider()); - if let Some(pos) = destination { - let (kp, ki, kd) = - comp::agent::pid_coefficients(&comp::Body::Ship(ship)).unwrap_or((1.0, 0.0, 0.0)); - fn pure_z(sp: Vec3, pv: Vec3) -> f32 { (sp - pv).z } - let agent = comp::Agent::from_body(&comp::Body::Ship(ship)) - .with_destination(pos) - .with_position_pid_controller(comp::PidController::new(kp, ki, kd, pos, 0.0, pure_z)); - builder = builder.with(agent); + + // if let Some(pos) = destination { + // let (kp, ki, kd) = + // comp::agent::pid_coefficients(&comp::Body::Ship(ship)).unwrap_or((1. + // 0, 0.0, 0.0)); fn pure_z(sp: Vec3, pv: Vec3) -> f32 { (sp - + // pv).z } let agent = comp::Agent::from_body(&comp::Body::Ship(ship)) + // .with_destination(pos) + // .with_position_pid_controller(comp::PidController::new(kp, ki, kd, + // pos, 0.0, pure_z)); builder = builder.with(agent); + // } + + let new_entity = builder.build(); + + if tethered == Some(true) { + let tether_leader = server + .state + .read_component_cloned::>(target) + .map(|is_rider| is_rider.mount) + .or_else(|| server.state.ecs().uid_from_entity(target)); + let tether_follower = server.state.ecs().uid_from_entity(new_entity); + + if let (Some(leader), Some(follower)) = (tether_leader, tether_follower) { + server + .state + .link(Tethered { + leader, + follower, + tether_length: 6.0, + }) + .map_err(|_| "Failed to tether entities")?; + } else { + return Err("Tether members don't have Uids.".into()); + } } - builder.build(); server.notify_client( client,