diff --git a/common/src/store.rs b/common/src/store.rs index 8942e4a722..e3ededbd25 100644 --- a/common/src/store.rs +++ b/common/src/store.rs @@ -50,20 +50,17 @@ impl Store { } pub fn ids(&self) -> impl Iterator> { - // NOTE: Assumes usize fits into 8 bytes. - (0..self.items.len() as u64).map(|i| Id(i, PhantomData)) + (0..self.items.len()).map(|i| Id(i as u64, PhantomData)) } - pub fn iter(&self) -> impl Iterator { self.items.iter() } + pub fn values(&self) -> impl Iterator { self.items.iter() } - pub fn iter_mut(&mut self) -> impl Iterator { self.items.iter_mut() } + pub fn values_mut(&mut self) -> impl Iterator { self.items.iter_mut() } - pub fn iter_ids(&self) -> impl Iterator, &T)> { - self.items - .iter() - .enumerate() - // NOTE: Assumes usize fits into 8 bytes. - .map(|(i, item)| (Id(i as u64, PhantomData), item)) + pub fn iter(&self) -> impl Iterator, &T)> { self.ids().zip(self.values()) } + + pub fn iter_mut(&mut self) -> impl Iterator, &mut T)> { + self.ids().zip(self.values_mut()) } pub fn insert(&mut self, item: T) -> Id { diff --git a/server-cli/.gitignore b/server-cli/.gitignore index 8b13789179..d9bcc8922a 100644 --- a/server-cli/.gitignore +++ b/server-cli/.gitignore @@ -1 +1 @@ - +economy.csv diff --git a/server-cli/.~lock.economy.csv# b/server-cli/.~lock.economy.csv# new file mode 100644 index 0000000000..7651efaee5 --- /dev/null +++ b/server-cli/.~lock.economy.csv# @@ -0,0 +1 @@ +,joshua,archbox.localdomain,17.06.2020 16:07,file:///home/joshua/.config/libreoffice/4; \ No newline at end of file diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 5a282766fd..5ee23d8328 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1437,7 +1437,7 @@ fn handle_debug_column( let spawn_rate = sim.get_interpolated(wpos, |chunk| chunk.spawn_rate)?; let chunk_pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| e / sz as i32); let chunk = sim.get(chunk_pos)?; - let col = sampler.get(wpos)?; + let col = sampler.get((wpos, server.world.index()))?; let downhill = chunk.downhill; let river = &chunk.river; let flux = chunk.flux; diff --git a/server/src/lib.rs b/server/src/lib.rs index 1447793c98..e958ec53a3 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -184,7 +184,7 @@ impl Server { ..WorldOpts::default() }); #[cfg(feature = "worldgen")] - let map = world.sim().get_map(); + let map = world.get_map_data(); #[cfg(not(feature = "worldgen"))] let world = World::generate(settings.world_seed); @@ -219,11 +219,11 @@ impl Server { // get a z cache for the collumn in which we want to spawn let mut block_sampler = world.sample_blocks(); let z_cache = block_sampler - .get_z_cache(spawn_location) + .get_z_cache(spawn_location, world.index()) .expect(&format!("no z_cache found for chunk: {}", spawn_chunk)); // get the minimum and maximum z values at which there could be soild blocks - let (min_z, _, max_z) = z_cache.get_z_limits(&mut block_sampler); + let (min_z, _, max_z) = z_cache.get_z_limits(&mut block_sampler, world.index()); // round range outwards, so no potential air block is missed let min_z = min_z.floor() as i32; let max_z = max_z.ceil() as i32; @@ -239,6 +239,7 @@ impl Server { Vec3::new(spawn_location.x, spawn_location.y, *z), Some(&z_cache), false, + world.index(), ) .map(|b| b.is_air()) .unwrap_or(false) diff --git a/world/src/block/mod.rs b/world/src/block/mod.rs index 7d3558e066..4edef6b8d4 100644 --- a/world/src/block/mod.rs +++ b/world/src/block/mod.rs @@ -3,7 +3,7 @@ mod natural; use crate::{ column::{ColumnGen, ColumnSample}, util::{RandomField, Sampler, SmallCache}, - CONFIG, + Index, CONFIG, }; use common::{ terrain::{structure::StructureBlock, Block, BlockKind, Structure}, @@ -29,8 +29,9 @@ impl<'a> BlockGen<'a> { column_gen: &ColumnGen<'a>, cache: &'b mut SmallCache>>, wpos: Vec2, + index: &Index, ) -> Option<&'b ColumnSample<'a>> { - cache.get(wpos, |wpos| column_gen.get(wpos)).as_ref() + cache.get(wpos, |wpos| column_gen.get((wpos, index))).as_ref() } fn get_cliff_height( @@ -40,11 +41,16 @@ impl<'a> BlockGen<'a> { close_cliffs: &[(Vec2, u32); 9], cliff_hill: f32, tolerance: f32, + index: &Index, ) -> f32 { close_cliffs.iter().fold( 0.0f32, - |max_height, (cliff_pos, seed)| match Self::sample_column(column_gen, cache, *cliff_pos) - { + |max_height, (cliff_pos, seed)| match Self::sample_column( + column_gen, + cache, + Vec2::from(*cliff_pos), + index, + ) { Some(cliff_sample) if cliff_sample.is_cliffs && cliff_sample.spawn_rate > 0.5 => { let cliff_pos3d = Vec3::from(*cliff_pos); @@ -84,14 +90,14 @@ impl<'a> BlockGen<'a> { ) } - pub fn get_z_cache(&mut self, wpos: Vec2) -> Option> { + pub fn get_z_cache(&mut self, wpos: Vec2, index: &'a Index) -> Option> { let BlockGen { column_cache, column_gen, } = self; // Main sample - let sample = column_gen.get(wpos)?; + let sample = column_gen.get((wpos, index))?; // Tree samples let mut structures = [None, None, None, None, None, None, None, None, None]; @@ -101,7 +107,7 @@ impl<'a> BlockGen<'a> { .zip(structures.iter_mut()) .for_each(|(close_structure, structure)| { if let Some(st) = *close_structure { - let st_sample = Self::sample_column(column_gen, column_cache, st.pos); + let st_sample = Self::sample_column(column_gen, column_cache, st.pos, index); if let Some(st_sample) = st_sample { let st_sample = st_sample.clone(); let st_info = match st.meta { @@ -111,6 +117,7 @@ impl<'a> BlockGen<'a> { st.pos, st.seed, &st_sample, + index, ), Some(meta) => Some(StructureInfo { pos: Vec3::from(st.pos) + Vec3::unit_z() * st_sample.alt as i32, @@ -137,6 +144,7 @@ impl<'a> BlockGen<'a> { wpos: Vec3, z_cache: Option<&ZCache>, only_structures: bool, + index: &Index, ) -> Option { let BlockGen { column_cache, @@ -208,6 +216,7 @@ impl<'a> BlockGen<'a> { &close_cliffs, cliff_hill, 0.0, + index, ); ( @@ -412,7 +421,7 @@ pub struct ZCache<'a> { } impl<'a> ZCache<'a> { - pub fn get_z_limits(&self, block_gen: &mut BlockGen) -> (f32, f32, f32) { + pub fn get_z_limits(&self, block_gen: &mut BlockGen, index: &Index) -> (f32, f32, f32) { let cave_depth = if self.sample.cave_xy.abs() > 0.9 && self.sample.water_level <= self.sample.alt { (self.sample.alt - self.sample.cave_alt + 8.0).max(0.0) @@ -430,6 +439,7 @@ impl<'a> ZCache<'a> { &self.sample.close_cliffs, self.sample.cliff_hill, 32.0, + index, ); let rocks = if self.sample.rock > 0.0 { 12.0 } else { 0.0 }; diff --git a/world/src/block/natural.rs b/world/src/block/natural.rs index 1ea1103ae9..845a033982 100644 --- a/world/src/block/natural.rs +++ b/world/src/block/natural.rs @@ -3,7 +3,7 @@ use crate::{ all::ForestKind, column::{ColumnGen, ColumnSample}, util::{RandomPerm, Sampler, SmallCache, UnitChooser}, - CONFIG, + Index, CONFIG, }; use common::terrain::Structure; use lazy_static::lazy_static; @@ -20,6 +20,7 @@ pub fn structure_gen<'a>( st_pos: Vec2, st_seed: u32, st_sample: &ColumnSample, + index: &'a Index, ) -> Option { // Assuming it's a tree... figure out when it SHOULDN'T spawn let random_seed = (st_seed as f64) / (u32::MAX as f64); @@ -39,6 +40,7 @@ pub fn structure_gen<'a>( &st_sample.close_cliffs, st_sample.cliff_hill, 0.0, + index, ); let wheight = st_sample.alt.max(cliff_height); diff --git a/world/src/civ/mod.rs b/world/src/civ/mod.rs index 326ec2b0ae..958f334ee8 100644 --- a/world/src/civ/mod.rs +++ b/world/src/civ/mod.rs @@ -6,7 +6,8 @@ use self::{Occupation::*, Stock::*}; use crate::{ sim::WorldSim, site::{Dungeon, Settlement, Site as WorldSite}, - util::{attempt, seed_expan, CARDINALS, NEIGHBORS}, + util::{attempt, seed_expan, MapVec, CARDINALS, NEIGHBORS}, + Index, }; use common::{ astar::Astar, @@ -70,7 +71,7 @@ impl<'a, R: Rng> GenCtx<'a, R> { } impl Civs { - pub fn generate(seed: u32, sim: &mut WorldSim) -> Self { + pub fn generate(seed: u32, sim: &mut WorldSim, index: &mut Index) -> Self { let mut this = Self::default(); let rng = ChaChaRng::from_seed(seed_expan::rng_state(seed)); let mut ctx = GenCtx { sim, rng }; @@ -103,7 +104,7 @@ impl Civs { last_exports: Stocks::from_default(0.0), export_targets: Stocks::from_default(0.0), - trade_states: Stocks::default(), + //trade_states: Stocks::default(), coin: 1000.0, }) }); @@ -116,13 +117,13 @@ impl Civs { } // Flatten ground around sites - for site in this.sites.iter() { + for site in this.sites.values() { let radius = 48i32; let wpos = site.center * TerrainChunkSize::RECT_SIZE.map(|e: u32| e as i32); let flatten_radius = match &site.kind { - SiteKind::Settlement => 10.0, + SiteKind::Settlement => 8.0, SiteKind::Dungeon => 2.0, }; @@ -143,7 +144,7 @@ impl Civs { let pos = site.center + offs; let factor = (1.0 - (site.center - pos).map(|e| e as f32).magnitude() / flatten_radius) - * 1.15; + * 0.8; ctx.sim .get_mut(pos) // Don't disrupt chunks that are near water @@ -161,33 +162,32 @@ impl Civs { // Place sites in world let mut cnt = 0; - for site in this.sites.iter() { + for sim_site in this.sites.values() { cnt += 1; - let wpos = site.center.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| { + let wpos = sim_site.center.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| { e * sz as i32 + sz as i32 / 2 }); let mut rng = ctx.reseed().rng; - let world_site = match &site.kind { + let site = index.sites.insert(match &sim_site.kind { SiteKind::Settlement => { - WorldSite::from(Settlement::generate(wpos, Some(ctx.sim), &mut rng)) + WorldSite::settlement(Settlement::generate(wpos, Some(ctx.sim), &mut rng)) }, SiteKind::Dungeon => { - WorldSite::from(Dungeon::generate(wpos, Some(ctx.sim), &mut rng)) + WorldSite::dungeon(Dungeon::generate(wpos, Some(ctx.sim), &mut rng)) }, - }; + }); + let site_ref = &index.sites[site]; let radius_chunks = - (world_site.radius() / TerrainChunkSize::RECT_SIZE.x as f32).ceil() as usize; + (site_ref.radius() / TerrainChunkSize::RECT_SIZE.x as f32).ceil() as usize; for pos in Spiral2d::new() - .map(|offs| site.center + offs) + .map(|offs| sim_site.center + offs) .take((radius_chunks * 2).pow(2)) { - ctx.sim - .get_mut(pos) - .map(|chunk| chunk.sites.push(world_site.clone())); + ctx.sim.get_mut(pos).map(|chunk| chunk.sites.push(site)); } - debug!(?site.center, "Placed site at location"); + debug!(?sim_site.center, "Placed site at location"); } info!(?cnt, "all sites placed"); @@ -198,18 +198,18 @@ impl Civs { pub fn place(&self, id: Id) -> &Place { self.places.get(id) } - pub fn sites(&self) -> impl Iterator + '_ { self.sites.iter() } + pub fn sites(&self) -> impl Iterator + '_ { self.sites.values() } #[allow(dead_code)] #[allow(clippy::print_literal)] // TODO: Pending review in #587 fn display_info(&self) { - for (id, civ) in self.civs.iter_ids() { + for (id, civ) in self.civs.iter() { println!("# Civilisation {:?}", id); println!("Name: {}", ""); println!("Homeland: {:#?}", self.places.get(civ.homeland)); } - for (id, site) in self.sites.iter_ids() { + for (id, site) in self.sites.iter() { println!("# Site {:?}", id); println!("{:#?}", site); } @@ -290,7 +290,7 @@ impl Civs { last_exports: Stocks::from_default(0.0), export_targets: Stocks::from_default(0.0), - trade_states: Stocks::default(), + //trade_states: Stocks::default(), coin: 1000.0, }) })?; @@ -380,7 +380,7 @@ impl Civs { const MAX_NEIGHBOR_DISTANCE: f32 = 500.0; let mut nearby = self .sites - .iter_ids() + .iter() .map(|(id, p)| (id, (p.center.distance_squared(loc) as f32).sqrt())) .filter(|(_, dist)| *dist < MAX_NEIGHBOR_DISTANCE) .collect::>(); @@ -440,7 +440,7 @@ impl Civs { } fn tick(&mut self, _ctx: &mut GenCtx, years: f32) { - for site in self.sites.iter_mut() { + for site in self.sites.values_mut() { site.simulate(years, &self.places.get(site.place).nat_res); } @@ -717,7 +717,7 @@ pub struct Site { last_exports: Stocks, export_targets: Stocks, - trade_states: Stocks, + //trade_states: Stocks, coin: f32, } @@ -996,79 +996,3 @@ impl Default for TradeState { } pub type Stocks = MapVec; - -#[derive(Clone, Debug)] -pub struct MapVec { - /// We use this hasher (FxHasher32) because - /// (1) we don't care about DDOS attacks (ruling out SipHash); - /// (2) we care about determinism across computers (ruling out AAHash); - /// (3) we have 1-byte keys (for which FxHash is supposedly fastest). - entries: HashMap>, - default: T, -} - -/// Need manual implementation of Default since K doesn't need that bound. -impl Default for MapVec { - fn default() -> Self { - Self { - entries: Default::default(), - default: Default::default(), - } - } -} - -impl MapVec { - pub fn from_list<'a>(i: impl IntoIterator, default: T) -> Self - where - K: 'a, - T: 'a, - { - Self { - entries: i.into_iter().cloned().collect(), - default, - } - } - - pub fn from_default(default: T) -> Self { - Self { - entries: HashMap::default(), - default, - } - } - - pub fn get_mut(&mut self, entry: K) -> &mut T { - let default = &self.default; - self.entries.entry(entry).or_insert_with(|| default.clone()) - } - - pub fn get(&self, entry: K) -> &T { self.entries.get(&entry).unwrap_or(&self.default) } - - pub fn map(self, mut f: impl FnMut(K, T) -> U) -> MapVec { - MapVec { - entries: self - .entries - .into_iter() - .map(|(s, v)| (s, f(s, v))) - .collect(), - default: U::default(), - } - } - - pub fn iter(&self) -> impl Iterator + '_ { - self.entries.iter().map(|(s, v)| (*s, v)) - } - - pub fn iter_mut(&mut self) -> impl Iterator + '_ { - self.entries.iter_mut().map(|(s, v)| (*s, v)) - } -} - -impl std::ops::Index for MapVec { - type Output = T; - - fn index(&self, entry: K) -> &Self::Output { self.get(entry) } -} - -impl std::ops::IndexMut for MapVec { - fn index_mut(&mut self, entry: K) -> &mut Self::Output { self.get_mut(entry) } -} diff --git a/world/src/column/mod.rs b/world/src/column/mod.rs index 7a0619b51f..bbcc762428 100644 --- a/world/src/column/mod.rs +++ b/world/src/column/mod.rs @@ -3,7 +3,7 @@ use crate::{ block::StructureMeta, sim::{local_cells, uniform_idx_as_vec2, vec2_as_uniform_idx, RiverKind, SimChunk, WorldSim}, util::Sampler, - CONFIG, + Index, CONFIG, }; use common::{terrain::TerrainChunkSize, vol::RectVolSize}; use noise::NoiseFn; @@ -165,8 +165,11 @@ pub fn quadratic_nearest_point( min_root } -impl<'a> Sampler<'a> for ColumnGen<'a> { - type Index = Vec2; +impl<'a, 'b> Sampler<'b> for ColumnGen<'a> +where + 'a: 'b, +{ + type Index = (Vec2, &'b Index); type Sample = Option>; #[allow(clippy::float_cmp)] // TODO: Pending review in #587 @@ -174,7 +177,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { #[allow(clippy::nonminimal_bool)] // TODO: Pending review in #587 #[allow(clippy::single_match)] // TODO: Pending review in #587 #[allow(clippy::bind_instead_of_map)] // TODO: Pending review in #587 - fn get(&self, wpos: Vec2) -> Option> { + fn get(&self, (wpos, index): Self::Index) -> Option> { let wposf = wpos.map(|e| e as f64); let chunk_pos = wpos.map2(TerrainChunkSize::RECT_SIZE, |e, sz: u32| e / sz as i32); @@ -1098,7 +1101,7 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { tree_density: if sim_chunk .sites .iter() - .all(|site| site.spawn_rules(wpos).trees) + .all(|site| index.sites[*site].spawn_rules(wpos).trees) { Lerp::lerp(0.0, tree_density, alt.sub(2.0).sub(basement).mul(0.5)) } else { diff --git a/world/src/index.rs b/world/src/index.rs new file mode 100644 index 0000000000..242cbf3dcc --- /dev/null +++ b/world/src/index.rs @@ -0,0 +1,8 @@ +use crate::site::Site; +use common::store::{Id, Store}; + +#[derive(Default)] +pub struct Index { + pub time: f32, + pub sites: Store, +} diff --git a/world/src/lib.rs b/world/src/lib.rs index 12e6112947..67769a1d67 100644 --- a/world/src/lib.rs +++ b/world/src/lib.rs @@ -8,8 +8,10 @@ mod block; pub mod civ; mod column; pub mod config; +pub mod index; pub mod layer; pub mod sim; +pub mod sim2; pub mod site; pub mod util; @@ -19,6 +21,7 @@ pub use crate::config::CONFIG; use crate::{ block::BlockGen, column::{ColumnGen, ColumnSample}, + index::Index, util::{Grid, Sampler}, }; use common::{ @@ -39,26 +42,35 @@ pub enum Error { pub struct World { sim: sim::WorldSim, civs: civ::Civs, + index: Index, } impl World { pub fn generate(seed: u32, opts: sim::WorldOpts) -> Self { let mut sim = sim::WorldSim::generate(seed, opts); - let civs = civ::Civs::generate(seed, &mut sim); - Self { sim, civs } + let mut index = Index::default(); + let civs = civ::Civs::generate(seed, &mut sim, &mut index); + + sim2::simulate(&mut index, &mut sim); + + Self { sim, civs, index } } pub fn sim(&self) -> &sim::WorldSim { &self.sim } pub fn civs(&self) -> &civ::Civs { &self.civs } + pub fn index(&self) -> &Index { &self.index } + pub fn tick(&self, _dt: Duration) { // TODO } + pub fn get_map_data(&self) -> Vec { self.sim.get_map(&self.index) } + pub fn sample_columns( &self, - ) -> impl Sampler, Sample = Option> + '_ { + ) -> impl Sampler, &Index), Sample = Option> + '_ { ColumnGen::new(&self.sim) } @@ -77,7 +89,7 @@ impl World { let grid_border = 4; let zcache_grid = Grid::populate_from( TerrainChunkSize::RECT_SIZE.map(|e| e as i32) + grid_border * 2, - |offs| sampler.get_z_cache(chunk_wpos2d - grid_border + offs), + |offs| sampler.get_z_cache(chunk_wpos2d - grid_border + offs, &self.index), ); let air = Block::empty(); @@ -132,7 +144,8 @@ impl World { _ => continue, }; - let (min_z, only_structures_min_z, max_z) = z_cache.get_z_limits(&mut sampler); + let (min_z, only_structures_min_z, max_z) = + z_cache.get_z_limits(&mut sampler, &self.index); (base_z..min_z as i32).for_each(|z| { let _ = chunk.set(Vec3::new(x, y, z), stone); @@ -144,7 +157,7 @@ impl World { let only_structures = lpos.z >= only_structures_min_z as i32; if let Some(block) = - sampler.get_with_z_cache(wpos, Some(&z_cache), only_structures) + sampler.get_with_z_cache(wpos, Some(&z_cache), only_structures, &self.index) { let _ = chunk.set(lpos, block); } @@ -163,10 +176,9 @@ impl World { let mut rng = rand::thread_rng(); // Apply site generation - sim_chunk - .sites - .iter() - .for_each(|site| site.apply_to(chunk_wpos2d, sample_get, &mut chunk)); + sim_chunk.sites.iter().for_each(|site| { + self.index.sites[*site].apply_to(chunk_wpos2d, sample_get, &mut chunk) + }); // Apply paths layer::apply_paths_to(chunk_wpos2d, sample_get, &mut chunk); @@ -217,7 +229,12 @@ impl World { // Apply site supplementary information sim_chunk.sites.iter().for_each(|site| { - site.apply_supplement(&mut rng, chunk_wpos2d, sample_get, &mut supplement) + self.index.sites[*site].apply_supplement( + &mut rng, + chunk_wpos2d, + sample_get, + &mut supplement, + ) }); Ok((chunk, supplement)) diff --git a/world/src/sim/map.rs b/world/src/sim/map.rs index 5365fba293..ebed7fae80 100644 --- a/world/src/sim/map.rs +++ b/world/src/sim/map.rs @@ -1,6 +1,6 @@ use crate::{ sim::{RiverKind, WorldSim, WORLD_SIZE}, - CONFIG, + Index, CONFIG, }; use common::{terrain::TerrainChunkSize, vol::RectVolSize}; use std::{f32, f64}; @@ -114,6 +114,7 @@ impl MapConfig { pub fn generate( &self, sampler: &WorldSim, + index: &Index, mut write_pixel: impl FnMut(Vec2, (u8, u8, u8, u8)), ) -> MapDebug { let MapConfig { @@ -170,7 +171,8 @@ impl MapConfig { sample.river.river_kind, sample.path.is_path(), sample.sites.iter().any(|site| { - site.get_origin() + index.sites[*site] + .get_origin() .distance_squared(pos * TerrainChunkSize::RECT_SIZE.x as i32) < 64i32.pow(2) }), diff --git a/world/src/sim/mod.rs b/world/src/sim/mod.rs index fc53e3a06f..fc5ca00208 100644 --- a/world/src/sim/mod.rs +++ b/world/src/sim/mod.rs @@ -28,7 +28,7 @@ use crate::{ civ::Place, site::Site, util::{seed_expan, FastNoise, RandomField, StructureGen2d, LOCALITY, NEIGHBORS}, - CONFIG, + Index, CONFIG, }; use common::{ assets, @@ -1305,14 +1305,14 @@ impl WorldSim { /// Draw a map of the world based on chunk information. Returns a buffer of /// u32s. - pub fn get_map(&self) -> Vec { + pub fn get_map(&self, index: &Index) -> Vec { let mut v = vec![0u32; WORLD_SIZE.x * WORLD_SIZE.y]; // TODO: Parallelize again. MapConfig { gain: self.max_height, ..MapConfig::default() } - .generate(&self, |pos, (r, g, b, a)| { + .generate(&self, index, |pos, (r, g, b, a)| { v[pos.y * WORLD_SIZE.x + pos.x] = u32::from_le_bytes([r, g, b, a]); }); v @@ -1788,7 +1788,7 @@ pub struct SimChunk { pub river: RiverData, pub warp_factor: f32, - pub sites: Vec, + pub sites: Vec>, pub place: Option>, pub path: PathData, pub contains_waypoint: bool, diff --git a/world/src/sim2/mod.rs b/world/src/sim2/mod.rs new file mode 100644 index 0000000000..8bcf961b61 --- /dev/null +++ b/world/src/sim2/mod.rs @@ -0,0 +1,250 @@ +use crate::{ + sim::WorldSim, + site::{ + economy::{Good, Labor}, + Site, + }, + util::MapVec, + Index, +}; +use common::store::Id; +use tracing::{debug, info, warn}; + +const MONTH: f32 = 30.0; +const YEAR: f32 = 12.0 * MONTH; +const TICK_PERIOD: f32 = 3.0 * MONTH; // 3 months +const HISTORY_DAYS: f32 = 500.0 * YEAR; // 500 years + +pub fn simulate(index: &mut Index, world: &mut WorldSim) { + use std::io::Write; + let mut f = std::fs::File::create("economy.csv").unwrap(); + write!(f, "Population,").unwrap(); + for g in Good::list() { + write!(f, "{:?} Value,", g).unwrap(); + } + for g in Good::list() { + write!(f, "{:?} Price,", g).unwrap(); + } + for g in Good::list() { + write!(f, "{:?} Stock,", g).unwrap(); + } + for g in Good::list() { + write!(f, "{:?} Surplus,", g).unwrap(); + } + for l in Labor::list() { + write!(f, "{:?} Labor,", l).unwrap(); + } + for l in Labor::list() { + write!(f, "{:?} Productivity,", l).unwrap(); + } + writeln!(f, "").unwrap(); + + for i in 0..(HISTORY_DAYS / TICK_PERIOD) as i32 { + if (index.time / YEAR) as i32 % 50 == 0 && (index.time % YEAR) as i32 == 0 { + debug!("Year {}", (index.time / YEAR) as i32); + } + + tick(index, world, TICK_PERIOD); + + if i % 5 == 0 { + let site = index.sites.values().next().unwrap(); + write!(f, "{},", site.economy.pop).unwrap(); + for g in Good::list() { + write!(f, "{:?},", site.economy.values[*g].unwrap_or(-1.0)).unwrap(); + } + for g in Good::list() { + write!(f, "{:?},", site.economy.prices[*g]).unwrap(); + } + for g in Good::list() { + write!(f, "{:?},", site.economy.stocks[*g]).unwrap(); + } + for g in Good::list() { + write!(f, "{:?},", site.economy.marginal_surplus[*g]).unwrap(); + } + for l in Labor::list() { + write!(f, "{:?},", site.economy.labors[*l] * site.economy.pop).unwrap(); + } + for l in Labor::list() { + write!(f, "{:?},", site.economy.productivity[*l]).unwrap(); + } + writeln!(f, "").unwrap(); + } + } +} + +pub fn tick(index: &mut Index, world: &mut WorldSim, dt: f32) { + for site in index.sites.ids() { + tick_site_economy(index, site, dt); + } + + index.time += dt; +} + +/// Simulate a site's economy. This simulation is roughly equivalent to the +/// Lange-Lerner model's solution to the socialist calculation problem. The +/// simulation begins by assigning arbitrary values to each commodity and then +/// incrementally updates them according to the final scarcity of the commodity +/// at the end of the tick. This results in the formulation of values that are +/// roughly analgous to prices for each commodity. The workforce is then +/// reassigned according to the respective commodity values. The simulation also +/// includes damping terms that prevent cyclical inconsistencies in value +/// rationalisation magnifying enough to crash the economy. We also ensure that +/// a small number of workers are allocated to every industry (even inactive +/// ones) each tick. This is not an accident: a small amount of productive +/// capacity in one industry allows the economy to quickly pivot to a different +/// prodution configuration should an additional commodity that acts as +/// production input become available. This means that the economy will +/// dynamically react to environmental changes. If a product becomes available +/// through a mechanism such as trade, an entire arm of the economy may +/// materialise to take advantage of this. +pub fn tick_site_economy(index: &mut Index, site: Id, dt: f32) { + let site = &mut index.sites[site]; + + let orders = site.economy.get_orders(); + let productivity = site.economy.get_productivity(); + + let mut demand = MapVec::from_default(0.0); + for (labor, orders) in &orders { + let scale = if let Some(labor) = labor { + site.economy.labors[*labor] + } else { + 1.0 + } * site.economy.pop; + for (good, amount) in orders { + demand[*good] += *amount * scale; + } + } + + let mut supply = MapVec::from_default(0.0); + for (labor, (output_good, _)) in productivity.iter() { + supply[*output_good] += + site.economy.yields[labor] * site.economy.labors[labor] * site.economy.pop; + } + + let stocks = &site.economy.stocks; + site.economy.surplus = demand + .clone() + .map(|g, demand| supply[g] + stocks[g] - demand); + site.economy.marginal_surplus = demand.clone().map(|g, demand| supply[g] - demand); + + // Update values according to the surplus of each stock + // Note that values are used for workforce allocation and are not the same thing + // as price + let values = &mut site.economy.values; + let marginal_surplus = &site.economy.marginal_surplus; + let stocks = &site.economy.stocks; + site.economy.surplus.iter().for_each(|(good, surplus)| { + // Value rationalisation + let val = 2.0f32.powf(1.0 - *surplus / demand[good]); + let smooth = 0.8; + values[good] = if val > 0.001 && val < 1000.0 { + Some(smooth * values[good].unwrap_or(val) + (1.0 - smooth) * val) + } else { + None + }; + }); + + site.economy.prices = site.economy.stocks.clone().map(|g, stock| { + // Price rationalisation + demand[g] / (supply[g] + stocks[g]) + }); + + // Update export targets based on relative values + let value_avg = values + .iter() + .map(|(_, v)| (*v).unwrap_or(0.0)) + .sum::() + .max(0.01) + / values.iter().filter(|(_, v)| v.is_some()).count() as f32; + //let export_targets = &mut site.economy.export_targets; + //let last_exports = &self.last_exports; + // site.economy.values.iter().for_each(|(stock, value)| { + // let rvalue = (*value).map(|v| v - value_avg).unwrap_or(0.0); + // //let factor = if export_targets[stock] > 0.0 { 1.0 / rvalue } else { + // rvalue }; //export_targets[stock] = last_exports[stock] - rvalue * + // 0.1; // + (trade_states[stock].sell_belief.price - + // trade_states[stock].buy_belief.price) * 0.025; }); + + //let pop = site.economy.pop; + + // Redistribute workforce according to relative good values + let labor_ratios = productivity.clone().map(|labor, (output_good, _)| { + site.economy.values[output_good].unwrap_or(0.0) * site.economy.productivity[labor] + //* demand[output_good] / supply[output_good].max(0.001) + }); + let labor_ratio_sum = labor_ratios.iter().map(|(_, r)| *r).sum::().max(0.01); + productivity.iter().for_each(|(labor, _)| { + let smooth = 0.8; + site.economy.labors[labor] = smooth * site.economy.labors[labor] + + (1.0 - smooth) + * (labor_ratios[labor].max(labor_ratio_sum / 1000.0) / labor_ratio_sum); + }); + + // Production + let stocks_before = site.economy.stocks.clone(); + for (labor, orders) in orders.iter() { + let scale = if let Some(labor) = labor { + site.economy.labors[*labor] + } else { + 1.0 + } * site.economy.pop; + + // For each order, we try to find the minimum satisfaction rate - this limits + // how much we can produce! For example, if we need 0.25 fish and + // 0.75 oats to make 1 unit of food, but only 0.5 units of oats are + // available then we only need to consume 2/3rds + // of other ingredients and leave the rest in stock + // In effect, this is the productivity + let labor_productivity = orders + .iter() + .map(|(good, amount)| { + // What quantity is this order requesting? + let _quantity = *amount * scale; + // What proportion of this order is the economy able to satisfy? + let satisfaction = (stocks_before[*good] / demand[*good]).min(1.0); + satisfaction + }) + .min_by(|a, b| a.partial_cmp(b).unwrap()) + .unwrap_or_else(|| panic!("Industry {:?} requires at least one input order", labor)); + + for (good, amount) in orders { + // What quantity is this order requesting? + let quantity = *amount * scale; + // What amount gets actually used in production? + let used = quantity * labor_productivity; + + // Deplete stocks accordingly + site.economy.stocks[*good] = (site.economy.stocks[*good] - used).max(0.0); + } + + // Industries produce things + if let Some(labor) = labor { + let (stock, rate) = productivity[*labor]; + let workers = site.economy.labors[*labor] * site.economy.pop; + let final_rate = rate; + let yield_per_worker = labor_productivity * final_rate; + site.economy.yields[*labor] = yield_per_worker; + site.economy.productivity[*labor] = labor_productivity; + site.economy.stocks[stock] += yield_per_worker * workers.powf(1.1); + } + } + + // Decay stocks + site.economy + .stocks + .iter_mut() + .for_each(|(c, v)| *v *= 1.0 - c.decay_rate()); + + // Decay stocks + site.economy.replenish(index.time); + + // Births/deaths + const NATURAL_BIRTH_RATE: f32 = 0.05; + const DEATH_RATE: f32 = 0.005; + let birth_rate = if site.economy.surplus[Good::Food] > 0.0 { + NATURAL_BIRTH_RATE + } else { + 0.0 + }; + site.economy.pop += dt / YEAR * site.economy.pop * (birth_rate - DEATH_RATE); +} diff --git a/world/src/site/block_mask.rs b/world/src/site/block_mask.rs new file mode 100644 index 0000000000..d94afcb6eb --- /dev/null +++ b/world/src/site/block_mask.rs @@ -0,0 +1,39 @@ +use common::{terrain::Block, vol::Vox}; + +#[derive(Copy, Clone)] +pub struct BlockMask { + block: Block, + priority: i32, +} + +impl BlockMask { + pub fn new(block: Block, priority: i32) -> Self { Self { block, priority } } + + pub fn nothing() -> Self { + Self { + block: Block::empty(), + priority: 0, + } + } + + pub fn with_priority(mut self, priority: i32) -> Self { + self.priority = priority; + self + } + + pub fn resolve_with(self, other: Self) -> Self { + if self.priority >= other.priority { + self + } else { + other + } + } + + pub fn finish(self) -> Option { + if self.priority > 0 { + Some(self.block) + } else { + None + } + } +} diff --git a/world/src/site/dungeon/mod.rs b/world/src/site/dungeon/mod.rs index 10cecd7807..1d20e97733 100644 --- a/world/src/site/dungeon/mod.rs +++ b/world/src/site/dungeon/mod.rs @@ -305,7 +305,7 @@ impl Floor { this.create_rooms(ctx, level, 7); // Create routes between all rooms - let room_areas = this.rooms.iter().map(|r| r.area).collect::>(); + let room_areas = this.rooms.values().map(|r| r.area).collect::>(); for a in room_areas.iter() { for b in room_areas.iter() { this.create_route(ctx, a.center(), b.center()); @@ -342,7 +342,7 @@ impl Floor { // Ensure no overlap if self .rooms - .iter() + .values() .any(|r| r.area.collides_with_rect(area_border)) { return None; diff --git a/world/src/site/economy.rs b/world/src/site/economy.rs new file mode 100644 index 0000000000..fcbacf85d9 --- /dev/null +++ b/world/src/site/economy.rs @@ -0,0 +1,145 @@ +use crate::util::{DHashMap, MapVec}; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum Good { + Wheat = 0, + Flour = 1, + Meat = 2, + Fish = 3, + Game = 4, + Food = 5, + Logs = 6, + Wood = 7, + Rock = 8, + Stone = 9, +} +use Good::*; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum Labor { + Farmer = 0, + Lumberjack = 1, + Miner = 2, + Fisher = 3, + Hunter = 4, + Cook = 5, +} +use Labor::*; + +pub struct Economy { + pub pop: f32, + + pub stocks: MapVec, + pub surplus: MapVec, + pub marginal_surplus: MapVec, + pub values: MapVec>, + pub prices: MapVec, + + pub labors: MapVec, + pub yields: MapVec, + pub productivity: MapVec, +} + +impl Default for Economy { + fn default() -> Self { + Self { + pop: 32.0, + + stocks: Default::default(), + surplus: Default::default(), + marginal_surplus: Default::default(), + values: Default::default(), + prices: Default::default(), + + labors: Default::default(), + yields: Default::default(), + productivity: Default::default(), + } + } +} + +impl Economy { + pub fn get_orders(&self) -> DHashMap, Vec<(Good, f32)>> { + vec![ + (None, vec![(Food, 0.5)]), + (Some(Cook), vec![ + (Flour, 12.0), + (Meat, 4.0), + (Wood, 1.5), + (Stone, 1.0), + ]), + (Some(Lumberjack), vec![(Logs, 0.5)]), + (Some(Miner), vec![(Rock, 0.5)]), + (Some(Fisher), vec![(Fish, 4.0)]), + (Some(Hunter), vec![(Game, 1.0)]), + (Some(Farmer), vec![(Wheat, 2.0)]), + ] + .into_iter() + .collect() + } + + pub fn get_productivity(&self) -> MapVec { + // Per labourer, per year + MapVec::from_list( + &[ + (Farmer, (Flour, 2.0)), + (Lumberjack, (Wood, 0.5)), + (Miner, (Stone, 0.5)), + (Fisher, (Meat, 3.0)), + (Hunter, (Meat, 1.0)), + (Cook, (Food, 16.0)), + ], + (Rock, 0.0), + ) + .map(|l, (good, v)| (good, v * (1.0 + self.labors[l]))) + } + + pub fn replenish(&mut self, time: f32) { + use rand::Rng; + for (i, (g, v)) in [(Wheat, 195.0), (Logs, 120.0), (Rock, 120.0), (Game, 20.0)] + .iter() + .enumerate() + { + self.stocks[*g] = (*v + * (1.25 + (((time * 0.0001 + i as f32).sin() + 1.0) % 1.0) * 0.5) + - self.stocks[*g]) + * 0.075; //rand::thread_rng().gen_range(0.05, 0.1); + } + } +} + +impl Default for Good { + fn default() -> Self { + Good::Rock // Arbitrary + } +} + +impl Good { + pub fn list() -> &'static [Self] { + static GOODS: [Good; 10] = [ + Wheat, Flour, Meat, Fish, Game, Food, Logs, Wood, Rock, Stone, + ]; + + &GOODS + } + + pub fn decay_rate(&self) -> f32 { + match self { + Food => 0.2, + Wheat => 0.1, + Meat => 0.25, + Fish => 0.2, + _ => 0.0, + } + } +} + +impl Labor { + pub fn list() -> &'static [Self] { + static LABORS: [Labor; 6] = [Farmer, Lumberjack, Miner, Fisher, Hunter, Cook]; + + &LABORS + } +} diff --git a/world/src/site/mod.rs b/world/src/site/mod.rs index b0fab2d161..fbfd8196db 100644 --- a/world/src/site/mod.rs +++ b/world/src/site/mod.rs @@ -1,8 +1,10 @@ +mod block_mask; mod dungeon; +pub mod economy; mod settlement; // Reexports -pub use self::{dungeon::Dungeon, settlement::Settlement}; +pub use self::{block_mask::BlockMask, dungeon::Dungeon, economy::Economy, settlement::Settlement}; use crate::column::ColumnSample; use common::{ @@ -14,44 +16,6 @@ use rand::Rng; use std::{fmt, sync::Arc}; use vek::*; -#[derive(Copy, Clone)] -pub struct BlockMask { - block: Block, - priority: i32, -} - -impl BlockMask { - pub fn new(block: Block, priority: i32) -> Self { Self { block, priority } } - - pub fn nothing() -> Self { - Self { - block: Block::empty(), - priority: 0, - } - } - - pub fn with_priority(mut self, priority: i32) -> Self { - self.priority = priority; - self - } - - pub fn resolve_with(self, other: Self) -> Self { - if self.priority >= other.priority { - self - } else { - other - } - } - - pub fn finish(self) -> Option { - if self.priority > 0 { - Some(self.block) - } else { - None - } - } -} - pub struct SpawnRules { pub trees: bool, } @@ -60,31 +24,49 @@ impl Default for SpawnRules { fn default() -> Self { Self { trees: true } } } -#[derive(Clone)] -pub enum Site { - Settlement(Arc), - Dungeon(Arc), +pub struct Site { + pub kind: SiteKind, + pub economy: Economy, +} + +pub enum SiteKind { + Settlement(Settlement), + Dungeon(Dungeon), } impl Site { + pub fn settlement(s: Settlement) -> Self { + Self { + kind: SiteKind::Settlement(s), + economy: Economy::default(), + } + } + + pub fn dungeon(d: Dungeon) -> Self { + Self { + kind: SiteKind::Dungeon(d), + economy: Economy::default(), + } + } + pub fn radius(&self) -> f32 { - match self { - Site::Settlement(settlement) => settlement.radius(), - Site::Dungeon(dungeon) => dungeon.radius(), + match &self.kind { + SiteKind::Settlement(settlement) => settlement.radius(), + SiteKind::Dungeon(dungeon) => dungeon.radius(), } } pub fn get_origin(&self) -> Vec2 { - match self { - Site::Settlement(s) => s.get_origin(), - Site::Dungeon(d) => d.get_origin(), + match &self.kind { + SiteKind::Settlement(s) => s.get_origin(), + SiteKind::Dungeon(d) => d.get_origin(), } } pub fn spawn_rules(&self, wpos: Vec2) -> SpawnRules { - match self { - Site::Settlement(s) => s.spawn_rules(wpos), - Site::Dungeon(d) => d.spawn_rules(wpos), + match &self.kind { + SiteKind::Settlement(s) => s.spawn_rules(wpos), + SiteKind::Dungeon(d) => d.spawn_rules(wpos), } } @@ -94,9 +76,9 @@ impl Site { get_column: impl FnMut(Vec2) -> Option<&'a ColumnSample<'a>>, vol: &mut (impl BaseVol + RectSizedVol + ReadVol + WriteVol), ) { - match self { - Site::Settlement(settlement) => settlement.apply_to(wpos2d, get_column, vol), - Site::Dungeon(dungeon) => dungeon.apply_to(wpos2d, get_column, vol), + match &self.kind { + SiteKind::Settlement(settlement) => settlement.apply_to(wpos2d, get_column, vol), + SiteKind::Dungeon(dungeon) => dungeon.apply_to(wpos2d, get_column, vol), } } @@ -107,28 +89,13 @@ impl Site { get_column: impl FnMut(Vec2) -> Option<&'a ColumnSample<'a>>, supplement: &mut ChunkSupplement, ) { - match self { - Site::Settlement(settlement) => { + match &self.kind { + SiteKind::Settlement(settlement) => { settlement.apply_supplement(rng, wpos2d, get_column, supplement) }, - Site::Dungeon(dungeon) => dungeon.apply_supplement(rng, wpos2d, get_column, supplement), - } - } -} - -impl From for Site { - fn from(settlement: Settlement) -> Self { Site::Settlement(Arc::new(settlement)) } -} - -impl From for Site { - fn from(dungeon: Dungeon) -> Self { Site::Dungeon(Arc::new(dungeon)) } -} - -impl fmt::Debug for Site { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Site::Settlement(_) => write!(f, "Settlement"), - Site::Dungeon(_) => write!(f, "Dungeon"), + SiteKind::Dungeon(dungeon) => { + dungeon.apply_supplement(rng, wpos2d, get_column, supplement) + }, } } } diff --git a/world/src/site/settlement/building/archetype/house.rs b/world/src/site/settlement/building/archetype/house.rs index e9a4d2129c..fd25aff1a6 100644 --- a/world/src/site/settlement/building/archetype/house.rs +++ b/world/src/site/settlement/building/archetype/house.rs @@ -12,7 +12,7 @@ use common::{ use rand::prelude::*; use vek::*; -const COLOR_THEMES: [Rgb; 11] = [ +const COLOR_THEMES: [Rgb; 17] = [ Rgb::new(0x1D, 0x4D, 0x45), Rgb::new(0xB3, 0x7D, 0x60), Rgb::new(0xAC, 0x5D, 0x26), @@ -24,6 +24,12 @@ const COLOR_THEMES: [Rgb; 11] = [ Rgb::new(0x2F, 0x32, 0x47), Rgb::new(0x8F, 0x35, 0x43), Rgb::new(0x6D, 0x1E, 0x3A), + Rgb::new(0x6D, 0xA7, 0x80), + Rgb::new(0x4F, 0xA0, 0x95), + Rgb::new(0xE2, 0xB9, 0x99), + Rgb::new(0x7A, 0x30, 0x22), + Rgb::new(0x4A, 0x06, 0x08), + Rgb::new(0x8E, 0xB4, 0x57), ]; pub struct House { @@ -149,10 +155,7 @@ impl Archetype for House { }; let this = Self { - roof_color: COLOR_THEMES - .choose(rng) - .unwrap() - .map(|e| e.saturating_add(rng.gen_range(0, 20)) - 10), + roof_color: *COLOR_THEMES.choose(rng).unwrap(), noise: RandomField::new(rng.gen()), roof_ribbing: rng.gen(), roof_ribbing_diagonal: rng.gen(), diff --git a/world/src/site/settlement/building/archetype/keep.rs b/world/src/site/settlement/building/archetype/keep.rs index 5d957a66cb..0016ca05a7 100644 --- a/world/src/site/settlement/building/archetype/keep.rs +++ b/world/src/site/settlement/building/archetype/keep.rs @@ -9,27 +9,38 @@ use vek::*; pub struct Keep; +pub struct Attr { + height: i32, + is_tower: bool, +} + impl Archetype for Keep { - type Attr = (); + type Attr = Attr; fn generate(rng: &mut R) -> (Self, Skeleton) { - let len = rng.gen_range(-8, 20).max(0); + let len = rng.gen_range(-8, 24).max(0); let skel = Skeleton { offset: -rng.gen_range(0, len + 7).clamped(0, len), ori: if rng.gen() { Ori::East } else { Ori::North }, root: Branch { len, - attr: Self::Attr::default(), - locus: 6 + rng.gen_range(0, 5), + attr: Attr { + height: rng.gen_range(12, 16), + is_tower: false, + }, + locus: 10 + rng.gen_range(0, 5), border: 3, children: (0..1) .map(|_| { ( rng.gen_range(-5, len + 5).clamped(0, len.max(1) - 1), Branch { - len: rng.gen_range(5, 12) * if rng.gen() { 1 } else { -1 }, - attr: Self::Attr::default(), - locus: 5 + rng.gen_range(0, 3), + len: 0, + attr: Attr { + height: rng.gen_range(20, 28), + is_tower: true, + }, + locus: 4 + rng.gen_range(0, 5), border: 3, children: Vec::new(), }, @@ -48,7 +59,7 @@ impl Archetype for Keep { pos: Vec3, dist: i32, bound_offset: Vec2, - _center_offset: Vec2, + center_offset: Vec2, z: i32, ori: Ori, branch: &Branch, @@ -60,18 +71,24 @@ impl Archetype for Keep { let important_layer = normal_layer + 1; let internal_layer = important_layer + 1; - let make_block = - |r, g, b| BlockMask::new(Block::new(BlockKind::Normal, Rgb::new(r, g, b)), normal_layer); + let make_block = |r, g, b| { + BlockMask::new( + Block::new(BlockKind::Normal, Rgb::new(r, g, b)), + normal_layer, + ) + }; let foundation = make_block(100, 100, 100); let wall = make_block(100, 100, 110); let floor = make_block(120, 80, 50).with_priority(important_layer); + let pole = make_block(90, 70, 50).with_priority(important_layer); + let flag = make_block(50, 170, 100).with_priority(important_layer); let internal = BlockMask::new(Block::empty(), internal_layer); let empty = BlockMask::nothing(); let width = branch.locus; let rampart_width = 2 + branch.locus; - let ceil_height = 12; + let ceil_height = branch.attr.height; let door_height = 6; let edge_pos = if (bound_offset.x == rampart_width) ^ (ori == Ori::East) { pos.y @@ -79,26 +96,49 @@ impl Archetype for Keep { pos.x }; let rampart_height = ceil_height + if edge_pos % 2 == 0 { 3 } else { 4 }; - let min_dist = bound_offset.reduce_max(); + let inner = Clamp::clamp( + center_offset, + Vec2::new(-5, -branch.len / 2 - 5), + Vec2::new(5, branch.len / 2 + 5), + ); + let min_dist = bound_offset.map(|e| e.pow(2) as f32).sum().powf(0.5) as i32; //(bound_offset.distance_squared(inner) as f32).sqrt() as i32 + 5;//bound_offset.reduce_max(); if profile.y <= 0 - (min_dist - width - 1).max(0) && min_dist < width + 3 { // Foundations foundation } else if profile.y == ceil_height && min_dist < rampart_width { - if min_dist < width { - floor - } else { - wall - } - } else if bound_offset.x.abs() == 4 && min_dist == width + 1 && profile.y < ceil_height { + if min_dist < width { floor } else { wall } + } else if !branch.attr.is_tower + && bound_offset.x.abs() == 4 + && min_dist == width + 1 + && profile.y < ceil_height + { wall - } else if bound_offset.x.abs() < 3 && profile.y < door_height - bound_offset.x.abs() && profile.y > 0 { + } else if bound_offset.x.abs() < 3 + && profile.y < door_height - bound_offset.x.abs() + && profile.y > 0 + { internal } else if min_dist == width && profile.y <= ceil_height { wall } else if profile.y >= ceil_height { if profile.y > ceil_height && min_dist < rampart_width { - internal + if branch.attr.is_tower + && center_offset == Vec2::zero() + && profile.y < ceil_height + 16 + { + pole + } else if branch.attr.is_tower + && center_offset.x == 0 + && center_offset.y > 0 + && center_offset.y < 8 + && profile.y > ceil_height + 8 + && profile.y < ceil_height + 14 + { + flag + } else { + empty + } } else if min_dist == rampart_width { if profile.y < rampart_height { wall diff --git a/world/src/site/settlement/building/mod.rs b/world/src/site/settlement/building/mod.rs index 6c578e94e6..7f6d2915e3 100644 --- a/world/src/site/settlement/building/mod.rs +++ b/world/src/site/settlement/building/mod.rs @@ -12,7 +12,6 @@ use vek::*; pub type HouseBuilding = Building; pub type KeepBuilding = Building; - pub struct Building { skel: Skeleton, archetype: A, @@ -44,7 +43,7 @@ impl Building { let aabr = self.bounds_2d(); Aabb { min: Vec3::from(aabr.min) + Vec3::unit_z() * (self.origin.z - 8), - max: Vec3::from(aabr.max) + Vec3::unit_z() * (self.origin.z + 32), + max: Vec3::from(aabr.max) + Vec3::unit_z() * (self.origin.z + 48), } } diff --git a/world/src/site/settlement/mod.rs b/world/src/site/settlement/mod.rs index 5fd57221e2..5704b69902 100644 --- a/world/src/site/settlement/mod.rs +++ b/world/src/site/settlement/mod.rs @@ -1,6 +1,10 @@ mod building; +mod town; -use self::building::{HouseBuilding, KeepBuilding}; +use self::{ + building::{HouseBuilding, KeepBuilding}, + town::{District, Town}, +}; use super::SpawnRules; use crate::{ column::ColumnSample, @@ -84,7 +88,6 @@ fn to_tile(e: i32) -> i32 { ((e as f32).div_euclid(AREA_SIZE as f32)).floor() as pub enum StructureKind { House(HouseBuilding), Keep(KeepBuilding), - } pub struct Structure { @@ -96,23 +99,20 @@ impl Structure { match &self.kind { StructureKind::House(house) => house.bounds_2d(), StructureKind::Keep(keep) => keep.bounds_2d(), - } } - + pub fn bounds(&self) -> Aabb { match &self.kind { StructureKind::House(house) => house.bounds(), StructureKind::Keep(keep) => keep.bounds(), - } } - + pub fn sample(&self, rpos: Vec3) -> Option { match &self.kind { StructureKind::House(house) => house.sample(rpos), StructureKind::Keep(keep) => keep.sample(rpos), - } } } @@ -127,10 +127,6 @@ pub struct Settlement { noise: RandomField, } -pub struct Town { - base_tile: Vec2, -} - pub struct Farm { #[allow(dead_code)] base_tile: Vec2, @@ -280,12 +276,28 @@ impl Settlement { Some(Plot::Dirt) => true, _ => false, }) { - self.land - .plot_at_mut(base_tile) - .map(|plot| *plot = Plot::Town); + // self.land + // .plot_at_mut(base_tile) + // .map(|plot| *plot = Plot::Town { district: None }); if i == 0 { - self.town = Some(Town { base_tile }); + let town = Town::generate(self.origin, base_tile, ctx); + + for (id, district) in town.districts().iter() { + let district_plot = + self.land.plots.insert(Plot::Town { district: Some(id) }); + + for x in district.aabr.min.x..district.aabr.max.x { + for y in district.aabr.min.y..district.aabr.max.y { + if !matches!(self.land.plot_at(Vec2::new(x, y)), Some(Plot::Hazard)) + { + self.land.set(Vec2::new(x, y), district_plot); + } + } + } + } + + self.town = Some(town); origin = base_tile; } } @@ -346,27 +358,25 @@ impl Settlement { return; }; - for (i, tile) in Spiral2d::new() + for tile in Spiral2d::new() .map(|offs| town_center + offs) .take(16usize.pow(2)) - .enumerate() { // This is a stupid way to decide how to place buildings - for _ in 0..ctx.rng.gen_range(2, 5) { + for i in 0..ctx.rng.gen_range(2, 5) { for _ in 0..25 { let house_pos = tile.map(|e| e * AREA_SIZE as i32 + AREA_SIZE as i32 / 2) + Vec2::::zero().map(|_| { ctx.rng - .gen_range(-(AREA_SIZE as i32) / 2, AREA_SIZE as i32 / 2) + .gen_range(-(AREA_SIZE as i32) / 4, AREA_SIZE as i32 / 4) }); let tile_pos = house_pos.map(|e| e.div_euclid(AREA_SIZE as i32)); - if !matches!(self.land.plot_at(tile_pos), Some(Plot::Town)) - || self - .land - .tile_at(tile_pos) - .map(|t| t.contains(WayKind::Path)) - .unwrap_or(true) + if self + .land + .tile_at(tile_pos) + .map(|t| t.contains(WayKind::Path)) + .unwrap_or(true) || ctx .sim .and_then(|sim| sim.get_nearest_path(self.origin + house_pos)) @@ -376,30 +386,30 @@ impl Settlement { continue; } + let alt = if let Some(Plot::Town { district }) = self.land.plot_at(tile_pos) { + district + .and_then(|d| self.town.as_ref().map(|t| t.districts().get(d))) + .map(|d| d.alt) + .unwrap_or_else(|| { + ctx.sim + .and_then(|sim| sim.get_alt_approx(self.origin + house_pos)) + .unwrap_or(0.0) + .ceil() as i32 + }) + } else { + continue; + }; + let structure = Structure { - kind: if i == 0 { + kind: if tile == town_center && i == 0 { StructureKind::Keep(KeepBuilding::generate( ctx.rng, - Vec3::new( - house_pos.x, - house_pos.y, - ctx.sim - .and_then(|sim| sim.get_alt_approx(self.origin + house_pos)) - .unwrap_or(0.0) - .ceil() as i32, - ), + Vec3::new(house_pos.x, house_pos.y, alt), )) } else { StructureKind::House(HouseBuilding::generate( ctx.rng, - Vec3::new( - house_pos.x, - house_pos.y, - ctx.sim - .and_then(|sim| sim.get_alt_approx(self.origin + house_pos)) - .unwrap_or(0.0) - .ceil() as i32, - ), + Vec3::new(house_pos.x, house_pos.y, alt), )) }, }; @@ -536,12 +546,13 @@ impl Settlement { } else { continue; }; - let surface_z = col_sample.riverless_alt.floor() as i32; + let land_surface_z = col_sample.riverless_alt.floor() as i32; + let mut surface_z = land_surface_z; // Sample settlement let sample = self.land.get_at_block(rpos); - let noisy_color = |col: Rgb, factor: u32| { + let noisy_color = move |col: Rgb, factor: u32| { let nz = self.noise.get(Vec3::new(wpos2d.x, wpos2d.y, surface_z)); col.map(|e| { (e as u32 + nz % (factor * 2)) @@ -550,6 +561,30 @@ impl Settlement { }) }; + // District alt + if let Some(Plot::Town { district }) = sample.plot { + if let Some(d) = + district.and_then(|d| self.town.as_ref().map(|t| t.districts().get(d))) + { + let other = self + .land + .plot_at(sample.second_closest) + .and_then(|p| match p { + Plot::Town { district } => *district, + _ => None, + }) + .and_then(|d| { + self.town.as_ref().map(|t| t.districts().get(d).alt as f32) + }) + .unwrap_or(surface_z as f32); + surface_z = Lerp::lerp( + (other + d.alt as f32) / 2.0, + d.alt as f32, + (1.25 * sample.edge_dist / (d.alt as f32 - other).abs()).min(1.0), + ) as i32; + } + } + // Paths if let Some((WayKind::Path, dist, nearest)) = sample.way { let inset = -1; @@ -599,7 +634,7 @@ impl Settlement { Some(Plot::Dirt) => Some(Rgb::new(90, 70, 50)), Some(Plot::Grass) => Some(Rgb::new(100, 200, 0)), Some(Plot::Water) => Some(Rgb::new(100, 150, 250)), - Some(Plot::Town) => { + Some(Plot::Town { district }) => { if let Some((_, path_nearest)) = col_sample.path { let path_dir = (path_nearest - wpos2d.map(|e| e as f32)) .rotated_z(f32::consts::PI / 2.0) @@ -693,7 +728,8 @@ impl Settlement { if let Some(color) = color { if col_sample.water_dist.map(|dist| dist > 2.0).unwrap_or(true) { - for z in -8..3 { + let diff = (surface_z - land_surface_z).abs(); + for z in -8 - diff..3 + diff { let pos = Vec3::new(offs.x, offs.y, surface_z + z); if let (0, Some(block)) = (z, surface_block) { @@ -767,7 +803,8 @@ impl Settlement { for x in bounds.min.x..bounds.max.x + 1 { for y in bounds.min.y..bounds.max.y + 1 { - let col = if let Some(col) = get_column(self.origin + Vec2::new(x, y) - wpos2d) { + let col = if let Some(col) = get_column(self.origin + Vec2::new(x, y) - wpos2d) + { col } else { continue; @@ -813,7 +850,7 @@ impl Settlement { let entity_wpos = Vec3::new(wpos2d.x as f32, wpos2d.y as f32, col_sample.alt + 3.0); - if matches!(sample.plot, Some(Plot::Town)) + if matches!(sample.plot, Some(Plot::Town { .. })) && RandomField::new(self.seed).chance(Vec3::from(wpos2d), 1.0 / (50.0 * 50.0)) { let is_human: bool; @@ -903,7 +940,7 @@ impl Settlement { Some(Plot::Dirt) => return Some(Rgb::new(90, 70, 50)), Some(Plot::Grass) => return Some(Rgb::new(100, 200, 0)), Some(Plot::Water) => return Some(Rgb::new(100, 150, 250)), - Some(Plot::Town) => { + Some(Plot::Town { .. }) => { return Some(Rgb::new(150, 110, 60).map2(Rgb::iota(), |e: u8, i: i32| { e.saturating_add((self.noise.get(Vec3::new(pos.x, pos.y, i * 5)) % 16) as u8) .saturating_sub(8) @@ -955,7 +992,9 @@ pub enum Plot { Dirt, Grass, Water, - Town, + Town { + district: Option>, + }, Field { farm: Id, seed: u32, @@ -1015,6 +1054,8 @@ pub struct Sample<'a> { plot: Option<&'a Plot>, way: Option<(&'a WayKind, f32, Vec2)>, tower: Option<(&'a Tower, Vec2)>, + edge_dist: f32, + second_closest: Vec2, } pub struct Land { @@ -1049,6 +1090,15 @@ impl Land { .min_by_key(|(center, _)| center.distance_squared(pos)) .unwrap() .0; + let second_closest = neighbors + .iter() + .filter(|(center, _)| *center != closest) + .min_by_key(|(center, _)| center.distance_squared(pos)) + .unwrap() + .0; + sample.second_closest = second_closest.map(to_tile); + sample.edge_dist = (second_closest - pos).map(|e| e as f32).magnitude() + - (closest - pos).map(|e| e as f32).magnitude(); let center_tile = self.tile_at(neighbors[4].0.map(to_tile)); diff --git a/world/src/site/settlement/town.rs b/world/src/site/settlement/town.rs new file mode 100644 index 0000000000..6d6a082cd9 --- /dev/null +++ b/world/src/site/settlement/town.rs @@ -0,0 +1,82 @@ +use super::{GenCtx, AREA_SIZE}; +use common::store::{Id, Store}; +use rand::prelude::*; +use vek::*; + +pub struct Town { + pub base_tile: Vec2, + radius: i32, + districts: Store, +} + +impl Town { + pub fn districts(&self) -> &Store { &self.districts } + + pub fn generate(origin: Vec2, base_tile: Vec2, ctx: &mut GenCtx) -> Self { + let mut this = Self { + base_tile, + radius: 4, + districts: Store::default(), + }; + + this.generate_districts(origin, ctx); + + this + } + + fn generate_districts(&mut self, origin: Vec2, ctx: &mut GenCtx) { + let base_aabr = Aabr { + min: self.base_tile - self.radius, + max: self.base_tile + self.radius, + }; + + gen_plot(base_aabr, ctx).for_each(base_aabr, &mut |aabr| { + if aabr.center().distance_squared(self.base_tile) < self.radius.pow(2) { + self.districts.insert(District { + seed: ctx.rng.gen(), + aabr, + alt: ctx + .sim + .and_then(|sim| { + sim.get_alt_approx( + origin + aabr.center() * AREA_SIZE as i32 + AREA_SIZE as i32 / 2, + ) + }) + .unwrap_or(0.0) as i32, + }); + } + }); + } +} + +pub struct District { + pub seed: u32, + pub aabr: Aabr, + pub alt: i32, +} + +enum Plot { + District, + Parent(Vec<(Aabr, Plot)>), +} + +impl Plot { + fn for_each(&self, aabr: Aabr, f: &mut impl FnMut(Aabr)) { + match self { + Plot::District => f(aabr), + Plot::Parent(children) => children.iter().for_each(|(aabr, p)| p.for_each(*aabr, f)), + } + } +} + +fn gen_plot(aabr: Aabr, ctx: &mut GenCtx) -> Plot { + if aabr.size().product() <= 9 { + Plot::District + } else if aabr.size().w < aabr.size().h { + let [a, b] = aabr.split_at_y(aabr.min.y + ctx.rng.gen_range(1, aabr.size().h)); + Plot::Parent(vec![(a, gen_plot(a, ctx)), (b, gen_plot(b, ctx))]) + } else { + let [a, b] = aabr.split_at_x(aabr.min.x + ctx.rng.gen_range(1, aabr.size().w)); + Plot::Parent(vec![(a, gen_plot(a, ctx)), (b, gen_plot(b, ctx))]) + } +} diff --git a/world/src/util/map_vec.rs b/world/src/util/map_vec.rs new file mode 100644 index 0000000000..47efe67d0a --- /dev/null +++ b/world/src/util/map_vec.rs @@ -0,0 +1,79 @@ +use crate::util::DHashMap; +use std::hash::Hash; + +#[derive(Clone, Debug)] +pub struct MapVec { + /// We use this hasher (FxHasher32) because + /// (1) we don't care about DDOS attacks (ruling out SipHash); + /// (2) we care about determinism across computers (ruling out AAHash); + /// (3) we have 1-byte keys (for which FxHash is supposedly fastest). + entries: DHashMap, + default: T, +} + +/// Need manual implementation of Default since K doesn't need that bound. +impl Default for MapVec { + fn default() -> Self { + Self { + entries: Default::default(), + default: Default::default(), + } + } +} + +impl MapVec { + pub fn from_list<'a>(i: impl IntoIterator, default: T) -> Self + where + K: 'a, + T: 'a, + { + Self { + entries: i.into_iter().cloned().collect(), + default, + } + } + + pub fn from_default(default: T) -> Self { + Self { + entries: DHashMap::default(), + default, + } + } + + pub fn get_mut(&mut self, entry: K) -> &mut T { + let default = &self.default; + self.entries.entry(entry).or_insert_with(|| default.clone()) + } + + pub fn get(&self, entry: K) -> &T { self.entries.get(&entry).unwrap_or(&self.default) } + + #[allow(clippy::clone_on_copy)] // TODO: Pending review in #587 + pub fn map(self, mut f: impl FnMut(K, T) -> U) -> MapVec { + MapVec { + entries: self + .entries + .into_iter() + .map(|(s, v)| (s.clone(), f(s, v))) + .collect(), + default: U::default(), + } + } + + pub fn iter(&self) -> impl Iterator + '_ { + self.entries.iter().map(|(s, v)| (*s, v)) + } + + pub fn iter_mut(&mut self) -> impl Iterator + '_ { + self.entries.iter_mut().map(|(s, v)| (*s, v)) + } +} + +impl std::ops::Index for MapVec { + type Output = T; + + fn index(&self, entry: K) -> &Self::Output { self.get(entry) } +} + +impl std::ops::IndexMut for MapVec { + fn index_mut(&mut self, entry: K) -> &mut Self::Output { self.get_mut(entry) } +} diff --git a/world/src/util/mod.rs b/world/src/util/mod.rs index 9ab6f91dce..7ddc7e85e0 100644 --- a/world/src/util/mod.rs +++ b/world/src/util/mod.rs @@ -1,5 +1,6 @@ pub mod fast_noise; pub mod grid; +pub mod map_vec; pub mod random; pub mod sampler; pub mod seed_expan; @@ -11,6 +12,7 @@ pub mod unit_chooser; pub use self::{ fast_noise::FastNoise, grid::Grid, + map_vec::MapVec, random::{RandomField, RandomPerm}, sampler::{Sampler, SamplerMut}, small_cache::SmallCache, @@ -18,8 +20,15 @@ pub use self::{ unit_chooser::UnitChooser, }; +use fxhash::{FxHasher32, FxHasher64}; +use hashbrown::{HashMap, HashSet}; +use std::hash::BuildHasherDefault; use vek::*; +// Deterministic HashMap and HashSet +pub type DHashMap = HashMap>; +pub type DHashSet = HashSet>; + pub fn attempt(max_iters: usize, mut f: impl FnMut() -> Option) -> Option { (0..max_iters).find_map(|_| f()) }