diff --git a/CHANGELOG.md b/CHANGELOG.md index cb78c2f892..e43717338f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,8 +74,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Jump has been decreased in height but extended in length as a result of the new gravity - Fall damage has been adjusted with the new gravity in mind - Projectiles now generally have a different arc because they no longer have their own gravity modifier -- Increased agent system target search efficiency speeding up the server -- Added more parallelization to terrain serialization and removed extra cloning speeding up the server ### Removed diff --git a/Cargo.lock b/Cargo.lock index ef8a66c119..b27aa7160e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5443,7 +5443,7 @@ dependencies = [ "veloren-common-frontend", "veloren-common-net", "veloren-common-state", - "veloren-common-systems", + "veloren-common-sys", "veloren-network", ] @@ -5557,7 +5557,7 @@ dependencies = [ ] [[package]] -name = "veloren-common-systems" +name = "veloren-common-sys" version = "0.9.0" dependencies = [ "hashbrown", @@ -5682,7 +5682,7 @@ dependencies = [ "veloren-common-ecs", "veloren-common-net", "veloren-common-state", - "veloren-common-systems", + "veloren-common-sys", "veloren-network", "veloren-plugin-api", "veloren-world", @@ -5770,7 +5770,7 @@ dependencies = [ "veloren-common-frontend", "veloren-common-net", "veloren-common-state", - "veloren-common-systems", + "veloren-common-sys", "veloren-server", "veloren-voxygen-anim", "veloren-world", diff --git a/Cargo.toml b/Cargo.toml index d1cfa92648..2eb30662d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ "common/ecs", "common/net", "common/state", - "common/systems", + "common/sys", "common/frontend", "client", "plugin/api", diff --git a/client/Cargo.toml b/client/Cargo.toml index e0113e12f2..8f7619c628 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Joshua Barretto "] edition = "2018" [features] -tracy = ["common/tracy", "common-base/tracy", "common-state/tracy", "common-systems/tracy", "common-net/tracy", "common-frontend/tracy"] +tracy = ["common/tracy", "common-base/tracy", "common-sys/tracy", "common-net/tracy", "common-frontend/tracy"] simd = ["vek/platform_intrinsics"] plugins = ["common-state/plugins"] bin_bot = ["common-ecs", "serde", "ron", "clap", "rustyline", "common-frontend", "async-channel"] @@ -16,7 +16,7 @@ default = ["simd"] common = { package = "veloren-common", path = "../common", features = ["no-assets"] } common-base = { package = "veloren-common-base", path = "../common/base" } common-state = { package = "veloren-common-state", path = "../common/state", default-features = false } -common-systems = { package = "veloren-common-systems", path = "../common/systems", default-features = false } +common-sys = { package = "veloren-common-sys", path = "../common/sys", default-features = false } common-net = { package = "veloren-common-net", path = "../common/net" } network = { package = "veloren-network", path = "../network", features = ["compression"], default-features = false } @@ -32,7 +32,6 @@ vek = { version = "=0.14.1", features = ["serde"] } hashbrown = { version = "0.9", features = ["rayon", "serde", "nightly"] } authc = { git = "https://gitlab.com/veloren/auth.git", rev = "fb3dcbc4962b367253f8f2f92760ef44d2679c9a" } -#TODO: put bot in a different crate #bot only async-channel = { version = "1.6", optional = true } common-ecs = { package = "veloren-common-ecs", path = "../common/ecs", optional = true } diff --git a/client/src/lib.rs b/client/src/lib.rs index 39ce8d113e..5f225758bc 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -55,7 +55,7 @@ use common_net::{ sync::WorldSyncExt, }; use common_state::State; -use common_systems::add_local_systems; +use common_sys::add_local_systems; use comp::BuffKind; use futures_util::FutureExt; use hashbrown::{HashMap, HashSet}; @@ -1924,7 +1924,7 @@ impl Client { match msg { ServerGeneral::TerrainChunkUpdate { key, chunk } => { if let Ok(chunk) = chunk { - self.state.insert_chunk(key, chunk); + self.state.insert_chunk(key, *chunk); } self.pending_chunks.remove(&key); }, diff --git a/common/net/src/msg/server.rs b/common/net/src/msg/server.rs index 239d3e10d0..9eabad35ca 100644 --- a/common/net/src/msg/server.rs +++ b/common/net/src/msg/server.rs @@ -12,7 +12,7 @@ use common::{ }; use hashbrown::HashMap; use serde::{Deserialize, Serialize}; -use std::{sync::Arc, time::Duration}; +use std::time::Duration; use vek::*; ///This struct contains all messages the server might send (on different @@ -106,7 +106,7 @@ pub enum ServerGeneral { // Ingame related AND terrain stream TerrainChunkUpdate { key: Vec2, - chunk: Result, ()>, + chunk: Result, ()>, }, TerrainBlockUpdates(HashMap, Block>), // Always possible diff --git a/common/src/cached_spatial_grid.rs b/common/src/cached_spatial_grid.rs deleted file mode 100644 index 92b453868f..0000000000 --- a/common/src/cached_spatial_grid.rs +++ /dev/null @@ -1,21 +0,0 @@ -use crate::util::SpatialGrid; - -/// Cached [`SpatialGrid`] for reuse within different ecs systems during a tick. -/// This is used to accelerate queries on entities within a specific area. -/// Updated within the physics system [`crate::sys::phys::Sys`] after new entity -/// positions are calculated for the tick. So any position modifications outside -/// the physics system will not be reflected here until the next tick when the -/// physics system runs. -pub struct CachedSpatialGrid(pub SpatialGrid); - -impl Default for CachedSpatialGrid { - fn default() -> Self { - let lg2_cell_size = 5; // 32 - let lg2_large_cell_size = 6; // 64 - let radius_cutoff = 8; - - let spatial_grid = SpatialGrid::new(lg2_cell_size, lg2_large_cell_size, radius_cutoff); - - Self(spatial_grid) - } -} diff --git a/common/src/lib.rs b/common/src/lib.rs index d4d8f6a094..c78e861453 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -26,8 +26,6 @@ pub use uuid; pub mod assets; #[cfg(not(target_arch = "wasm32"))] pub mod astar; #[cfg(not(target_arch = "wasm32"))] -mod cached_spatial_grid; -#[cfg(not(target_arch = "wasm32"))] pub mod character; #[cfg(not(target_arch = "wasm32"))] pub mod clock; #[cfg(not(target_arch = "wasm32"))] pub mod cmd; @@ -80,8 +78,6 @@ pub mod uid; #[cfg(not(target_arch = "wasm32"))] pub mod volumes; -#[cfg(not(target_arch = "wasm32"))] -pub use cached_spatial_grid::CachedSpatialGrid; pub use combat::DamageSource; #[cfg(not(target_arch = "wasm32"))] pub use combat::{Damage, GroupTarget, Knockback, KnockbackDir}; diff --git a/common/src/util/mod.rs b/common/src/util/mod.rs index bfad8f01e9..2dbbc44a4f 100644 --- a/common/src/util/mod.rs +++ b/common/src/util/mod.rs @@ -4,9 +4,6 @@ pub mod find_dist; mod option; pub mod plane; pub mod projection; -/// Contains [`SpatialGrid`] which is useful for accelerating queries of nearby -/// entities -mod spatial_grid; pub const GIT_VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/githash")); pub const GIT_TAG: &str = include_str!(concat!(env!("OUT_DIR"), "/gittag")); @@ -31,7 +28,6 @@ lazy_static::lazy_static! { pub use color::*; pub use dir::*; -pub use option::either_with; -pub use plane::Plane; -pub use projection::Projection; -pub use spatial_grid::SpatialGrid; +pub use option::*; +pub use plane::*; +pub use projection::*; diff --git a/common/state/src/build_areas.rs b/common/state/src/build_areas.rs deleted file mode 100644 index 8d1e273e77..0000000000 --- a/common/state/src/build_areas.rs +++ /dev/null @@ -1,54 +0,0 @@ -use common::depot::{Depot, Id}; -use hashbrown::{hash_map, HashMap}; -use vek::*; - -/// NOTE: Please don't add `Deserialize` without checking to make sure we -/// can guarantee the invariant that every entry in `area_names` points to a -/// valid id in `areas`. -#[derive(Default)] -pub struct BuildAreas { - areas: Depot>, - area_names: HashMap>>, -} - -pub enum BuildAreaError { - /// This build area name is reserved by the system. - Reserved, - /// The build area name was not found. - NotFound, -} - -/// Build area names that can only be inserted, not removed. -const RESERVED_BUILD_AREA_NAMES: &[&str] = &["world"]; - -impl BuildAreas { - pub fn areas(&self) -> &Depot> { &self.areas } - - pub fn area_names(&self) -> &HashMap>> { &self.area_names } - - /// If the area_name is already in the map, returns Err(area_name). - pub fn insert(&mut self, area_name: String, area: Aabb) -> Result>, String> { - let area_name_entry = match self.area_names.entry(area_name) { - hash_map::Entry::Occupied(o) => return Err(o.replace_key()), - hash_map::Entry::Vacant(v) => v, - }; - let bb_id = self.areas.insert(area.made_valid()); - area_name_entry.insert(bb_id); - Ok(bb_id) - } - - pub fn remove(&mut self, area_name: &str) -> Result, BuildAreaError> { - if RESERVED_BUILD_AREA_NAMES.contains(&area_name) { - return Err(BuildAreaError::Reserved); - } - let bb_id = self - .area_names - .remove(area_name) - .ok_or(BuildAreaError::NotFound)?; - let area = self.areas.remove(bb_id).expect( - "Entries in `areas` are added before entries in `area_names` in `insert`, and that is \ - the only exposed way to add elements to `area_names`.", - ); - Ok(area) - } -} diff --git a/common/state/src/lib.rs b/common/state/src/lib.rs index d0047fe6de..c633e60771 100644 --- a/common/state/src/lib.rs +++ b/common/state/src/lib.rs @@ -1,9 +1,589 @@ -//! This crate contains the [`State`] and shared between -//! server (`veloren-server`) and the client (`veloren-client`) - -mod build_areas; #[cfg(feature = "plugins")] pub mod plugin; -mod state; -// TODO: breakup state module and remove glob -pub use build_areas::{BuildAreaError, BuildAreas}; -pub use state::{BlockChange, State, TerrainChanges}; + +#[cfg(feature = "plugins")] +use crate::plugin::memory_manager::EcsWorld; +#[cfg(feature = "plugins")] +use crate::plugin::PluginMgr; +#[cfg(feature = "plugins")] +use common::uid::UidAllocator; +use common::{ + comp, + depot::{Depot, Id}, + event::{EventBus, LocalEvent, ServerEvent}, + outcome::Outcome, + region::RegionMap, + resources::{DeltaTime, GameMode, PlayerEntity, PlayerPhysicsSettings, Time, TimeOfDay}, + slowjob::SlowJobPool, + terrain::{Block, TerrainChunk, TerrainGrid}, + time::DayPeriod, + trade::Trades, + vol::{ReadVol, WriteVol}, +}; +use common_base::span; +use common_ecs::{PhysicsMetrics, SysMetrics}; +use common_net::sync::{interpolation as sync_interp, WorldSyncExt}; +use core::{convert::identity, time::Duration}; +use hashbrown::{hash_map, HashMap, HashSet}; +use rayon::{ThreadPool, ThreadPoolBuilder}; +use specs::{ + prelude::Resource, + shred::{Fetch, FetchMut}, + storage::{MaskedStorage as EcsMaskedStorage, Storage as EcsStorage}, + Component, DispatcherBuilder, Entity as EcsEntity, WorldExt, +}; +use std::sync::Arc; +use vek::*; + +/// How much faster should an in-game day be compared to a real day? +// TODO: Don't hard-code this. +const DAY_CYCLE_FACTOR: f64 = 24.0 * 2.0; + +/// At what point should we stop speeding up physics to compensate for lag? If +/// we speed physics up too fast, we'd skip important physics events like +/// collisions. This constant determines the upper limit. If delta time exceeds +/// this value, the game's physics will begin to produce time lag. Ideally, we'd +/// avoid such a situation. +const MAX_DELTA_TIME: f32 = 1.0; + +/// NOTE: Please don't add `Deserialize` without checking to make sure we +/// can guarantee the invariant that every entry in `area_names` points to a +/// valid id in `areas`. +#[derive(Default)] +pub struct BuildAreas { + areas: Depot>, + area_names: HashMap>>, +} + +pub enum BuildAreaError { + /// This build area name is reserved by the system. + Reserved, + /// The build area name was not found. + NotFound, +} + +/// Build area names that can only be inserted, not removed. +const RESERVED_BUILD_AREA_NAMES: &[&str] = &["world"]; + +impl BuildAreas { + pub fn areas(&self) -> &Depot> { &self.areas } + + pub fn area_names(&self) -> &HashMap>> { &self.area_names } + + /// If the area_name is already in the map, returns Err(area_name). + pub fn insert(&mut self, area_name: String, area: Aabb) -> Result>, String> { + let area_name_entry = match self.area_names.entry(area_name) { + hash_map::Entry::Occupied(o) => return Err(o.replace_key()), + hash_map::Entry::Vacant(v) => v, + }; + let bb_id = self.areas.insert(area.made_valid()); + area_name_entry.insert(bb_id); + Ok(bb_id) + } + + pub fn remove(&mut self, area_name: &str) -> Result, BuildAreaError> { + if RESERVED_BUILD_AREA_NAMES.contains(&area_name) { + return Err(BuildAreaError::Reserved); + } + let bb_id = self + .area_names + .remove(area_name) + .ok_or(BuildAreaError::NotFound)?; + let area = self.areas.remove(bb_id).expect( + "Entries in `areas` are added before entries in `area_names` in `insert`, and that is \ + the only exposed way to add elements to `area_names`.", + ); + Ok(area) + } +} + +#[derive(Default)] +pub struct BlockChange { + blocks: HashMap, Block>, +} + +impl BlockChange { + pub fn set(&mut self, pos: Vec3, block: Block) { self.blocks.insert(pos, block); } + + pub fn try_set(&mut self, pos: Vec3, block: Block) -> Option<()> { + if !self.blocks.contains_key(&pos) { + self.blocks.insert(pos, block); + Some(()) + } else { + None + } + } + + pub fn clear(&mut self) { self.blocks.clear(); } +} + +#[derive(Default)] +pub struct TerrainChanges { + pub new_chunks: HashSet>, + pub modified_chunks: HashSet>, + pub removed_chunks: HashSet>, + pub modified_blocks: HashMap, Block>, +} + +impl TerrainChanges { + pub fn clear(&mut self) { + self.new_chunks.clear(); + self.modified_chunks.clear(); + self.removed_chunks.clear(); + } +} + +#[derive(Copy, Clone)] +pub enum ExecMode { + Server, + Client, + Singleplayer, +} + +/// A type used to represent game state stored on both the client and the +/// server. This includes things like entity components, terrain data, and +/// global states like weather, time of day, etc. +pub struct State { + ecs: specs::World, + // Avoid lifetime annotation by storing a thread pool instead of the whole dispatcher + thread_pool: Arc, +} + +impl State { + /// Create a new `State` in client mode. + pub fn client() -> Self { Self::new(GameMode::Client) } + + /// Create a new `State` in server mode. + pub fn server() -> Self { Self::new(GameMode::Server) } + + pub fn new(game_mode: GameMode) -> Self { + let thread_name_infix = match game_mode { + GameMode::Server => "s", + GameMode::Client => "c", + GameMode::Singleplayer => "sp", + }; + + let thread_pool = Arc::new( + ThreadPoolBuilder::new() + .thread_name(move |i| format!("rayon-{}-{}", thread_name_infix, i)) + .build() + .unwrap(), + ); + Self { + ecs: Self::setup_ecs_world(game_mode, &thread_pool), + thread_pool, + } + } + + /// Creates ecs world and registers all the common components and resources + // TODO: Split up registering into server and client (e.g. move + // EventBus to the server) + fn setup_ecs_world(game_mode: GameMode, thread_pool: &Arc) -> specs::World { + let mut ecs = specs::World::new(); + // Uids for sync + ecs.register_sync_marker(); + // Register server -> all clients synced components. + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + + // Register components send from clients -> server + ecs.register::(); + + // Register components send directly from server -> all but one client + ecs.register::(); + + // Register components synced from client -> server -> all other clients + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + + // Register common unsynced components + ecs.register::(); + ecs.register::(); + + // Register client-local components + // TODO: only register on the client + ecs.register::(); + ecs.register::>(); + ecs.register::>(); + ecs.register::>(); + + // Register server-local components + // TODO: only register on the server + ecs.register::>(); + ecs.register::>(); + ecs.register::>(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + ecs.register::(); + + // Register synced resources used by the ECS. + ecs.insert(TimeOfDay(0.0)); + + // Register unsynced resources used by the ECS. + ecs.insert(Time(0.0)); + ecs.insert(DeltaTime(0.0)); + ecs.insert(PlayerEntity(None)); + ecs.insert(TerrainGrid::new().unwrap()); + ecs.insert(BlockChange::default()); + ecs.insert(BuildAreas::default()); + ecs.insert(TerrainChanges::default()); + ecs.insert(EventBus::::default()); + ecs.insert(game_mode); + ecs.insert(Vec::::new()); + + let slow_limit = thread_pool.current_num_threads().max(2) as u64; + let slow_limit = slow_limit / 2 + slow_limit / 4; + tracing::trace!(?slow_limit, "Slow Thread limit"); + ecs.insert(SlowJobPool::new(slow_limit, Arc::clone(&thread_pool))); + + // TODO: only register on the server + ecs.insert(EventBus::::default()); + ecs.insert(comp::group::GroupManager::default()); + ecs.insert(RegionMap::new()); + ecs.insert(SysMetrics::default()); + ecs.insert(PhysicsMetrics::default()); + ecs.insert(Trades::default()); + ecs.insert(PlayerPhysicsSettings::default()); + + // Load plugins from asset directory + #[cfg(feature = "plugins")] + ecs.insert(match PluginMgr::from_assets() { + Ok(plugin_mgr) => { + let ecs_world = EcsWorld { + entities: &ecs.entities(), + health: ecs.read_component().into(), + uid: ecs.read_component().into(), + uid_allocator: &ecs.read_resource::().into(), + player: ecs.read_component().into(), + }; + if let Err(e) = plugin_mgr + .execute_event(&ecs_world, &plugin_api::event::PluginLoadEvent { + game_mode, + }) + { + tracing::debug!(?e, "Failed to run plugin init"); + tracing::info!("Plugins disabled, enable debug logging for more information."); + PluginMgr::default() + } else { + plugin_mgr + } + }, + Err(e) => { + tracing::debug!(?e, "Failed to read plugins from assets"); + tracing::info!("Plugins disabled, enable debug logging for more information."); + PluginMgr::default() + }, + }); + + ecs + } + + /// Register a component with the state's ECS. + pub fn with_component(mut self) -> Self + where + ::Storage: Default, + { + self.ecs.register::(); + self + } + + /// Write a component attributed to a particular entity, ignoring errors. + /// + /// This should be used *only* when we can guarantee that the rest of the + /// code does not rely on the insert having succeeded (meaning the + /// entity is no longer alive!). + /// + /// Returns None if the entity was dead or there was no previous entry for + /// this component; otherwise, returns Some(old_component). + pub fn write_component_ignore_entity_dead( + &mut self, + entity: EcsEntity, + comp: C, + ) -> Option { + self.ecs + .write_storage() + .insert(entity, comp) + .ok() + .and_then(identity) + } + + /// Delete a component attributed to a particular entity. + pub fn delete_component(&mut self, entity: EcsEntity) -> Option { + self.ecs.write_storage().remove(entity) + } + + /// Read a component attributed to a particular entity. + pub fn read_component_cloned(&self, entity: EcsEntity) -> Option { + self.ecs.read_storage().get(entity).cloned() + } + + /// Read a component attributed to a particular entity. + pub fn read_component_copied(&self, entity: EcsEntity) -> Option { + self.ecs.read_storage().get(entity).copied() + } + + /// Given mutable access to the resource R, assuming the resource + /// component exists (this is already the behavior of functions like `fetch` + /// and `write_component_ignore_entity_dead`). Since all of our resources + /// are generated up front, any failure here is definitely a code bug. + pub fn mut_resource(&mut self) -> &mut R { + self.ecs.get_mut::().expect( + "Tried to fetch an invalid resource even though all our resources should be known at \ + compile time.", + ) + } + + /// Get a read-only reference to the storage of a particular component type. + pub fn read_storage(&self) -> EcsStorage>> { + self.ecs.read_storage::() + } + + /// Get a reference to the internal ECS world. + pub fn ecs(&self) -> &specs::World { &self.ecs } + + /// Get a mutable reference to the internal ECS world. + pub fn ecs_mut(&mut self) -> &mut specs::World { &mut self.ecs } + + /// Get a reference to the `TerrainChanges` structure of the state. This + /// contains information about terrain state that has changed since the + /// last game tick. + pub fn terrain_changes(&self) -> Fetch { self.ecs.read_resource() } + + /// Get the current in-game time of day. + /// + /// Note that this should not be used for physics, animations or other such + /// localised timings. + pub fn get_time_of_day(&self) -> f64 { self.ecs.read_resource::().0 } + + /// Get the current in-game day period (period of the day/night cycle) + /// Get the current in-game day period (period of the day/night cycle) + pub fn get_day_period(&self) -> DayPeriod { self.get_time_of_day().into() } + + /// Get the current in-game time. + /// + /// Note that this does not correspond to the time of day. + pub fn get_time(&self) -> f64 { self.ecs.read_resource::