From 24d1f6d970b087e313c04d51b68a5a34e56b5fba Mon Sep 17 00:00:00 2001 From: Imbris Date: Thu, 3 Oct 2019 20:41:42 -0400 Subject: [PATCH 01/10] Add initial region system implementation --- Cargo.lock | 8 + common/Cargo.toml | 3 + common/src/lib.rs | 1 + common/src/region.rs | 342 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 354 insertions(+) create mode 100644 common/src/region.rs diff --git a/Cargo.lock b/Cargo.lock index 7fd91c26e2..c84f74e7c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1577,6 +1577,11 @@ dependencies = [ "tiff 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "indexmap" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "inflate" version = "0.3.4" @@ -3592,7 +3597,9 @@ dependencies = [ "dot_vox 4.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "find_folder 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "hashbrown 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "hibitset 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", "image 0.22.2 (registry+https://github.com/rust-lang/crates.io-index)", + "indexmap 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "lz4-compress 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -4144,6 +4151,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" "checksum image 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "545f000e8aa4e569e93f49c446987133452e0091c2494ac3efd3606aa3d309f2" "checksum image 0.22.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ee0665404aa0f2ad154021777b785878b0e5b1c1da030455abc3d9ed257c2c67" +"checksum indexmap 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a61202fbe46c4a951e9404a720a0180bcf3212c750d735cb5c4ba4dc551299f3" "checksum inflate 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f5f9f47468e9a76a6452271efadc88fe865a82be91fe75e6c0c57b87ccea59d4" "checksum inflate 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "1cdb29978cc5797bd8dcc8e5bf7de604891df2a8dc576973d71a281e916db2ff" "checksum inotify 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "24e40d6fd5d64e2082e0c796495c8ef5ad667a96d03e5aaa0becfd9d47bcbfb8" diff --git a/common/Cargo.toml b/common/Cargo.toml index f24e52d3d5..d1825f29fd 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -28,6 +28,9 @@ find_folder = "0.3.0" parking_lot = "0.9.0" crossbeam = "0.7.2" notify = "5.0.0-pre.1" +indexmap = "1.2.0" +# TODO: remove when upgrading to specs 0.15 +hibitset = "0.5.3" [dev-dependencies] criterion = "0.3" diff --git a/common/src/lib.rs b/common/src/lib.rs index 34848ad1c8..06a775c88d 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -19,6 +19,7 @@ pub mod figure; pub mod msg; pub mod npc; pub mod ray; +pub mod region; pub mod state; pub mod sys; pub mod terrain; diff --git a/common/src/region.rs b/common/src/region.rs new file mode 100644 index 0000000000..0f29f5d1e8 --- /dev/null +++ b/common/src/region.rs @@ -0,0 +1,342 @@ +use crate::comp::{Pos, Vel}; +use hashbrown::hash_map::DefaultHashBuilder; +use hibitset::BitSetLike; +use indexmap::IndexMap; +use specs::{BitSet, Entities, Join, ReadStorage}; +use vek::*; + +/// Region consisting of a bitset of entities within it +struct Region { + // Use specs bitset for simplicity (and joinability) + bitset: BitSet, + // Indices of neighboring regions + neighbors: [Option; 8], + // Keep track of subscribers + subscribers: Vec, +} +impl Region { + fn with_entity(entity: u32) -> Self { + let mut bitset = BitSet::new(); + bitset.add(entity); + Self { + bitset, + neighbors: [None; 8], + subscribers: Vec::new(), + } + } +} + +/// How far can an entity roam outside its region before it is switched over to the neighboring one +/// In units of blocks (i.e. world pos) +/// Used to prevent rapid switching of entities between regions +const TETHER_LENGTH: u32 = 16; +/// Region Size in chunks +const REGION_SIZE: u16 = 16; +const REGION_LOG2: u8 = 4; +/// Offsets to iterate though neighbors +/// Counter-clockwise order +const NEIGHBOR_OFFSETS: [Vec2; 8] = [ + Vec2::new(0, 1), + Vec2::new(-1, 1), + Vec2::new(-1, 0), + Vec2::new(-1, -1), + Vec2::new(0, -1), + Vec2::new(1, -1), + Vec2::new(1, 0), + Vec2::new(1, 1), +]; + +// TODO generic region size (16x16 for now) +// TODO compare to sweep and prune approach +/// A region system that tracks where entities are +pub struct RegionMap { + // Tree? + // Sorted Vec? (binary search lookup) + // Sort into multiple vecs (say 32) using lower bits of morton code, then binary search via upper bits? <-- sounds very promising to me (might not be super good though?) + regions: IndexMap, Region, DefaultHashBuilder>, + // If an entity isn't here it needs to be added to a region + tracked_entities: BitSet, + // Re-useable vecs + // (src, entity, pos) + entities_to_move: Vec<(usize, u32, Vec3)>, + // (region, entity) + entities_to_remove: Vec<(usize, u32)>, +} +impl RegionMap { + pub fn new() -> Self { + Self { + regions: IndexMap::default(), + tracked_entities: BitSet::new(), + entities_to_move: Vec::new(), + entities_to_remove: Vec::new(), + } + } + // TODO maintain within a system + pub fn maintain( + &mut self, + pos: ReadStorage, + vel: ReadStorage, + entities: Entities, + tick: u64, + ) { + // Add any untracked entites + for (pos, id) in (&pos, &entities, !&self.tracked_entities) + .join() + .map(|(pos, e, _)| (pos, e.id())) + .collect::>() + { + // Add entity + self.add_entity(id, pos.0.map(|e| e as i32)); + } + + self.entities_to_move.clear(); + self.entities_to_remove.clear(); + + for i in 0..self.regions.len() { + for (maybe_pos, maybe_vel, entity) in ( + pos.maybe(), + vel.maybe(), + &self.regions.get_index(i).map(|(_, v)| v).unwrap().bitset, + ) + .join() + { + 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) => { + let pos = pos.0.map(|e| e as i32); + let current_region = self.index_key(i).unwrap(); + let key = Self::pos_key(pos); + // Consider switching + // Caculate distance outside border + if key != current_region + && (Vec2::::from(pos) - Self::key_pos(current_region)) + .map(|e| e.abs() as u32) + .reduce_max() + > TETHER_LENGTH + { + // Switch + self.entities_to_move.push((i, entity, pos)); + } + } + // Remove any non-existant entities (or just ones that lost their position component) + // TODO: distribute this between ticks + None => { + // TODO: shouldn't there be a way to extract the bitset of entities with positions directly from specs? + self.entities_to_remove.push((i, entity)); + } + } + } + + // Remove region if it is empty + // TODO: distribute this betweeen ticks + if self + .regions + .get_index(i) + .map(|(_, v)| v) + .unwrap() + .bitset + .is_empty() + { + self.remove_index(i); + } + } + + // Mutate + // Note entity moving is outside the whole loop so that the same entity is not checked twice (this may be fine though...) + while let Some((i, entity, pos)) = self.entities_to_move.pop() { + self.regions + .get_index_mut(i) + .map(|(_, v)| v) + .unwrap() + .bitset + .remove(entity); + self.add_entity_untracked(entity, pos); + } + for (i, entity) in self.entities_to_remove.drain(..) { + self.regions + .get_index_mut(i) + .map(|(_, v)| v) + .unwrap() + .bitset + .remove(entity); + } + + // Maintain subscriptions ??? + } + fn add_entity(&mut self, id: u32, pos: Vec3) { + self.tracked_entities.add(id); + self.add_entity_untracked(id, pos); + } + fn add_entity_untracked(&mut self, id: u32, pos: Vec3) { + let key = Self::pos_key(pos); + if let Some(region) = self.regions.get_mut(&key) { + region.bitset.add(id); + return; + } + + self.insert(key, id); + } + fn pos_key>>(pos: P) -> Vec2 { + pos.into().map(|e| e >> REGION_LOG2) + } + fn key_pos(key: Vec2) -> Vec2 { + key.map(|e| e << REGION_LOG2) + } + fn key_index(&self, key: Vec2) -> Option { + self.regions.get_full(&key).map(|(i, _, _)| i) + } + fn index_key(&self, index: usize) -> Option> { + self.regions.get_index(index).map(|(k, _)| k).copied() + } + /// Adds a new region + fn insert(&mut self, key: Vec2, entity: u32) { + let (index, old_region) = self.regions.insert_full(key, Region::with_entity(entity)); + if old_region.is_some() { + panic!("Inserted a region that already exists!!!(this should never need to occur"); + } + // Add neighbors and add to neighbors + let mut neighbors = [None; 8]; + for i in 0..8 { + if let Some((idx, _, region)) = self.regions.get_full_mut(&(key + NEIGHBOR_OFFSETS[i])) + { + // Add neighbor to the new region + neighbors[i] = Some(idx); + // Add new region to neighbor + region.neighbors[(i + 4) % 8] = Some(index); + } + } + self.regions + .get_index_mut(index) + .map(|(_, v)| v) + .unwrap() + .neighbors = neighbors; + } + /// Remove a region using its key + fn remove(&mut self, key: Vec2) { + if let Some(index) = self.key_index(key) { + self.remove_index(index); + } + } + /// Add a region using its key + fn remove_index(&mut self, index: usize) { + // Remap neighbor indices for neighbors of the region that will be moved from the end of the index map + let moved_neighbors = self + .regions + .get_index(index) + .map(|(_, v)| v) + .unwrap() + .neighbors; + for i in 0..8 { + if let Some(idx) = moved_neighbors[i] { + self.regions + .get_index_mut(idx) + .map(|(_, v)| v) + .unwrap() + .neighbors[(i + 4) % 8] = Some(index); + } + } + if let Some(region) = self + .regions + .swap_remove_index(index) + .map(|(_, region)| region) + { + if !region.bitset.is_empty() { + panic!("Removed region containing entities"); + } + // Remove from neighbors + for i in 0..8 { + if let Some(idx) = region.neighbors[i] { + self.regions + .get_index_mut(idx) + .map(|(_, v)| v) + .unwrap() + .neighbors[(i + 4) % 8] = None; + } + } + } + } +} + +/*pub struct RegionManager { + region_map: RegionMap + // If an entity isn't here it needs to be added to a region + tracked_entities: BitSet, +} +impl RegionManager { + // TODO maintain within a system? + pub fn maintain(&mut self, pos: ReadStorage, vel: ReadStorage, entities: Entities, tick: u64) { + let Self { + ref mut region_map, + ref mut tracked_entities, + } = + // Add any untracked entites + for (pos, e, _) in (&pos, &entities, !&self.tracked_entities).join() { + let id = e.id(); + // Add entity + self.add_entity(id, pos.0.map(|e| e as i32)); + } + // Iterate through regions + for i in 0..self.regions.len() { + for (maybe_pos, maybe_vel, entity) in + (pos.maybe(), vel.maybe(), &self.regions.get_index(i).map(|(_, v)| v).unwrap().bitset).join() + { + 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) => { + let pos = pos.0.map(|e| e as i32); + let current_region = self.index_key(i).unwrap(); + let key = Self::pos_key(pos); + // Consider switching + // Caculate distance outside border + if key != current_region + && (Vec2::::from(pos) - Self::key_pos(current_region)) + .map(|e| e.abs() as u32) + .reduce_max() + > TETHER_LENGTH + { + // Switch + self.regions.get_index_mut(i).map(|(_, v)| v).unwrap().bitset.remove(entity); + self.add_entity_untracked(entity, pos); + } + } + // Remove any non-existant entities (or just ones that lost their position component) + // TODO: distribute this between ticks + None => { + // TODO: shouldn't there be a way to extract the bitset of entities with positions directly from specs? + self.regions.get_index_mut(i).map(|(_, v)| v).unwrap().bitset.remove(entity); + } + } + } + + // Remove region if it is empty + // TODO: distribute this betweeen ticks + if self.regions.get_index(i).map(|(_, v)| v).unwrap().bitset.is_empty() { + self.remove_index(i); + } + } + + // Maintain subscriptions ??? + + } +}*/ +// Iterator designed for use in collision systems +// Iterates through all regions yielding them along with half of their neighbors + +/*fn interleave_i32_with_zeros(mut x: i32) -> i64 { + x = (x ^ (x << 16)) & 0x0000ffff0000ffff; + x = (x ^ (x << 8)) & 0x00ff00ff00ff00ff; + x = (x ^ (x << 4)) & 0x0f0f0f0f0f0f0f0f; + x = (x ^ (x << 2)) & 0x3333333333333333; + x = (x ^ (x << 1)) & 0x5555555555555555; + x +} + +fn morton_code(pos: Vec2) -> i64 { + interleave_i32_with_zeros(pos.x) | (interleave_i32_with_zeros(pos.y) << 1) +}*/ From b09bddda794bb96283d3e7d7e44cf114446354be Mon Sep 17 00:00:00 2001 From: Imbris Date: Sun, 6 Oct 2019 13:35:47 -0400 Subject: [PATCH 02/10] Make clients subscribed to nearby regions and only send physics updates from those regions. --- Cargo.lock | 3 + client/src/lib.rs | 5 + common/src/msg/server.rs | 1 + common/src/region.rs | 321 +++++++++++++------------ common/src/state.rs | 9 + server/Cargo.toml | 7 +- server/src/client.rs | 50 +++- server/src/lib.rs | 490 ++++++++++++++++++++++++++++----------- 8 files changed, 595 insertions(+), 291 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c84f74e7c0..0a4aa514c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3625,6 +3625,8 @@ dependencies = [ "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)", "crossbeam 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "hashbrown 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "hibitset 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", + "indexmap 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "portpicker 0.1.0 (git+https://github.com/wusyong/portpicker-rs?branch=fix_ipv6)", @@ -3637,6 +3639,7 @@ dependencies = [ "serde 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.101 (registry+https://github.com/rust-lang/crates.io-index)", "specs 0.14.3 (registry+https://github.com/rust-lang/crates.io-index)", + "specs-idvs 0.1.0 (git+https://gitlab.com/veloren/specs-idvs.git)", "uvth 3.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "vek 0.9.9 (registry+https://github.com/rust-lang/crates.io-index)", "veloren-common 0.4.0", diff --git a/client/src/lib.rs b/client/src/lib.rs index df7aa88a1d..0ec11c93d4 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -501,6 +501,11 @@ impl Client { ServerMsg::EcsSync(sync_package) => { self.state.ecs_mut().sync_with_package(sync_package) } + ServerMsg::DeleteEntity(entity) => { + if let Some(entity) = self.state.ecs().entity_from_uid(entity) { + let _ = self.state.ecs_mut().delete_entity(entity); + } + } ServerMsg::EntityPos { entity, pos } => { if let Some(entity) = self.state.ecs().entity_from_uid(entity) { self.state.write_component(entity, pos); diff --git a/common/src/msg/server.rs b/common/src/msg/server.rs index d0074d8e0d..cbf1d9ac8a 100644 --- a/common/src/msg/server.rs +++ b/common/src/msg/server.rs @@ -41,6 +41,7 @@ pub enum ServerMsg { }, SetPlayerEntity(u64), EcsSync(sphynx::SyncPackage), + DeleteEntity(u64), EntityPos { entity: u64, pos: comp::Pos, diff --git a/common/src/region.rs b/common/src/region.rs index 0f29f5d1e8..34120defad 100644 --- a/common/src/region.rs +++ b/common/src/region.rs @@ -1,38 +1,64 @@ use crate::comp::{Pos, Vel}; -use hashbrown::hash_map::DefaultHashBuilder; +use hashbrown::{hash_map::DefaultHashBuilder, HashSet}; use hibitset::BitSetLike; use indexmap::IndexMap; -use specs::{BitSet, Entities, Join, ReadStorage}; +use specs::{BitSet, Entities, Entity as EcsEntity, Join, ReadStorage}; use vek::*; +pub enum Event { + // Contains the key of the region the entity moved to + Left(u32, Option>), + // Contains the key of the region the entity came from + Entered(u32, Option>), +} + /// Region consisting of a bitset of entities within it -struct Region { +pub struct Region { // Use specs bitset for simplicity (and joinability) bitset: BitSet, // Indices of neighboring regions neighbors: [Option; 8], - // Keep track of subscribers - subscribers: Vec, + // TODO consider SmallVec for these + // Entites that left or entered this region + events: Vec, } -impl Region { - fn with_entity(entity: u32) -> Self { - let mut bitset = BitSet::new(); - bitset.add(entity); +impl Region { + fn new() -> Self { Self { - bitset, + bitset: BitSet::new(), neighbors: [None; 8], - subscribers: Vec::new(), + events: Vec::new(), } } + /// Checks if the region contains no entities and no events + fn removable(&self) -> bool { + self.bitset.is_empty() && self.events.is_empty() + } + fn add(&mut self, id: u32, from: Option>) { + self.bitset.add(id); + self.events.push(Event::Entered(id, from)); + } + fn remove(&mut self, id: u32, to: Option>) { + self.bitset.remove(id); + self.events.push(Event::Left(id, to)); + } + pub fn events(&self) -> &[Event] { + &self.events + } + pub fn entities(&self) -> &BitSet { + &self.bitset + } } /// How far can an entity roam outside its region before it is switched over to the neighboring one /// In units of blocks (i.e. world pos) /// Used to prevent rapid switching of entities between regions -const TETHER_LENGTH: u32 = 16; -/// Region Size in chunks -const REGION_SIZE: u16 = 16; -const REGION_LOG2: u8 = 4; +pub const TETHER_LENGTH: u32 = 16; +/// Region Size in blocks +pub const REGION_SIZE: u32 = 16 * 32; +/// Shift between region to world pos +/// TODO: don't use this :P +const REGION_LOG2: u8 = 9; /// Offsets to iterate though neighbors /// Counter-clockwise order const NEIGHBOR_OFFSETS: [Vec2; 8] = [ @@ -49,11 +75,11 @@ const NEIGHBOR_OFFSETS: [Vec2; 8] = [ // TODO generic region size (16x16 for now) // TODO compare to sweep and prune approach /// A region system that tracks where entities are -pub struct RegionMap { +pub struct RegionMap { // Tree? // Sorted Vec? (binary search lookup) // Sort into multiple vecs (say 32) using lower bits of morton code, then binary search via upper bits? <-- sounds very promising to me (might not be super good though?) - regions: IndexMap, Region, DefaultHashBuilder>, + regions: IndexMap, Region, DefaultHashBuilder>, // If an entity isn't here it needs to be added to a region tracked_entities: BitSet, // Re-useable vecs @@ -61,24 +87,24 @@ pub struct RegionMap { entities_to_move: Vec<(usize, u32, Vec3)>, // (region, entity) entities_to_remove: Vec<(usize, u32)>, + // Track the current tick, used to enable not checking everything every tick + tick: u64, } -impl RegionMap { +impl RegionMap { pub fn new() -> Self { Self { regions: IndexMap::default(), tracked_entities: BitSet::new(), entities_to_move: Vec::new(), entities_to_remove: Vec::new(), + // rate is depedent on the rate the caller calls region_manager.tick() + tick: 0, } } - // TODO maintain within a system - pub fn maintain( - &mut self, - pos: ReadStorage, - vel: ReadStorage, - entities: Entities, - tick: u64, - ) { + // TODO maintain within a system? + // TODO special case large entities + pub fn tick(&mut self, pos: ReadStorage, vel: ReadStorage, entities: Entities) { + self.tick += 1; // Add any untracked entites for (pos, id) in (&pos, &entities, !&self.tracked_entities) .join() @@ -86,14 +112,22 @@ impl RegionMap { .collect::>() { // Add entity - self.add_entity(id, pos.0.map(|e| e as i32)); + self.tracked_entities.add(id); + self.add_entity(id, pos.0.map(|e| e as i32), None); } - self.entities_to_move.clear(); - self.entities_to_remove.clear(); + let mut regions_to_remove = Vec::new(); for i in 0..self.regions.len() { - for (maybe_pos, maybe_vel, entity) in ( + // Clear events within each region + self.regions + .get_index_mut(i) + .map(|(_, v)| v) + .unwrap() + .events + .clear(); + + for (maybe_pos, _maybe_vel, id) in ( pos.maybe(), vel.maybe(), &self.regions.get_index(i).map(|(_, v)| v).unwrap().bitset, @@ -118,14 +152,14 @@ impl RegionMap { > TETHER_LENGTH { // Switch - self.entities_to_move.push((i, entity, pos)); + self.entities_to_move.push((i, id, pos)); } } // Remove any non-existant entities (or just ones that lost their position component) // TODO: distribute this between ticks None => { // TODO: shouldn't there be a way to extract the bitset of entities with positions directly from specs? - self.entities_to_remove.push((i, entity)); + self.entities_to_remove.push((i, id)); } } } @@ -137,63 +171,65 @@ impl RegionMap { .get_index(i) .map(|(_, v)| v) .unwrap() - .bitset - .is_empty() + .removable() { - self.remove_index(i); + regions_to_remove.push(i); } } + for index in regions_to_remove { + self.remove_index(index); + } // Mutate // Note entity moving is outside the whole loop so that the same entity is not checked twice (this may be fine though...) - while let Some((i, entity, pos)) = self.entities_to_move.pop() { - self.regions - .get_index_mut(i) - .map(|(_, v)| v) - .unwrap() - .bitset - .remove(entity); - self.add_entity_untracked(entity, pos); - } - for (i, entity) in self.entities_to_remove.drain(..) { - self.regions - .get_index_mut(i) - .map(|(_, v)| v) - .unwrap() - .bitset - .remove(entity); - } + while let Some((i, id, pos)) = self.entities_to_move.pop() { + let (prev_key, region) = self.regions.get_index_mut(i).map(|(k, v)| (*k, v)).unwrap(); + region.remove(id, Some(Self::pos_key(pos))); - // Maintain subscriptions ??? + self.add_entity(id, pos, Some(prev_key)); + } + for (i, id) in self.entities_to_remove.drain(..) { + self.regions + .get_index_mut(i) + .map(|(_, v)| v) + .unwrap() + .remove(id, None); + self.tracked_entities.remove(id); + } } - fn add_entity(&mut self, id: u32, pos: Vec3) { - self.tracked_entities.add(id); - self.add_entity_untracked(id, pos); + pub fn add(&mut self, entity: EcsEntity, pos: Vec3) { + self.add_entity(entity.id(), pos.map(|e| e as i32), None); } - fn add_entity_untracked(&mut self, id: u32, pos: Vec3) { + fn add_entity(&mut self, id: u32, pos: Vec3, from: Option>) { let key = Self::pos_key(pos); if let Some(region) = self.regions.get_mut(&key) { - region.bitset.add(id); + region.add(id, from); return; } - self.insert(key, id); + let index = self.insert(key); + self.regions + .get_index_mut(index) + .map(|(_, v)| v) + .unwrap() + .add(id, None); } fn pos_key>>(pos: P) -> Vec2 { pos.into().map(|e| e >> REGION_LOG2) } - fn key_pos(key: Vec2) -> Vec2 { + pub fn key_pos(key: Vec2) -> Vec2 { key.map(|e| e << REGION_LOG2) } - fn key_index(&self, key: Vec2) -> Option { - self.regions.get_full(&key).map(|(i, _, _)| i) - } + //fn key_index(&self, key: Vec2) -> Option { + // self.regions.get_full(&key).map(|(i, _, _)| i) + //} fn index_key(&self, index: usize) -> Option> { self.regions.get_index(index).map(|(k, _)| k).copied() } /// Adds a new region - fn insert(&mut self, key: Vec2, entity: u32) { - let (index, old_region) = self.regions.insert_full(key, Region::with_entity(entity)); + /// Returns the index of the region in the index map + fn insert(&mut self, key: Vec2) -> usize { + let (index, old_region) = self.regions.insert_full(key, Region::new()); if old_region.is_some() { panic!("Inserted a region that already exists!!!(this should never need to occur"); } @@ -213,29 +249,33 @@ impl RegionMap { .map(|(_, v)| v) .unwrap() .neighbors = neighbors; + + index } /// Remove a region using its key - fn remove(&mut self, key: Vec2) { - if let Some(index) = self.key_index(key) { - self.remove_index(index); - } - } + //fn remove(&mut self, key: Vec2) { + // if let Some(index) = self.key_index(key) { + // self.remove_index(index); + // } + //} /// Add a region using its key fn remove_index(&mut self, index: usize) { // Remap neighbor indices for neighbors of the region that will be moved from the end of the index map - let moved_neighbors = self - .regions - .get_index(index) - .map(|(_, v)| v) - .unwrap() - .neighbors; - for i in 0..8 { - if let Some(idx) = moved_neighbors[i] { - self.regions - .get_index_mut(idx) - .map(|(_, v)| v) - .unwrap() - .neighbors[(i + 4) % 8] = Some(index); + if index != self.regions.len() - 1 { + let moved_neighbors = self + .regions + .get_index(self.regions.len() - 1) + .map(|(_, v)| v) + .unwrap() + .neighbors; + for i in 0..8 { + if let Some(idx) = moved_neighbors[i] { + self.regions + .get_index_mut(idx) + .map(|(_, v)| v) + .unwrap() + .neighbors[(i + 4) % 8] = Some(index); + } } } if let Some(region) = self @@ -258,75 +298,58 @@ impl RegionMap { } } } -} - -/*pub struct RegionManager { - region_map: RegionMap - // If an entity isn't here it needs to be added to a region - tracked_entities: BitSet, -} -impl RegionManager { - // TODO maintain within a system? - pub fn maintain(&mut self, pos: ReadStorage, vel: ReadStorage, entities: Entities, tick: u64) { - let Self { - ref mut region_map, - ref mut tracked_entities, - } = - // Add any untracked entites - for (pos, e, _) in (&pos, &entities, !&self.tracked_entities).join() { - let id = e.id(); - // Add entity - self.add_entity(id, pos.0.map(|e| e as i32)); - } - // Iterate through regions - for i in 0..self.regions.len() { - for (maybe_pos, maybe_vel, entity) in - (pos.maybe(), vel.maybe(), &self.regions.get_index(i).map(|(_, v)| v).unwrap().bitset).join() - { - 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) => { - let pos = pos.0.map(|e| e as i32); - let current_region = self.index_key(i).unwrap(); - let key = Self::pos_key(pos); - // Consider switching - // Caculate distance outside border - if key != current_region - && (Vec2::::from(pos) - Self::key_pos(current_region)) - .map(|e| e.abs() as u32) - .reduce_max() - > TETHER_LENGTH - { - // Switch - self.regions.get_index_mut(i).map(|(_, v)| v).unwrap().bitset.remove(entity); - self.add_entity_untracked(entity, pos); - } - } - // Remove any non-existant entities (or just ones that lost their position component) - // TODO: distribute this between ticks - None => { - // TODO: shouldn't there be a way to extract the bitset of entities with positions directly from specs? - self.regions.get_index_mut(i).map(|(_, v)| v).unwrap().bitset.remove(entity); - } - } - } - - // Remove region if it is empty - // TODO: distribute this betweeen ticks - if self.regions.get_index(i).map(|(_, v)| v).unwrap().bitset.is_empty() { - self.remove_index(i); - } - } - - // Maintain subscriptions ??? - + // Returns a region given a key + pub fn get(&self, key: Vec2) -> Option<&Region> { + self.regions.get(&key) } -}*/ + // Returns an iterator of (Position, Region) + pub fn iter(&self) -> impl Iterator, &Region)> { + self.regions.iter().map(|(key, r)| (*key, r)) + } +} + +// Note vd is in blocks in this case +pub fn region_in_vd(key: Vec2, pos: Vec3, vd: f32) -> bool { + let vd_extended = vd + TETHER_LENGTH as f32 * 2.0f32.sqrt(); + + let min_region_pos = RegionMap::key_pos(key).map(|e| e as f32); + // Should be diff to closest point on the square (which can be in the middle of an edge) + let diff = (min_region_pos - Vec2::from(pos)).map(|e| { + if e < 0.0 { + (e + REGION_SIZE as f32).min(0.0) + } else { + e + } + }); + + diff.magnitude_squared() < vd_extended.powi(2) +} + +// Note vd is in blocks in this case +pub fn regions_in_vd(pos: Vec3, vd: f32) -> HashSet> { + let mut set = HashSet::new(); + + let pos_xy = Vec2::::from(pos); + let vd_extended = vd + TETHER_LENGTH as f32 * 2.0f32.sqrt(); + + let max = RegionMap::pos_key(pos_xy.map(|e| (e + vd_extended) as i32)); + let min = RegionMap::pos_key(pos_xy.map(|e| (e - vd_extended) as i32)); + + for x in min.x..=max.x { + for y in min.y..=max.y { + let key = Vec2::new(x, y); + + if region_in_vd(key, pos, vd) { + set.insert(key); + } + } + } + + set +} // Iterator designed for use in collision systems // Iterates through all regions yielding them along with half of their neighbors +// .................. /*fn interleave_i32_with_zeros(mut x: i32) -> i64 { x = (x ^ (x << 16)) & 0x0000ffff0000ffff; diff --git a/common/src/state.rs b/common/src/state.rs index a49af51625..0859323d4d 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -5,6 +5,7 @@ use crate::{ comp, event::{EventBus, LocalEvent, ServerEvent}, msg::{EcsCompPacket, EcsResPacket}, + region::RegionMap, sys, terrain::{Block, TerrainChunk, TerrainGrid}, vol::WriteVol, @@ -172,6 +173,7 @@ impl State { ecs.add_resource(TerrainChanges::default()); ecs.add_resource(EventBus::::default()); ecs.add_resource(EventBus::::default()); + ecs.add_resource(RegionMap::new()); } /// Register a component with the state's ECS. @@ -387,6 +389,13 @@ impl State { self.ecs.maintain(); + // Run RegionMap tick to update enitity region occupancy + self.ecs.write_resource::().tick( + self.ecs.read_storage::(), + self.ecs.read_storage::(), + self.ecs.entities(), + ); + // Apply terrain changes let mut terrain = self.ecs.write_resource::(); self.ecs diff --git a/server/Cargo.toml b/server/Cargo.toml index f972a05e65..6505d76458 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -8,6 +8,8 @@ edition = "2018" common = { package = "veloren-common", path = "../common" } world = { package = "veloren-world", path = "../world" } +specs-idvs = { git = "https://gitlab.com/veloren/specs-idvs.git" } + log = "0.4.8" specs = "0.14.2" vek = "0.9.9" @@ -24,4 +26,7 @@ crossbeam = "0.7.2" prometheus = "0.7" prometheus-static-metric = "0.2" rouille = "3.0.0" -portpicker = { git = "https://github.com/wusyong/portpicker-rs", branch = "fix_ipv6" } \ No newline at end of file +portpicker = { git = "https://github.com/wusyong/portpicker-rs", branch = "fix_ipv6" } +indexmap = "1.2.0" +# TODO: remove when upgrading to specs 0.15 +hibitset = "0.5.3" diff --git a/server/src/client.rs b/server/src/client.rs index a6856c08f8..2e182e8df0 100644 --- a/server/src/client.rs +++ b/server/src/client.rs @@ -2,8 +2,12 @@ use common::{ msg::{ClientMsg, ClientState, RequestStateError, ServerMsg}, net::PostBox, }; -use hashbrown::HashMap; +use hashbrown::{hash_map::DefaultHashBuilder, HashSet}; +use indexmap::IndexMap; use specs::Entity as EcsEntity; +use specs::{Component, FlaggedStorage}; +use specs_idvs::IDVStorage; +use vek::*; pub struct Client { pub client_state: ClientState, @@ -31,13 +35,13 @@ impl Client { } pub struct Clients { - clients: HashMap, + clients: IndexMap, } impl Clients { pub fn empty() -> Self { Self { - clients: HashMap::new(), + clients: IndexMap::default(), } } @@ -54,17 +58,40 @@ impl Clients { } pub fn get_mut<'a>(&'a mut self, entity: &EcsEntity) -> Option<&'a mut Client> { - self.clients.get_mut(entity) + self.clients.get_mut(entit:y) } pub fn remove<'a>(&'a mut self, entity: &EcsEntity) -> Option { self.clients.remove(entity) } + pub fn get_client_index_ingame<'a>(&'a mut self, entity: &EcsEntity) -> Option { + self.clients.get_full(entity).and_then(|(i, _, c)| { + if c.client_state == ClientState::Spectator + || c.client_state == ClientState::Character + || c.client_state == ClientState::Dead + { + Some(i) + } else { + None + } + }) + } + + //pub fn get_index_mut<'a>(&'a mut self, index: u32) -> Option<&'a mut Client> { + // self.clients.get_index_mut(index) + //} + pub fn remove_if bool>(&mut self, mut f: F) { self.clients.retain(|entity, client| !f(*entity, client)); } + pub fn notify_index(&mut self, index: usize, msg: ServerMsg) { + if let Some((_, client)) = self.clients.get_index_mut(index) { + client.notify(msg); + } + } + pub fn notify(&mut self, entity: EcsEntity, msg: ServerMsg) { if let Some(client) = self.clients.get_mut(&entity) { client.notify(msg); @@ -138,3 +165,18 @@ impl Clients { } } } + +// Distance from fuzzy_chunk before snapping to current chunk +pub const CHUNK_FUZZ: u32 = 2; +// Distance out of the range of a region before removing it from subscriptions +pub const REGION_FUZZ: u32 = 16; + +#[derive(Clone, Debug)] +pub struct RegionSubscription { + pub fuzzy_chunk: Vec2, + pub regions: HashSet>, +} + +impl Component for RegionSubscription { + type Storage = FlaggedStorage>; +} diff --git a/server/src/lib.rs b/server/src/lib.rs index 5a114e220c..606b24c6ad 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -14,7 +14,7 @@ pub use crate::{error::Error, input::Input, settings::ServerSettings}; use crate::{ auth_provider::AuthProvider, - client::{Client, Clients}, + client::{Client, Clients, RegionSubscription}, cmd::CHAT_COMMANDS, }; use common::{ @@ -86,6 +86,10 @@ pub struct Server { server_info: ServerInfo, metrics: ServerMetrics, + // Tick count used for throttling network updates + // Note this doesn't account for dt (so update rate changes with tick rate) + tick: u64, + // TODO: anything but this accounts: AuthProvider, } @@ -102,6 +106,7 @@ impl Server { state .ecs_mut() .add_resource(EventBus::::default()); + state.ecs_mut().register::(); // Set starting time for the server. state.ecs_mut().write_resource::().0 = settings.start_time; @@ -128,6 +133,7 @@ impl Server { }, metrics: ServerMetrics::new(settings.metrics_address) .expect("Failed to initialize server metrics submodule."), + tick: 0, accounts: AuthProvider::new(), server_settings: settings.clone(), }; @@ -547,6 +553,7 @@ impl Server { /// Execute a single server tick, handle input and update the game state by the given duration. pub fn tick(&mut self, _input: Input, dt: Duration) -> Result, Error> { + self.tick += 1; // This tick function is the centre of the Veloren universe. Most server-side things are // managed from here, and as such it's important that it stays organised. Please consult // the core developers before making significant changes to this code. Here is the @@ -792,6 +799,7 @@ impl Server { } // Remove NPCs that are outside the view distances of all players + // This is done by removing NPCs in unloaded chunks let to_delete = { let terrain = self.state.terrain(); ( @@ -1116,6 +1124,7 @@ impl Server { }), &server_settings, ); + Self::initialize_region_subscription(state, client, entity); } ClientState::Character => { client.error_state(RequestStateError::Already) @@ -1329,161 +1338,368 @@ impl Server { // Save player metadata (for example the username). state.write_component(entity, player); - // Sync physics of all entities - for (&uid, &pos, vel, ori, character_state) in ( - &state.ecs().read_storage::(), - &state.ecs().read_storage::(), // We assume all these entities have a position - state.ecs().read_storage::().maybe(), - state.ecs().read_storage::().maybe(), - state.ecs().read_storage::().maybe(), - ) - .join() - { - client.notify(ServerMsg::EntityPos { - entity: uid.into(), - pos, - }); - if let Some(vel) = vel.copied() { - client.notify(ServerMsg::EntityVel { - entity: uid.into(), - vel, - }); - } - if let Some(ori) = ori.copied() { - client.notify(ServerMsg::EntityOri { - entity: uid.into(), - ori, - }); - } - if let Some(character_state) = character_state.copied() { - client.notify(ServerMsg::EntityCharacterState { - entity: uid.into(), - character_state, - }); - } - } - // Tell the client its request was successful. client.allow_state(ClientState::Registered); } + /// Initialize region subscription, entity should be the client's entity + fn initialize_region_subscription( + state: &mut State, + client: &mut Client, + entity: specs::Entity, + ) { + let mut subscription = None; + + if let (Some(client_pos), Some(client_vd)) = ( + state.ecs().read_storage::().get(entity), + state + .ecs() + .read_storage::() + .get(entity) + .map(|pl| pl.view_distance) + .and_then(|v| v), + ) { + use common::region::RegionMap; + + let fuzzy_chunk = (Vec2::::from(client_pos.0)) + .map2(TerrainChunkSize::RECT_SIZE, |e, sz| e as i32 / sz as i32); + let chunk_size = TerrainChunkSize::RECT_SIZE.reduce_max() as f32; + let regions = common::region::regions_in_vd( + client_pos.0, + (client_vd as f32 * chunk_size) as f32 + + (client::CHUNK_FUZZ as f32 + chunk_size) * 2.0f32.sqrt(), + ); + + for (_, region) in state + .ecs() + .read_resource::() + .iter() + .filter(|(key, _)| regions.contains(key)) + { + // Sync physics of all entities in this region + for (&uid, &pos, vel, ori, character_state, _) in ( + &state.ecs().read_storage::(), + &state.ecs().read_storage::(), // We assume all these entities have a position + state.ecs().read_storage::().maybe(), + state.ecs().read_storage::().maybe(), + state.ecs().read_storage::().maybe(), + region.entities(), + ) + .join() + { + client.notify(ServerMsg::EntityPos { + entity: uid.into(), + pos, + }); + if let Some(vel) = vel.copied() { + client.notify(ServerMsg::EntityVel { + entity: uid.into(), + vel, + }); + } + if let Some(ori) = ori.copied() { + client.notify(ServerMsg::EntityOri { + entity: uid.into(), + ori, + }); + } + if let Some(character_state) = character_state.copied() { + client.notify(ServerMsg::EntityCharacterState { + entity: uid.into(), + character_state, + }); + } + } + } + + subscription = Some(RegionSubscription { + fuzzy_chunk, + regions, + }); + } + if let Some(subscription) = subscription { + state.write_component(entity, subscription); + } + } + /// Sync client states with the most up to date information. fn sync_clients(&mut self) { - // Sync 'logical' state using Sphynx. - self.clients - .notify_registered(ServerMsg::EcsSync(self.state.ecs_mut().next_sync_package())); + use common::region::{region_in_vd, regions_in_vd, Event as RegionEvent, RegionMap}; + //use hibitset::BitSetLike; let ecs = self.state.ecs_mut(); + let clients = &mut self.clients; - // Sync physics - for (entity, &uid, &pos, force_update) in ( + // Sync 'logical' state using Sphynx. + clients.notify_registered(ServerMsg::EcsSync(ecs.next_sync_package())); + + // To update subscriptions + // 1. Iterate through clients + // 2. Calculate current chunk position + // 3. If chunk is the same return, otherwise continue (use fuzzyiness) + // 4. Iterate through subscribed regions + // 5. Check if region is still in range (use fuzzyiness) + // 6. If not in range + // - remove from hashset + // - inform client of which entities to remove + // 7. Determine list of regions that are in range and iterate through it + // - check if in hashset (hash calc) if not add it + let mut regions_to_remove = Vec::new(); + for (entity, subscription, pos, vd) in ( &ecs.entities(), - &ecs.read_storage::(), + &mut ecs.write_storage::(), &ecs.read_storage::(), - ecs.read_storage::().maybe(), + &ecs.read_storage::(), ) .join() + .filter_map(|(e, s, pos, player)| player.view_distance.map(|v| (e, s, pos, v))) { - let clients = &mut self.clients; - - let in_vd = |entity| { - if let (Some(client_pos), Some(client_vd)) = ( - ecs.read_storage::().get(entity), - ecs.read_storage::() - .get(entity) - .map(|pl| pl.view_distance) - .and_then(|v| v), - ) { - { - // Check if the entity is in the client's range - Vec2::from(pos.0 - client_pos.0) - .map2(TerrainChunkSize::RECT_SIZE, |d: f32, sz| { - (d.abs() as u32 / sz).checked_sub(2).unwrap_or(0) - }) - .magnitude_squared() - < client_vd.pow(2) + let chunk = (Vec2::::from(pos.0)) + .map2(TerrainChunkSize::RECT_SIZE, |e, sz| e as i32 / sz as i32); + if chunk != subscription.fuzzy_chunk + && (subscription + .fuzzy_chunk + .map2(TerrainChunkSize::RECT_SIZE, |e, sz| { + (e as f32 + 0.5) * sz as f32 + }) + - Vec2::from(pos.0)) + .map2(TerrainChunkSize::RECT_SIZE, |e, sz| { + e.abs() > (sz / 2 + client::CHUNK_FUZZ) as f32 + }) + .reduce_or() + { + let chunk_size = TerrainChunkSize::RECT_SIZE.reduce_max() as f32; + for key in &subscription.regions { + if !region_in_vd( + *key, + pos.0, + (vd as f32 * chunk_size) + + (client::CHUNK_FUZZ as f32 + client::REGION_FUZZ as f32 + chunk_size) + * 2.0f32.sqrt(), + ) { + regions_to_remove.push(*key); + } + } + + let mut client = clients.get_mut(&entity); + + for key in regions_to_remove.drain(..) { + subscription.regions.remove(&key); + // Inform the client to delete these entities + if let (Some(ref mut client), Some(region)) = + (&mut client, ecs.read_resource::().get(key)) + { + // Process entity left events since they won't be processed below because this region is no longer subscribed to + for event in region.events() { + match event { + RegionEvent::Entered(_, _) => {} // These don't need to be processed because this region is being thrown out anyway + RegionEvent::Left(id, maybe_key) => { + // Lookup UID for entity + if let Some(&uid) = + ecs.read_storage::().get(ecs.entities().entity(*id)) + { + if !maybe_key + .as_ref() + .map(|key| subscription.regions.contains(key)) + .unwrap_or(false) + { + client.notify(ServerMsg::DeleteEntity(uid.into())); + } + } + } + } + } + for (&uid, _) in (&ecs.read_storage::(), region.entities()).join() { + client.notify(ServerMsg::DeleteEntity(uid.into())) + } + } + } + + for key in regions_in_vd( + pos.0, + (vd as f32 * chunk_size) + + (client::CHUNK_FUZZ as f32 + chunk_size) * 2.0f32.sqrt(), + ) { + if subscription.regions.insert(key) { + // TODO: send the client initial infromation for all the entities in this region + } + } + } + } + + // To send entity updates + // 1. Iterate through regions + // 2. Iterate through region subscribers (ie clients) + // - Collect a list of entity ids for clients who are subscribed to this region (hash calc to check each) + // 3. Iterate through events from that region + // - For each entity left event, iterate through the client list and check if they are subscribed to the destination (hash calc per subscribed client per entity left event) + // - Do something with entity entered events when sphynx is removed?? + // 4. Iterate through entities in that region + // 5. Inform clients of the component changes for that entity + // - Throttle update rate base on distance to each client + + // Sync physics + // via iterating through regions + for (key, region) in ecs.read_resource::().iter() { + let subscriptions = ecs.read_storage::(); + let subscribers = ( + &ecs.entities(), + &subscriptions, + &ecs.read_storage::(), + ) + .join() + .filter_map(|(entity, subscription, pos)| { + if subscription.regions.contains(&key) { + clients + .get_client_index_ingame(&entity) + .map(|index| (index, &subscription.regions, entity, *pos)) + } else { + None + } + }) + .collect::>(); + + for event in region.events() { + match event { + RegionEvent::Entered(_, _) => {} // TODO use this + RegionEvent::Left(id, maybe_key) => { + // Lookup UID for entity + if let Some(&uid) = + ecs.read_storage::().get(ecs.entities().entity(*id)) + { + for (client_index, regions, _, _) in &subscribers { + if !maybe_key + .as_ref() + .map(|key| regions.contains(key)) + .unwrap_or(false) + { + clients.notify_index( + *client_index, + ServerMsg::DeleteEntity(uid.into()), + ); + } + } + } + } + } + } + + let tick = self.tick; + let send_msg = |msg: ServerMsg, + entity: EcsEntity, + pos: comp::Pos, + force_update, + clients: &mut Clients| { + for (index, _, client_entity, client_pos) in &subscribers { + match force_update { + None if client_entity == &entity => {} + _ => { + let distance_sq = client_pos.0.distance_squared(pos.0); + + // Throttle update rate based on distance to player + let update = if distance_sq < 100.0f32.powi(2) { + true // Closer than 100.0 blocks + } else if distance_sq < 150.0f32.powi(2) { + tick + entity.id() as u64 % 2 == 0 + } else if distance_sq < 200.0f32.powi(2) { + tick + entity.id() as u64 % 4 == 0 + } else if distance_sq < 250.0f32.powi(2) { + tick + entity.id() as u64 % 8 == 0 + } else if distance_sq < 300.0f32.powi(2) { + tick + entity.id() as u64 % 8 == 0 + } else { + tick + entity.id() as u64 % 16 == 0 + }; + + if update { + clients.notify_index(*index, msg.clone()); + } + } } - } else { - false } }; - let mut last_pos = ecs.write_storage::>(); - let mut last_vel = ecs.write_storage::>(); - let mut last_ori = ecs.write_storage::>(); - let mut last_character_state = ecs.write_storage::>(); - - if let Some(client_pos) = ecs.read_storage::().get(entity) { - if last_pos - .get(entity) - .map(|&l| l.0 != *client_pos) - .unwrap_or(true) - { - let _ = last_pos.insert(entity, comp::Last(*client_pos)); - let msg = ServerMsg::EntityPos { - entity: uid.into(), - pos: *client_pos, - }; - match force_update { - Some(_) => clients.notify_ingame_if(msg, in_vd), - None => clients.notify_ingame_if_except(entity, msg, in_vd), - } - } - } - - if let Some(client_vel) = ecs.read_storage::().get(entity) { - if last_vel - .get(entity) - .map(|&l| l.0 != *client_vel) - .unwrap_or(true) - { - let _ = last_vel.insert(entity, comp::Last(*client_vel)); - let msg = ServerMsg::EntityVel { - entity: uid.into(), - vel: *client_vel, - }; - match force_update { - Some(_) => clients.notify_ingame_if(msg, in_vd), - None => clients.notify_ingame_if_except(entity, msg, in_vd), - } - } - } - - if let Some(client_ori) = ecs.read_storage::().get(entity) { - if last_ori - .get(entity) - .map(|&l| l.0 != *client_ori) - .unwrap_or(true) - { - let _ = last_ori.insert(entity, comp::Last(*client_ori)); - let msg = ServerMsg::EntityOri { - entity: uid.into(), - ori: *client_ori, - }; - match force_update { - Some(_) => clients.notify_ingame_if(msg, in_vd), - None => clients.notify_ingame_if_except(entity, msg, in_vd), - } - } - } - - if let Some(client_character_state) = - ecs.read_storage::().get(entity) + for (_, entity, &uid, &pos, maybe_vel, maybe_ori, character_state, force_update) in ( + region.entities(), + &ecs.entities(), + &ecs.read_storage::(), + &ecs.read_storage::(), + ecs.read_storage::().maybe(), + ecs.read_storage::().maybe(), + ecs.read_storage::().maybe(), + ecs.read_storage::().maybe(), + ) + .join() { - if last_character_state - .get(entity) - .map(|&l| !client_character_state.is_same_state(&l.0)) - .unwrap_or(true) - { - let _ = - last_character_state.insert(entity, comp::Last(*client_character_state)); - let msg = ServerMsg::EntityCharacterState { - entity: uid.into(), - character_state: *client_character_state, - }; - match force_update { - Some(_) => clients.notify_ingame_if(msg, in_vd), - None => clients.notify_ingame_if_except(entity, msg, in_vd), + let mut last_pos = ecs.write_storage::>(); + let mut last_vel = ecs.write_storage::>(); + let mut last_ori = ecs.write_storage::>(); + let mut last_character_state = + ecs.write_storage::>(); + + if last_pos.get(entity).map(|&l| l.0 != pos).unwrap_or(true) { + let _ = last_pos.insert(entity, comp::Last(pos)); + send_msg( + ServerMsg::EntityPos { + entity: uid.into(), + pos, + }, + entity, + pos, + force_update, + clients, + ); + } + + if let Some(&vel) = maybe_vel { + if last_vel.get(entity).map(|&l| l.0 != vel).unwrap_or(true) { + let _ = last_vel.insert(entity, comp::Last(vel)); + send_msg( + ServerMsg::EntityVel { + entity: uid.into(), + vel, + }, + entity, + pos, + force_update, + clients, + ); + } + } + + if let Some(&ori) = maybe_ori { + if last_ori.get(entity).map(|&l| l.0 != ori).unwrap_or(true) { + let _ = last_ori.insert(entity, comp::Last(ori)); + send_msg( + ServerMsg::EntityOri { + entity: uid.into(), + ori, + }, + entity, + pos, + force_update, + clients, + ); + } + } + + if let Some(&character_state) = character_state { + if last_character_state + .get(entity) + .map(|&l| !character_state.is_same_state(&l.0)) + .unwrap_or(true) + { + let _ = last_character_state.insert(entity, comp::Last(character_state)); + send_msg( + ServerMsg::EntityCharacterState { + entity: uid.into(), + character_state, + }, + entity, + pos, + force_update, + clients, + ); } } } From 10f9f10cb369cb7ab5ded25c9543ef92364bc243 Mon Sep 17 00:00:00 2001 From: Imbris Date: Sun, 6 Oct 2019 15:38:54 -0400 Subject: [PATCH 03/10] Fix some panics when removing regions --- common/src/region.rs | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/common/src/region.rs b/common/src/region.rs index 34120defad..14124580f6 100644 --- a/common/src/region.rs +++ b/common/src/region.rs @@ -176,9 +176,6 @@ impl RegionMap { regions_to_remove.push(i); } } - for index in regions_to_remove { - self.remove_index(index); - } // Mutate // Note entity moving is outside the whole loop so that the same entity is not checked twice (this may be fine though...) @@ -196,6 +193,14 @@ impl RegionMap { .remove(id, None); self.tracked_entities.remove(id); } + for index in regions_to_remove { + let (k, r) = self.regions.get_index(index).unwrap(); + // Check that the region is still removable + if r.removable() { + // Note we have to use key's here since the index can change when others are removed + self.remove(*k); + } + } } pub fn add(&mut self, entity: EcsEntity, pos: Vec3) { self.add_entity(entity.id(), pos.map(|e| e as i32), None); @@ -220,9 +225,9 @@ impl RegionMap { pub fn key_pos(key: Vec2) -> Vec2 { key.map(|e| e << REGION_LOG2) } - //fn key_index(&self, key: Vec2) -> Option { - // self.regions.get_full(&key).map(|(i, _, _)| i) - //} + fn key_index(&self, key: Vec2) -> Option { + self.regions.get_full(&key).map(|(i, _, _)| i) + } fn index_key(&self, index: usize) -> Option> { self.regions.get_index(index).map(|(k, _)| k).copied() } @@ -253,11 +258,11 @@ impl RegionMap { index } /// Remove a region using its key - //fn remove(&mut self, key: Vec2) { - // if let Some(index) = self.key_index(key) { - // self.remove_index(index); - // } - //} + fn remove(&mut self, key: Vec2) { + if let Some(index) = self.key_index(key) { + self.remove_index(index); + } + } /// Add a region using its key fn remove_index(&mut self, index: usize) { // Remap neighbor indices for neighbors of the region that will be moved from the end of the index map From ab7740f378f143ac9be9a4f6139631b43b715160 Mon Sep 17 00:00:00 2001 From: Imbris Date: Sun, 6 Oct 2019 16:50:35 -0400 Subject: [PATCH 04/10] Fix not not sending updates outside a certain range and not sending character state when it changes due to update throttling --- server/src/lib.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/server/src/lib.rs b/server/src/lib.rs index 606b24c6ad..caeefac861 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -1589,7 +1589,8 @@ impl Server { entity: EcsEntity, pos: comp::Pos, force_update, - clients: &mut Clients| { + clients: &mut Clients, + throttle: bool| { for (index, _, client_entity, client_pos) in &subscribers { match force_update { None if client_entity == &entity => {} @@ -1597,18 +1598,18 @@ impl Server { let distance_sq = client_pos.0.distance_squared(pos.0); // Throttle update rate based on distance to player - let update = if distance_sq < 100.0f32.powi(2) { + let update = if !throttle || distance_sq < 100.0f32.powi(2) { true // Closer than 100.0 blocks } else if distance_sq < 150.0f32.powi(2) { - tick + entity.id() as u64 % 2 == 0 + (tick + entity.id() as u64) % 2 == 0 } else if distance_sq < 200.0f32.powi(2) { - tick + entity.id() as u64 % 4 == 0 + (tick + entity.id() as u64) % 4 == 0 } else if distance_sq < 250.0f32.powi(2) { - tick + entity.id() as u64 % 8 == 0 + (tick + entity.id() as u64) % 8 == 0 } else if distance_sq < 300.0f32.powi(2) { - tick + entity.id() as u64 % 8 == 0 + (tick + entity.id() as u64) % 16 == 0 } else { - tick + entity.id() as u64 % 16 == 0 + (tick + entity.id() as u64) % 32 == 0 }; if update { @@ -1648,6 +1649,7 @@ impl Server { pos, force_update, clients, + true, ); } @@ -1663,6 +1665,7 @@ impl Server { pos, force_update, clients, + true, ); } } @@ -1679,6 +1682,7 @@ impl Server { pos, force_update, clients, + true, ); } } @@ -1699,6 +1703,7 @@ impl Server { pos, force_update, clients, + false, ); } } From 53ddbec527042c9dfa48f8e5944922ac5f114e27 Mon Sep 17 00:00:00 2001 From: Imbris Date: Sun, 6 Oct 2019 21:00:47 -0400 Subject: [PATCH 05/10] Actually fix region removal panic, update fuzzy chunk location, sync entity removal when unloading chunks, region size constant tweak --- client/src/lib.rs | 4 +++- common/src/region.rs | 25 +++++++++---------------- server/src/lib.rs | 14 ++++++++++++-- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/client/src/lib.rs b/client/src/lib.rs index 0ec11c93d4..73350e9bf2 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -503,7 +503,9 @@ impl Client { } ServerMsg::DeleteEntity(entity) => { if let Some(entity) = self.state.ecs().entity_from_uid(entity) { - let _ = self.state.ecs_mut().delete_entity(entity); + if entity != self.entity { + let _ = self.state.ecs_mut().delete_entity(entity); + } } } ServerMsg::EntityPos { entity, pos } => { diff --git a/common/src/region.rs b/common/src/region.rs index 14124580f6..da8b2fa90a 100644 --- a/common/src/region.rs +++ b/common/src/region.rs @@ -54,11 +54,10 @@ impl Region { /// In units of blocks (i.e. world pos) /// Used to prevent rapid switching of entities between regions pub const TETHER_LENGTH: u32 = 16; -/// Region Size in blocks -pub const REGION_SIZE: u32 = 16 * 32; -/// Shift between region to world pos -/// TODO: don't use this :P +/// Bitshift between region and world pos, i.e. log2(REGION_SIZE) const REGION_LOG2: u8 = 9; +/// Region Size in blocks +pub const REGION_SIZE: u32 = 1 << REGION_LOG2; /// Offsets to iterate though neighbors /// Counter-clockwise order const NEIGHBOR_OFFSETS: [Vec2; 8] = [ @@ -166,14 +165,9 @@ impl RegionMap { // Remove region if it is empty // TODO: distribute this betweeen ticks - if self - .regions - .get_index(i) - .map(|(_, v)| v) - .unwrap() - .removable() - { - regions_to_remove.push(i); + let (key, region) = self.regions.get_index(i).unwrap(); + if region.removable() { + regions_to_remove.push(*key); } } @@ -193,12 +187,11 @@ impl RegionMap { .remove(id, None); self.tracked_entities.remove(id); } - for index in regions_to_remove { - let (k, r) = self.regions.get_index(index).unwrap(); + for key in regions_to_remove.into_iter() { // Check that the region is still removable - if r.removable() { + if self.regions.get(&key).unwrap().removable() { // Note we have to use key's here since the index can change when others are removed - self.remove(*k); + self.remove(key); } } } diff --git a/server/src/lib.rs b/server/src/lib.rs index caeefac861..c7c0e40851 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -1456,6 +1456,8 @@ impl Server { { let chunk = (Vec2::::from(pos.0)) .map2(TerrainChunkSize::RECT_SIZE, |e, sz| e as i32 / sz as i32); + // Only update regions when moving to a new chunk + // uses a fuzzy border to prevent rapid triggering when moving along chunk boundaries if chunk != subscription.fuzzy_chunk && (subscription .fuzzy_chunk @@ -1468,8 +1470,14 @@ impl Server { }) .reduce_or() { + // Update current chunk + subscription.fuzzy_chunk = (Vec2::::from(pos.0)) + .map2(TerrainChunkSize::RECT_SIZE, |e, sz| e as i32 / sz as i32); + // Use the largest side length as our chunk size let chunk_size = TerrainChunkSize::RECT_SIZE.reduce_max() as f32; + // Iterate through currently subscribed regions for key in &subscription.regions { + // Check if the region is not within range anymore if !region_in_vd( *key, pos.0, @@ -1477,15 +1485,17 @@ impl Server { + (client::CHUNK_FUZZ as f32 + client::REGION_FUZZ as f32 + chunk_size) * 2.0f32.sqrt(), ) { + // Add to the list of regions to remove regions_to_remove.push(*key); } } let mut client = clients.get_mut(&entity); - + // Iterate through regions to remove for key in regions_to_remove.drain(..) { + // Remove region from this clients set of subscribed regions subscription.regions.remove(&key); - // Inform the client to delete these entities + // Tell the client to delete the entities in that region if it exists in the RegionMap if let (Some(ref mut client), Some(region)) = (&mut client, ecs.read_resource::().get(key)) { From c02cd0a730d984f9949286502b3c48f6c9c3e553 Mon Sep 17 00:00:00 2001 From: Imbris Date: Sun, 6 Oct 2019 21:08:28 -0400 Subject: [PATCH 06/10] Cleanup unused code --- server/src/client.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/server/src/client.rs b/server/src/client.rs index 2e182e8df0..b0e02112ba 100644 --- a/server/src/client.rs +++ b/server/src/client.rs @@ -78,10 +78,6 @@ impl Clients { }) } - //pub fn get_index_mut<'a>(&'a mut self, index: u32) -> Option<&'a mut Client> { - // self.clients.get_index_mut(index) - //} - pub fn remove_if bool>(&mut self, mut f: F) { self.clients.retain(|entity, client| !f(*entity, client)); } From 2703c8afe1e00e449166b3f6e2e4fc608ac43e33 Mon Sep 17 00:00:00 2001 From: Imbris Date: Tue, 15 Oct 2019 00:06:14 -0400 Subject: [PATCH 07/10] Move serverside client to a component and communications into server ecs systems --- Cargo.lock | 1 - client/src/lib.rs | 63 +- common/src/comp/controller.rs | 22 +- common/src/comp/mod.rs | 4 +- common/src/event.rs | 10 + common/src/msg/client.rs | 8 +- common/src/region.rs | 3 - common/src/state.rs | 24 +- common/src/sys/agent.rs | 26 +- common/src/sys/controller.rs | 73 +- common/src/sys/movement.rs | 16 +- server/Cargo.toml | 1 - server/src/client.rs | 151 +-- server/src/cmd.rs | 165 ++-- server/src/lib.rs | 1186 +++++++----------------- server/src/sys/message.rs | 320 +++++++ server/src/sys/mod.rs | 19 + server/src/sys/subscription.rs | 125 +++ server/src/sys/sync.rs | 226 +++++ voxygen/src/menu/char_selection/mod.rs | 2 +- voxygen/src/session.rs | 52 +- 21 files changed, 1272 insertions(+), 1225 deletions(-) create mode 100644 server/src/sys/message.rs create mode 100644 server/src/sys/mod.rs create mode 100644 server/src/sys/subscription.rs create mode 100644 server/src/sys/sync.rs diff --git a/Cargo.lock b/Cargo.lock index 0a4aa514c9..272df33c8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3626,7 +3626,6 @@ dependencies = [ "crossbeam 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", "hashbrown 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "hibitset 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "indexmap 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)", "portpicker 0.1.0 (git+https://github.com/wusyong/portpicker-rs?branch=fix_ipv6)", diff --git a/client/src/lib.rs b/client/src/lib.rs index 73350e9bf2..1aeb7c7b31 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -8,7 +8,7 @@ pub use crate::error::Error; pub use specs::{join::Join, saveload::Marker, Entity as EcsEntity, ReadStorage}; use common::{ - comp, + comp::{self, ControlEvent, Controller, ControllerInputs, InventoryManip}, msg::{ validate_chat_msg, ChatMsgValidationError, ClientMsg, ClientState, RequestStateError, ServerError, ServerInfo, ServerMsg, MAX_BYTES_CHAT_MSG, @@ -201,22 +201,33 @@ impl Client { // Can't fail } - pub fn use_inventory_slot(&mut self, x: usize) { - self.postbox.send_message(ClientMsg::UseInventorySlot(x)) + pub fn use_inventory_slot(&mut self, slot: usize) { + self.postbox + .send_message(ClientMsg::ControlEvent(ControlEvent::InventoryManip( + InventoryManip::Use(slot), + ))); } pub fn swap_inventory_slots(&mut self, a: usize, b: usize) { self.postbox - .send_message(ClientMsg::SwapInventorySlots(a, b)) + .send_message(ClientMsg::ControlEvent(ControlEvent::InventoryManip( + InventoryManip::Swap(a, b), + ))); } - pub fn drop_inventory_slot(&mut self, x: usize) { - self.postbox.send_message(ClientMsg::DropInventorySlot(x)) + pub fn drop_inventory_slot(&mut self, slot: usize) { + self.postbox + .send_message(ClientMsg::ControlEvent(ControlEvent::InventoryManip( + InventoryManip::Drop(slot), + ))); } pub fn pick_up(&mut self, entity: EcsEntity) { if let Some(uid) = self.state.ecs().read_storage::().get(entity).copied() { - self.postbox.send_message(ClientMsg::PickUp(uid.id())); + self.postbox + .send_message(ClientMsg::ControlEvent(ControlEvent::InventoryManip( + InventoryManip::Pickup(uid), + ))); } } @@ -228,6 +239,18 @@ impl Client { .is_some() } + pub fn mount(&mut self, entity: EcsEntity) { + if let Some(uid) = self.state.ecs().read_storage::().get(entity).copied() { + self.postbox + .send_message(ClientMsg::ControlEvent(ControlEvent::Mount(uid))); + } + } + + pub fn unmount(&mut self) { + self.postbox + .send_message(ClientMsg::ControlEvent(ControlEvent::Unmount)); + } + pub fn view_distance(&self) -> Option { self.view_distance } @@ -283,16 +306,15 @@ impl Client { } pub fn collect_block(&mut self, pos: Vec3) { - self.postbox.send_message(ClientMsg::CollectBlock(pos)); + self.postbox + .send_message(ClientMsg::ControlEvent(ControlEvent::InventoryManip( + InventoryManip::Collect(pos), + ))); } /// Execute a single client tick, handle input and update the game state by the given duration. #[allow(dead_code)] - pub fn tick( - &mut self, - controller: comp::Controller, - dt: Duration, - ) -> Result, Error> { + pub fn tick(&mut self, inputs: ControllerInputs, dt: Duration) -> Result, Error> { // This tick function is the centre of the Veloren universe. Most client-side things are // managed from here, and as such it's important that it stays organised. Please consult // the core developers before making significant changes to this code. Here is the @@ -310,8 +332,15 @@ impl Client { // 1) Handle input from frontend. // Pass character actions from frontend input to the player's entity. if let ClientState::Character | ClientState::Dead = self.client_state { - self.state.write_component(self.entity, controller.clone()); - self.postbox.send_message(ClientMsg::Controller(controller)); + self.state.write_component( + self.entity, + Controller { + inputs: inputs.clone(), + events: Vec::new(), + }, + ); + self.postbox + .send_message(ClientMsg::ControllerInputs(inputs)); } // 2) Build up a list of events for this frame, to be passed to the frontend. @@ -319,7 +348,7 @@ impl Client { // Prepare for new events { - let ecs = self.state.ecs_mut(); + let ecs = self.state.ecs(); for (entity, _) in (&ecs.entities(), &ecs.read_storage::()).join() { let mut last_character_states = ecs.write_storage::>(); @@ -343,7 +372,7 @@ impl Client { // 3) Update client local data // 4) Tick the client's LocalState - self.state.tick(dt); + self.state.tick(dt, |_| {}); // 5) Terrain let pos = self diff --git a/common/src/comp/controller.rs b/common/src/comp/controller.rs index 178e5a628e..79de5e1d8f 100644 --- a/common/src/comp/controller.rs +++ b/common/src/comp/controller.rs @@ -7,14 +7,14 @@ use vek::*; pub enum ControlEvent { Mount(Uid), Unmount, + InventoryManip(InventoryManip), + //Respawn, } #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] -pub struct Controller { +pub struct ControllerInputs { pub primary: bool, pub secondary: bool, - pub move_dir: Vec2, - pub look_dir: Vec3, pub sit: bool, pub jump: bool, pub roll: bool, @@ -23,6 +23,13 @@ pub struct Controller { pub climb_down: bool, pub wall_leap: bool, pub respawn: bool, + pub move_dir: Vec2, + pub look_dir: Vec3, +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +pub struct Controller { + pub inputs: ControllerInputs, pub events: Vec, } @@ -60,3 +67,12 @@ pub struct Mounting(pub Uid); impl Component for Mounting { type Storage = FlaggedStorage>; } + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum InventoryManip { + Pickup(Uid), + Collect(Vec3), + Use(usize), + Swap(usize, usize), + Drop(usize), +} diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 438da87db6..5df88295b5 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -18,7 +18,9 @@ pub use admin::Admin; pub use agent::Agent; pub use body::{humanoid, object, quadruped, quadruped_medium, Body}; pub use character_state::{ActionState, CharacterState, MovementState}; -pub use controller::{ControlEvent, Controller, MountState, Mounting}; +pub use controller::{ + ControlEvent, Controller, ControllerInputs, InventoryManip, MountState, Mounting, +}; pub use inputs::CanBuild; pub use inventory::{item, Inventory, InventoryUpdate, Item}; pub use last::Last; diff --git a/common/src/event.rs b/common/src/event.rs index b2c771ab8a..8bc938d5e0 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -30,6 +30,7 @@ pub enum ServerEvent { entity: EcsEntity, cause: comp::HealthSource, }, + InventoryManip(EcsEntity, comp::InventoryManip), Respawn(EcsEntity), Shoot { entity: EcsEntity, @@ -46,6 +47,15 @@ pub enum ServerEvent { Mount(EcsEntity, EcsEntity), Unmount(EcsEntity), Possess(Uid, Uid), + CreatePlayer { + entity: EcsEntity, + name: String, + body: comp::Body, + main: Option, + }, + ClientDisconnect(EcsEntity), + ChunkRequest(EcsEntity, Vec2), + ChatCmd(EcsEntity, String), } pub struct EventBus { diff --git a/common/src/msg/client.rs b/common/src/msg/client.rs index 1e98732283..c699db93d1 100644 --- a/common/src/msg/client.rs +++ b/common/src/msg/client.rs @@ -14,12 +14,12 @@ pub enum ClientMsg { body: comp::Body, main: Option, }, - Controller(comp::Controller), + ControllerInputs(comp::ControllerInputs), + ControlEvent(comp::ControlEvent), RequestState(ClientState), SetViewDistance(u32), BreakBlock(Vec3), PlaceBlock(Vec3, Block), - CollectBlock(Vec3), Ping, Pong, ChatMsg { @@ -31,10 +31,6 @@ pub enum ClientMsg { vel: comp::Vel, ori: comp::Ori, }, - UseInventorySlot(usize), - SwapInventorySlots(usize, usize), - DropInventorySlot(usize), - PickUp(u64), TerrainChunkRequest { key: Vec2, }, diff --git a/common/src/region.rs b/common/src/region.rs index da8b2fa90a..1bbcc72eeb 100644 --- a/common/src/region.rs +++ b/common/src/region.rs @@ -195,9 +195,6 @@ impl RegionMap { } } } - pub fn add(&mut self, entity: EcsEntity, pos: Vec3) { - self.add_entity(entity.id(), pos.map(|e| e as i32), None); - } fn add_entity(&mut self, id: u32, pos: Vec3, from: Option>) { let key = Self::pos_key(pos); if let Some(region) = self.regions.get_mut(&key) { diff --git a/common/src/state.rs b/common/src/state.rs index 0859323d4d..640b283520 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -309,7 +309,7 @@ impl State { } /// Execute a single tick, simulating the game state by the given duration. - pub fn tick(&mut self, dt: Duration) { + pub fn tick(&mut self, dt: Duration, add_foreign_systems: impl Fn(&mut DispatcherBuilder)) { // Change the time accordingly. self.ecs.write_resource::().0 += dt.as_secs_f64() * DAY_CYCLE_FACTOR; self.ecs.write_resource::