diff --git a/Cargo.lock b/Cargo.lock index 3a1c24ede9..2ce962ac5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1839,6 +1839,27 @@ dependencies = [ "syn 1.0.100", ] +[[package]] +name = "enum-map" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a56d54c8dd9b3ad34752ed197a4eb2a6601bc010808eb097a04a58ae4c43e1" +dependencies = [ + "enum-map-derive", + "serde", +] + +[[package]] +name = "enum-map-derive" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9045e2676cd5af83c3b167d917b0a5c90a4d8e266e2683d6631b235c457fc27" +dependencies = [ + "proc-macro2 1.0.43", + "quote 1.0.21", + "syn 1.0.100", +] + [[package]] name = "enumset" version = "1.0.11" @@ -6655,6 +6676,8 @@ dependencies = [ "crossbeam-utils 0.8.11", "csv", "dot_vox", + "enum-iterator 1.1.3", + "enum-map", "fxhash", "hashbrown 0.12.3", "indexmap", @@ -6888,9 +6911,11 @@ dependencies = [ name = "veloren-rtsim" version = "0.10.0" dependencies = [ + "enum-map", "hashbrown 0.12.3", "ron 0.8.0", "serde", + "vek 0.15.8", "veloren-common", "veloren-world", ] @@ -6907,6 +6932,7 @@ dependencies = [ "chrono-tz", "crossbeam-channel", "drop_guard", + "enum-map", "enumset", "futures-util", "hashbrown 0.12.3", @@ -7120,6 +7146,7 @@ dependencies = [ "csv", "deflate", "enum-iterator 1.1.3", + "enum-map", "fallible-iterator", "flate2", "fxhash", diff --git a/client/src/lib.rs b/client/src/lib.rs index f7c527bb36..9c5ee7d6a5 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1855,6 +1855,7 @@ impl Client { true, None, &self.connected_server_constants, + |_, _, _, _| {}, ); // TODO: avoid emitting these in the first place let _ = self diff --git a/common/Cargo.toml b/common/Cargo.toml index 7288871985..8ff5bd5ee9 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -26,6 +26,8 @@ common-base = { package = "veloren-common-base", path = "base" } serde = { version = "1.0.110", features = ["derive", "rc"] } # Util +enum-iterator = "1.1.3" +enum-map = "2.4" vek = { version = "0.15.8", features = ["serde"] } cfg-if = "1.0.0" chrono = "0.4.22" diff --git a/common/src/generation.rs b/common/src/generation.rs index 764165c793..e12864c2c8 100644 --- a/common/src/generation.rs +++ b/common/src/generation.rs @@ -8,7 +8,9 @@ use crate::{ lottery::LootSpec, npc::{self, NPC_NAMES}, trade::SiteInformation, + rtsim, }; +use enum_map::EnumMap; use serde::Deserialize; use vek::*; @@ -449,6 +451,7 @@ impl EntityInfo { #[derive(Default)] pub struct ChunkSupplement { pub entities: Vec, + pub rtsim_max_resources: EnumMap, } impl ChunkSupplement { diff --git a/common/src/rtsim.rs b/common/src/rtsim.rs index d8ff8f0423..1dc03a0250 100644 --- a/common/src/rtsim.rs +++ b/common/src/rtsim.rs @@ -4,6 +4,7 @@ // module in `server`. use specs::Component; +use serde::{Serialize, Deserialize}; use vek::*; use crate::comp::dialogue::MoodState; @@ -82,3 +83,10 @@ impl RtSimController { } } } + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, enum_map::Enum)] +pub enum ChunkResource { + Grass, + Flax, + Cotton, +} diff --git a/common/src/terrain/block.rs b/common/src/terrain/block.rs index 6e8669586b..614b424bb6 100644 --- a/common/src/terrain/block.rs +++ b/common/src/terrain/block.rs @@ -4,6 +4,7 @@ use crate::{ consts::FRIC_GROUND, lottery::LootSpec, make_case_elim, + rtsim, }; use num_derive::FromPrimitive; use num_traits::FromPrimitive; @@ -195,6 +196,26 @@ impl Block { } } + /// Returns the rtsim resource, if any, that this block corresponds to. If you want the scarcity of a block to change with rtsim's resource depletion tracking, you can do so by editing this function. + #[inline] + pub fn get_rtsim_resource(&self) -> Option { + match self.get_sprite()? { + SpriteKind::LongGrass + | SpriteKind::MediumGrass + | SpriteKind::ShortGrass + | SpriteKind::LargeGrass + | SpriteKind::GrassSnow + | SpriteKind::GrassBlue + | SpriteKind::SavannaGrass + | SpriteKind::TallSavannaGrass + | SpriteKind::RedSavannaGrass + | SpriteKind::JungleRedGrass => Some(rtsim::ChunkResource::Grass), + SpriteKind::WildFlax => Some(rtsim::ChunkResource::Flax), + SpriteKind::Cotton => Some(rtsim::ChunkResource::Cotton), + _ => None, + } + } + #[inline] pub fn get_glow(&self) -> Option { match self.kind() { diff --git a/common/state/src/state.rs b/common/state/src/state.rs index 6bc741ee84..1a3cf2b458 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -524,7 +524,12 @@ impl State { } // Apply terrain changes - pub fn apply_terrain_changes(&self) { self.apply_terrain_changes_internal(false); } + pub fn apply_terrain_changes( + &self, + mut block_update: impl FnMut(&specs::World, Vec3, Block, Block), + ) { + self.apply_terrain_changes_internal(false, block_update); + } /// `during_tick` is true if and only if this is called from within /// [State::tick]. @@ -534,7 +539,11 @@ impl State { /// from within both the client and the server ticks, right after /// handling terrain messages; currently, client sets it to true and /// server to false. - fn apply_terrain_changes_internal(&self, during_tick: bool) { + fn apply_terrain_changes_internal( + &self, + during_tick: bool, + mut block_update: impl FnMut(&specs::World, Vec3, Block, Block), + ) { span!( _guard, "apply_terrain_changes", @@ -575,14 +584,17 @@ impl State { } // Apply block modifications // Only include in `TerrainChanges` if successful - modified_blocks.retain(|pos, block| { - let res = terrain.set(*pos, *block); + modified_blocks.retain(|pos, new_block| { + let res = terrain.map(*pos, |old_block| { + block_update(&self.ecs, *pos, old_block, *new_block); + *new_block + }); if let (&Ok(old_block), true) = (&res, during_tick) { // NOTE: If the changes are applied during the tick, we push the *old* value as // the modified block (since it otherwise can't be recovered after the tick). // Otherwise, the changes will be applied after the tick, so we push the *new* // value. - *block = old_block; + *new_block = old_block; } res.is_ok() }); @@ -597,6 +609,7 @@ impl State { update_terrain_and_regions: bool, mut metrics: Option<&mut StateTickMetrics>, server_constants: &ServerConstants, + block_update: impl FnMut(&specs::World, Vec3, Block, Block), ) { span!(_guard, "tick", "State::tick"); @@ -643,7 +656,7 @@ impl State { drop(guard); if update_terrain_and_regions { - self.apply_terrain_changes_internal(true); + self.apply_terrain_changes_internal(true, block_update); } // Process local events diff --git a/rtsim/Cargo.toml b/rtsim/Cargo.toml index 662da3835c..504d1d50cd 100644 --- a/rtsim/Cargo.toml +++ b/rtsim/Cargo.toml @@ -9,3 +9,5 @@ world = { package = "veloren-world", path = "../world" } ron = "0.8" serde = { version = "1.0.110", features = ["derive"] } hashbrown = { version = "0.12", features = ["rayon", "serde", "nightly"] } +enum-map = { version = "2.4", features = ["serde"] } +vek = { version = "0.15.8", features = ["serde"] } diff --git a/rtsim/src/data/actor.rs b/rtsim/src/data/actor.rs index 81893f29f5..88d3e58ee3 100644 --- a/rtsim/src/data/actor.rs +++ b/rtsim/src/data/actor.rs @@ -1,13 +1,16 @@ use hashbrown::HashMap; +use serde::{Serialize, Deserialize}; -#[derive(Copy, Clone, Hash, PartialEq, Eq)] +#[derive(Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct ActorId { pub idx: u32, pub gen: u32, } +#[derive(Clone, Serialize, Deserialize)] pub struct Actor {} +#[derive(Clone, Serialize, Deserialize)] pub struct Actors { pub actors: HashMap, } diff --git a/rtsim/src/data/helper.rs b/rtsim/src/data/helper.rs deleted file mode 100644 index 6ea6b221d1..0000000000 --- a/rtsim/src/data/helper.rs +++ /dev/null @@ -1,55 +0,0 @@ -use serde::{ - de::{DeserializeOwned, Error}, - Deserialize, Deserializer, Serialize, Serializer, -}; - -#[derive(Copy, Clone, Default, PartialEq, Eq, Hash)] -pub struct V(pub T); - -impl Serialize for V { - fn serialize(&self, serializer: S) -> Result { - self.0.serialize(serializer) - } -} - -impl<'de, T: Version> Deserialize<'de> for V { - fn deserialize>(deserializer: D) -> Result { - T::try_from_value_compat(ron::Value::deserialize(deserializer)?) - .map(Self) - .map_err(|e| D::Error::custom(e)) - } -} - -impl> Latest for V { - fn to_unversioned(self) -> U { self.0.to_unversioned() } - - fn from_unversioned(x: &U) -> Self { Self(T::from_unversioned(x)) } -} - -pub trait Latest { - fn to_unversioned(self) -> T; - fn from_unversioned(x: &T) -> Self; -} - -pub trait Version: Sized + DeserializeOwned { - type Prev: Version; - - fn migrate(prev: Self::Prev) -> Self; - - fn try_from_value_compat(value: ron::Value) -> Result { - value.clone().into_rust().or_else(|e| { - Ok(Self::migrate( - ::Prev::try_from_value_compat(value).map_err(|_| e)?, - )) - }) - } -} - -#[derive(Deserialize)] -pub enum Bottom {} - -impl Version for Bottom { - type Prev = Self; - - fn migrate(prev: Self::Prev) -> Self { prev } -} diff --git a/rtsim/src/data/mod.rs b/rtsim/src/data/mod.rs index fd56da5a5e..d4dd3d97e9 100644 --- a/rtsim/src/data/mod.rs +++ b/rtsim/src/data/mod.rs @@ -1,6 +1,3 @@ -pub mod helper; -pub mod version; - pub mod actor; pub mod nature; @@ -11,9 +8,10 @@ pub use self::{ use self::helper::Latest; use ron::error::SpannedResult; -use serde::Deserialize; +use serde::{Serialize, Deserialize}; use std::io::{Read, Write}; +#[derive(Clone, Serialize, Deserialize)] pub struct Data { pub nature: Nature, pub actors: Actors, @@ -21,10 +19,10 @@ pub struct Data { impl Data { pub fn from_reader(reader: R) -> SpannedResult { - ron::de::from_reader(reader).map(version::LatestData::to_unversioned) + ron::de::from_reader(reader) } pub fn write_to(&self, writer: W) -> Result<(), ron::Error> { - ron::ser::to_writer(writer, &version::LatestData::from_unversioned(self)) + ron::ser::to_writer(writer, self) } } diff --git a/rtsim/src/data/nature.rs b/rtsim/src/data/nature.rs index dbc53fab83..9fccf7ad2a 100644 --- a/rtsim/src/data/nature.rs +++ b/rtsim/src/data/nature.rs @@ -1 +1,44 @@ -pub struct Nature {} +use serde::{Serialize, Deserialize}; +use enum_map::EnumMap; +use common::{ + grid::Grid, + rtsim::ChunkResource, +}; +use world::World; +use vek::*; + +#[derive(Clone, Serialize, Deserialize)] +pub struct Nature { + chunks: Grid, +} + +impl Nature { + pub fn generate(world: &World) -> Self { + Self { + chunks: Grid::populate_from( + world.sim().get_size().map(|e| e as i32), + |pos| Chunk { + res: EnumMap::<_, f32>::default().map(|_, _| 1.0), + }, + ), + } + } + + // TODO: Clean up this API a bit + pub fn get_chunk_resources(&self, key: Vec2) -> EnumMap { + self.chunks + .get(key) + .map(|c| c.res) + .unwrap_or_default() + } + pub fn set_chunk_resources(&mut self, key: Vec2, res: EnumMap) { + if let Some(chunk) = self.chunks.get_mut(key) { + chunk.res = res; + } + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct Chunk { + res: EnumMap, +} diff --git a/rtsim/src/data/version/actor.rs b/rtsim/src/data/version/actor.rs deleted file mode 100644 index 490e0a14d2..0000000000 --- a/rtsim/src/data/version/actor.rs +++ /dev/null @@ -1,85 +0,0 @@ -use super::*; -use crate::data::{Actor, ActorId, Actors}; -use hashbrown::HashMap; - -// ActorId - -impl Latest for ActorIdV0 { - fn to_unversioned(self) -> ActorId { - ActorId { - idx: self.idx, - gen: self.gen, - } - } - - fn from_unversioned(id: &ActorId) -> Self { - Self { - idx: id.idx, - gen: id.gen, - } - } -} - -#[derive(Serialize, Deserialize, Copy, Clone, Hash, PartialEq, Eq)] -pub struct ActorIdV0 { - pub idx: u32, - pub gen: u32, -} - -impl Version for ActorIdV0 { - type Prev = Bottom; - - fn migrate(x: Self::Prev) -> Self { match x {} } -} - -// Actor - -impl Latest for ActorV0 { - fn to_unversioned(self) -> Actor { Actor {} } - - fn from_unversioned(actor: &Actor) -> Self { Self {} } -} - -#[derive(Serialize, Deserialize, Copy, Clone, Hash, PartialEq, Eq)] -pub struct ActorV0 {} - -impl Version for ActorV0 { - type Prev = Bottom; - - fn migrate(x: Self::Prev) -> Self { match x {} } -} - -// Actors - -impl Latest for ActorsV0 { - fn to_unversioned(self) -> Actors { - Actors { - actors: self - .actors - .into_iter() - .map(|(k, v)| (k.to_unversioned(), v.to_unversioned())) - .collect(), - } - } - - fn from_unversioned(actors: &Actors) -> Self { - Self { - actors: actors - .actors - .iter() - .map(|(k, v)| (Latest::from_unversioned(k), Latest::from_unversioned(v))) - .collect(), - } - } -} - -#[derive(Serialize, Deserialize)] -pub struct ActorsV0 { - actors: HashMap, V>, -} - -impl Version for ActorsV0 { - type Prev = Bottom; - - fn migrate(x: Self::Prev) -> Self { match x {} } -} diff --git a/rtsim/src/data/version/mod.rs b/rtsim/src/data/version/mod.rs deleted file mode 100644 index cc86307cee..0000000000 --- a/rtsim/src/data/version/mod.rs +++ /dev/null @@ -1,84 +0,0 @@ -// # Hey, you! Yes, you! -// -// Don't touch anything in this module, or any sub-modules. No, really. Bad -// stuff will happen. -// -// You're only an exception to this rule if you fulfil the following criteria: -// -// - You *really* understand exactly how the versioning system in `helper.rs` -// works, what assumptions it makes, and how all of this can go badly wrong. -// -// - You are creating a new version of a data structure, and *not* modifying an -// existing one. -// -// - You've thought really carefully about things and you've come to the -// conclusion that there's just no way to add the feature you want to add -// without creating a new version of the data structure in question. -// -// Please note that in *very specific* cases, it is possible to make a change to -// an existing data structure that is backward-compatible. For example, adding a -// new variant to an enum or a new field to a struct (where said field is -// annotated with `#[serde(default)]`) is generally considered to be a -// backward-compatible change. -// -// That said, here's how to make a breaking change to one of the structures in -// this module, or submodules. -// -// 1) Duplicate the latest version of the data structure and the `Version` impl -// for it (later versions should be kept at the top of each file). -// -// 2) Rename the duplicated version, incrementing the version number (i.e: V0 -// becomes V1). -// -// 3) Change the `type Prev =` associated type in the new `Version` impl to the -// previous versions' type. You will need to write an implementation of -// `migrate` that migrates from the old version to the new version. -// -// 4) *Change* the existing `Latest` impl so that it uses the new version you -// have created. -// -// 5) If your data structure is contained within another data structure, you -// will need to similarly update the parent data structure too, also -// following these instructions. -// -// The *golden rule* is that, once merged to master, an old version's type must -// not be changed! - -pub mod actor; -pub mod nature; - -use super::{ - helper::{Bottom, Latest, Version, V}, - Data, -}; -use serde::{Deserialize, Serialize}; - -pub type LatestData = DataV0; - -impl Latest for LatestData { - fn to_unversioned(self) -> Data { - Data { - nature: self.nature.to_unversioned(), - actors: self.actors.to_unversioned(), - } - } - - fn from_unversioned(data: &Data) -> Self { - Self { - nature: Latest::from_unversioned(&data.nature), - actors: Latest::from_unversioned(&data.actors), - } - } -} - -#[derive(Serialize, Deserialize)] -pub struct DataV0 { - nature: V, - actors: V, -} - -impl Version for DataV0 { - type Prev = Bottom; - - fn migrate(x: Self::Prev) -> Self { match x {} } -} diff --git a/rtsim/src/data/version/nature.rs b/rtsim/src/data/version/nature.rs deleted file mode 100644 index 1a10f0b929..0000000000 --- a/rtsim/src/data/version/nature.rs +++ /dev/null @@ -1,17 +0,0 @@ -use super::*; -use crate::data::Nature; - -impl Latest for NatureV0 { - fn to_unversioned(self) -> Nature { Nature {} } - - fn from_unversioned(nature: &Nature) -> Self { Self {} } -} - -#[derive(Serialize, Deserialize)] -pub struct NatureV0 {} - -impl Version for NatureV0 { - type Prev = Bottom; - - fn migrate(x: Self::Prev) -> Self { match x {} } -} diff --git a/rtsim/src/gen/mod.rs b/rtsim/src/gen/mod.rs index d1f9032a42..f1ce167d1d 100644 --- a/rtsim/src/gen/mod.rs +++ b/rtsim/src/gen/mod.rs @@ -5,7 +5,7 @@ use world::World; impl Data { pub fn generate(world: &World) -> Self { Self { - nature: Nature {}, + nature: Nature::generate(world), actors: Actors { actors: HashMap::default(), }, diff --git a/server/Cargo.toml b/server/Cargo.toml index 6d3659fe7c..38b1ea391f 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -64,6 +64,7 @@ authc = { git = "https://gitlab.com/veloren/auth.git", rev = "fb3dcbc4962b367253 slab = "0.4" rand_distr = "0.4.0" enumset = "1.0.8" +enum-map = "2.4" noise = { version = "0.7", default-features = false } censor = "0.2" diff --git a/server/src/chunk_generator.rs b/server/src/chunk_generator.rs index e5a28f3315..f5b34e7f4a 100644 --- a/server/src/chunk_generator.rs +++ b/server/src/chunk_generator.rs @@ -1,4 +1,7 @@ -use crate::metrics::ChunkGenMetrics; +use crate::{ + metrics::ChunkGenMetrics, + rtsim2::RtSim, +}; #[cfg(not(feature = "worldgen"))] use crate::test_world::{IndexOwned, World}; use common::{ @@ -44,6 +47,10 @@ impl ChunkGenerator { key: Vec2, slowjob_pool: &SlowJobPool, world: Arc, + #[cfg(feature = "worldgen")] + rtsim: &RtSim, + #[cfg(not(feature = "worldgen"))] + rtsim: &(), index: IndexOwned, time: (TimeOfDay, Calendar), ) { @@ -56,10 +63,17 @@ impl ChunkGenerator { v.insert(Arc::clone(&cancel)); let chunk_tx = self.chunk_tx.clone(); self.metrics.chunks_requested.inc(); + + // Get state for this chunk from rtsim + #[cfg(feature = "worldgen")] + let rtsim_resources = Some(rtsim.get_chunk_resources(key)); + #[cfg(not(feature = "worldgen"))] + let rtsim_resources = None; + slowjob_pool.spawn("CHUNK_GENERATOR", move || { let index = index.as_index_ref(); let payload = world - .generate_chunk(index, key, || cancel.load(Ordering::Relaxed), Some(time)) + .generate_chunk(index, key, rtsim_resources, || cancel.load(Ordering::Relaxed), Some(time)) // FIXME: Since only the first entity who cancels a chunk is notified, we end up // delaying chunk re-requests for up to 3 seconds for other clients, which isn't // great. We *could* store all the other requesting clients here, but it could diff --git a/server/src/lib.rs b/server/src/lib.rs index 329857f909..df1dca17e4 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -82,7 +82,7 @@ use common::{ rtsim::RtSimEntity, shared_server_config::ServerConstants, slowjob::SlowJobPool, - terrain::{TerrainChunk, TerrainChunkSize}, + terrain::{TerrainChunk, TerrainChunkSize, Block}, vol::RectRasterableVol, }; use common_ecs::run_now; @@ -342,6 +342,7 @@ impl Server { pool.configure("CHUNK_DROP", |_n| 1); pool.configure("CHUNK_GENERATOR", |n| n / 2 + n / 4); pool.configure("CHUNK_SERIALIZER", |n| n / 2); + pool.configure("RTSIM_SAVE", |_| 1); } state .ecs_mut() @@ -700,6 +701,13 @@ impl Server { let before_state_tick = Instant::now(); + fn on_block_update(ecs: &specs::World, wpos: Vec3, old_block: Block, new_block: Block) { + // When a resource block updates, inform rtsim + if old_block.get_rtsim_resource().is_some() || new_block.get_rtsim_resource().is_some() { + ecs.write_resource::().hook_block_update(wpos, old_block, new_block); + } + } + // 4) Tick the server's LocalState. // 5) Fetch any generated `TerrainChunk`s and insert them into the terrain. // in sys/terrain.rs @@ -726,6 +734,7 @@ impl Server { false, Some(&mut state_tick_metrics), &self.server_constants, + on_block_update, ); let before_handle_events = Instant::now(); @@ -749,7 +758,7 @@ impl Server { self.state.update_region_map(); // NOTE: apply_terrain_changes sends the *new* value since it is not being // synchronized during the tick. - self.state.apply_terrain_changes(); + self.state.apply_terrain_changes(on_block_update); let before_sync = Instant::now(); @@ -994,6 +1003,10 @@ impl Server { let mut chunk_generator = ecs.write_resource::(); let client = ecs.read_storage::(); let mut terrain = ecs.write_resource::(); + #[cfg(feature = "worldgen")] + let rtsim = ecs.read_resource::(); + #[cfg(not(feature = "worldgen"))] + let rtsim = (); // Cancel all pending chunks. chunk_generator.cancel_all(); @@ -1009,6 +1022,7 @@ impl Server { pos, &slow_jobs, Arc::clone(world), + &rtsim, index.clone(), ( *ecs.read_resource::(), @@ -1172,11 +1186,16 @@ impl Server { pub fn generate_chunk(&mut self, entity: EcsEntity, key: Vec2) { let ecs = self.state.ecs(); let slow_jobs = ecs.read_resource::(); + #[cfg(feature = "worldgen")] + let rtsim = ecs.read_resource::(); + #[cfg(not(feature = "worldgen"))] + let rtsim = (); ecs.write_resource::().generate_chunk( Some(entity), key, &slow_jobs, Arc::clone(&self.world), + &rtsim, self.index.clone(), ( *ecs.read_resource::(), diff --git a/server/src/rtsim2/mod.rs b/server/src/rtsim2/mod.rs index 11bda686ad..f0aaaf0693 100644 --- a/server/src/rtsim2/mod.rs +++ b/server/src/rtsim2/mod.rs @@ -1,17 +1,31 @@ pub mod tick; -use common::grid::Grid; +use common::{ + grid::Grid, + slowjob::SlowJobPool, + rtsim::ChunkResource, + terrain::{TerrainChunk, Block}, + vol::RectRasterableVol, +}; use common_ecs::{dispatch, System}; use rtsim2::{data::Data, RtState}; use specs::{DispatcherBuilder, WorldExt}; -use std::{fs::File, io, path::PathBuf, sync::Arc}; -use tracing::info; +use std::{ + fs::{self, File}, + path::PathBuf, + sync::Arc, + time::Instant, + io::{self, Write}, +}; +use enum_map::EnumMap; +use tracing::{error, warn, info}; use vek::*; use world::World; pub struct RtSim { file_path: PathBuf, - chunk_states: Grid, // true = loaded + last_saved: Option, + chunk_states: Grid>, state: RtState, } @@ -20,20 +34,46 @@ impl RtSim { let file_path = Self::get_file_path(data_dir); Ok(Self { - chunk_states: Grid::populate_from(world.sim().get_size().as_(), |_| false), + chunk_states: Grid::populate_from(world.sim().get_size().as_(), |_| None), + last_saved: None, state: RtState { data: { info!("Looking for rtsim state in {}...", file_path.display()); - match File::open(&file_path) { - Ok(file) => { - info!("Rtsim state found. Attending to load..."); - Data::from_reader(file)? - }, - Err(e) if e.kind() == io::ErrorKind::NotFound => { - info!("No rtsim state found. Generating from initial world state..."); - Data::generate(&world) - }, - Err(e) => return Err(e.into()), + 'load: { + match File::open(&file_path) { + Ok(file) => { + info!("Rtsim state found. Attempting to load..."); + match Data::from_reader(file) { + Ok(data) => { info!("Rtsim state loaded."); break 'load data }, + Err(e) => { + error!("Rtsim state failed to load: {}", e); + let mut i = 0; + loop { + let mut backup_path = file_path.clone(); + backup_path.set_extension(if i == 0 { + format!("ron_backup_{}", i) + } else { + "ron_backup".to_string() + }); + if !backup_path.exists() { + fs::rename(&file_path, &backup_path)?; + warn!("Failed rtsim state was moved to {}", backup_path.display()); + info!("A fresh rtsim state will now be generated."); + break; + } + i += 1; + } + }, + } + }, + Err(e) if e.kind() == io::ErrorKind::NotFound => + info!("No rtsim state found. Generating from initial world state..."), + Err(e) => return Err(e.into()), + } + + let data = Data::generate(&world); + info!("Rtsim state generated."); + data } }, }, @@ -52,17 +92,88 @@ impl RtSim { path } - pub fn hook_load_chunk(&mut self, key: Vec2) { - if let Some(is_loaded) = self.chunk_states.get_mut(key) { - *is_loaded = true; + pub fn hook_load_chunk(&mut self, key: Vec2, max_res: EnumMap) { + if let Some(chunk_state) = self.chunk_states.get_mut(key) { + *chunk_state = Some(LoadedChunkState { max_res }); } } pub fn hook_unload_chunk(&mut self, key: Vec2) { - if let Some(is_loaded) = self.chunk_states.get_mut(key) { - *is_loaded = false; + if let Some(chunk_state) = self.chunk_states.get_mut(key) { + *chunk_state = None; } } + + pub fn save(&mut self, slowjob_pool: &SlowJobPool) { + info!("Beginning rtsim state save..."); + let file_path = self.file_path.clone(); + let data = self.state.data.clone(); + info!("Starting rtsim save job..."); + // TODO: Use slow job + // slowjob_pool.spawn("RTSIM_SAVE", move || { + std::thread::spawn(move || { + let tmp_file_name = "state_tmp.ron"; + if let Err(e) = file_path + .parent() + .map(|dir| { + fs::create_dir_all(dir)?; + // We write to a temporary file and then rename to avoid corruption. + Ok(dir.join(tmp_file_name)) + }) + .unwrap_or_else(|| Ok(tmp_file_name.into())) + .and_then(|tmp_file_path| { + Ok((File::create(&tmp_file_path)?, tmp_file_path)) + }) + .map_err(|e: io::Error| ron::Error::from(e)) + .and_then(|(mut file, tmp_file_path)| { + info!("Writing rtsim state to file..."); + data.write_to(&mut file)?; + file.flush()?; + drop(file); + fs::rename(tmp_file_path, file_path)?; + info!("Rtsim state saved."); + Ok(()) + }) + { + error!("Saving rtsim state failed: {}", e); + } + }); + self.last_saved = Some(Instant::now()); + } + + // TODO: Clean up this API a bit + pub fn get_chunk_resources(&self, key: Vec2) -> EnumMap { + self.state.data.nature.get_chunk_resources(key) + } + pub fn hook_block_update(&mut self, wpos: Vec3, old_block: Block, new_block: Block) { + let key = wpos + .xy() + .map2(TerrainChunk::RECT_SIZE, |e, sz| e.div_euclid(sz as i32)); + if let Some(Some(chunk_state)) = self.chunk_states.get(key) { + let mut chunk_res = self.get_chunk_resources(key); + // Remove resources + if let Some(res) = old_block.get_rtsim_resource() { + if chunk_state.max_res[res] > 0 { + chunk_res[res] = (chunk_res[res] - 1.0 / chunk_state.max_res[res] as f32).max(0.0); + println!("Subbing {} to resources", 1.0 / chunk_state.max_res[res] as f32); + } + } + // Add resources + if let Some(res) = new_block.get_rtsim_resource() { + if chunk_state.max_res[res] > 0 { + chunk_res[res] = (chunk_res[res] + 1.0 / chunk_state.max_res[res] as f32).min(1.0); + println!("Added {} to resources", 1.0 / chunk_state.max_res[res] as f32); + } + } + println!("Chunk resources are {:?}", chunk_res); + self.state.data.nature.set_chunk_resources(key, chunk_res); + } + } +} + +struct LoadedChunkState { + // The maximum possible number of each resource in this chunk + max_res: EnumMap, } pub fn add_server_systems(dispatch_builder: &mut DispatcherBuilder) { diff --git a/server/src/rtsim2/tick.rs b/server/src/rtsim2/tick.rs index 5222dcff0a..4e3e2b1e13 100644 --- a/server/src/rtsim2/tick.rs +++ b/server/src/rtsim2/tick.rs @@ -7,10 +7,11 @@ use common::{ event::{EventBus, ServerEvent}, generation::{BodyBuilder, EntityConfig, EntityInfo}, resources::{DeltaTime, Time}, + slowjob::SlowJobPool, }; use common_ecs::{Job, Origin, Phase, System}; use specs::{Join, Read, ReadExpect, ReadStorage, WriteExpect, WriteStorage}; -use std::sync::Arc; +use std::{sync::Arc, time::Duration}; #[derive(Default)] pub struct Sys; @@ -22,6 +23,7 @@ impl<'a> System<'a> for Sys { WriteExpect<'a, RtSim>, ReadExpect<'a, Arc>, ReadExpect<'a, world::IndexOwned>, + ReadExpect<'a, SlowJobPool>, ); const NAME: &'static str = "rtsim::tick"; @@ -30,9 +32,14 @@ impl<'a> System<'a> for Sys { fn run( _job: &mut Job, - (dt, time, server_event_bus, mut rtsim, world, index): Self::SystemData, + (dt, time, server_event_bus, mut rtsim, world, index, slow_jobs): Self::SystemData, ) { let rtsim = &mut *rtsim; + + if rtsim.last_saved.map_or(true, |ls| ls.elapsed() > Duration::from_secs(60)) { + rtsim.save(&slow_jobs); + } + // rtsim.tick += 1; // Update unloaded rtsim entities, in groups at a time diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 94acc0df25..4d3bdd2b94 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -7,6 +7,7 @@ use crate::{ presence::{Presence, RepositionOnChunkLoad}, settings::Settings, sys::sentinel::DeletedEntities, + rtsim2::RtSim, wiring, BattleModeBuffer, SpawnPoint, }; use common::{ @@ -497,6 +498,10 @@ impl StateExt for State { { let ecs = self.ecs(); let slow_jobs = ecs.write_resource::(); + #[cfg(feature = "worldgen")] + let rtsim = ecs.read_resource::(); + #[cfg(not(feature = "worldgen"))] + let rtsim = (); let mut chunk_generator = ecs.write_resource::(); let chunk_pos = self.terrain().pos_key(pos.0.map(|e| e as i32)); @@ -517,7 +522,7 @@ impl StateExt for State { #[cfg(feature = "worldgen")] { let time = (*ecs.read_resource::(), (*ecs.read_resource::()).clone()); - chunk_generator.generate_chunk(None, chunk_key, &slow_jobs, Arc::clone(world), index.clone(), time); + chunk_generator.generate_chunk(None, chunk_key, &slow_jobs, Arc::clone(world), &rtsim, index.clone(), time); } }); } diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index d7bd86276f..ce5765c839 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -112,7 +112,7 @@ impl<'a> System<'a> for Sys { mut terrain_changes, mut chunk_requests, //mut rtsim, - mut rtsim2, + mut rtsim, mut _terrain_persistence, mut positions, presences, @@ -137,6 +137,7 @@ impl<'a> System<'a> for Sys { request.key, &slow_jobs, Arc::clone(&world), + &rtsim, index.clone(), (*time_of_day, calendar.clone()), ) @@ -182,7 +183,7 @@ impl<'a> System<'a> for Sys { } else { terrain_changes.new_chunks.insert(key); #[cfg(feature = "worldgen")] - rtsim2.hook_load_chunk(key); + rtsim.hook_load_chunk(key, supplement.rtsim_max_resources); } // Handle chunk supplement @@ -387,7 +388,7 @@ impl<'a> System<'a> for Sys { terrain.remove(key).map(|chunk| { terrain_changes.removed_chunks.insert(key); #[cfg(feature = "worldgen")] - rtsim2.hook_unload_chunk(key); + rtsim.hook_unload_chunk(key); chunk }) }) diff --git a/world/Cargo.toml b/world/Cargo.toml index 9d053952c4..0d8ed510a9 100644 --- a/world/Cargo.toml +++ b/world/Cargo.toml @@ -21,6 +21,7 @@ common-dynlib = {package = "veloren-common-dynlib", path = "../common/dynlib", o bincode = "1.3.1" bitvec = "1.0.1" enum-iterator = "1.1.3" +enum-map = "2.4" fxhash = "0.2.1" image = { version = "0.24", default-features = false, features = ["png"] } itertools = "0.10" diff --git a/world/src/canvas.rs b/world/src/canvas.rs index 4df2ed8ac1..2a1a2e36ac 100644 --- a/world/src/canvas.rs +++ b/world/src/canvas.rs @@ -141,6 +141,7 @@ pub struct Canvas<'a> { pub(crate) info: CanvasInfo<'a>, pub(crate) chunk: &'a mut TerrainChunk, pub(crate) entities: Vec, + pub(crate) rtsim_resource_blocks: Vec>, } impl<'a> Canvas<'a> { @@ -159,11 +160,20 @@ impl<'a> Canvas<'a> { } pub fn set(&mut self, pos: Vec3, block: Block) { + if block.get_rtsim_resource().is_some() { + self.rtsim_resource_blocks.push(pos); + } let _ = self.chunk.set(pos - self.wpos(), block); } pub fn map(&mut self, pos: Vec3, f: impl FnOnce(Block) -> Block) { - let _ = self.chunk.map(pos - self.wpos(), f); + let _ = self.chunk.map(pos - self.wpos(), |b| { + let new_block = f(b); + if new_block.get_rtsim_resource().is_some() { + self.rtsim_resource_blocks.push(pos); + } + new_block + }); } pub fn set_sprite_cfg(&mut self, pos: Vec3, sprite_cfg: SpriteCfg) { diff --git a/world/src/lib.rs b/world/src/lib.rs index 5b187fcae3..76c4b7c55d 100644 --- a/world/src/lib.rs +++ b/world/src/lib.rs @@ -53,8 +53,10 @@ use common::{ Block, BlockKind, SpriteKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize, TerrainGrid, }, vol::{ReadVol, RectVolSize, WriteVol}, + rtsim::ChunkResource, }; use common_net::msg::{world_msg, WorldMapMsg}; +use enum_map::EnumMap; use rand::{prelude::*, Rng}; use rand_chacha::ChaCha8Rng; use rayon::iter::ParallelIterator; @@ -235,7 +237,7 @@ impl World { // Unwrapping because generate_chunk only returns err when should_continue evals // to true let (tc, _cs) = self - .generate_chunk(index, chunk_pos, || false, None) + .generate_chunk(index, chunk_pos, None, || false, None) .unwrap(); tc.find_accessible_pos(spawn_wpos, ascending) @@ -246,6 +248,7 @@ impl World { &self, index: IndexRef, chunk_pos: Vec2, + rtsim_resources: Option>, // TODO: misleading name mut should_continue: impl FnMut() -> bool, time: Option<(TimeOfDay, Calendar)>, @@ -377,6 +380,7 @@ impl World { }, chunk: &mut chunk, entities: Vec::new(), + rtsim_resource_blocks: Vec::new(), }; if index.features.train_tracks { @@ -416,9 +420,12 @@ impl World { .iter() .for_each(|site| index.sites[*site].apply_to(&mut canvas, &mut dynamic_rng)); + let mut rtsim_resource_blocks = std::mem::take(&mut canvas.rtsim_resource_blocks); let mut supplement = ChunkSupplement { - entities: canvas.entities, + entities: std::mem::take(&mut canvas.entities), + rtsim_max_resources: Default::default(), }; + drop(canvas); let gen_entity_pos = |dynamic_rng: &mut ChaCha8Rng| { let lpos2d = TerrainChunkSize::RECT_SIZE @@ -485,6 +492,33 @@ impl World { // Finally, defragment to minimize space consumption. chunk.defragment(); + // Before we finish, we check candidate rtsim resource blocks, deduplicating positions and only keeping those + // that actually do have resources. Although this looks potentially very expensive, only blocks that are rtsim + // resources (i.e: a relatively small number of sprites) are processed here. + if let Some(rtsim_resources) = rtsim_resources { + rtsim_resource_blocks.sort_unstable_by_key(|pos| pos.into_array()); + rtsim_resource_blocks.dedup(); + for wpos in rtsim_resource_blocks { + chunk.map( + wpos - chunk_wpos2d.with_z(0), + |block| if let Some(res) = block.get_rtsim_resource() { + // Note: this represents the upper limit, not the actual number spanwed, so we increment this before deciding whether we're going to spawn the resource. + supplement.rtsim_max_resources[res] += 1; + // Throw a dice to determine whether this resource should actually spawn + // TODO: Don't throw a dice, try to generate the *exact* correct number + if dynamic_rng.gen_bool(rtsim_resources[res] as f64) { + block + } else { + block.into_vacant() + } + } else { + block + }, + ); + } + } + + Ok((chunk, supplement)) }