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, + ); } } }