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

Add field to Presence to control when an entity is synced to other clients

See merge request veloren/veloren!3972
This commit is contained in:
Imbris 2023-08-16 00:10:35 +00:00
commit e8361d5abf
8 changed files with 230 additions and 100 deletions

View File

@ -55,6 +55,18 @@ impl PresenceKind {
None
}
}
/// Controls whether this entity is synced to other clients.
///
/// Note, if it ends up being useful this could be generalized to an
/// independent component that is required for any entity to be synced
/// (as an independent component it could use NullStorage).
pub fn sync_me(&self) -> bool {
match self {
Self::Spectator | Self::LoadingCharacter(_) => false,
Self::Character(_) | Self::Possessor => true,
}
}
}
#[derive(PartialEq, Debug, Clone, Copy)]

View File

@ -1,4 +1,4 @@
use crate::comp::{Pos, Vel};
use crate::comp::{Pos, Presence, Vel};
use common_base::span;
use hashbrown::{hash_map::DefaultHashBuilder, HashSet};
use indexmap::IndexMap;
@ -69,7 +69,12 @@ const NEIGHBOR_OFFSETS: [Vec2<i32>; 8] = [
#[derive(Default)]
// TODO generic region size (16x16 for now)
// TODO compare to sweep and prune approach
/// A region system that tracks where entities are
/// A region system that tracks where entities are.
///
/// Note, this structure is primarily intended for tracking which entities need
/// to be synchronized to which clients (and as part of that what entities are
/// already synchronized). If an entity is marked to not be synchronized to
/// other clients it may not appear here.
pub struct RegionMap {
// Tree?
// Sorted Vec? (binary search lookup)
@ -92,7 +97,13 @@ impl RegionMap {
// TODO maintain within a system?
// TODO special case large entities
pub fn tick(&mut self, pos: ReadStorage<Pos>, vel: ReadStorage<Vel>, entities: Entities) {
pub fn tick(
&mut self,
pos: ReadStorage<Pos>,
vel: ReadStorage<Vel>,
presence: ReadStorage<Presence>,
entities: Entities,
) {
span!(_guard, "tick", "Region::tick");
self.tick += 1;
// Clear events within each region
@ -101,9 +112,10 @@ impl RegionMap {
});
// Add any untracked entities
for (pos, id) in (&pos, &entities, !&self.tracked_entities)
for (pos, id) in (&pos, &entities, presence.maybe(), !&self.tracked_entities)
.join()
.map(|(pos, e, _)| (pos, e.id()))
.filter(|(_, _, presence, _)| presence.map_or(true, |p| p.kind.sync_me()))
.map(|(pos, e, _, _)| (pos, e.id()))
.collect::<Vec<_>>()
{
// Add entity
@ -123,15 +135,21 @@ impl RegionMap {
.iter()
.enumerate()
.for_each(|(i, (&current_region, region_data))| {
for (maybe_pos, _maybe_vel, id) in
(pos.maybe(), vel.maybe(), &region_data.bitset).join()
for (maybe_pos, _maybe_vel, maybe_presence, id) in (
pos.maybe(),
vel.maybe(),
presence.maybe(),
&region_data.bitset,
)
.join()
{
let should_sync = maybe_presence.map_or(true, |p| p.kind.sync_me());
match maybe_pos {
// Switch regions for entities which need switching
// TODO don't check every tick (use velocity) (and use id to stagger)
// Starting parameters at v = 0 check every 100 ticks
// tether_length^2 / vel^2 (with a max of every tick)
Some(pos) => {
Some(pos) if should_sync => {
let pos = pos.0.map(|e| e as i32);
let key = Self::pos_key(pos);
// Consider switching
@ -148,7 +166,7 @@ impl RegionMap {
},
// Remove any non-existant entities (or just ones that lost their position
// component) TODO: distribute this between ticks
None => {
None | Some(_) => {
// TODO: shouldn't there be a way to extract the bitset of entities with
// positions directly from specs? Yes, with `.mask()` on the component
// storage.
@ -191,6 +209,13 @@ impl RegionMap {
}
}
/// Must be called immediately after succesfully deleting an entity from the
/// ecs (i.e. when deleting the entity did not generate a WrongGeneration
/// error).
pub fn entity_deleted(&mut self, entity: specs::Entity) {
self.tracked_entities.remove(entity.id());
}
fn add_entity(&mut self, id: u32, pos: Vec3<i32>, from: Option<Vec2<i32>>) {
let key = Self::pos_key(pos);
if let Some(region) = self.regions.get_mut(&key) {
@ -229,6 +254,8 @@ impl RegionMap {
}
}
}
} else if !self.tracked_entities.contains(id) {
return None;
} else {
// Check neighbors
for o in &NEIGHBOR_OFFSETS {
@ -251,6 +278,11 @@ impl RegionMap {
None
}
/// Checks if this entity is located in the `RegionMap`.
pub fn in_region_map(&self, entity: specs::Entity) -> bool {
self.tracked_entities.contains(entity.id())
}
fn key_index(&self, key: Vec2<i32>) -> Option<usize> {
self.regions.get_full(&key).map(|(i, _, _)| i)
}

View File

@ -12,7 +12,6 @@ use common::{
link::Is,
mounting::{Mount, Rider, VolumeRider, VolumeRiders},
outcome::Outcome,
region::RegionMap,
resources::{
DeltaTime, EntitiesDiedLastTick, GameMode, PlayerEntity, PlayerPhysicsSettings, Time,
TimeOfDay, TimeScale,
@ -294,7 +293,6 @@ impl State {
// TODO: only register on the server
ecs.insert(EventBus::<ServerEvent>::default());
ecs.insert(comp::group::GroupManager::default());
ecs.insert(RegionMap::new());
ecs.insert(SysMetrics::default());
ecs.insert(PhysicsMetrics::default());
ecs.insert(Trades::default());
@ -528,16 +526,6 @@ impl State {
}
}
// Run RegionMap tick to update entity region occupancy
pub fn update_region_map(&self) {
span!(_guard, "update_region_map", "State::update_region_map");
self.ecs.write_resource::<RegionMap>().tick(
self.ecs.read_storage::<comp::Pos>(),
self.ecs.read_storage::<comp::Vel>(),
self.ecs.entities(),
);
}
// Apply terrain changes
pub fn apply_terrain_changes(&self, block_update: impl FnMut(&specs::World, Vec<BlockDiff>)) {
self.apply_terrain_changes_internal(false, block_update);
@ -547,10 +535,9 @@ impl State {
/// [State::tick].
///
/// This only happens if [State::tick] is asked to update terrain itself
/// (using `update_terrain_and_regions: true`). [State::tick] is called
/// from within both the client and the server ticks, right after
/// handling terrain messages; currently, client sets it to true and
/// server to false.
/// (using `update_terrain: true`). [State::tick] is called from within
/// both the client and the server ticks, right after handling terrain
/// messages; currently, client sets it to true and server to false.
fn apply_terrain_changes_internal(
&self,
during_tick: bool,
@ -628,7 +615,7 @@ impl State {
&mut self,
dt: Duration,
add_systems: impl Fn(&mut DispatcherBuilder),
update_terrain_and_regions: bool,
update_terrain: bool,
mut metrics: Option<&mut StateTickMetrics>,
server_constants: &ServerConstants,
block_update: impl FnMut(&specs::World, Vec<BlockDiff>),
@ -656,10 +643,6 @@ impl State {
self.ecs.write_resource::<DeltaTime>().0 =
(dt.as_secs_f32() * time_scale as f32).min(MAX_DELTA_TIME);
if update_terrain_and_regions {
self.update_region_map();
}
section_span!(guard, "create dispatcher");
// Run systems to update the world.
// Create and run a dispatcher for ecs systems.
@ -679,7 +662,7 @@ impl State {
self.ecs.maintain();
drop(guard);
if update_terrain_and_regions {
if update_terrain {
self.apply_terrain_changes_internal(true, block_update);
}

View File

@ -127,15 +127,16 @@ pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity, skip_persisten
error!("handle_exit_ingame called with entity that is missing expected components");
}
let maybe_character = state
let (maybe_character, sync_me) = state
.read_storage::<Presence>()
.get(entity)
.and_then(|p| p.kind.character_id());
.map(|p| (p.kind.character_id(), p.kind.sync_me()))
.unzip();
let maybe_rtsim = state.read_component_copied::<common::rtsim::RtSimEntity>(entity);
state.mut_resource::<IdMaps>().remove_entity(
Some(entity),
None, // Uid re-mapped, we don't want to remove the mapping
maybe_character,
maybe_character.flatten(),
maybe_rtsim,
);
@ -143,7 +144,9 @@ pub fn handle_exit_ingame(server: &mut Server, entity: EcsEntity, skip_persisten
// Uid to a new entity (and e.g. don't want it to be unmapped).
//
// Delete old entity
if let Err(e) = crate::state_ext::delete_entity_common(state, entity, maybe_uid) {
if let Err(e) =
crate::state_ext::delete_entity_common(state, entity, maybe_uid, sync_me.unwrap_or(true))
{
error!(
?e,
?entity,

View File

@ -76,6 +76,7 @@ use common::{
cmd::ServerChatCommand,
comp,
event::{EventBus, ServerEvent},
region::RegionMap,
resources::{BattleMode, GameMode, Time, TimeOfDay},
rtsim::{RtSimEntity, RtSimVehicle},
shared_server_config::ServerConstants,
@ -377,6 +378,9 @@ impl Server {
.ecs_mut()
.insert(sys::PersistenceScheduler::every(Duration::from_secs(10)));
// Region map (spatial structure for entity synchronization)
state.ecs_mut().insert(RegionMap::new());
// Server-only components
state.ecs_mut().register::<RegionSubscription>();
state.ecs_mut().register::<Client>();
@ -754,7 +758,7 @@ impl Server {
// events so that changes made by server events will be immediately
// visible to client synchronization systems, minimizing the latency of
// `ServerEvent` mediated effects
self.state.update_region_map();
self.update_region_map();
// NOTE: apply_terrain_changes sends the *new* value since it is not being
// synchronized during the tick.
self.state.apply_terrain_changes(on_block_update);
@ -1092,6 +1096,18 @@ impl Server {
.map(|mut t| t.maintain());
}
// Run RegionMap tick to update entity region occupancy
fn update_region_map(&mut self) {
prof_span!("Server::update_region_map");
let ecs = self.state().ecs();
ecs.write_resource::<RegionMap>().tick(
ecs.read_storage::<comp::Pos>(),
ecs.read_storage::<comp::Vel>(),
ecs.read_storage::<comp::Presence>(),
ecs.entities(),
);
}
fn initialize_client(&mut self, client: connection_handler::IncomingClient) -> Entity {
let entity = self
.state

View File

@ -1190,20 +1190,21 @@ impl StateExt for State {
// NOTE: We expect that these 3 components are never removed from an entity (nor
// mutated) (at least not without updating the relevant mappings)!
let maybe_uid = self.read_component_copied::<Uid>(entity);
let maybe_character = self
let (maybe_character, sync_me) = self
.read_storage::<Presence>()
.get(entity)
.and_then(|p| p.kind.character_id());
.map(|p| (p.kind.character_id(), p.kind.sync_me()))
.unzip();
let maybe_rtsim = self.read_component_copied::<RtSimEntity>(entity);
self.mut_resource::<IdMaps>().remove_entity(
Some(entity),
maybe_uid,
maybe_character,
maybe_character.flatten(),
maybe_rtsim,
);
delete_entity_common(self, entity, maybe_uid)
delete_entity_common(self, entity, maybe_uid, sync_me.unwrap_or(true))
}
fn entity_as_actor(&self, entity: EcsEntity) -> Option<Actor> {
@ -1241,6 +1242,7 @@ pub(crate) fn delete_entity_common(
state: &mut State,
entity: EcsEntity,
maybe_uid: Option<Uid>,
sync_me: bool,
) -> Result<(), specs::error::WrongGeneration> {
if maybe_uid.is_none() {
// For now we expect all entities have a Uid component.
@ -1248,8 +1250,24 @@ pub(crate) fn delete_entity_common(
}
let maybe_pos = state.read_component_copied::<comp::Pos>(entity);
let res = state.ecs_mut().delete_entity(entity);
// TODO: workaround for https://github.com/amethyst/specs/pull/766
let actual_gen = state.ecs().entities().entity(entity.id()).gen();
let res = if actual_gen == entity.gen() {
state.ecs_mut().delete_entity(entity)
} else {
Err(specs::error::WrongGeneration {
action: "delete",
actual_gen,
entity,
})
};
if res.is_ok() {
let region_map = state.mut_resource::<common::region::RegionMap>();
let uid_pos_region_key = maybe_uid
.zip(maybe_pos)
.map(|(uid, pos)| (uid, pos, region_map.find_region(entity, pos.0)));
region_map.entity_deleted(entity);
// Note: Adding the `Uid` to the deleted list when exiting "in-game" relies on
// the client not being able to immediately re-enter the game in the
// same tick (since we could then mix up the ordering of things and
@ -1257,16 +1275,14 @@ pub(crate) fn delete_entity_common(
//
// The client will ignore requests to delete its own entity that are triggered
// by this.
if let Some((uid, pos)) = maybe_uid.zip(maybe_pos) {
let region_key = state
.ecs()
.read_resource::<common::region::RegionMap>()
.find_region(entity, pos.0);
if let Some((uid, pos, region_key)) = uid_pos_region_key {
if let Some(region_key) = region_key {
state
.mut_resource::<DeletedEntities>()
.record_deleted_entity(uid, region_key);
} else {
// If there is a position and sync_me is true, but the entity is not
// in a region, something might be wrong.
} else if sync_me {
// Don't panic if the entity wasn't found in a region, maybe it was just created
// and then deleted before the region manager had a chance to assign it a region
warn!(

View File

@ -4,10 +4,13 @@ use common::{
calendar::Calendar,
comp::{Collider, ForceUpdate, InventoryUpdate, Last, Ori, Player, Pos, Presence, Vel},
event::EventBus,
link::Is,
mounting::Rider,
outcome::Outcome,
region::{Event as RegionEvent, RegionMap},
resources::{PlayerPhysicsSettings, Time, TimeOfDay, TimeScale},
terrain::TerrainChunkSize,
uid::Uid,
vol::RectVolSize,
};
use common_ecs::{Job, Origin, Phase, System};
@ -233,32 +236,26 @@ impl<'a> System<'a> for Sys {
for (client, _, client_entity, client_pos) in &mut subscribers {
let mut comp_sync_package = CompSyncPackage::new();
for (_, entity, &uid, (&pos, last_pos), vel, ori, force_update, collider) in (
for (_, entity, &uid, (&pos, last_pos), vel, ori, collider) in (
region.entities(),
&entities,
uids,
(&positions, last_pos.mask().maybe()),
(&velocities, last_vel.mask().maybe()).maybe(),
(&orientations, last_vel.mask().maybe()).maybe(),
force_updates.maybe(),
colliders.maybe(),
)
.join()
{
// Decide how regularly to send physics updates.
let send_now = if client_entity == &entity {
let player_physics_setting = players
.get(entity)
.and_then(|p| {
player_physics_settings.settings.get(&p.uuid()).copied()
})
.unwrap_or_default();
// Don't send client physics updates about itself unless force update is
// set or the client is subject to
// server-authoritative physics
force_update.map_or(false, |f| f.is_forced())
|| player_physics_setting.server_authoritative()
|| is_rider.get(entity).is_some()
should_sync_client_physics(
entity,
&player_physics_settings,
&players,
&force_updates,
is_rider,
)
} else if matches!(collider, Some(Collider::Voxel { .. })) {
// Things with a voxel collider (airships, etc.) need to have very
// stable physics so we always send updated
@ -288,27 +285,15 @@ impl<'a> System<'a> for Sys {
}
};
if last_pos.is_none() {
comp_sync_package.comp_inserted(uid, pos);
} else if send_now {
comp_sync_package.comp_modified(uid, pos);
}
if let Some((v, last_vel)) = vel {
if last_vel.is_none() {
comp_sync_package.comp_inserted(uid, *v);
} else if send_now {
comp_sync_package.comp_modified(uid, *v);
}
}
if let Some((o, last_ori)) = ori {
if last_ori.is_none() {
comp_sync_package.comp_inserted(uid, *o);
} else if send_now {
comp_sync_package.comp_modified(uid, *o);
}
}
add_physics_components(
send_now,
&mut comp_sync_package,
uid,
pos,
last_pos,
ori,
vel,
);
}
// TODO: force update counter only needs to be sent once per frame (and only if
@ -326,6 +311,41 @@ impl<'a> System<'a> for Sys {
drop(guard);
job.cpu_stats.measure(common_ecs::ParMode::Single);
// Sync components that are only synced for the client's own entity.
for (entity, client, &uid, (maybe_pos, last_pos), vel, ori) in (
&entities,
&clients,
uids,
(positions.maybe(), last_pos.mask().maybe()),
(&velocities, last_vel.mask().maybe()).maybe(),
(&orientations, last_vel.mask().maybe()).maybe(),
)
.join()
{
// Include additional components for clients that aren't in a region (e.g. due
// to having no position or have sync_me as `false`) since those
// won't be synced above.
let include_all_comps = region_map.in_region_map(entity);
let mut comp_sync_package = trackers.create_sync_from_client_package(
&tracked_storages,
entity,
include_all_comps,
);
if include_all_comps && let Some(&pos) = maybe_pos {
let send_now = should_sync_client_physics(entity, &player_physics_settings, &players, &force_updates, is_rider);
add_physics_components(send_now, &mut comp_sync_package, uid, pos, last_pos, ori, vel);
}
if !comp_sync_package.is_empty() {
client.send_fallible(ServerGeneral::CompSync(
comp_sync_package,
force_updates.get(entity).map_or(0, |f| f.counter()),
));
}
}
// Update the last physics components for each entity
for (_, &pos, vel, ori, last_pos, last_vel, last_ori) in (
&entities,
@ -362,8 +382,6 @@ impl<'a> System<'a> for Sys {
}
}
// TODO: Sync clients that don't have a position?
// Sync inventories
for (inventory, update, client) in (inventories, &mut inventory_updates, &clients).join() {
client.send_fallible(ServerGeneral::InventoryUpdate(
@ -372,18 +390,6 @@ impl<'a> System<'a> for Sys {
));
}
// 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,
force_updates.get(entity).map_or(0, |f| f.counter()),
));
}
}
// Consume/clear the current outcomes and convert them to a vec
let outcomes = outcomes.recv_all().collect::<Vec<_>>();
@ -438,3 +444,61 @@ impl<'a> System<'a> for Sys {
}
}
}
/// Determines whether a client should receive an update about its own physics
/// components.
fn should_sync_client_physics(
entity: specs::Entity,
player_physics_settings: &PlayerPhysicsSettings,
players: &ReadStorage<'_, Player>,
force_updates: &WriteStorage<'_, ForceUpdate>,
is_rider: &ReadStorage<'_, Is<Rider>>,
) -> bool {
let player_physics_setting = players
.get(entity)
.and_then(|p| player_physics_settings.settings.get(&p.uuid()).copied())
.unwrap_or_default();
// Don't send client physics updates about itself unless force update is
// set or the client is subject to
// server-authoritative physics
force_updates.get(entity).map_or(false, |f| f.is_forced())
|| player_physics_setting.server_authoritative()
|| is_rider.contains(entity)
}
/// Adds physics components if `send_now` is true or `Option<Last<T>>` is
/// `None`.
///
/// If `Last<T>` isn't present, this is recorded as an insertion rather than a
/// modification.
fn add_physics_components(
send_now: bool,
comp_sync_package: &mut CompSyncPackage<common_net::msg::EcsCompPacket>,
uid: Uid,
pos: Pos,
last_pos: Option<u32>,
ori: Option<(&Ori, Option<u32>)>,
vel: Option<(&Vel, Option<u32>)>,
) {
if last_pos.is_none() {
comp_sync_package.comp_inserted(uid, pos);
} else if send_now {
comp_sync_package.comp_modified(uid, pos);
}
if let Some((v, last_vel)) = vel {
if last_vel.is_none() {
comp_sync_package.comp_inserted(uid, *v);
} else if send_now {
comp_sync_package.comp_modified(uid, *v);
}
}
if let Some((o, last_ori)) = ori {
if last_ori.is_none() {
comp_sync_package.comp_inserted(uid, *o);
} else if send_now {
comp_sync_package.comp_modified(uid, *o);
}
}
}

View File

@ -231,10 +231,14 @@ macro_rules! trackers {
/// Create sync package for components that are only synced for the client's entity.
///
/// This can optionally include components that are synced "for any entity" for cases
/// where other mechanisms don't sync those components.
pub fn create_sync_from_client_package(
&self,
comps: &TrackedStorages,
entity: specs::Entity,
include_all_comps: bool,
) -> 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
@ -249,10 +253,10 @@ macro_rules! trackers {
};
$(
if matches!(
<$component_type as NetSync>::SYNC_FROM,
SyncFrom::ClientEntity,
) {
if match <$component_type as NetSync>::SYNC_FROM {
SyncFrom::ClientEntity => true,
SyncFrom::AnyEntity => include_all_comps,
} {
comp_sync_package.add_component_update(
&self.$component_name,
&comps.$component_name,