Added basic tethering

This commit is contained in:
Joshua Barretto 2023-05-26 21:02:32 +01:00
parent c422616f70
commit 9aa757cd09
9 changed files with 332 additions and 16 deletions

View File

@ -689,6 +689,11 @@ impl ServerChatCommand {
Optional, Optional,
), ),
Float("destination_degrees_ccw_of_east", 90.0, 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", "Spawns a ship",
Some(Admin), Some(Admin),
@ -723,6 +728,7 @@ impl ServerChatCommand {
Integer("amount", 1, Optional), Integer("amount", 1, Optional),
Boolean("ai", "true".to_string(), Optional), Boolean("ai", "true".to_string(), Optional),
Float("scale", 1.0, Optional), Float("scale", 1.0, Optional),
Boolean("tethered", "false".to_string(), Optional),
], ],
"Spawn a test entity", "Spawn a test entity",
Some(Admin), Some(Admin),

View File

@ -1214,6 +1214,8 @@ impl Body {
.into() .into()
} }
pub fn tether_offset(&self) -> Vec3<f32> { Vec3::new(0.0, self.dimensions().y * 0.5, 0.0) }
pub fn localize(&self) -> Content { pub fn localize(&self) -> Content {
match self { match self {
Self::BipedLarge(biped_large) => biped_large.localize(), Self::BipedLarge(biped_large) => biped_large.localize(),

View File

@ -57,6 +57,7 @@ pub mod spiral;
pub mod states; pub mod states;
pub mod store; pub mod store;
pub mod terrain; pub mod terrain;
pub mod tether;
pub mod time; pub mod time;
pub mod trade; pub mod trade;
pub mod util; pub mod util;

View File

@ -2,7 +2,8 @@ use crate::{
comp::{self, pet::is_mountable, ship::figuredata::VOXEL_COLLIDER_MANIFEST}, comp::{self, pet::is_mountable, ship::figuredata::VOXEL_COLLIDER_MANIFEST},
link::{Is, Link, LinkHandle, Role}, link::{Is, Link, LinkHandle, Role},
terrain::{Block, TerrainGrid}, terrain::{Block, TerrainGrid},
uid::{IdMaps, Uid}, tether,
uid::{Uid, UidAllocator},
vol::ReadVol, vol::ReadVol,
}; };
use hashbrown::HashSet; use hashbrown::HashSet;
@ -45,6 +46,7 @@ impl Link for Mounting {
WriteStorage<'a, Is<Mount>>, WriteStorage<'a, Is<Mount>>,
WriteStorage<'a, Is<Rider>>, WriteStorage<'a, Is<Rider>>,
ReadStorage<'a, Is<VolumeRider>>, ReadStorage<'a, Is<VolumeRider>>,
ReadStorage<'a, Is<tether::Follower>>,
); );
type DeleteData<'a> = ( type DeleteData<'a> = (
Read<'a, IdMaps>, Read<'a, IdMaps>,
@ -67,7 +69,7 @@ impl Link for Mounting {
fn create( fn create(
this: &LinkHandle<Self>, this: &LinkHandle<Self>,
(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> { ) -> Result<(), Self::Error> {
let entity = |uid: Uid| id_maps.uid_entity(uid); let entity = |uid: Uid| id_maps.uid_entity(uid);
@ -79,7 +81,7 @@ impl Link for Mounting {
// relationship // relationship
if !is_mounts.contains(mount) if !is_mounts.contains(mount)
&& !is_riders.contains(rider) && !is_riders.contains(rider)
&& !is_riders.contains(rider) && !is_followers.contains(rider)
// TODO: Does this definitely prevent mount cycles? // TODO: Does this definitely prevent mount cycles?
&& (!is_mounts.contains(rider) || !is_riders.contains(mount)) && (!is_mounts.contains(rider) || !is_riders.contains(mount))
&& !is_volume_rider.contains(rider) && !is_volume_rider.contains(rider)

140
common/src/tether.rs Normal file
View File

@ -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<Leader>>,
WriteStorage<'a, Is<Follower>>,
ReadStorage<'a, Is<Rider>>,
ReadStorage<'a, Is<Mount>>,
ReadStorage<'a, Is<VolumeRider>>,
);
type DeleteData<'a> = (
Read<'a, UidAllocator>,
WriteStorage<'a, Is<Leader>>,
WriteStorage<'a, Is<Follower>>,
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<Leader>>,
ReadStorage<'a, Is<Follower>>,
ReadStorage<'a, comp::CharacterState>,
);
fn create(
this: &LinkHandle<Self>,
(
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<Self>,
(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<Self>,
(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));
}
}

View File

@ -19,6 +19,7 @@ use common::{
shared_server_config::ServerConstants, shared_server_config::ServerConstants,
slowjob::SlowJobPool, slowjob::SlowJobPool,
terrain::{Block, MapSizeLg, TerrainChunk, TerrainGrid}, terrain::{Block, MapSizeLg, TerrainChunk, TerrainGrid},
tether,
time::DayPeriod, time::DayPeriod,
trade::Trades, trade::Trades,
vol::{ReadVol, WriteVol}, vol::{ReadVol, WriteVol},
@ -202,6 +203,8 @@ impl State {
ecs.register::<Is<Mount>>(); ecs.register::<Is<Mount>>();
ecs.register::<Is<Rider>>(); ecs.register::<Is<Rider>>();
ecs.register::<Is<VolumeRider>>(); ecs.register::<Is<VolumeRider>>();
ecs.register::<Is<tether::Leader>>();
ecs.register::<Is<tether::Follower>>();
ecs.register::<comp::Mass>(); ecs.register::<comp::Mass>();
ecs.register::<comp::Density>(); ecs.register::<comp::Density>();
ecs.register::<comp::Collider>(); ecs.register::<comp::Collider>();

View File

@ -13,6 +13,7 @@ pub mod phys;
pub mod projectile; pub mod projectile;
mod shockwave; mod shockwave;
mod stats; mod stats;
mod tether;
// External // External
use common_ecs::{dispatch, System}; use common_ecs::{dispatch, System};
@ -21,6 +22,7 @@ use specs::DispatcherBuilder;
pub fn add_local_systems(dispatch_builder: &mut DispatcherBuilder) { pub fn add_local_systems(dispatch_builder: &mut DispatcherBuilder) {
//TODO: don't run interpolation on server //TODO: don't run interpolation on server
dispatch::<interpolation::Sys>(dispatch_builder, &[]); dispatch::<interpolation::Sys>(dispatch_builder, &[]);
dispatch::<tether::Sys>(dispatch_builder, &[]);
dispatch::<mount::Sys>(dispatch_builder, &[]); dispatch::<mount::Sys>(dispatch_builder, &[]);
dispatch::<controller::Sys>(dispatch_builder, &[&mount::Sys::sys_name()]); dispatch::<controller::Sys>(dispatch_builder, &[&mount::Sys::sys_name()]);
dispatch::<character_behavior::Sys>(dispatch_builder, &[&controller::Sys::sys_name()]); dispatch::<character_behavior::Sys>(dispatch_builder, &[&controller::Sys::sys_name()]);

View File

@ -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<Leader>>,
ReadStorage<'a, Is<Follower>>,
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<Self>,
(
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);
}
}
}
}
}

View File

@ -46,7 +46,8 @@ use common::{
resources::{BattleMode, PlayerPhysicsSettings, Secs, Time, TimeOfDay, TimeScale}, resources::{BattleMode, PlayerPhysicsSettings, Secs, Time, TimeOfDay, TimeScale},
rtsim::{Actor, Role}, rtsim::{Actor, Role},
terrain::{Block, BlockKind, CoordinateConversions, SpriteKind, TerrainChunkSize}, terrain::{Block, BlockKind, CoordinateConversions, SpriteKind, TerrainChunkSize},
uid::Uid, tether::Tethered,
uid::{IdMaps, Uid},
vol::ReadVol, vol::ReadVol,
weather, Damage, DamageKind, DamageSource, Explosion, LoadoutBuilder, RadiusEffect, weather, Damage, DamageKind, DamageSource, Explosion, LoadoutBuilder, RadiusEffect,
}; };
@ -1558,8 +1559,15 @@ fn handle_spawn(
args: Vec<String>, args: Vec<String>,
action: &ServerChatCommand, action: &ServerChatCommand,
) -> CmdResult<()> { ) -> CmdResult<()> {
match parse_cmd_args!(args, String, npc::NpcBody, u32, bool, f32) { 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) => { (
Some(opt_align),
Some(npc::NpcBody(id, mut body)),
opt_amount,
opt_ai,
opt_scale,
opt_tethered,
) => {
let uid = uid(server, target, "target")?; let uid = uid(server, target, "target")?;
let alignment = parse_alignment(uid, &opt_align)?; let alignment = parse_alignment(uid, &opt_align)?;
let amount = opt_amount.filter(|x| *x > 0).unwrap_or(1).min(50); 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(); let new_entity = entity_base.build();
if opt_tethered == Some(true) {
let tether_leader = server
.state
.read_component_cloned::<Is<Rider>>(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 // Add to group system if a pet
if matches!(alignment, comp::Alignment::Owned { .. }) { if matches!(alignment, comp::Alignment::Owned { .. }) {
let server_eventbus = let server_eventbus =
@ -1748,7 +1778,7 @@ fn handle_spawn_ship(
args: Vec<String>, args: Vec<String>,
_action: &ServerChatCommand, _action: &ServerChatCommand,
) -> CmdResult<()> { ) -> 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")?; let mut pos = position(server, target, "target")?;
pos.0.z += 2.0; pos.0.z += 2.0;
const DESTINATION_RADIUS: f32 = 2000.0; const DESTINATION_RADIUS: f32 = 2000.0;
@ -1767,16 +1797,40 @@ fn handle_spawn_ship(
let mut builder = server let mut builder = server
.state .state
.create_ship(pos, ori, ship, |ship| ship.make_collider()); .create_ship(pos, ori, ship, |ship| ship.make_collider());
if let Some(pos) = destination {
let (kp, ki, kd) = // if let Some(pos) = destination {
comp::agent::pid_coefficients(&comp::Body::Ship(ship)).unwrap_or((1.0, 0.0, 0.0)); // let (kp, ki, kd) =
fn pure_z(sp: Vec3<f32>, pv: Vec3<f32>) -> f32 { (sp - pv).z } // comp::agent::pid_coefficients(&comp::Body::Ship(ship)).unwrap_or((1.
let agent = comp::Agent::from_body(&comp::Body::Ship(ship)) // 0, 0.0, 0.0)); fn pure_z(sp: Vec3<f32>, pv: Vec3<f32>) -> f32 { (sp -
.with_destination(pos) // pv).z } let agent = comp::Agent::from_body(&comp::Body::Ship(ship))
.with_position_pid_controller(comp::PidController::new(kp, ki, kd, pos, 0.0, pure_z)); // .with_destination(pos)
builder = builder.with(agent); // .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::<Is<Rider>>(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( server.notify_client(
client, client,