Merge branch 'imbris/component-sync' into 'master'

Sync some components only from the client's own entity.

See merge request veloren/veloren!3108
This commit is contained in:
Imbris 2022-01-19 07:44:14 +00:00
commit 59d195b9bc
18 changed files with 754 additions and 672 deletions

View File

@ -6,6 +6,10 @@ pub use userdata_dir::userdata_dir;
#[cfg(feature = "tracy")] pub use tracy_client;
/// Allows downstream crates to conditionally do things based on whether tracy
/// is enabled without having to expose a cargo feature themselves.
pub const TRACY_ENABLED: bool = cfg!(feature = "tracy");
#[cfg(not(feature = "tracy"))]
#[macro_export]
macro_rules! plot {

View File

@ -2,3 +2,4 @@
#![feature(generic_const_exprs, const_fn_floating_point_arithmetic)]
pub mod msg;
pub mod sync;
pub mod synced_components;

View File

@ -1,224 +1,106 @@
use crate::sync;
use common::{
comp,
link::Is,
mounting::{Mount, Rider},
resources::Time,
};
use crate::sync::{self, NetSync};
use common::comp;
use serde::{Deserialize, Serialize};
use specs::WorldExt;
use std::marker::PhantomData;
use sum_type::sum_type;
// Automatically derive From<T> for EcsCompPacket
// for each variant EcsCompPacket::T(T.)
sum_type! {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum EcsCompPacket {
Body(comp::Body),
Player(comp::Player),
CanBuild(comp::CanBuild),
Stats(comp::Stats),
SkillSet(comp::SkillSet),
ActiveAbilities(comp::ActiveAbilities),
Buffs(comp::Buffs),
Auras(comp::Auras),
Energy(comp::Energy),
Combo(comp::Combo),
Health(comp::Health),
Poise(comp::Poise),
LightEmitter(comp::LightEmitter),
Inventory(comp::Inventory),
Item(comp::Item),
Scale(comp::Scale),
Group(comp::Group),
IsMount(Is<Mount>),
IsRider(Is<Rider>),
Mass(comp::Mass),
Density(comp::Density),
Collider(comp::Collider),
Sticky(comp::Sticky),
CharacterState(comp::CharacterState),
Pos(comp::Pos),
Vel(comp::Vel),
Ori(comp::Ori),
Shockwave(comp::Shockwave),
BeamSegment(comp::BeamSegment),
Alignment(comp::Alignment),
}
}
// Automatically derive From<T> for EcsCompPhantom
// for each variant EcsCompPhantom::T(PhantomData<T>).
sum_type! {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum EcsCompPhantom {
Body(PhantomData<comp::Body>),
Player(PhantomData<comp::Player>),
CanBuild(PhantomData<comp::CanBuild>),
Stats(PhantomData<comp::Stats>),
SkillSet(PhantomData<comp::SkillSet>),
ActiveAbilities(PhantomData<comp::ActiveAbilities>),
Buffs(PhantomData<comp::Buffs>),
Auras(PhantomData<comp::Auras>),
Energy(PhantomData<comp::Energy>),
Combo(PhantomData<comp::Combo>),
Health(PhantomData<comp::Health>),
Poise(PhantomData<comp::Poise>),
LightEmitter(PhantomData<comp::LightEmitter>),
Inventory(PhantomData<comp::Inventory>),
Item(PhantomData<comp::Item>),
Scale(PhantomData<comp::Scale>),
Group(PhantomData<comp::Group>),
IsMount(PhantomData<Is<Mount>>),
IsRider(PhantomData<Is<Rider>>),
Mass(PhantomData<comp::Mass>),
Density(PhantomData<comp::Density>),
Collider(PhantomData<comp::Collider>),
Sticky(PhantomData<comp::Sticky>),
CharacterState(PhantomData<comp::CharacterState>),
Pos(PhantomData<comp::Pos>),
Vel(PhantomData<comp::Vel>),
Ori(PhantomData<comp::Ori>),
Shockwave(PhantomData<comp::Shockwave>),
BeamSegment(PhantomData<comp::BeamSegment>),
Alignment(PhantomData<comp::Alignment>),
}
}
impl sync::CompPacket for EcsCompPacket {
type Phantom = EcsCompPhantom;
/// This macro defines [`EcsCompPacke`]
///
/// It is meant to be passed to the `synced_components!` macro which will call
/// it with a list of components.
macro_rules! comp_packet {
($($component_name:ident: $component_type:ident,)*) => {
fn apply_insert(self, entity: specs::Entity, world: &specs::World, force_update: bool) {
match self {
EcsCompPacket::Body(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Player(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::CanBuild(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Stats(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::SkillSet(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::ActiveAbilities(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Buffs(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Auras(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Energy(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Combo(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Health(mut comp) => {
// Time isn't synced between client and server so replace the Time from the
// server with the Client's local Time to enable accurate comparison.
comp.last_change.time = *world.read_resource::<Time>();
sync::handle_insert(comp, entity, world)
},
EcsCompPacket::Poise(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::LightEmitter(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Inventory(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Item(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Scale(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Group(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::IsMount(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::IsRider(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Mass(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Density(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Collider(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Sticky(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::CharacterState(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Pos(comp) => {
sync::handle_interp_insert(comp, entity, world, force_update)
},
EcsCompPacket::Vel(comp) => {
sync::handle_interp_insert(comp, entity, world, force_update)
},
EcsCompPacket::Ori(comp) => {
sync::handle_interp_insert(comp, entity, world, force_update)
},
EcsCompPacket::Shockwave(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::BeamSegment(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Alignment(comp) => sync::handle_insert(comp, entity, world),
// `sum_type!` will automatically derive From<T> for EcsCompPacket
// for each variant EcsCompPacket::T(T).
sum_type::sum_type! {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum EcsCompPacket {
// Note: also use the component_type identifier
// to name the enum variant that contains the component.
$($component_type($component_type),)*
// These aren't included in the "synced_components" because the way
// we determine if they are changed and when to send them is different
// from the other components.
Pos(comp::Pos),
Vel(comp::Vel),
Ori(comp::Ori),
}
}
}
fn apply_modify(self, entity: specs::Entity, world: &specs::World, force_update: bool) {
match self {
EcsCompPacket::Body(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Player(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::CanBuild(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Stats(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::SkillSet(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::ActiveAbilities(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Buffs(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Auras(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Energy(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Combo(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Health(mut comp) => {
// Time isn't synced between client and server so replace the Time from the
// server with the Client's local Time to enable accurate comparison.
comp.last_change.time = *world.read_resource::<Time>();
sync::handle_modify(comp, entity, world)
},
EcsCompPacket::Poise(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::LightEmitter(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Inventory(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Item(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Scale(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Group(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::IsMount(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::IsRider(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Mass(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Density(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Collider(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Sticky(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::CharacterState(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Pos(comp) => {
sync::handle_interp_modify(comp, entity, world, force_update)
},
EcsCompPacket::Vel(comp) => {
sync::handle_interp_modify(comp, entity, world, force_update)
},
EcsCompPacket::Ori(comp) => {
sync::handle_interp_modify(comp, entity, world, force_update)
},
EcsCompPacket::Shockwave(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::BeamSegment(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Alignment(comp) => sync::handle_modify(comp, entity, world),
// `sum_type!` will automatically derive From<PhantomData<T>> for EcsCompPhantom
// for each variant EcsCompPhantom::T(PhantomData<T>).
sum_type::sum_type! {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum EcsCompPhantom {
$($component_type(PhantomData<$component_type>),)*
Pos(PhantomData<comp::Pos>),
Vel(PhantomData<comp::Vel>),
Ori(PhantomData<comp::Ori>),
}
}
}
fn apply_remove(phantom: Self::Phantom, entity: specs::Entity, world: &specs::World) {
match phantom {
EcsCompPhantom::Body(_) => sync::handle_remove::<comp::Body>(entity, world),
EcsCompPhantom::Player(_) => sync::handle_remove::<comp::Player>(entity, world),
EcsCompPhantom::CanBuild(_) => sync::handle_remove::<comp::CanBuild>(entity, world),
EcsCompPhantom::Stats(_) => sync::handle_remove::<comp::Stats>(entity, world),
EcsCompPhantom::SkillSet(_) => sync::handle_remove::<comp::SkillSet>(entity, world),
EcsCompPhantom::ActiveAbilities(_) => {
sync::handle_remove::<comp::ActiveAbilities>(entity, world)
},
EcsCompPhantom::Buffs(_) => sync::handle_remove::<comp::Buffs>(entity, world),
EcsCompPhantom::Auras(_) => sync::handle_remove::<comp::Auras>(entity, world),
EcsCompPhantom::Energy(_) => sync::handle_remove::<comp::Energy>(entity, world),
EcsCompPhantom::Combo(_) => sync::handle_remove::<comp::Combo>(entity, world),
EcsCompPhantom::Health(_) => sync::handle_remove::<comp::Health>(entity, world),
EcsCompPhantom::Poise(_) => sync::handle_remove::<comp::Poise>(entity, world),
EcsCompPhantom::LightEmitter(_) => {
sync::handle_remove::<comp::LightEmitter>(entity, world)
},
EcsCompPhantom::Inventory(_) => sync::handle_remove::<comp::Inventory>(entity, world),
EcsCompPhantom::Item(_) => sync::handle_remove::<comp::Item>(entity, world),
EcsCompPhantom::Scale(_) => sync::handle_remove::<comp::Scale>(entity, world),
EcsCompPhantom::Group(_) => sync::handle_remove::<comp::Group>(entity, world),
EcsCompPhantom::IsMount(_) => sync::handle_remove::<Is<Mount>>(entity, world),
EcsCompPhantom::IsRider(_) => sync::handle_remove::<Is<Rider>>(entity, world),
EcsCompPhantom::Mass(_) => sync::handle_remove::<comp::Mass>(entity, world),
EcsCompPhantom::Density(_) => sync::handle_remove::<comp::Density>(entity, world),
EcsCompPhantom::Collider(_) => sync::handle_remove::<comp::Collider>(entity, world),
EcsCompPhantom::Sticky(_) => sync::handle_remove::<comp::Sticky>(entity, world),
EcsCompPhantom::CharacterState(_) => {
sync::handle_remove::<comp::CharacterState>(entity, world)
},
EcsCompPhantom::Pos(_) => sync::handle_interp_remove::<comp::Pos>(entity, world),
EcsCompPhantom::Vel(_) => sync::handle_interp_remove::<comp::Vel>(entity, world),
EcsCompPhantom::Ori(_) => sync::handle_interp_remove::<comp::Ori>(entity, world),
EcsCompPhantom::Shockwave(_) => sync::handle_remove::<comp::Shockwave>(entity, world),
EcsCompPhantom::BeamSegment(_) => {
sync::handle_remove::<comp::BeamSegment>(entity, world)
},
EcsCompPhantom::Alignment(_) => sync::handle_remove::<comp::Alignment>(entity, world),
impl sync::CompPacket for EcsCompPacket {
type Phantom = EcsCompPhantom;
fn apply_insert(self, entity: specs::Entity, world: &specs::World, force_update: bool) {
match self {
$(Self::$component_type(mut comp) => {
comp.pre_insert(world);
sync::handle_insert(comp, entity, world);
},)*
Self::Pos(comp) => {
sync::handle_interp_insert(comp, entity, world, force_update)
},
Self::Vel(comp) => {
sync::handle_interp_insert(comp, entity, world, force_update)
},
Self::Ori(comp) => {
sync::handle_interp_insert(comp, entity, world, force_update)
},
}
}
fn apply_modify(self, entity: specs::Entity, world: &specs::World, force_update: bool) {
match self {
$(Self::$component_type(mut comp) => {
comp.pre_modify(world);
sync::handle_modify(comp, entity, world);
},)*
Self::Pos(comp) => {
sync::handle_interp_modify(comp, entity, world, force_update)
},
Self::Vel(comp) => {
sync::handle_interp_modify(comp, entity, world, force_update)
},
Self::Ori(comp) => {
sync::handle_interp_modify(comp, entity, world, force_update)
},
}
}
fn apply_remove(phantom: Self::Phantom, entity: specs::Entity, world: &specs::World) {
match phantom {
$(EcsCompPhantom::$component_type(_) => {
sync::handle_remove::<$component_type>(entity, world);
},)*
EcsCompPhantom::Pos(_) => {
sync::handle_interp_remove::<comp::Pos>(entity, world)
},
EcsCompPhantom::Vel(_) => {
sync::handle_interp_remove::<comp::Vel>(entity, world)
},
EcsCompPhantom::Ori(_) => {
sync::handle_interp_remove::<comp::Ori>(entity, world)
},
}
}
}
}
}
// Import all the component types so they will be available when expanding the
// macro below.
use crate::synced_components::*;
// Pass `comp_packet!` macro to this "x macro" which will invoke it with a list
// of components. This will declare the types defined in the macro above.
crate::synced_components!(comp_packet);

View File

@ -1,12 +1,14 @@
// Note: Currently only one-way sync is supported until a usecase for two-way
// sync arises
pub mod interpolation;
mod net_sync;
mod packet;
mod sync_ext;
mod track;
// Reexports
pub use common::uid::{Uid, UidAllocator};
pub use net_sync::{NetSync, SyncFrom};
pub use packet::{
handle_insert, handle_interp_insert, handle_interp_modify, handle_interp_remove, handle_modify,
handle_remove, CompPacket, CompSyncPackage, EntityPackage, EntitySyncPackage,

View File

@ -0,0 +1,50 @@
//! Types of syncing:
//! * synced from any entity (within range)
//! * synced only from the client's entity
//!
//! Types of updating
//! * Plain copy of the new component state
//! * (unimplemented) Diff to update component, two variants
//! * Keep a full copy of the component and generate diff from that
//! * Intercept changes to the component (no need to compute diff or keep a
//! full copy)
//!
//! NOTE: rapidly updated components like Pos/Vel/Ori are not covered here
/// Trait that must be implemented for most components that are synced over the
/// network.
pub trait NetSync: specs::Component + Clone + Send + Sync
where
Self::Storage: specs::storage::Tracked,
{
// TODO: this scheme theoretically supports diffing withing the
// impl of `From<UpdateFrom> for Update` but there is no automatic
// machinery to provide the `UpdateFrom` value yet. Might need to
// rework this when actuall implementing though.
//
//type UpdateFrom = Self;
//type Update: From<Self::UpdateFrom> = Self;
/// Determines what for entities this component is synced to the client.
///
/// For example, [`SyncFrom::ClientEntity`] can be used to only sync the
/// components for the client's own entity.
const SYNC_FROM: SyncFrom;
// sync::handle_modify(comp, entity, world)
/// Allows making modifications before the synced component is inserted on
/// the client.
fn pre_insert(&mut self, world: &specs::World) { let _world = world; }
/// Allows making modifications before the synced component is overwritten
/// with this version on the client.
fn pre_modify(&mut self, world: &specs::World) { let _world = world; }
}
/// Whether a component is synced to the client for any entity or for just the
/// client's own entity.
pub enum SyncFrom {
AnyEntity,
ClientEntity,
}

View File

@ -168,15 +168,13 @@ impl<P: CompPacket> CompSyncPackage<P> {
.push((uid.into(), CompUpdateKind::Removed(PhantomData::<C>.into())));
}
#[must_use]
pub fn with_component<'a, C: Component + Clone + Send + Sync>(
mut self,
pub fn add_component_updates<'a, C: Component + Clone + Send + Sync>(
&mut self,
uids: &ReadStorage<'a, Uid>,
tracker: &UpdateTracker<C>,
storage: &ReadStorage<'a, C>,
filter: impl Join + Copy,
) -> Self
where
) where
P: From<C>,
C: TryFrom<P>,
P::Phantom: From<PhantomData<C>>,
@ -184,6 +182,29 @@ impl<P: CompPacket> CompSyncPackage<P> {
C::Storage: specs::storage::Tracked,
{
tracker.get_updates_for(uids, storage, filter, &mut self.comp_updates);
self
}
/// If there was an update to the component `C` on the provided entity this
/// will add the update to this package.
pub fn add_component_update<'a, C: Component + Clone + Send + Sync>(
&mut self,
tracker: &UpdateTracker<C>,
storage: &ReadStorage<'a, C>,
uid: u64,
entity: Entity,
) where
P: From<C>,
C: TryFrom<P>,
P::Phantom: From<PhantomData<C>>,
P::Phantom: TryInto<PhantomData<C>>,
C::Storage: specs::storage::Tracked,
{
if let Some(comp_update) = tracker.get_update(storage, entity) {
self.comp_updates.push((uid, comp_update))
}
}
/// Returns whether this package is empty, useful for not sending an empty
/// message.
pub fn is_empty(&self) -> bool { self.comp_updates.is_empty() }
}

View File

@ -17,7 +17,7 @@ impl<C: Component> UpdateTracker<C>
where
C::Storage: specs::storage::Tracked,
{
pub fn new(specs_world: &mut World) -> Self {
pub fn new(specs_world: &World) -> Self {
Self {
reader_id: specs_world.write_storage::<C>().register_reader(),
inserted: BitSet::new(),
@ -122,4 +122,38 @@ impl<C: Component + Clone + Send + Sync> UpdateTracker<C> {
));
}
}
/// Returns `Some(update)` if the tracked component was modified for this
/// entity.
pub fn get_update<'a, P>(
&self,
storage: &specs::ReadStorage<'a, C>,
entity: Entity,
) -> Option<CompUpdateKind<P>>
where
P: CompPacket,
P: From<C>,
C: TryFrom<P>,
P::Phantom: From<PhantomData<C>>,
P::Phantom: TryInto<PhantomData<C>>,
C::Storage: specs::storage::Tracked,
{
let id = entity.id();
// Generate update if one exists.
//
// Note: presence of the id in these bitsets should be mutually exclusive
if self.modified.contains(id) {
storage
.get(entity)
.map(|comp| CompUpdateKind::Modified(P::from(comp.clone())))
} else if self.inserted.contains(id) {
storage
.get(entity)
.map(|comp| CompUpdateKind::Inserted(P::from(comp.clone())))
} else if self.removed.contains(id) {
Some(CompUpdateKind::Removed(P::Phantom::from(PhantomData::<C>)))
} else {
None
}
}
}

View File

@ -0,0 +1,231 @@
//! Contains an "x macro" for all synced components as well as [NetSync]
//! implementations for those components.
//!
//!
//! An x macro accepts another macro as input and calls it with a list of
//! inputs. This allows adding components to the list in the x macro declaration
//! and then writing macros that will accept this list and generate code that
//! handles every synced component without further repitition of the component
//! set.
//!
//! This module also re-exports all the component types that are synced.
//!
//! A glob import from this can be used so that the component types are in scope
//! when using the x macro defined here which requires this.
/// This provides a lowercase name and the component type.
///
/// See [module](self) level docs for more details.
#[macro_export]
macro_rules! synced_components {
($macro:ident) => {
$macro! {
body: Body,
stats: Stats,
buffs: Buffs,
auras: Auras,
energy: Energy,
health: Health,
poise: Poise,
light_emitter: LightEmitter,
item: Item,
scale: Scale,
group: Group,
is_mount: IsMount,
is_rider: IsRider,
mass: Mass,
density: Density,
collider: Collider,
sticky: Sticky,
character_state: CharacterState,
shockwave: Shockwave,
beam_segment: BeamSegment,
alignment: Alignment,
// TODO: evaluate if this is used on the client,
// and if so what it is used for
player: Player,
// TODO: change this to `SyncFrom::ClientEntity` and sync the bare minimum
// from other entities (e.g. just keys needed to show appearance
// based on their loadout). Also, it looks like this actually has
// an alternative sync method implemented in entity_sync via
// ServerGeneral::InventoryUpdate so we could use that instead
// or remove the part where it clones the inventory.
inventory: Inventory,
// TODO: this is used in combat rating calculation in voxygen but we can probably
// remove it from that and then see if it's used for anything else and try to move
// to only being synced for the client's entity.
skill_set: SkillSet,
// Synced to the client only for its own entity
combo: Combo,
active_abilities: ActiveAbilities,
can_build: CanBuild,
}
};
}
macro_rules! reexport_comps {
($($name:ident: $type:ident,)*) => {
mod inner {
pub use common::comp::*;
use common::link::Is;
use common::mounting::{Mount, Rider};
// We alias these because the identifier used for the
// component's type is reused as an enum variant name
// in the macro's that we pass to `synced_components!`.
//
// This is also the reason we need this inner module, since
// we can't just re-export all the types directly from `common::comp`.
pub type IsMount = Is<Mount>;
pub type IsRider = Is<Rider>;
}
// Re-export all the component types. So that uses of `synced_components!` outside this
// module can bring them into scope with a single glob import.
$(pub use inner::$type;)*
}
}
// Pass `reexport_comps` macro to the "x macro" which will invoke it with a list
// of components.
//
// Note: this brings all these components into scope for the implementations
// below.
synced_components!(reexport_comps);
// ===============================
// === NetSync implementations ===
// ===============================
use crate::sync::{NetSync, SyncFrom};
// These are synced from any entity within range.
impl NetSync for Body {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Stats {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Buffs {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Auras {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Energy {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Health {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
fn pre_insert(&mut self, world: &specs::World) {
use common::resources::Time;
use specs::WorldExt;
// Time isn't synced between client and server so replace the Time from the
// server with the Client's local Time to enable accurate comparison.
self.last_change.time = *world.read_resource::<Time>();
}
fn pre_modify(&mut self, world: &specs::World) {
use common::resources::Time;
use specs::WorldExt;
// Time isn't synced between client and server so replace the Time from the
// server with the Client's local Time to enable accurate comparison.
self.last_change.time = *world.read_resource::<Time>();
}
}
impl NetSync for Poise {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for LightEmitter {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Item {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Scale {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Group {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for IsMount {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for IsRider {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Mass {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Density {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Collider {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Sticky {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for CharacterState {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Shockwave {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for BeamSegment {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Alignment {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Player {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for Inventory {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
impl NetSync for SkillSet {
const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity;
}
// These are synced only from the client's own entity.
impl NetSync for Combo {
const SYNC_FROM: SyncFrom = SyncFrom::ClientEntity;
}
impl NetSync for ActiveAbilities {
const SYNC_FROM: SyncFrom = SyncFrom::ClientEntity;
}
impl NetSync for CanBuild {
const SYNC_FROM: SyncFrom = SyncFrom::ClientEntity;
}

View File

@ -4,7 +4,7 @@ use specs::{Component, DerefFlaggedStorage};
use specs_idvs::IdvStorage;
use std::ops::Mul;
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
/// Energy is represented by u32s within the module, but treated as a float by
/// the rest of the game.
// As a general rule, all input and output values to public functions should be
@ -54,14 +54,31 @@ impl Energy {
/// Returns the fraction of energy an entity has remaining
pub fn fraction(&self) -> f32 { self.current() / self.maximum().max(1.0) }
/// Updates the maximum value for energy
pub fn update_maximum(&mut self, modifiers: comp::stats::StatsModifier) {
/// Calculates a new maximum value and returns it if the value differs from
/// the current maximum.
///
/// Note: The returned value uses an internal format so don't expect it to
/// be useful for anything other than a parameter to
/// [`Self::update_maximum`].
pub fn needs_maximum_update(&self, modifiers: comp::stats::StatsModifier) -> Option<u32> {
let maximum = modifiers
.compute_maximum(self.base_max())
.mul(Self::SCALING_FACTOR_FLOAT)
// NaN does not need to be handled here as rust will automatically change to 0 when casting to u32
.clamp(0.0, Self::MAX_SCALED_ENERGY as f32) as u32;
(maximum != self.maximum).then(|| maximum)
}
/// Updates the maximum value for energy.
///
/// Note: The accepted `u32` value is in the internal format of this type.
/// So attempting to pass values that weren't returned from
/// [`Self::needs_maximum_update`] can produce strange or unexpected
/// results.
pub fn update_internal_integer_maximum(&mut self, maximum: u32) {
self.maximum = maximum;
// Clamp the current energy to enforce the current <= maximum invariant.
self.current = self.current.min(self.maximum);
}

View File

@ -90,13 +90,29 @@ impl Health {
/// Returns the fraction of health an entity has remaining
pub fn fraction(&self) -> f32 { self.current() / self.maximum().max(1.0) }
/// Updates the maximum value for health
pub fn update_maximum(&mut self, modifiers: comp::stats::StatsModifier) {
/// Calculates a new maximum value and returns it if the value differs from
/// the current maximum.
///
/// Note: The returned value uses an internal format so don't expect it to
/// be useful for anything other than a parameter to
/// [`Self::update_maximum`].
pub fn needs_maximum_update(&self, modifiers: comp::stats::StatsModifier) -> Option<u32> {
let maximum = modifiers
.compute_maximum(self.base_max())
.mul(Self::SCALING_FACTOR_FLOAT)
// NaN does not need to be handled here as rust will automatically change to 0 when casting to u32
.clamp(0.0, Self::MAX_SCALED_HEALTH as f32) as u32;
(maximum != self.maximum).then(|| maximum)
}
/// Updates the maximum value for health.
///
/// Note: The accepted `u32` value is in the internal format of this type.
/// So attempting to pass values that weren't returned from
/// [`Self::needs_maximum_update`] can produce strange or unexpected
/// results.
pub fn update_internal_integer_maximum(&mut self, maximum: u32) {
self.maximum = maximum;
// Clamp the current health to enforce the current <= maximum invariant.
self.current = self.current.min(self.maximum);

View File

@ -30,6 +30,7 @@ impl StatsModifier {
base_value * self.mult_mod + self.add_mod
}
// Note: unused for now
pub fn update_maximum(&self) -> bool {
self.add_mod.abs() > f32::EPSILON || (self.mult_mod - 1.0).abs() > f32::EPSILON
}

View File

@ -139,7 +139,7 @@ pub struct JoinStruct<'a> {
pub vel: &'a mut Vel,
pub ori: &'a mut Ori,
pub mass: &'a Mass,
pub density: &'a mut Density,
pub density: FlaggedAccessMut<'a, &'a mut Density, Density>,
pub energy: FlaggedAccessMut<'a, &'a mut Energy, Energy>,
pub inventory: Option<&'a Inventory>,
pub controller: &'a mut Controller,
@ -171,7 +171,7 @@ impl<'a> JoinData<'a> {
vel: j.vel,
ori: j.ori,
mass: j.mass,
density: j.density,
density: &j.density,
energy: &j.energy,
inventory: j.inventory,
controller: j.controller,

View File

@ -105,7 +105,7 @@ impl<'a> System<'a> for Sys {
vel,
ori,
mass,
mut density,
density,
energy,
inventory,
controller,
@ -180,7 +180,7 @@ impl<'a> System<'a> for Sys {
vel,
ori,
mass,
density: &mut density,
density,
energy,
inventory,
controller,
@ -238,15 +238,28 @@ impl Sys {
mut state_update: StateUpdate,
output_events: &mut OutputEvents,
) {
// TODO: if checking equality is expensive use optional field in StateUpdate
// Here we check for equality with the previous value of these components before
// updating them so that the modification detection will not be
// triggered unnecessarily. This is important for minimizing updates
// sent to the clients (and thus keeping bandwidth usage down).
//
// TODO: if checking equality is expensive for char_state use optional field in
// StateUpdate
if *join.char_state != state_update.character {
*join.char_state = state_update.character
}
if *join.density != state_update.density {
*join.density = state_update.density
}
if *join.energy != state_update.energy {
*join.energy = state_update.energy;
};
// These components use a different type of change detection.
*join.pos = state_update.pos;
*join.vel = state_update.vel;
*join.ori = state_update.ori;
*join.density = state_update.density;
*join.energy = state_update.energy;
join.controller
.queued_inputs
.append(&mut state_update.queued_inputs);

View File

@ -92,32 +92,23 @@ impl<'a> System<'a> for Sys {
}
let stat = stats;
let update_max_hp = {
stat.max_health_modifiers.update_maximum()
|| (health.base_max() - health.maximum()).abs() > Health::HEALTH_EPSILON
};
if update_max_hp {
health.update_maximum(stat.max_health_modifiers);
if let Some(new_max) = health.needs_maximum_update(stat.max_health_modifiers) {
// Only call this if we need to since mutable access will trigger sending an
// update to the client.
health.update_internal_integer_maximum(new_max);
}
let (change_energy, energy_mods) = {
// Calculates energy scaling from stats and inventory
let energy_mods = StatsModifier {
add_mod: stat.max_energy_modifiers.add_mod
+ combat::compute_max_energy_mod(inventory),
mult_mod: stat.max_energy_modifiers.mult_mod,
};
(
energy_mods.update_maximum()
|| (energy.base_max() - energy.maximum()).abs() > Energy::ENERGY_EPSILON,
energy_mods,
)
// Calculates energy scaling from stats and inventory
let energy_mods = StatsModifier {
add_mod: stat.max_energy_modifiers.add_mod
+ combat::compute_max_energy_mod(inventory),
mult_mod: stat.max_energy_modifiers.mult_mod,
};
// If modifier sufficiently different, mutably access energy
if change_energy {
energy.update_maximum(energy_mods);
if let Some(new_max) = energy.needs_maximum_update(energy_mods) {
// Only call this if we need to since mutable access will trigger sending an
// update to the client.
energy.update_internal_integer_maximum(new_max);
}
}

View File

@ -60,7 +60,7 @@ use crate::{
presence::{Presence, RegionSubscription, RepositionOnChunkLoad},
rtsim::RtSim,
state_ext::StateExt,
sys::sentinel::{DeletedEntities, TrackedComps},
sys::sentinel::{DeletedEntities, TrackedStorages},
};
#[cfg(not(feature = "worldgen"))]
use common::grid::Grid;
@ -447,7 +447,7 @@ impl Server {
state.ecs_mut().write_resource::<TimeOfDay>().0 = settings.start_time;
// Register trackers
sys::sentinel::register_trackers(state.ecs_mut());
sys::sentinel::UpdateTrackers::register(state.ecs_mut());
state.ecs_mut().insert(DeletedEntities::default());
@ -1025,7 +1025,7 @@ impl Server {
)
.send(ServerInit::GameSync {
// Send client their entity
entity_package: TrackedComps::fetch(self.state.ecs())
entity_package: TrackedStorages::fetch(self.state.ecs())
.create_entity_package(entity, None, None, None)
.expect(
"We just created this entity as marked() (using create_entity_synced) so \

View File

@ -1,4 +1,4 @@
use super::sentinel::{DeletedEntities, ReadTrackers, TrackedComps};
use super::sentinel::{DeletedEntities, TrackedStorages, UpdateTrackers};
use crate::{
client::Client,
presence::{Presence, RegionSubscription},
@ -6,9 +6,7 @@ use crate::{
};
use common::{
calendar::Calendar,
comp::{Collider, ForceUpdate, Inventory, InventoryUpdate, Last, Ori, Player, Pos, Vel},
link::Is,
mounting::Rider,
comp::{Collider, ForceUpdate, InventoryUpdate, Last, Ori, Pos, Vel},
outcome::Outcome,
region::{Event as RegionEvent, RegionMap},
resources::{PlayerPhysicsSettings, TimeOfDay},
@ -31,30 +29,25 @@ impl<'a> System<'a> for Sys {
type SystemData = (
Entities<'a>,
Read<'a, Tick>,
Read<'a, PlayerPhysicsSettings>,
TrackedStorages<'a>,
ReadExpect<'a, TimeOfDay>,
ReadExpect<'a, Calendar>,
ReadExpect<'a, RegionMap>,
ReadStorage<'a, Uid>,
ReadExpect<'a, UpdateTrackers>,
ReadStorage<'a, Pos>,
ReadStorage<'a, Vel>,
ReadStorage<'a, Ori>,
ReadStorage<'a, Inventory>,
ReadStorage<'a, RegionSubscription>,
ReadStorage<'a, Presence>,
ReadStorage<'a, Collider>,
ReadStorage<'a, Client>,
WriteStorage<'a, Last<Pos>>,
WriteStorage<'a, Last<Vel>>,
WriteStorage<'a, Last<Ori>>,
ReadStorage<'a, Client>,
WriteStorage<'a, ForceUpdate>,
WriteStorage<'a, InventoryUpdate>,
Write<'a, DeletedEntities>,
Write<'a, Vec<Outcome>>,
Read<'a, PlayerPhysicsSettings>,
ReadStorage<'a, Is<Rider>>,
ReadStorage<'a, Player>,
TrackedComps<'a>,
ReadTrackers<'a>,
);
const NAME: &'static str = "entity_sync";
@ -66,33 +59,37 @@ impl<'a> System<'a> for Sys {
(
entities,
tick,
player_physics_settings,
tracked_storages,
time_of_day,
calendar,
region_map,
uids,
trackers,
positions,
velocities,
orientations,
inventories,
subscriptions,
presences,
colliders,
clients,
mut last_pos,
mut last_vel,
mut last_ori,
clients,
mut force_updates,
mut inventory_updates,
mut deleted_entities,
mut outcomes,
player_physics_settings,
is_rider,
players,
tracked_comps,
trackers,
): Self::SystemData,
) {
let tick = tick.0;
// Storages already provided in `TrackedStorages` that we need to use
// for other things besides change detection.
let uids = &tracked_storages.uid;
let colliders = &tracked_storages.collider;
let inventories = &tracked_storages.inventory;
let players = &tracked_storages.player;
let is_rider = &tracked_storages.is_rider;
// To send entity updates
// 1. Iterate through regions
// 2. Iterate through region subscribers (ie clients)
@ -161,7 +158,7 @@ impl<'a> System<'a> for Sys {
.get(entity)
.map(|pos| (pos, velocities.get(entity), orientations.get(entity)))
.and_then(|(pos, vel, ori)| {
tracked_comps.create_entity_package(
tracked_storages.create_entity_package(
entity,
Some(*pos),
vel.copied(),
@ -203,7 +200,7 @@ impl<'a> System<'a> for Sys {
// Sync tracked components
// Get deleted entities in this region from DeletedEntities
let (entity_sync_package, comp_sync_package) = trackers.create_sync_packages(
&tracked_comps,
&tracked_storages,
region.entities(),
deleted_entities_in_region,
);
@ -232,7 +229,7 @@ impl<'a> System<'a> for Sys {
for (_, entity, &uid, (&pos, last_pos), vel, ori, force_update, collider) in (
region.entities(),
&entities,
&uids,
uids,
(&positions, last_pos.mask().maybe()),
(&velocities, last_vel.mask().maybe()).maybe(),
(&orientations, last_vel.mask().maybe()).maybe(),
@ -353,13 +350,22 @@ impl<'a> System<'a> for Sys {
// TODO: Sync clients that don't have a position?
// Sync inventories
for (inventory, update, client) in (&inventories, &inventory_updates, &clients).join() {
for (inventory, update, client) in (inventories, &inventory_updates, &clients).join() {
client.send_fallible(ServerGeneral::InventoryUpdate(
inventory.clone(),
update.event(),
));
}
// Sync components that are only synced for the client's own entity.
for (entity, client) in (&entities, &clients).join() {
let comp_sync_package =
trackers.create_sync_from_client_package(&tracked_storages, entity);
if !comp_sync_package.is_empty() {
client.send_fallible(ServerGeneral::CompSync(comp_sync_package));
}
}
// Sync outcomes
for (presence, pos, client) in (presences.maybe(), positions.maybe(), &clients).join() {
let is_near = |o_pos: Vec3<f32>| {

View File

@ -2,18 +2,14 @@
use common::{
comp::{
item::{tool::AbilityMap, MaterialStatManifest},
ActiveAbilities, Alignment, Auras, BeamSegment, Body, Buffs, CanBuild, CharacterState,
Collider, Combo, Density, Energy, Group, Health, Inventory, Item, LightEmitter, Mass, Ori,
Player, Poise, Pos, Scale, Shockwave, SkillSet, Stats, Sticky, Vel,
Ori, Pos, Vel,
},
link::Is,
mounting::{Mount, Rider},
uid::Uid,
};
use common_ecs::{Job, Origin, Phase, System};
use common_net::{
msg::EcsCompPacket,
sync::{CompSyncPackage, EntityPackage, EntitySyncPackage, UpdateTracker, WorldSyncExt},
sync::{CompSyncPackage, EntityPackage, EntitySyncPackage, NetSync, SyncFrom, UpdateTracker},
};
use hashbrown::HashMap;
use specs::{
@ -28,400 +24,217 @@ use vek::*;
#[derive(Default)]
pub struct Sys;
impl<'a> System<'a> for Sys {
type SystemData = (TrackedComps<'a>, WriteTrackers<'a>);
type SystemData = (TrackedStorages<'a>, WriteExpect<'a, UpdateTrackers>);
const NAME: &'static str = "sentinel";
const ORIGIN: Origin = Origin::Server;
const PHASE: Phase = Phase::Create;
fn run(_job: &mut Job<Self>, (comps, mut trackers): Self::SystemData) {
record_changes(&comps, &mut trackers);
fn run(_job: &mut Job<Self>, (storages, mut trackers): Self::SystemData) {
trackers.record_changes(&storages);
}
}
// Probably more difficult than it needs to be :p
#[derive(SystemData)]
pub struct TrackedComps<'a> {
pub uid: ReadStorage<'a, Uid>,
pub body: ReadStorage<'a, Body>,
pub player: ReadStorage<'a, Player>,
pub stats: ReadStorage<'a, Stats>,
pub skill_set: ReadStorage<'a, SkillSet>,
pub active_abilities: ReadStorage<'a, ActiveAbilities>,
pub buffs: ReadStorage<'a, Buffs>,
pub auras: ReadStorage<'a, Auras>,
pub energy: ReadStorage<'a, Energy>,
pub combo: ReadStorage<'a, Combo>,
pub health: ReadStorage<'a, Health>,
pub poise: ReadStorage<'a, Poise>,
pub can_build: ReadStorage<'a, CanBuild>,
pub light_emitter: ReadStorage<'a, LightEmitter>,
pub item: ReadStorage<'a, Item>,
pub scale: ReadStorage<'a, Scale>,
pub is_mount: ReadStorage<'a, Is<Mount>>,
pub is_rider: ReadStorage<'a, Is<Rider>>,
pub group: ReadStorage<'a, Group>,
pub mass: ReadStorage<'a, Mass>,
pub density: ReadStorage<'a, Density>,
pub collider: ReadStorage<'a, Collider>,
pub sticky: ReadStorage<'a, Sticky>,
pub inventory: ReadStorage<'a, Inventory>,
pub character_state: ReadStorage<'a, CharacterState>,
pub shockwave: ReadStorage<'a, Shockwave>,
pub beam_segment: ReadStorage<'a, BeamSegment>,
pub alignment: ReadStorage<'a, Alignment>,
/// Holds state like modified bitsets, modification event readers
macro_rules! trackers {
// Every place where we have `$( /* ... */ )*` will be repeated for each synced component.
($($component_name:ident: $component_type:ident,)*) => {
#[derive(SystemData)]
pub struct TrackedStorages<'a> {
// Uids are tracked to detect created entities that should be synced over the network.
// Additionally we need access to the uids when generating packets to send to the clients.
pub uid: ReadStorage<'a, Uid>,
$(pub $component_name: ReadStorage<'a, $component_type>,)*
// TODO: these may be used to duplicate items when we attempt to remove
// cloning them.
pub _ability_map: ReadExpect<'a, AbilityMap>,
pub _msm: ReadExpect<'a, MaterialStatManifest>,
}
pub ability_map: ReadExpect<'a, AbilityMap>,
pub msm: ReadExpect<'a, MaterialStatManifest>,
}
impl<'a> TrackedComps<'a> {
pub fn create_entity_package(
&self,
entity: EcsEntity,
pos: Option<Pos>,
vel: Option<Vel>,
ori: Option<Ori>,
) -> Option<EntityPackage<EcsCompPacket>> {
let uid = self.uid.get(entity).copied()?.0;
let mut comps = Vec::new();
self.body.get(entity).copied().map(|c| comps.push(c.into()));
self.player
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.stats
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.skill_set
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.active_abilities
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.buffs
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.auras
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.energy
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.combo
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.health
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.poise
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.can_build
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.light_emitter
.get(entity)
.copied()
.map(|c| comps.push(c.into()));
self.item
.get(entity)
.map(|item| item.duplicate(&self.ability_map, &self.msm))
.map(|c| comps.push(c.into()));
self.scale
.get(entity)
.copied()
.map(|c| comps.push(c.into()));
self.is_mount
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.is_rider
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.group
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.mass.get(entity).copied().map(|c| comps.push(c.into()));
self.density
.get(entity)
.copied()
.map(|c| comps.push(c.into()));
self.collider
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.sticky
.get(entity)
.copied()
.map(|c| comps.push(c.into()));
self.inventory
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.character_state
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.shockwave
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.beam_segment
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.alignment
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
// Add untracked comps
pos.map(|c| comps.push(c.into()));
vel.map(|c| comps.push(c.into()));
ori.map(|c| comps.push(c.into()));
impl TrackedStorages<'_> {
/// Create a package containing all the synced components for this entity. This is
/// used to initialized the entity's representation on the client (e.g. used when a new
/// entity is within the area synced to the client).
///
/// Note: This is only for components that are synced to the client for all entities.
pub fn create_entity_package(
&self,
entity: EcsEntity,
pos: Option<Pos>,
vel: Option<Vel>,
ori: Option<Ori>,
) -> Option<EntityPackage<EcsCompPacket>> {
let uid = self.uid.get(entity).copied()?.0;
let mut comps = Vec::new();
// NOTE: we could potentially include a bitmap indicating which components are present instead of tagging
// components with the type in order to save bandwidth
//
// if the number of optional components sent is less than 1/8 of the number of component types then
// then the suggested approach would no longer be favorable
$(
// Only add components that are synced from any entity.
if matches!(
<$component_type as NetSync>::SYNC_FROM,
SyncFrom::AnyEntity,
) {
self
.$component_name
.get(entity)
// TODO: should duplicate rather than clone the item
//
// NetClone trait?
//
//.map(|item| item.duplicate(&self.ability_map, &self.msm))
.cloned()
.map(|c| comps.push(c.into()));
}
)*
// Add untracked comps
pos.map(|c| comps.push(c.into()));
vel.map(|c| comps.push(c.into()));
ori.map(|c| comps.push(c.into()));
Some(EntityPackage { uid, comps })
}
}
#[derive(SystemData)]
pub struct ReadTrackers<'a> {
pub uid: ReadExpect<'a, UpdateTracker<Uid>>,
pub body: ReadExpect<'a, UpdateTracker<Body>>,
pub player: ReadExpect<'a, UpdateTracker<Player>>,
pub stats: ReadExpect<'a, UpdateTracker<Stats>>,
pub skill_set: ReadExpect<'a, UpdateTracker<SkillSet>>,
pub active_abilities: ReadExpect<'a, UpdateTracker<ActiveAbilities>>,
pub buffs: ReadExpect<'a, UpdateTracker<Buffs>>,
pub auras: ReadExpect<'a, UpdateTracker<Auras>>,
pub energy: ReadExpect<'a, UpdateTracker<Energy>>,
pub combo: ReadExpect<'a, UpdateTracker<Combo>>,
pub health: ReadExpect<'a, UpdateTracker<Health>>,
pub poise: ReadExpect<'a, UpdateTracker<Poise>>,
pub can_build: ReadExpect<'a, UpdateTracker<CanBuild>>,
pub light_emitter: ReadExpect<'a, UpdateTracker<LightEmitter>>,
pub inventory: ReadExpect<'a, UpdateTracker<Inventory>>,
pub item: ReadExpect<'a, UpdateTracker<Item>>,
pub scale: ReadExpect<'a, UpdateTracker<Scale>>,
pub is_mount: ReadExpect<'a, UpdateTracker<Is<Mount>>>,
pub is_rider: ReadExpect<'a, UpdateTracker<Is<Rider>>>,
pub group: ReadExpect<'a, UpdateTracker<Group>>,
pub mass: ReadExpect<'a, UpdateTracker<Mass>>,
pub density: ReadExpect<'a, UpdateTracker<Density>>,
pub collider: ReadExpect<'a, UpdateTracker<Collider>>,
pub sticky: ReadExpect<'a, UpdateTracker<Sticky>>,
pub character_state: ReadExpect<'a, UpdateTracker<CharacterState>>,
pub shockwave: ReadExpect<'a, UpdateTracker<Shockwave>>,
pub beam_segment: ReadExpect<'a, UpdateTracker<BeamSegment>>,
pub alignment: ReadExpect<'a, UpdateTracker<Alignment>>,
}
impl<'a> ReadTrackers<'a> {
pub fn create_sync_packages(
&self,
comps: &TrackedComps,
filter: impl Join + Copy,
deleted_entities: Vec<u64>,
) -> (EntitySyncPackage, CompSyncPackage<EcsCompPacket>) {
let entity_sync_package =
EntitySyncPackage::new(&comps.uid, &self.uid, filter, deleted_entities);
let comp_sync_package = CompSyncPackage::new()
.with_component(&comps.uid, &*self.body, &comps.body, filter)
.with_component(&comps.uid, &*self.player, &comps.player, filter)
.with_component(&comps.uid, &*self.stats, &comps.stats, filter)
.with_component(&comps.uid, &*self.skill_set, &comps.skill_set, filter)
.with_component(
&comps.uid,
&*self.active_abilities,
&comps.active_abilities,
filter,
)
.with_component(&comps.uid, &*self.buffs, &comps.buffs, filter)
.with_component(&comps.uid, &*self.auras, &comps.auras, filter)
.with_component(&comps.uid, &*self.energy, &comps.energy, filter)
.with_component(&comps.uid, &*self.combo, &comps.combo, filter)
.with_component(&comps.uid, &*self.health, &comps.health, filter)
.with_component(&comps.uid, &*self.poise, &comps.poise, filter)
.with_component(&comps.uid, &*self.can_build, &comps.can_build, filter)
.with_component(
&comps.uid,
&*self.light_emitter,
&comps.light_emitter,
filter,
)
.with_component(&comps.uid, &*self.item, &comps.item, filter)
.with_component(&comps.uid, &*self.scale, &comps.scale, filter)
.with_component(&comps.uid, &*self.is_mount, &comps.is_mount, filter)
.with_component(&comps.uid, &*self.is_rider, &comps.is_rider, filter)
.with_component(&comps.uid, &*self.group, &comps.group, filter)
.with_component(&comps.uid, &*self.mass, &comps.mass, filter)
.with_component(&comps.uid, &*self.density, &comps.density, filter)
.with_component(&comps.uid, &*self.collider, &comps.collider, filter)
.with_component(&comps.uid, &*self.sticky, &comps.sticky, filter)
.with_component(&comps.uid, &*self.inventory, &comps.inventory, filter)
.with_component(
&comps.uid,
&*self.character_state,
&comps.character_state,
filter,
)
.with_component(&comps.uid, &*self.shockwave, &comps.shockwave, filter)
.with_component(&comps.uid, &*self.beam_segment, &comps.beam_segment, filter)
.with_component(&comps.uid, &*self.alignment, &comps.alignment, filter);
Some(EntityPackage { uid, comps })
}
}
(entity_sync_package, comp_sync_package)
/// Contains an [`UpdateTracker`] for every synced component (that uses this method of
/// change detection).
///
/// This should be inserted into the ecs as a Resource
pub struct UpdateTrackers {
pub uid: UpdateTracker<Uid>,
$($component_name: UpdateTracker<$component_type>,)*
}
impl UpdateTrackers {
/// Constructs the update trackers and inserts it into the world as a resource.
///
/// Components that will be synced must already be registered.
pub fn register(world: &mut specs::World) {
let trackers = UpdateTrackers {
uid: UpdateTracker::<Uid>::new(&world),
$($component_name: UpdateTracker::<$component_type>::new(&world),)*
};
world.insert(trackers);
// TODO: if we held copies of components for doing diffing, the components that hold that data could be registered here
}
/// Records updates to components that are provided from the tracked storages as a series of events into bitsets
/// that can later be joined on.
fn record_changes(&mut self, comps: &TrackedStorages) {
self.uid.record_changes(&comps.uid);
$(
self.$component_name.record_changes(&comps.$component_name);
)*
// Enable for logging of counts of component update events.
const LOG_COUNTS: bool = false;
// Plotting counts via tracy. Env var provided to toggle on so there's no need to
// recompile if you are already have a tracy build.
let plot_counts = common_base::TRACY_ENABLED && matches!(std::env::var("PLOT_UPDATE_COUNTS").as_deref(), Ok("1"));
macro_rules! log_counts {
($comp:ident, $name:expr) => {
if LOG_COUNTS || plot_counts {
let tracker = &self.$comp;
let inserted = tracker.inserted().into_iter().count();
let modified = tracker.modified().into_iter().count();
let removed = tracker.removed().into_iter().count();
if plot_counts {
let sum = inserted + modified + removed;
common_base::plot!(concat!($name, "updates"), sum as f64);
}
if LOG_COUNTS {
tracing::warn!("{:6} insertions detected for {}", inserted, $name);
tracing::warn!("{:6} modifications detected for {}", modified, $name);
tracing::warn!("{:6} deletions detected for {}", removed, $name);
}
}
};
}
$(log_counts!($component_name, concat!(stringify!($component_name), 's'));)*
}
/// Create a [`EntitySyncPackage`] and a [`CompSyncPackage`] to provide updates
/// for the set entities specified by the provided filter (e.g. for a region).
///
/// A deleted entities must be externally constructed and provided here.
pub fn create_sync_packages(
&self,
comps: &TrackedStorages,
filter: impl Join + Copy,
deleted_entities: Vec<u64>,
) -> (EntitySyncPackage, CompSyncPackage<EcsCompPacket>) {
let entity_sync_package =
EntitySyncPackage::new(&comps.uid, &self.uid, filter, deleted_entities);
let mut comp_sync_package = CompSyncPackage::new();
$(
if matches!(
<$component_type as NetSync>::SYNC_FROM,
SyncFrom::AnyEntity,
) {
comp_sync_package.add_component_updates(
&comps.uid,
&self.$component_name,
&comps.$component_name,
filter,
);
}
)*
(entity_sync_package, comp_sync_package)
}
/// Create sync package for components that are only synced for the client's entity.
pub fn create_sync_from_client_package(
&self,
comps: &TrackedStorages,
entity: specs::Entity,
) -> CompSyncPackage<EcsCompPacket> {
// TODO: this type repeats the entity uid for each component but
// we know they will all be the same here, using it for now for
// convenience but it could help to make a specific type for this
// later.
let mut comp_sync_package = CompSyncPackage::new();
let uid = match comps.uid.get(entity) {
Some(uid) => (*uid).into(),
// Return empty package if we can't get uid for this entity
None => return comp_sync_package,
};
$(
if matches!(
<$component_type as NetSync>::SYNC_FROM,
SyncFrom::ClientEntity,
) {
comp_sync_package.add_component_update(
&self.$component_name,
&comps.$component_name,
uid,
entity,
);
}
)*
comp_sync_package
}
}
}
}
#[derive(SystemData)]
pub struct WriteTrackers<'a> {
uid: WriteExpect<'a, UpdateTracker<Uid>>,
body: WriteExpect<'a, UpdateTracker<Body>>,
player: WriteExpect<'a, UpdateTracker<Player>>,
stats: WriteExpect<'a, UpdateTracker<Stats>>,
skill_set: WriteExpect<'a, UpdateTracker<SkillSet>>,
active_abilities: WriteExpect<'a, UpdateTracker<ActiveAbilities>>,
buffs: WriteExpect<'a, UpdateTracker<Buffs>>,
auras: WriteExpect<'a, UpdateTracker<Auras>>,
energy: WriteExpect<'a, UpdateTracker<Energy>>,
combo: WriteExpect<'a, UpdateTracker<Combo>>,
health: WriteExpect<'a, UpdateTracker<Health>>,
poise: WriteExpect<'a, UpdateTracker<Poise>>,
can_build: WriteExpect<'a, UpdateTracker<CanBuild>>,
light_emitter: WriteExpect<'a, UpdateTracker<LightEmitter>>,
item: WriteExpect<'a, UpdateTracker<Item>>,
scale: WriteExpect<'a, UpdateTracker<Scale>>,
is_mounts: WriteExpect<'a, UpdateTracker<Is<Mount>>>,
is_riders: WriteExpect<'a, UpdateTracker<Is<Rider>>>,
group: WriteExpect<'a, UpdateTracker<Group>>,
mass: WriteExpect<'a, UpdateTracker<Mass>>,
density: WriteExpect<'a, UpdateTracker<Density>>,
collider: WriteExpect<'a, UpdateTracker<Collider>>,
sticky: WriteExpect<'a, UpdateTracker<Sticky>>,
inventory: WriteExpect<'a, UpdateTracker<Inventory>>,
character_state: WriteExpect<'a, UpdateTracker<CharacterState>>,
shockwave: WriteExpect<'a, UpdateTracker<Shockwave>>,
beam: WriteExpect<'a, UpdateTracker<BeamSegment>>,
alignment: WriteExpect<'a, UpdateTracker<Alignment>>,
}
fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
// Update trackers
trackers.uid.record_changes(&comps.uid);
trackers.body.record_changes(&comps.body);
trackers.player.record_changes(&comps.player);
trackers.stats.record_changes(&comps.stats);
trackers.skill_set.record_changes(&comps.skill_set);
trackers
.active_abilities
.record_changes(&comps.active_abilities);
trackers.buffs.record_changes(&comps.buffs);
trackers.auras.record_changes(&comps.auras);
trackers.energy.record_changes(&comps.energy);
trackers.combo.record_changes(&comps.combo);
trackers.health.record_changes(&comps.health);
trackers.poise.record_changes(&comps.poise);
trackers.can_build.record_changes(&comps.can_build);
trackers.light_emitter.record_changes(&comps.light_emitter);
trackers.item.record_changes(&comps.item);
trackers.scale.record_changes(&comps.scale);
trackers.is_mounts.record_changes(&comps.is_mount);
trackers.is_riders.record_changes(&comps.is_rider);
trackers.group.record_changes(&comps.group);
trackers.mass.record_changes(&comps.mass);
trackers.density.record_changes(&comps.density);
trackers.collider.record_changes(&comps.collider);
trackers.sticky.record_changes(&comps.sticky);
trackers.inventory.record_changes(&comps.inventory);
trackers
.character_state
.record_changes(&comps.character_state);
trackers.shockwave.record_changes(&comps.shockwave);
trackers.beam.record_changes(&comps.beam_segment);
trackers.alignment.record_changes(&comps.alignment);
// Debug how many updates are being sent
/*
macro_rules! log_counts {
($comp:ident, $name:expr) => {
// Note: if this will be used in actual server it would be more efficient to
// count during record_changes
let tracker = &trackers.$comp;
let inserted = tracker.inserted().into_iter().count();
let modified = tracker.modified().into_iter().count();
let removed = tracker.removed().into_iter().count();
tracing::warn!("{:6} insertions detected for {}", inserted, $name);
tracing::warn!("{:6} modifications detected for {}", modified, $name);
tracing::warn!("{:6} deletions detected for {}", removed, $name);
};
};
log_counts!(uid, "Uids");
log_counts!(body, "Bodies");
log_counts!(buffs, "Buffs");
log_counts!(auras, "Auras");
log_counts!(player, "Players");
log_counts!(stats, "Stats");
log_counts!(skill_set, "SkillSet");
log_counts!(active_abilities, "ActiveAbilities");
log_counts!(energy, "Energies");
log_counts!(combo, "Combos");
log_vounts!(health, "Healths");
log_vounts!(poise, "Poises");
log_counts!(light_emitter, "Light emitters");
log_counts!(item, "Items");
log_counts!(scale, "Scales");
log_counts!(is_mounts, "mounts");
log_counts!(is_riders, "riders");
log_counts!(mass, "Masses");
log_counts!(mass, "Densities");
log_counts!(collider, "Colliders");
log_counts!(sticky, "Stickies");
log_counts!(loadout, "Loadouts");
log_counts!(character_state, "Character States");
log_counts!(shockwave, "Shockwaves");
log_counts!(beam, "Beams");
log_counts!(alignment, "Alignments");
*/
}
pub fn register_trackers(world: &mut World) {
world.register_tracker::<Uid>();
world.register_tracker::<Body>();
world.register_tracker::<Player>();
world.register_tracker::<Stats>();
world.register_tracker::<SkillSet>();
world.register_tracker::<ActiveAbilities>();
world.register_tracker::<Buffs>();
world.register_tracker::<Auras>();
world.register_tracker::<Energy>();
world.register_tracker::<Combo>();
world.register_tracker::<Health>();
world.register_tracker::<Poise>();
world.register_tracker::<CanBuild>();
world.register_tracker::<LightEmitter>();
world.register_tracker::<Item>();
world.register_tracker::<Scale>();
world.register_tracker::<Is<Mount>>();
world.register_tracker::<Is<Rider>>();
world.register_tracker::<Group>();
world.register_tracker::<Mass>();
world.register_tracker::<Density>();
world.register_tracker::<Collider>();
world.register_tracker::<Sticky>();
world.register_tracker::<Inventory>();
world.register_tracker::<CharacterState>();
world.register_tracker::<Shockwave>();
world.register_tracker::<BeamSegment>();
world.register_tracker::<Alignment>();
}
// Import all the component types so they will be available when expanding the
// macro below.
use common_net::synced_components::*;
// Pass `trackers!` macro to this "x macro" which will invoke it with a list
// of components. This will declare the types defined in the macro above.
common_net::synced_components!(trackers);
/// Deleted entities grouped by region
pub struct DeletedEntities {

View File

@ -1,4 +1,4 @@
use super::sentinel::{DeletedEntities, TrackedComps};
use super::sentinel::{DeletedEntities, TrackedStorages};
use crate::{
client::Client,
presence::{self, Presence, RegionSubscription},
@ -34,7 +34,7 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, Client>,
WriteStorage<'a, RegionSubscription>,
Read<'a, DeletedEntities>,
TrackedComps<'a>,
TrackedStorages<'a>,
);
const NAME: &'static str = "subscription";
@ -222,7 +222,7 @@ pub fn initialize_region_subscription(world: &World, entity: specs::Entity) {
);
let region_map = world.read_resource::<RegionMap>();
let tracked_comps = TrackedComps::fetch(world);
let tracked_comps = TrackedStorages::fetch(world);
for key in &regions {
if let Some(region) = region_map.get(*key) {
(