From e2f5162e4f96f4124aa43488f7245d341b3dcfd4 Mon Sep 17 00:00:00 2001 From: Joshua Yanovski Date: Thu, 20 Aug 2020 04:28:38 +0200 Subject: [PATCH] World colors are all hotloadable. They live in assets/world/style/colors.ron. Only a small handful of hardcoed colors remain in World; they are either part of the map, or difficult to disentangle from the rest of the computation. Comments are made where appropriate. --- .../voxygen/voxel/humanoid_color_manifest.ron | 2 + assets/world/style/colors.ron | 152 +++++++++++++++ common/src/assets/watch.rs | 10 +- common/src/lib.rs | 2 + common/src/terrain/block.rs | 4 +- common/src/terrain/structure.rs | 43 +++-- common/src/{util => }/typed.rs | 26 ++- common/src/util/mod.rs | 2 - server/src/chunk_generator.rs | 19 +- server/src/cmd.rs | 2 +- server/src/lib.rs | 63 +++++- server/src/sys/terrain.rs | 5 +- server/src/test_world.rs | 20 +- world/examples/water.rs | 7 +- world/src/block/mod.rs | 180 +++++++----------- world/src/block/natural.rs | 4 +- world/src/column/mod.rs | 92 ++++++--- world/src/index.rs | 97 +++++++++- world/src/layer/mod.rs | 24 ++- world/src/lib.rs | 66 ++++--- world/src/sim/map.rs | 9 +- world/src/sim/mod.rs | 4 +- world/src/site/castle/mod.rs | 19 +- world/src/site/dungeon/mod.rs | 36 +++- world/src/site/mod.rs | 17 +- .../settlement/building/archetype/house.rs | 144 +++++++++----- .../settlement/building/archetype/keep.rs | 71 +++++-- .../site/settlement/building/archetype/mod.rs | 10 +- world/src/site/settlement/building/mod.rs | 10 +- world/src/site/settlement/mod.rs | 150 +++++++-------- world/src/util/mod.rs | 1 - 31 files changed, 894 insertions(+), 397 deletions(-) create mode 100644 assets/world/style/colors.ron rename common/src/{util => }/typed.rs (73%) diff --git a/assets/voxygen/voxel/humanoid_color_manifest.ron b/assets/voxygen/voxel/humanoid_color_manifest.ron index 05c6fbb8e3..3d3fa28efa 100644 --- a/assets/voxygen/voxel/humanoid_color_manifest.ron +++ b/assets/voxygen/voxel/humanoid_color_manifest.ron @@ -1,3 +1,5 @@ +#![enable(unwrap_newtypes)] + ( // NOTE: You can't change the legnths of these arrays without updating num_hair_colors() in // common/src/comp/body/humanoid.rs. That's because this is a hack; we should really use enum diff --git a/assets/world/style/colors.ron b/assets/world/style/colors.ron new file mode 100644 index 0000000000..bc03c21078 --- /dev/null +++ b/assets/world/style/colors.ron @@ -0,0 +1,152 @@ +#![enable(unwrap_newtypes)] +#![enable(implicit_some)] + +// NOTE: Many of these colors are not used directly, but are modified in various ways (e.g. via +// lerping). So don't be too frustrated if a color change seems to have a different effect in +// different places; just follow the trends. +( + block: ( + pyramid: (203, 170, 146), + + // These are all ranges from low to high. + structure_blocks: ( + None: None, + // Samples the surface color. + Grass: None, + // Water blocks ignore color information, and even if they didn't would not be lerped. + Water: None, + GreenSludge: None, + // Leaves all actually get interpolated. + TemperateLeaves: (start: (0, 132, 94), end: (142, 181, 0)), + PineLeaves: (start: (0, 60, 50), end: (30, 100, 10)), + PalmLeavesInner: (start: (61, 166, 43), end: (29, 130, 32)), + PalmLeavesOuter: (start: (62, 171, 38), end: (45, 171, 65)), + Acacia: (start: (15, 126, 50), end: (30, 180, 10)), + Liana: (start: (0, 125, 107), end: (0, 155, 129)), + Mangrove: (start: (32, 56, 22), end: (57, 69, 27)), + ) + + // Water blocks ignore color now so this isn't used, but just in case this color was worth + // remembering here it is. + // green_sludge: (30.0, 126.0, 23.0) + ), + column: ( + cold_grass: (0.0, 0.5, 0.25), + warm_grass: (0.4, 0.8, 0.0), + dark_grass: (0.15, 0.4, 0.1), + wet_grass: (0.1, 0.8, 0.2), + cold_stone: (0.57, 0.67, 0.8), + hot_stone: (0.07, 0.07, 0.06), + warm_stone: (0.77, 0.77, 0.64), + beach_sand: (0.8, 0.75, 0.5), + desert_sand: (0.7, 0.4, 0.25), + snow: (0.8, 0.85, 1.0), + + stone_col: (195, 187, 201), + + dirt_low: (0.075, 0.07, 0.3), + dirt_high: (0.75, 0.55, 0.1), + + snow_high: (0.01, 0.3, 0.0), + warm_stone_high: (0.3, 0.12, 0.2), + + grass_high: (0.15, 0.2, 0.15), + tropical_high: (0.87, 0.62, 0.56), + ), + // NOTE: I think (but am not sure) that this is the color of stuff below the bottom-most + // ground. I'm not sure how easy it is to see. + deep_stone_color: (125, 120, 130), + layer: ( + bridge: (80, 80, 100), + stalagtite: (200, 200, 200), + ), + site: ( + castle: (), + dungeon: ( + stone: (150, 150, 175), + ), + settlement: ( + building: ( + archetype: ( + keep: ( + brick_base: (80, 80, 80), + floor_base: (80, 60, 10), + pole: (90, 70, 50), + flag: ( + Evil: (80, 10, 130), + Good: (200, 80, 40), + ), + stone: ( + Evil: (65, 60, 55), + Good: (100, 100, 110), + ), + ), + house: ( + foundation: (100, 100, 100), + floor: (100, 75, 50), + roof: ( + Roof1: (0x99, 0x5E, 0x54), + Roof2: (0x43, 0x63, 0x64), + Roof3: (0x76, 0x6D, 0x68), + Roof4: (0x7B, 0x41, 0x61), + Roof5: (0x52, 0x20, 0x20), + Roof6: (0x1A, 0x4A, 0x59), + Roof7: (0xCC, 0x76, 0x4E), + // (0x1D, 0x4D, 0x45), + // (0xB3, 0x7D, 0x60), + // (0xAC, 0x5D, 0x26), + // (0x32, 0x46, 0x6B), + // (0x2B, 0x19, 0x0F), + // (0x93, 0x78, 0x51), + // (0x92, 0x57, 0x24), + // (0x4A, 0x4E, 0x4E), + // (0x2F, 0x32, 0x47), + // (0x8F, 0x35, 0x43), + // (0x6D, 0x1E, 0x3A), + // (0x6D, 0xA7, 0x80), + // (0x4F, 0xA0, 0x95), + // (0xE2, 0xB9, 0x99), + // (0x7A, 0x30, 0x22), + // (0x4A, 0x06, 0x08), + // (0x8E, 0xB4, 0x57), + ), + wall: ( + Wall1: (200, 180, 150), + Wall2: (0xB8, 0xB4, 0xA4), + Wall3: (0x76, 0x6D, 0x68), + Wall4: (0xF3, 0xC9, 0x8F), + Wall5: (0xD3, 0xB7, 0x99), + Wall6: (0xE1, 0xAB, 0x91), + Wall7: (0x82, 0x57, 0x4C), + Wall8: (0xB9, 0x96, 0x77), + Wall9: (0xAE, 0x8D, 0x9C), + ), + support: ( + Support1: (60, 45, 30), + Support2: (0x65, 0x55, 0x56), + Support3: (0x53, 0x33, 0x13), + Support4: (0x58, 0x42, 0x33), + ), + ), + ), + ), + plot_town_path: (100, 95, 65), + + plot_field_dirt: (80, 55, 35), + plot_field_mound: (70, 80, 30), + + wall_low: (130, 100, 0), + wall_high :(90, 70, 50), + + tower_color: (50, 50, 50), + + // NOTE: Ideally these would be part of a make_case_elim, but we can't use it beacuse + // it doesn't support struct variants yet. + plot_dirt: (90, 70, 50), + plot_grass: (100, 200, 0), + plot_water: (100, 150, 250), + plot_town: (150, 110, 60), + // TODO: Add field furrow stuff. + ), + ), +) diff --git a/common/src/assets/watch.rs b/common/src/assets/watch.rs index f425c106f1..c5e8739f85 100644 --- a/common/src/assets/watch.rs +++ b/common/src/assets/watch.rs @@ -170,5 +170,13 @@ impl ReloadIndicator { } // Returns true if the watched file was changed - pub fn reloaded(&self) -> bool { self.reloaded.swap(false, Ordering::Acquire) } + pub fn reloaded(&self) -> bool { + // Optimize for the common case by performing an initial relaxed read, avoiding + // the atomic write. + if self.reloaded.load(Ordering::Relaxed) { + self.reloaded.swap(false, Ordering::Acquire) + } else { + false + } + } } diff --git a/common/src/lib.rs b/common/src/lib.rs index a52f7116af..dacdedd767 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -4,6 +4,7 @@ #![feature( arbitrary_enum_discriminant, const_checked_int_methods, + fundamental, option_unwrap_none, bool_to_option, label_break_value, @@ -38,6 +39,7 @@ pub mod store; pub mod sync; pub mod sys; pub mod terrain; +pub mod typed; pub mod util; pub mod vol; pub mod volumes; diff --git a/common/src/terrain/block.rs b/common/src/terrain/block.rs index 72717bd0fb..19e5499dbd 100644 --- a/common/src/terrain/block.rs +++ b/common/src/terrain/block.rs @@ -453,10 +453,10 @@ pub struct Block { } impl Block { - pub fn new(kind: BlockKind, color: Rgb) -> Self { + pub const fn new(kind: BlockKind, color: Rgb) -> Self { Self { kind, - color: color.into_array(), + color: [color.r, color.g, color.b], } } diff --git a/common/src/terrain/structure.rs b/common/src/terrain/structure.rs index 25501a4ac2..abe9168f4f 100644 --- a/common/src/terrain/structure.rs +++ b/common/src/terrain/structure.rs @@ -1,6 +1,7 @@ use super::BlockKind; use crate::{ assets::{self, Asset}, + make_case_elim, vol::{BaseVol, ReadVol, SizedVol, Vox, WriteVol}, volumes::dyna::{Dyna, DynaError}, }; @@ -9,25 +10,29 @@ use serde::Deserialize; use std::{fs::File, io::BufReader, sync::Arc}; use vek::*; -#[derive(Copy, Clone, PartialEq)] -pub enum StructureBlock { - None, - Grass, - TemperateLeaves, - PineLeaves, - Acacia, - Mangrove, - PalmLeavesInner, - PalmLeavesOuter, - Water, - GreenSludge, - Fruit, - Coconut, - Chest, - Hollow, - Liana, - Normal(Rgb), -} +make_case_elim!( + structure_block, + #[derive(Copy, Clone, PartialEq)] + #[repr(u32)] + pub enum StructureBlock { + None = 0, + Grass = 1, + TemperateLeaves = 2, + PineLeaves = 3, + Acacia = 4, + Mangrove = 5, + PalmLeavesInner = 6, + PalmLeavesOuter = 7, + Water = 8, + GreenSludge = 9, + Fruit = 10, + Coconut = 11, + Chest = 12, + Hollow = 13, + Liana = 14, + Normal(color: Rgb) = 15, + } +); impl Vox for StructureBlock { fn empty() -> Self { StructureBlock::None } diff --git a/common/src/util/typed.rs b/common/src/typed.rs similarity index 73% rename from common/src/util/typed.rs rename to common/src/typed.rs index 446e8cf173..7c67bba295 100644 --- a/common/src/util/typed.rs +++ b/common/src/typed.rs @@ -22,6 +22,7 @@ impl, T, S> Typed, S> for T { fn reduce(self, context: Context) -> (Pure, S) { (Pure(self), context.sub_context()) } } +#[fundamental] pub struct ElimCase { pub expr: Expr, pub cases: Cases, @@ -38,7 +39,7 @@ macro_rules! as_item { #[macro_export] macro_rules! make_case_elim { ($mod:ident, $( #[$ty_attr:meta] )* $vis:vis enum $ty:ident { - $( $constr:ident $( ( $( $arg_name:ident : $arg_ty:ty ),* ) )? = $index:tt ),* $(,)? + $( $constr:ident $( ( $( $arg_name:ident : $arg_ty:ty ),* ) )? = $index:expr ),* $(,)? }) => { $crate::as_item! { $( #[$ty_attr] )* @@ -48,9 +49,14 @@ macro_rules! make_case_elim { } #[allow(non_snake_case)] + #[allow(dead_code)] $vis mod $mod { use ::serde::{Deserialize, Serialize}; + pub const NUM_VARIANTS: usize = 0 $( + { let _ = $index; 1 } )*; + + pub const ALL_INDICES: [u32; NUM_VARIANTS] = [ $( $index, )* ]; + pub trait PackedElim { $( type $constr; )* } @@ -60,25 +66,25 @@ macro_rules! make_case_elim { $( pub $constr : Elim::$constr, )* } - impl PackedElim for $crate::util::Pure { + impl PackedElim for $crate::typed::Pure { $( type $constr = T; )* } - pub type PureCases = Cases<$crate::util::Pure>; + pub type PureCases = Cases<$crate::typed::Pure>; } #[allow(unused_parens)] impl<'a, Elim: $mod::PackedElim, Context, Type, S> - $crate::util::Typed for $crate::util::ElimCase<&'a $ty, &'a $mod::Cases, Type> + $crate::typed::Typed for $crate::typed::ElimCase<&'a $ty, &'a $mod::Cases, Type> where - $( &'a Elim::$constr: $crate::util::Typed<($( ($( &'a $arg_ty, )*), )? Context), Type, S>, )* + $( &'a Elim::$constr: $crate::typed::Typed<($( ($( &'a $arg_ty, )*), )? Context), Type, S>, )* { fn reduce(self, context: Context) -> (Type, S) { let Self { expr, cases, .. } = self; match expr { $( $ty::$constr $( ($( $arg_name, )*) )? => - <_ as $crate::util::Typed<_, Type, _>>::reduce( + <_ as $crate::typed::Typed<_, Type, _>>::reduce( &cases.$constr, ($( ($( $arg_name, )*), )? context), ), @@ -91,11 +97,11 @@ macro_rules! make_case_elim { pub fn elim_case<'a, Elim: $mod::PackedElim, Context, S, Type>(&'a self, cases: &'a $mod::Cases, context: Context) -> (Type, S) where - $crate::util::ElimCase<&'a $ty, &'a $mod::Cases, Type> : $crate::util::Typed, + $crate::typed::ElimCase<&'a $ty, &'a $mod::Cases, Type> : $crate::typed::Typed, { - use $crate::util::Typed; + use $crate::typed::Typed; - let case = $crate::util::ElimCase { + let case = $crate::typed::ElimCase { expr: self, cases, ty: ::core::marker::PhantomData, @@ -105,7 +111,7 @@ macro_rules! make_case_elim { pub fn elim_case_pure<'a, Type>(&'a self, cases: &'a $mod::PureCases) -> &'a Type { - let ($crate::util::Pure(expr), ()) = self.elim_case(cases, ()); + let ($crate::typed::Pure(expr), ()) = self.elim_case(cases, ()); expr } } diff --git a/common/src/util/mod.rs b/common/src/util/mod.rs index e7083811a9..21fe9a89ac 100644 --- a/common/src/util/mod.rs +++ b/common/src/util/mod.rs @@ -1,6 +1,5 @@ mod color; mod dir; -mod typed; pub const GIT_VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/githash")); @@ -11,4 +10,3 @@ lazy_static::lazy_static! { pub use color::*; pub use dir::*; -pub use typed::*; diff --git a/server/src/chunk_generator.rs b/server/src/chunk_generator.rs index 3e769956da..b69d597c12 100644 --- a/server/src/chunk_generator.rs +++ b/server/src/chunk_generator.rs @@ -1,5 +1,5 @@ #[cfg(not(feature = "worldgen"))] -use crate::test_world::World; +use crate::test_world::{IndexOwned, World}; use common::{generation::ChunkSupplement, terrain::TerrainChunk}; use crossbeam::channel; use hashbrown::{hash_map::Entry, HashMap}; @@ -9,11 +9,12 @@ use std::sync::{ Arc, }; use vek::*; -#[cfg(feature = "worldgen")] use world::World; +#[cfg(feature = "worldgen")] +use world::{IndexOwned, World}; type ChunkGenResult = ( Vec2, - Result<(TerrainChunk, ChunkSupplement), EcsEntity>, + Result<(TerrainChunk, ChunkSupplement), Option>, ); pub struct ChunkGenerator { @@ -34,10 +35,11 @@ impl ChunkGenerator { pub fn generate_chunk( &mut self, - entity: EcsEntity, + entity: Option, key: Vec2, thread_pool: &mut uvth::ThreadPool, world: Arc, + index: IndexOwned, ) { let v = if let Entry::Vacant(v) = self.pending_chunks.entry(key) { v @@ -48,8 +50,9 @@ impl ChunkGenerator { v.insert(Arc::clone(&cancel)); let chunk_tx = self.chunk_tx.clone(); thread_pool.execute(move || { + let index = index.as_index_ref(); let payload = world - .generate_chunk(key, || cancel.load(Ordering::Relaxed)) + .generate_chunk(index, key, || cancel.load(Ordering::Relaxed)) .map_err(|_| entity); let _ = chunk_tx.send((key, payload)); }); @@ -74,4 +77,10 @@ impl ChunkGenerator { cancel.store(true, Ordering::Relaxed); } } + + pub fn cancel_all(&mut self) { + self.pending_chunks.drain().for_each(|(_, cancel)| { + cancel.store(true, Ordering::Relaxed); + }); + } } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 7a3da0ba47..2d15bc6921 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -1473,7 +1473,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, server.world.index()))?; + let col = sampler.get((wpos, server.index.as_index_ref()))?; 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 4d9151781c..aef6fe1dbb 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -55,7 +55,7 @@ use std::{ time::{Duration, Instant}, }; #[cfg(not(feature = "worldgen"))] -use test_world::World; +use test_world::{IndexOwned, World}; use tracing::{debug, error, info, warn}; use uvth::{ThreadPool, ThreadPoolBuilder}; use vek::*; @@ -63,7 +63,7 @@ use vek::*; use world::{ civ::SiteKind, sim::{FileOpts, WorldOpts, DEFAULT_WORLD_MAP}, - World, + IndexOwned, World, }; #[macro_use] extern crate diesel; @@ -82,6 +82,7 @@ pub struct Tick(u64); pub struct Server { state: State, world: Arc, + index: IndexOwned, map: WorldMapMsg, network: Network, @@ -173,7 +174,7 @@ impl Server { state.ecs_mut().insert(AliasValidator::new(banned_words)); #[cfg(feature = "worldgen")] - let world = World::generate(settings.world_seed, WorldOpts { + let (world, index) = World::generate(settings.world_seed, WorldOpts { seed_elements: true, world_file: if let Some(ref opts) = settings.map_file { opts.clone() @@ -184,10 +185,10 @@ impl Server { ..WorldOpts::default() }); #[cfg(feature = "worldgen")] - let map = world.get_map_data(); + let map = world.get_map_data(index.as_index_ref()); #[cfg(not(feature = "worldgen"))] - let world = World::generate(settings.world_seed); + let (world, index) = World::generate(settings.world_seed); #[cfg(not(feature = "worldgen"))] let map = WorldMapMsg { dimensions: Vec2::new(1, 1), @@ -198,6 +199,7 @@ impl Server { #[cfg(feature = "worldgen")] let spawn_point = { + let index = index.as_index_ref(); // NOTE: all of these `.map(|e| e as [type])` calls should compile into no-ops, // but are needed to be explicit about casting (and to make the compiler stop // complaining) @@ -224,11 +226,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, world.index()) + .get_z_cache(spawn_location, 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, world.index()); + let (min_z, _, max_z) = z_cache.get_z_limits(&mut block_sampler, 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; @@ -244,7 +246,7 @@ impl Server { Vec3::new(spawn_location.x, spawn_location.y, *z), Some(&z_cache), false, - world.index(), + index, ) .map(|b| b.is_air()) .unwrap_or(false) @@ -288,6 +290,7 @@ impl Server { let this = Self { state, world: Arc::new(world), + index, map, network, @@ -494,6 +497,42 @@ impl Server { }, }); + { + // Check for new chunks; cancel and regenerate all chunks if the asset has been + // reloaded. Note that all of these assignments are no-ops, so the + // only work we do here on the fast path is perform a relaxed read on an atomic. + // boolean. + let index = &mut self.index; + let thread_pool = &mut self.thread_pool; + let world = &mut self.world; + let ecs = self.state.ecs_mut(); + + index.reload_colors_if_changed(|index| { + let mut chunk_generator = ecs.write_resource::(); + let client = ecs.read_storage::(); + let mut terrain = ecs.write_resource::(); + + // Cancel all pending chunks. + chunk_generator.cancel_all(); + + if client.is_empty() { + // No cilents, so just clear all terrain. + terrain.clear(); + } else { + // There's at leasat one client, so regenerate all chunks. + terrain.iter().for_each(|(pos, _)| { + chunk_generator.generate_chunk( + None, + pos, + thread_pool, + world.clone(), + index.clone(), + ); + }); + } + }); + } + let end_of_server_tick = Instant::now(); // 8) Update Metrics @@ -728,7 +767,13 @@ impl Server { self.state .ecs() .write_resource::() - .generate_chunk(entity, key, &mut self.thread_pool, self.world.clone()); + .generate_chunk( + Some(entity), + key, + &mut self.thread_pool, + self.world.clone(), + self.index.clone(), + ); } fn process_chat_cmd(&mut self, entity: EcsEntity, cmd: String) { diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index 739d1988b1..65277a7d9a 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -60,7 +60,7 @@ impl<'a> System<'a> for Sys { 'insert_terrain_chunks: while let Some((key, res)) = chunk_generator.recv_new_chunk() { let (chunk, supplement) = match res { Ok((chunk, supplement)) => (chunk, supplement), - Err(entity) => { + Err(Some(entity)) => { if let Some(client) = clients.get_mut(entity) { client.notify(ServerMsg::TerrainChunkUpdate { key, @@ -69,6 +69,9 @@ impl<'a> System<'a> for Sys { } continue 'insert_terrain_chunks; }, + Err(None) => { + continue 'insert_terrain_chunks; + }, }; // Send the chunk to all nearby players. for (view_distance, pos, client) in (&players, &positions, &mut clients) diff --git a/server/src/test_world.rs b/server/src/test_world.rs index e286e46bb4..4df254bf21 100644 --- a/server/src/test_world.rs +++ b/server/src/test_world.rs @@ -16,8 +16,25 @@ const DEFAULT_WORLD_CHUNKS_LG: MapSizeLg = pub struct World; +#[derive(Clone)] +pub struct IndexOwned; + +#[derive(Clone, Copy)] +pub struct IndexRef<'a>(&'a IndexOwned); + +impl IndexOwned { + pub fn reload_colors_if_changed( + &mut self, + _reload: impl FnOnce(&mut Self) -> R, + ) -> Option { + None + } + + pub fn as_index_ref(&self) -> IndexRef { IndexRef(self) } +} + impl World { - pub fn generate(_seed: u32) -> Self { Self } + pub fn generate(_seed: u32) -> (Self, IndexOwned) { (Self, IndexOwned) } pub fn tick(&self, dt: Duration) {} @@ -26,6 +43,7 @@ impl World { pub fn generate_chunk( &self, + _index: IndexRef, chunk_pos: Vec2, _should_continue: impl FnMut() -> bool, ) -> Result<(TerrainChunk, ChunkSupplement), ()> { diff --git a/world/examples/water.rs b/world/examples/water.rs index d3602feb07..1b595a1bf1 100644 --- a/world/examples/water.rs +++ b/world/examples/water.rs @@ -36,13 +36,14 @@ fn main() { let mut _map_file = PathBuf::from("./maps"); _map_file.push(map_file); - let world = World::generate(5284, WorldOpts { + let (world, index) = World::generate(5284, WorldOpts { seed_elements: false, world_file: sim::FileOpts::LoadAsset(veloren_world::sim::DEFAULT_WORLD_MAP.into()), // world_file: sim::FileOpts::Load(_map_file), // world_file: sim::FileOpts::Save, ..WorldOpts::default() }); + let index = index.as_index_ref(); tracing::info!("Sampling data..."); let sampler = world.sim(); let map_size_lg = sampler.map_size_lg(); @@ -55,7 +56,7 @@ fn main() { column_sample.get(( uniform_idx_as_vec2(map_size_lg, posi) * TerrainChunkSize::RECT_SIZE.map(|e| e as i32), - world.index(), + index, )) }) .collect::>() @@ -68,7 +69,7 @@ fn main() { sample_pos( config, sampler, - world.index(), + index, samples, uniform_idx_as_vec2(map_size_lg, posi), ) diff --git a/world/src/block/mod.rs b/world/src/block/mod.rs index 624de4d327..6d34ccc466 100644 --- a/world/src/block/mod.rs +++ b/world/src/block/mod.rs @@ -3,15 +3,28 @@ mod natural; use crate::{ column::{ColumnGen, ColumnSample}, util::{RandomField, Sampler, SmallCache}, - Index, + IndexRef, }; use common::{ - terrain::{structure::StructureBlock, Block, BlockKind, Structure}, + terrain::{ + structure::{self, StructureBlock}, + Block, BlockKind, Structure, + }, vol::{ReadVol, Vox}, }; -use std::ops::{Div, Mul}; +use core::ops::{Div, Mul, Range}; +use serde::{Deserialize, Serialize}; use vek::*; +#[derive(Deserialize, Serialize)] +pub struct Colors { + pub pyramid: (u8, u8, u8), + // TODO(@Sharp): After the merge, construct enough infrastructure to make it convenient to + // define mapping functions over the input; i.e. we should be able to interpret some fields as + // defining App, Arg>, where Fun : (Context, Arg) → (S, Type). + pub structure_blocks: structure::structure_block::PureCases>>, +} + pub struct BlockGen<'a> { pub column_cache: SmallCache>>, pub column_gen: ColumnGen<'a>, @@ -29,7 +42,7 @@ impl<'a> BlockGen<'a> { column_gen: &ColumnGen<'a>, cache: &'b mut SmallCache>>, wpos: Vec2, - index: &'a Index, + index: IndexRef<'a>, ) -> Option<&'b ColumnSample<'a>> { cache .get(wpos, |wpos| column_gen.get((wpos, index))) @@ -43,7 +56,7 @@ impl<'a> BlockGen<'a> { close_cliffs: &[(Vec2, u32); 9], cliff_hill: f32, tolerance: f32, - index: &'a Index, + index: IndexRef<'a>, ) -> f32 { close_cliffs.iter().fold( 0.0f32, @@ -89,7 +102,7 @@ impl<'a> BlockGen<'a> { ) } - pub fn get_z_cache(&mut self, wpos: Vec2, index: &'a Index) -> Option> { + pub fn get_z_cache(&mut self, wpos: Vec2, index: IndexRef<'a>) -> Option> { let BlockGen { column_cache, column_gen, @@ -143,7 +156,7 @@ impl<'a> BlockGen<'a> { wpos: Vec3, z_cache: Option<&ZCache>, only_structures: bool, - index: &'a Index, + index: IndexRef<'a>, ) -> Option { let BlockGen { column_cache, @@ -237,18 +250,7 @@ impl<'a> BlockGen<'a> { // Sample blocks - // let stone_col = Rgb::new(195, 187, 201); - - // let dirt_col = Rgb::new(79, 67, 60); - - let _air = Block::empty(); - // let stone = Block::new(2, stone_col); - // let surface_stone = Block::new(1, Rgb::new(200, 220, 255)); - // let dirt = Block::new(1, dirt_col); - // let sand = Block::new(1, Rgb::new(180, 150, 50)); - // let warm_stone = Block::new(1, Rgb::new(165, 165, 130)); - - let water = Block::new(BlockKind::Water, Rgb::new(60, 90, 190)); + let water = Block::new(BlockKind::Water, Rgb::zero()); let grass_depth = (1.5 + 2.0 * chaos).min(height - basement_height); let block = if (wposf.z as f32) < height - grass_depth { @@ -389,7 +391,7 @@ impl<'a> BlockGen<'a> { .iter() .find_map(|st| { let (st, st_sample) = st.as_ref()?; - st.get(wpos, st_sample) + st.get(index, wpos, st_sample) }) .or(block); @@ -407,7 +409,7 @@ impl<'a> ZCache<'a> { pub fn get_z_limits<'b>( &self, block_gen: &mut BlockGen<'b>, - index: &'b Index, + index: IndexRef<'b>, ) -> (f32, f32, f32) { let min = self.sample.alt - (self.sample.chaos.min(1.0) * 16.0); let min = min - 4.0; @@ -496,7 +498,7 @@ impl StructureInfo { } } - fn get(&self, wpos: Vec3, sample: &ColumnSample) -> Option { + fn get(&self, index: IndexRef, wpos: Vec3, sample: &ColumnSample) -> Option { match self.meta { StructureMeta::Pyramid { height } => { if wpos.z - self.pos.z @@ -505,7 +507,10 @@ impl StructureInfo { .map(|e: i32| (e.abs() / 2) * 2) .reduce_max() { - Some(Block::new(BlockKind::Dense, Rgb::new(203, 170, 146))) + Some(Block::new( + BlockKind::Dense, + index.colors.block.pyramid.into(), + )) } else { None } @@ -521,6 +526,7 @@ impl StructureInfo { .ok() .and_then(|b| { block_from_structure( + index, *b, block_pos, self.pos.into(), @@ -534,6 +540,7 @@ impl StructureInfo { } pub fn block_from_structure( + index: IndexRef, sblock: StructureBlock, pos: Vec3, structure_pos: Vec2, @@ -545,119 +552,60 @@ pub fn block_from_structure( let lerp = ((field.get(Vec3::from(structure_pos)).rem_euclid(256)) as f32 / 255.0) * 0.85 + ((field.get(pos + std::i32::MAX / 2).rem_euclid(256)) as f32 / 255.0) * 0.15; - let saturate_leaves = |col: Rgb| { - // /*saturate_srgb(col / 255.0, 0.65)*/ - /* let rgb = srgb_to_linear(col / 255.0); - /* let mut xyy = rgb_to_xyy(rgb); - xyy.x *= xyy.x; - xyy.y *= xyy.y; - linear_to_srgb(xyy_to_rgb(xyy).map(|e| e.min(1.0).max(0.0))).map(|e| e * 255.0) */ - /* let xyz = rgb_to_xyz(rgb); - let col_adjusted = if xyz.y == 0.0 { - Rgb::zero() - } else { - rgb / xyz.y - }; - let col = col_adjusted * col_adjusted * xyz.y; - linear_to_srgb(col).map(|e| e * 255.0) */ - /* let mut hsv = rgb_to_hsv(rgb); - hsv.y *= hsv.y; - linear_to_srgb(hsv_to_rgb(hsv).map(|e| e.min(1.0).max(0.0))).map(|e| e * 255.0) */ - linear_to_srgb(rgb * rgb).map(|e| e * 255.0) */ - col - }; - match sblock { StructureBlock::None => None, + StructureBlock::Hollow => Some(Block::empty()), StructureBlock::Grass => Some(Block::new( BlockKind::Normal, sample.surface_color.map(|e| (e * 255.0) as u8), )), - StructureBlock::TemperateLeaves => Some(Block::new( - BlockKind::Leaves, - Lerp::lerp( - saturate_leaves(Rgb::new(0.0, 132.0, 94.0)), - saturate_leaves(Rgb::new(142.0, 181.0, 0.0)), - lerp, - ) - .map(|e| e as u8), - )), - StructureBlock::PineLeaves => Some(Block::new( - BlockKind::Leaves, - Lerp::lerp(Rgb::new(0.0, 60.0, 50.0), Rgb::new(30.0, 100.0, 10.0), lerp) - .map(|e| e as u8), - )), - StructureBlock::PalmLeavesInner => Some(Block::new( - BlockKind::Leaves, - Lerp::lerp( - Rgb::new(61.0, 166.0, 43.0), - Rgb::new(29.0, 130.0, 32.0), - lerp, - ) - .map(|e| e as u8), - )), - StructureBlock::PalmLeavesOuter => Some(Block::new( - BlockKind::Leaves, - Lerp::lerp( - Rgb::new(62.0, 171.0, 38.0), - Rgb::new(45.0, 171.0, 65.0), - lerp, - ) - .map(|e| e as u8), - )), - StructureBlock::Water => Some(Block::new( - BlockKind::Water, - saturate_leaves(Rgb::new(100.0, 150.0, 255.0)).map(|e| e as u8), - )), + StructureBlock::Normal(color) => { + Some(Block::new(BlockKind::Normal, color)).filter(|block| !block.is_empty()) + }, + // Water / sludge throw away their color bits currently, so we don't set anyway. + StructureBlock::Water => Some(Block::new(BlockKind::Water, Rgb::zero())), StructureBlock::GreenSludge => Some(Block::new( BlockKind::Water, - saturate_leaves(Rgb::new(30.0, 126.0, 23.0)).map(|e| e as u8), - )), - StructureBlock::Acacia => Some(Block::new( - BlockKind::Leaves, - Lerp::lerp( - Rgb::new(15.0, 126.0, 50.0), - Rgb::new(30.0, 180.0, 10.0), - lerp, - ) - .map(|e| e as u8), + // TODO: If/when liquid supports other colors again, revisit this. + Rgb::zero(), )), + // None of these BlockKinds has an orientation, so we just use zero for the other color + // bits. StructureBlock::Fruit => Some(if field.get(pos + structure_pos) % 3 > 0 { Block::empty() } else { - Block::new(BlockKind::Apple, Rgb::new(1, 1, 1)) + Block::new(BlockKind::Apple, Rgb::zero()) }), StructureBlock::Coconut => Some(if field.get(pos + structure_pos) % 3 > 0 { Block::empty() } else { - Block::new(BlockKind::Coconut, Rgb::new(1, 1, 1)) + Block::new(BlockKind::Coconut, Rgb::zero()) }), StructureBlock::Chest => Some(if structure_seed % 10 < 7 { Block::empty() } else { - Block::new(BlockKind::Chest, Rgb::new(1, 1, 1)) + Block::new(BlockKind::Chest, Rgb::zero()) }), - StructureBlock::Liana => Some(Block::new( - BlockKind::Liana, - Lerp::lerp( - saturate_leaves(Rgb::new(0.0, 125.0, 107.0)), - saturate_leaves(Rgb::new(0.0, 155.0, 129.0)), - lerp, - ) - .map(|e| e as u8), - )), - StructureBlock::Mangrove => Some(Block::new( - BlockKind::Leaves, - Lerp::lerp( - saturate_leaves(Rgb::new(32.0, 56.0, 22.0)), - saturate_leaves(Rgb::new(57.0, 69.0, 27.0)), - lerp, - ) - .map(|e| e as u8), - )), - StructureBlock::Hollow => Some(Block::empty()), - StructureBlock::Normal(color) => { - Some(Block::new(BlockKind::Normal, color)).filter(|block| !block.is_empty()) - }, + // We interpolate all these BlockKinds as needed. + StructureBlock::TemperateLeaves + | StructureBlock::PineLeaves + | StructureBlock::PalmLeavesInner + | StructureBlock::PalmLeavesOuter + | StructureBlock::Acacia + | StructureBlock::Liana + | StructureBlock::Mangrove => sblock + .elim_case_pure(&index.colors.block.structure_blocks) + .as_ref() + .map(|range| { + Block::new( + BlockKind::Leaves, + Rgb::::lerp( + Rgb::::from(range.start).map(f32::from), + Rgb::::from(range.end).map(f32::from), + lerp, + ) + .map(|e| e as u8), + ) + }), } } diff --git a/world/src/block/natural.rs b/world/src/block/natural.rs index e09ca2158a..8ccff506b0 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}, - Index, CONFIG, + IndexRef, CONFIG, }; use common::terrain::Structure; use lazy_static::lazy_static; @@ -20,7 +20,7 @@ pub fn structure_gen<'a>( st_pos: Vec2, st_seed: u32, st_sample: &ColumnSample, - index: &'a Index, + index: IndexRef<'a>, ) -> Option { // Assuming it's a tree... figure out when it SHOULDN'T spawn let random_seed = (st_seed as f64) / (u32::MAX as f64); diff --git a/world/src/column/mod.rs b/world/src/column/mod.rs index 7f40c7e9e0..c8e07476e1 100644 --- a/world/src/column/mod.rs +++ b/world/src/column/mod.rs @@ -3,7 +3,7 @@ use crate::{ block::StructureMeta, sim::{local_cells, Cave, Path, RiverKind, SimChunk, WorldSim}, util::Sampler, - Index, CONFIG, + IndexRef, CONFIG, }; use common::{ terrain::{ @@ -13,6 +13,7 @@ use common::{ vol::RectVolSize, }; use noise::NoiseFn; +use serde::{Deserialize, Serialize}; use std::{ cmp::Reverse, f32, f64, @@ -25,6 +26,31 @@ pub struct ColumnGen<'a> { pub sim: &'a WorldSim, } +#[derive(Deserialize, Serialize)] +pub struct Colors { + pub cold_grass: (f32, f32, f32), + pub warm_grass: (f32, f32, f32), + pub dark_grass: (f32, f32, f32), + pub wet_grass: (f32, f32, f32), + pub cold_stone: (f32, f32, f32), + pub hot_stone: (f32, f32, f32), + pub warm_stone: (f32, f32, f32), + pub beach_sand: (f32, f32, f32), + pub desert_sand: (f32, f32, f32), + pub snow: (f32, f32, f32), + + pub stone_col: (u8, u8, u8), + + pub dirt_low: (f32, f32, f32), + pub dirt_high: (f32, f32, f32), + + pub snow_high: (f32, f32, f32), + pub warm_stone_high: (f32, f32, f32), + + pub grass_high: (f32, f32, f32), + pub tropical_high: (f32, f32, f32), +} + impl<'a> ColumnGen<'a> { pub fn new(sim: &'a WorldSim) -> Self { Self { sim } } @@ -80,7 +106,7 @@ impl<'a> ColumnGen<'a> { } impl<'a> Sampler<'a> for ColumnGen<'a> { - type Index = (Vec2, &'a Index); + type Index = (Vec2, IndexRef<'a>); type Sample = Option>; #[allow(clippy::float_cmp)] // TODO: Pending review in #587 @@ -765,25 +791,47 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { .add(marble_small.sub(0.5).mul(0.25)); // Colours - let cold_grass = Rgb::new(0.0, 0.5, 0.25); - let warm_grass = Rgb::new(0.4, 0.8, 0.0); - let dark_grass = Rgb::new(0.15, 0.4, 0.1); - let wet_grass = Rgb::new(0.1, 0.8, 0.2); - let cold_stone = Rgb::new(0.57, 0.67, 0.8); - let hot_stone = Rgb::new(0.07, 0.07, 0.06); - let warm_stone = Rgb::new(0.77, 0.77, 0.64); - let beach_sand = Rgb::new(0.8, 0.75, 0.5); - let desert_sand = Rgb::new(0.7, 0.4, 0.25); - let snow = Rgb::new(0.8, 0.85, 1.0); + let Colors { + cold_grass, + warm_grass, + dark_grass, + wet_grass, + cold_stone, + hot_stone, + warm_stone, + beach_sand, + desert_sand, + snow, + stone_col, + dirt_low, + dirt_high, + snow_high, + warm_stone_high, + grass_high, + tropical_high, + } = index.colors.column; - let stone_col = Rgb::new(195, 187, 201); - let dirt = Lerp::lerp( - Rgb::new(0.075, 0.07, 0.3), - Rgb::new(0.75, 0.55, 0.1), - marble, - ); - let tundra = Lerp::lerp(snow, Rgb::new(0.01, 0.3, 0.0), 0.4 + marble * 0.6); - let dead_tundra = Lerp::lerp(warm_stone, Rgb::new(0.3, 0.12, 0.2), marble); + let cold_grass = cold_grass.into(); + let warm_grass = warm_grass.into(); + let dark_grass = dark_grass.into(); + let wet_grass = wet_grass.into(); + let cold_stone = cold_stone.into(); + let hot_stone = hot_stone.into(); + let warm_stone: Rgb = warm_stone.into(); + let beach_sand = beach_sand.into(); + let desert_sand = desert_sand.into(); + let snow = snow.into(); + let stone_col = stone_col.into(); + let dirt_low: Rgb = dirt_low.into(); + let dirt_high = dirt_high.into(); + let snow_high = snow_high.into(); + let warm_stone_high = warm_stone_high.into(); + let grass_high = grass_high.into(); + let tropical_high = tropical_high.into(); + + let dirt = Lerp::lerp(dirt_low, dirt_high, marble); + let tundra = Lerp::lerp(snow, snow_high, 0.4 + marble * 0.6); + let dead_tundra = Lerp::lerp(warm_stone, warm_stone_high, marble); let cliff = Rgb::lerp(cold_stone, hot_stone, marble); let grass = Rgb::lerp( @@ -799,14 +847,14 @@ impl<'a> Sampler<'a> for ColumnGen<'a> { let tropical = Rgb::lerp( Rgb::lerp( grass, - Rgb::new(0.15, 0.2, 0.15), + grass_high, marble_small .sub(0.5) .mul(0.2) .add(0.75.mul(1.0.sub(humidity))) .powf(0.667), ), - Rgb::new(0.87, 0.62, 0.56), + tropical_high, marble.powf(1.5).sub(0.5).mul(4.0), ); diff --git a/world/src/index.rs b/world/src/index.rs index ed6a6ce157..c22f4eaa0b 100644 --- a/world/src/index.rs +++ b/world/src/index.rs @@ -1,21 +1,104 @@ -use crate::site::Site; -use common::store::Store; +use crate::{site::Site, Colors}; +use common::{ + assets::{self, watch::ReloadIndicator}, + store::Store, +}; +use core::ops::Deref; use noise::{Seedable, SuperSimplex}; +use std::sync::Arc; + +const WORLD_COLORS_MANIFEST: &str = "world.style.colors"; pub struct Index { pub seed: u32, pub time: f32, pub noise: Noise, pub sites: Store, + indicator: ReloadIndicator, +} + +/// An owned reference to indexed data. +/// +/// The data are split out so that we can replace the colors without disturbing +/// the rest of the index, while also keeping all the adta within a single +/// indirection (though possibly not contiguous). +#[derive(Clone)] +pub struct IndexOwned { + colors: Arc, + index: Arc, +} + +impl Deref for IndexOwned { + type Target = Index; + + fn deref(&self) -> &Self::Target { &self.index } +} + +/// A shared reference to indexed data. +/// +/// This is copyable and can be used from either style of index. +#[derive(Clone, Copy)] +pub struct IndexRef<'a> { + pub colors: &'a Colors, + pub index: &'a Index, +} + +impl<'a> Deref for IndexRef<'a> { + type Target = Index; + + fn deref(&self) -> &Self::Target { &self.index } } impl Index { - pub fn new(seed: u32) -> Self { + /// NOTE: Panics if the color manifest cannot be loaded. + pub fn new(seed: u32) -> (Self, Arc) { + let mut indicator = ReloadIndicator::new(); + let colors = assets::load_watched::(WORLD_COLORS_MANIFEST, &mut indicator) + .expect("Could not load world colors!"); + + ( + Self { + seed, + time: 0.0, + noise: Noise::new(seed), + sites: Store::default(), + indicator, + }, + colors, + ) + } +} + +impl IndexOwned { + pub fn new(index: Index, colors: Arc) -> Self { Self { - seed, - time: 0.0, - noise: Noise::new(seed), - sites: Store::default(), + index: Arc::new(index), + colors, + } + } + + /// NOTE: Callback is called only when colors actually have to be reloaded. + /// The server is responsible for making sure that all affected chunks are + /// reloaded; a naive approach will just regenerate every chunk on the + /// server, but it is possible that eventually we can find a better + /// solution. + /// + /// Ideally, this should be called about once per tick. + pub fn reload_colors_if_changed( + &mut self, + reload: impl FnOnce(&mut Self) -> R, + ) -> Option { + self.indicator.reloaded().then(move || { + // We know the asset was loaded before, so load_expect should be fine. + self.colors = assets::load_expect::(WORLD_COLORS_MANIFEST); + reload(self) + }) + } + + pub fn as_index_ref(&self) -> IndexRef { + IndexRef { + colors: &self.colors, + index: &self.index, } } } diff --git a/world/src/layer/mod.rs b/world/src/layer/mod.rs index 72986bb1ec..d358e6286e 100644 --- a/world/src/layer/mod.rs +++ b/world/src/layer/mod.rs @@ -2,7 +2,7 @@ use crate::{ column::ColumnSample, sim::SimChunk, util::{RandomField, Sampler}, - Index, + IndexRef, }; use common::{ assets, comp, @@ -13,12 +13,19 @@ use common::{ }; use noise::NoiseFn; use rand::prelude::*; +use serde::{Deserialize, Serialize}; use std::{ f32, ops::{Mul, Sub}, }; use vek::*; +#[derive(Deserialize, Serialize)] +pub struct Colors { + pub bridge: (u8, u8, u8), + pub stalagtite: (u8, u8, u8), +} + fn close(x: f32, tgt: f32, falloff: f32) -> f32 { (1.0 - (x - tgt).abs() / falloff).max(0.0).powf(0.5) } @@ -27,7 +34,7 @@ pub fn apply_scatter_to<'a>( wpos2d: Vec2, mut get_column: impl FnMut(Vec2) -> Option<&'a ColumnSample<'a>>, vol: &mut (impl BaseVol + RectSizedVol + ReadVol + WriteVol), - index: &Index, + index: IndexRef, chunk: &SimChunk, ) { use BlockKind::*; @@ -150,7 +157,7 @@ pub fn apply_paths_to<'a>( wpos2d: Vec2, mut get_column: impl FnMut(Vec2) -> Option<&'a ColumnSample<'a>>, vol: &mut (impl BaseVol + RectSizedVol + ReadVol + WriteVol), - _index: &Index, + index: IndexRef, ) { for y in 0..vol.size_xy().y as i32 { for x in 0..vol.size_xy().x as i32 { @@ -213,7 +220,10 @@ pub fn apply_paths_to<'a>( let _ = vol.set( Vec3::new(offs.x, offs.y, surface_z + z), if bridge_offset >= 2.0 && path_dist >= 3.0 || z < inset - 1 { - Block::new(BlockKind::Normal, noisy_color(Rgb::new(80, 80, 100), 8)) + Block::new( + BlockKind::Normal, + noisy_color(index.colors.layer.bridge.into(), 8), + ) } else { let path_color = path.surface_color( col_sample.sub_surface_color.map(|e| (e * 255.0) as u8), @@ -238,7 +248,7 @@ pub fn apply_caves_to<'a>( wpos2d: Vec2, mut get_column: impl FnMut(Vec2) -> Option<&'a ColumnSample<'a>>, vol: &mut (impl BaseVol + RectSizedVol + ReadVol + WriteVol), - index: &Index, + index: IndexRef, ) { for y in 0..vol.size_xy().y as i32 { for x in 0..vol.size_xy().x as i32 { @@ -297,7 +307,7 @@ pub fn apply_caves_to<'a>( for z in cave_roof - stalagtites..cave_roof { let _ = vol.set( Vec3::new(offs.x, offs.y, z), - Block::new(BlockKind::Rock, Rgb::broadcast(200)), + Block::new(BlockKind::Rock, index.colors.layer.stalagtite.into()), ); } @@ -325,7 +335,7 @@ pub fn apply_caves_supplement<'a>( wpos2d: Vec2, mut get_column: impl FnMut(Vec2) -> Option<&'a ColumnSample<'a>>, vol: &(impl BaseVol + RectSizedVol + ReadVol + WriteVol), - index: &Index, + index: IndexRef, supplement: &mut ChunkSupplement, ) { for y in 0..vol.size_xy().y as i32 { diff --git a/world/src/lib.rs b/world/src/lib.rs index 55a5f12331..6a1a6e954d 100644 --- a/world/src/lib.rs +++ b/world/src/lib.rs @@ -3,6 +3,7 @@ #![allow(clippy::option_map_unit_fn)] #![feature( arbitrary_enum_discriminant, + bool_to_option, const_generics, const_panic, label_break_value, @@ -25,6 +26,7 @@ pub mod util; pub use crate::config::CONFIG; pub use block::BlockGen; pub use column::ColumnSample; +pub use index::{IndexOwned, IndexRef}; use crate::{ column::ColumnGen, @@ -32,6 +34,7 @@ use crate::{ util::{Grid, Sampler}, }; use common::{ + assets::{self, Asset}, comp::{self, bird_medium, critter, quadruped_low, quadruped_medium, quadruped_small}, generation::{ChunkSupplement, EntityInfo}, msg::server::WorldMapMsg, @@ -39,7 +42,8 @@ use common::{ vol::{ReadVol, RectVolSize, Vox, WriteVol}, }; use rand::Rng; -use std::time::Duration; +use serde::{Deserialize, Serialize}; +use std::{fs::File, io::BufReader, time::Duration}; use vek::*; #[derive(Debug)] @@ -50,35 +54,51 @@ pub enum Error { pub struct World { sim: sim::WorldSim, civs: civ::Civs, - index: Index, +} + +#[derive(Deserialize, Serialize)] +pub struct Colors { + pub deep_stone_color: (u8, u8, u8), + pub block: block::Colors, + pub column: column::Colors, + pub layer: layer::Colors, + pub site: site::Colors, +} + +impl Asset for Colors { + const ENDINGS: &'static [&'static str] = &["ron"]; + + fn parse(buf_reader: BufReader) -> Result { + ron::de::from_reader(buf_reader).map_err(assets::Error::parse_error) + } } impl World { - pub fn generate(seed: u32, opts: sim::WorldOpts) -> Self { + pub fn generate(seed: u32, opts: sim::WorldOpts) -> (Self, IndexOwned) { + // NOTE: Generating index first in order to quickly fail if the color manifest + // is broken. + let (mut index, colors) = Index::new(seed); let mut sim = sim::WorldSim::generate(seed, opts); - let mut index = Index::new(seed); let civs = civ::Civs::generate(seed, &mut sim, &mut index); sim2::simulate(&mut index, &mut sim); - Self { sim, civs, index } + (Self { sim, civs }, IndexOwned::new(index, colors)) } 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) -> WorldMapMsg { self.sim.get_map(&self.index) } + pub fn get_map_data(&self, index: IndexRef) -> WorldMapMsg { self.sim.get_map(index) } pub fn sample_columns( &self, - ) -> impl Sampler, &Index), Sample = Option> + '_ { + ) -> impl Sampler, IndexRef), Sample = Option> + '_ { ColumnGen::new(&self.sim) } @@ -87,6 +107,7 @@ impl World { #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 pub fn generate_chunk( &self, + index: IndexRef, chunk_pos: Vec2, // TODO: misleading name mut should_continue: impl FnMut() -> bool, @@ -97,7 +118,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, &self.index), + |offs| sampler.get_z_cache(chunk_wpos2d - grid_border + offs, index), ); let air = Block::empty(); @@ -107,9 +128,9 @@ impl World { .get(grid_border + TerrainChunkSize::RECT_SIZE.map(|e| e as i32) / 2) .and_then(|zcache| zcache.as_ref()) .map(|zcache| zcache.sample.stone_col) - .unwrap_or(Rgb::new(125, 120, 130)), + .unwrap_or(index.colors.deep_stone_color.into()), ); - let water = Block::new(BlockKind::Water, Rgb::new(60, 90, 190)); + let water = Block::new(BlockKind::Water, Rgb::zero()); let _chunk_size2d = TerrainChunkSize::RECT_SIZE; let (base_z, sim_chunk) = match self @@ -153,7 +174,7 @@ impl World { }; let (min_z, only_structures_min_z, max_z) = - z_cache.get_z_limits(&mut sampler, &self.index); + z_cache.get_z_limits(&mut sampler, index); (base_z..min_z as i32).for_each(|z| { let _ = chunk.set(Vec3::new(x, y, z), stone); @@ -165,7 +186,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, &self.index) + sampler.get_with_z_cache(wpos, Some(&z_cache), only_structures, index) { let _ = chunk.set(lpos, block); } @@ -184,13 +205,13 @@ impl World { let mut rng = rand::thread_rng(); // Apply layers (paths, caves, etc.) - layer::apply_scatter_to(chunk_wpos2d, sample_get, &mut chunk, &self.index, sim_chunk); - layer::apply_paths_to(chunk_wpos2d, sample_get, &mut chunk, &self.index); - layer::apply_caves_to(chunk_wpos2d, sample_get, &mut chunk, &self.index); + layer::apply_scatter_to(chunk_wpos2d, sample_get, &mut chunk, index, sim_chunk); + layer::apply_paths_to(chunk_wpos2d, sample_get, &mut chunk, index); + layer::apply_caves_to(chunk_wpos2d, sample_get, &mut chunk, index); // Apply site generation sim_chunk.sites.iter().for_each(|site| { - self.index.sites[*site].apply_to(chunk_wpos2d, sample_get, &mut chunk) + index.sites[*site].apply_to(index, chunk_wpos2d, sample_get, &mut chunk) }); let gen_entity_pos = || { @@ -243,18 +264,13 @@ impl World { chunk_wpos2d, sample_get, &chunk, - &self.index, + index, &mut supplement, ); // Apply site supplementary information sim_chunk.sites.iter().for_each(|site| { - self.index.sites[*site].apply_supplement( - &mut rng, - chunk_wpos2d, - sample_get, - &mut supplement, - ) + 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 1d3dd88831..02254e01a1 100644 --- a/world/src/sim/map.rs +++ b/world/src/sim/map.rs @@ -1,7 +1,7 @@ use crate::{ column::ColumnSample, sim::{RiverKind, WorldSim}, - Index, CONFIG, + IndexRef, CONFIG, }; use common::{ terrain::{ @@ -63,10 +63,15 @@ pub fn sample_wpos(config: &MapConfig, sampler: &WorldSim, wpos: Vec2) -> f /// are to be used for some reason, one should pass a custom function to /// generate instead (e.g. one that just looks up the color in a cached /// array). +// NOTE: Deliberately not putting Rgb colors here in the config file; they +// aren't hot reloaded anyway, and for various reasons they're probably not a +// good idea to update in that way (for example, we currently want water colors +// to match voxygen's). Eventually we'll fix these sorts of issues in some +// other way. pub fn sample_pos( config: &MapConfig, sampler: &WorldSim, - index: &Index, + index: IndexRef, samples: Option<&[Option]>, pos: Vec2, ) -> MapSample { diff --git a/world/src/sim/mod.rs b/world/src/sim/mod.rs index ff058a4db0..7eff3d9d8a 100644 --- a/world/src/sim/mod.rs +++ b/world/src/sim/mod.rs @@ -29,7 +29,7 @@ use crate::{ column::ColumnGen, site::Site, util::{seed_expan, FastNoise, RandomField, Sampler, StructureGen2d, LOCALITY, NEIGHBORS}, - Index, CONFIG, + IndexRef, CONFIG, }; use common::{ assets, @@ -1408,7 +1408,7 @@ impl WorldSim { /// Draw a map of the world based on chunk information. Returns a buffer of /// u32s. - pub fn get_map(&self, index: &Index) -> WorldMapMsg { + pub fn get_map(&self, index: IndexRef) -> WorldMapMsg { let mut map_config = MapConfig::orthographic( self.map_size_lg(), core::ops::RangeInclusive::new(CONFIG.sea_level, CONFIG.sea_level + self.max_height), diff --git a/world/src/site/castle/mod.rs b/world/src/site/castle/mod.rs index cc367448ed..ab5b15e638 100644 --- a/world/src/site/castle/mod.rs +++ b/world/src/site/castle/mod.rs @@ -3,9 +3,10 @@ use crate::{ column::ColumnSample, sim::WorldSim, site::settlement::building::{ - archetype::keep::{Attr, Keep as KeepArchetype}, + archetype::keep::{Attr, FlagColor, Keep as KeepArchetype, StoneColor}, Archetype, Ori, }, + IndexRef, }; use common::{ generation::ChunkSupplement, @@ -14,6 +15,7 @@ use common::{ }; use core::f32; use rand::prelude::*; +use serde::{Deserialize, Serialize}; use vek::*; struct Keep { @@ -47,6 +49,9 @@ pub struct GenCtx<'a, R: Rng> { rng: &'a mut R, } +#[derive(Deserialize, Serialize)] +pub struct Colors; + impl Castle { #[allow(clippy::let_and_return)] // TODO: Pending review in #587 pub fn generate(wpos: Vec2, sim: Option<&mut WorldSim>, rng: &mut impl Rng) -> Self { @@ -167,6 +172,7 @@ impl Castle { pub fn apply_to<'a>( &'a self, + index: IndexRef, wpos2d: Vec2, mut get_column: impl FnMut(Vec2) -> Option<&'a ColumnSample<'a>>, vol: &mut (impl BaseVol + RectSizedVol + ReadVol + WriteVol), @@ -273,14 +279,14 @@ impl Castle { let keep_archetype = KeepArchetype { flag_color: if self.evil { - Rgb::new(80, 10, 130) + FlagColor::Evil } else { - Rgb::new(200, 80, 40) + FlagColor::Good }, stone_color: if self.evil { - Rgb::new(65, 60, 55) + StoneColor::Evil } else { - Rgb::new(100, 100, 110) + StoneColor::Good }, }; @@ -294,6 +300,7 @@ impl Castle { } let mut mask = keep_archetype.draw( + index, Vec3::from(wall_rpos) + Vec3::unit_z() * wpos.z - wall_alt, wall_dist, border_pos, @@ -323,6 +330,7 @@ impl Castle { let border_pos = (tower_wpos - wpos).xy().map(|e| e.abs()); mask = mask.resolve_with(keep_archetype.draw( + index, if (tower_wpos.x - wpos.x).abs() < (tower_wpos.y - wpos.y).abs() { wpos - tower_wpos } else { @@ -364,6 +372,7 @@ impl Castle { let border_pos = (keep_wpos - wpos).xy().map(|e| e.abs()); mask = mask.resolve_with(keep_archetype.draw( + index, if (keep_wpos.x - wpos.x).abs() < (keep_wpos.y - wpos.y).abs() { wpos - keep_wpos } else { diff --git a/world/src/site/dungeon/mod.rs b/world/src/site/dungeon/mod.rs index b34619c487..4b3fe2f8a7 100644 --- a/world/src/site/dungeon/mod.rs +++ b/world/src/site/dungeon/mod.rs @@ -5,6 +5,7 @@ use crate::{ sim::WorldSim, site::BlockMask, util::{attempt, Grid, RandomField, Sampler, CARDINALS, DIRS}, + IndexRef, }; use common::{ assets, @@ -20,6 +21,7 @@ use core::{f32, hash::BuildHasherDefault}; use fxhash::FxHasher64; use lazy_static::lazy_static; use rand::prelude::*; +use serde::{Deserialize, Serialize}; use std::sync::Arc; use vek::*; @@ -37,6 +39,11 @@ pub struct GenCtx<'a, R: Rng> { rng: &'a mut R, } +#[derive(Deserialize, Serialize)] +pub struct Colors { + pub stone: (u8, u8, u8), +} + const ALT_OFFSET: i32 = -2; const LEVELS: usize = 5; @@ -80,6 +87,7 @@ impl Dungeon { pub fn apply_to<'a>( &'a self, + index: IndexRef, wpos2d: Vec2, mut get_column: impl FnMut(Vec2) -> Option<&'a ColumnSample<'a>>, vol: &mut (impl BaseVol + RectSizedVol + ReadVol + WriteVol), @@ -112,7 +120,14 @@ impl Dungeon { .ok() .copied() .map(|sb| { - block_from_structure(sb, spos, self.origin, self.seed, col_sample) + block_from_structure( + index, + sb, + spos, + self.origin, + self.seed, + col_sample, + ) }) .unwrap_or(None) { @@ -125,7 +140,7 @@ impl Dungeon { for floor in &self.floors { z -= floor.total_depth(); - let mut sampler = floor.col_sampler(rpos, z); + let mut sampler = floor.col_sampler(index, rpos, z); for rz in 0..floor.total_depth() { if let Some(block) = sampler(rz).finish() { @@ -574,16 +589,29 @@ impl Floor { } #[allow(clippy::unnested_or_patterns)] // TODO: Pending review in #587 - pub fn col_sampler(&self, pos: Vec2, floor_z: i32) -> impl FnMut(i32) -> BlockMask + '_ { + pub fn col_sampler<'a>( + &'a self, + index: IndexRef<'a>, + pos: Vec2, + floor_z: i32, + ) -> impl FnMut(i32) -> BlockMask + 'a { let rpos = pos - self.tile_offset * TILE_SIZE; let tile_pos = rpos.map(|e| e.div_euclid(TILE_SIZE)); let tile_center = tile_pos * TILE_SIZE + TILE_SIZE / 2; let rtile_pos = rpos - tile_center; + let colors = &index.colors.site.dungeon; + let empty = BlockMask::new(Block::empty(), 1); let make_staircase = move |pos: Vec3, radius: f32, inner_radius: f32, stretch: f32| { - let stone = BlockMask::new(Block::new(BlockKind::Normal, Rgb::new(150, 150, 175)), 5); + let stone = BlockMask::new( + Block::new( + BlockKind::Normal, + /* Rgb::new(150, 150, 175) */ colors.stone.into(), + ), + 5, + ); if (pos.xy().magnitude_squared() as f32) < inner_radius.powf(2.0) { stone diff --git a/world/src/site/mod.rs b/world/src/site/mod.rs index 82c83b2183..01f9985f4d 100644 --- a/world/src/site/mod.rs +++ b/world/src/site/mod.rs @@ -10,15 +10,23 @@ pub use self::{ settlement::Settlement, }; -use crate::column::ColumnSample; +use crate::{column::ColumnSample, IndexRef}; use common::{ generation::ChunkSupplement, terrain::Block, vol::{BaseVol, ReadVol, RectSizedVol, WriteVol}, }; use rand::Rng; +use serde::{Deserialize, Serialize}; use vek::*; +#[derive(Deserialize, Serialize)] +pub struct Colors { + pub castle: castle::Colors, + pub dungeon: dungeon::Colors, + pub settlement: settlement::Colors, +} + pub struct SpawnRules { pub trees: bool, } @@ -86,14 +94,15 @@ impl Site { pub fn apply_to<'a>( &'a self, + index: IndexRef, wpos2d: Vec2, get_column: impl FnMut(Vec2) -> Option<&'a ColumnSample<'a>>, vol: &mut (impl BaseVol + RectSizedVol + ReadVol + WriteVol), ) { match &self.kind { - SiteKind::Settlement(s) => s.apply_to(wpos2d, get_column, vol), - SiteKind::Dungeon(d) => d.apply_to(wpos2d, get_column, vol), - SiteKind::Castle(c) => c.apply_to(wpos2d, get_column, vol), + SiteKind::Settlement(s) => s.apply_to(index, wpos2d, get_column, vol), + SiteKind::Dungeon(d) => d.apply_to(index, wpos2d, get_column, vol), + SiteKind::Castle(c) => c.apply_to(index, wpos2d, get_column, vol), } } diff --git a/world/src/site/settlement/building/archetype/house.rs b/world/src/site/settlement/building/archetype/house.rs index 970e3a4698..aa15f844e8 100644 --- a/world/src/site/settlement/building/archetype/house.rs +++ b/world/src/site/settlement/building/archetype/house.rs @@ -4,64 +4,103 @@ use super::{super::skeleton::*, Archetype}; use crate::{ site::BlockMask, util::{RandomField, Sampler}, + IndexRef, }; use common::{ + make_case_elim, terrain::{Block, BlockKind}, vol::Vox, }; use rand::prelude::*; +use serde::{Deserialize, Serialize}; use vek::*; -pub struct ColorTheme { - roof: Rgb, - wall: Rgb, - support: Rgb, +#[derive(Deserialize, Serialize)] +pub struct Colors { + pub foundation: (u8, u8, u8), + pub floor: (u8, u8, u8), + pub roof: roof_color::PureCases<(u8, u8, u8)>, + pub wall: wall_color::PureCases<(u8, u8, u8)>, + pub support: support_color::PureCases<(u8, u8, u8)>, } -const ROOF_COLORS: &[Rgb] = &[ - // Rgb::new(0x1D, 0x4D, 0x45), - // Rgb::new(0xB3, 0x7D, 0x60), - // Rgb::new(0xAC, 0x5D, 0x26), - // Rgb::new(0x32, 0x46, 0x6B), - // Rgb::new(0x2B, 0x19, 0x0F), - // Rgb::new(0x93, 0x78, 0x51), - // Rgb::new(0x92, 0x57, 0x24), - // Rgb::new(0x4A, 0x4E, 0x4E), - // 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), - Rgb::new(0x99, 0x5E, 0x54), - Rgb::new(0x43, 0x63, 0x64), - Rgb::new(0x76, 0x6D, 0x68), - Rgb::new(0x7B, 0x41, 0x61), - Rgb::new(0x52, 0x20, 0x20), - Rgb::new(0x1A, 0x4A, 0x59), - Rgb::new(0xCC, 0x76, 0x4E), +pub struct ColorTheme { + roof: RoofColor, + wall: WallColor, + support: SupportColor, +} + +make_case_elim!( + roof_color, + #[repr(u32)] + #[derive(Clone, Copy)] + pub enum RoofColor { + Roof1 = 0, + Roof2 = 1, + Roof3 = 2, + Roof4 = 3, + Roof5 = 4, + Roof6 = 5, + Roof7 = 6, + } +); + +make_case_elim!( + wall_color, + #[repr(u32)] + #[derive(Clone, Copy)] + pub enum WallColor { + Wall1 = 0, + Wall2 = 1, + Wall3 = 2, + Wall4 = 3, + Wall5 = 4, + Wall6 = 5, + Wall7 = 6, + Wall8 = 7, + Wall9 = 8, + } +); + +make_case_elim!( + support_color, + #[repr(u32)] + #[derive(Clone, Copy)] + pub enum SupportColor { + Support1 = 0, + Support2 = 1, + Support3 = 2, + Support4 = 3, + } +); + +const ROOF_COLORS: [RoofColor; roof_color::NUM_VARIANTS] = [ + RoofColor::Roof1, + RoofColor::Roof2, + RoofColor::Roof3, + RoofColor::Roof4, + RoofColor::Roof4, + RoofColor::Roof6, + RoofColor::Roof7, ]; -const WALL_COLORS: &[Rgb] = &[ - Rgb::new(200, 180, 150), - Rgb::new(0xB8, 0xB4, 0xA4), - Rgb::new(0x76, 0x6D, 0x68), - Rgb::new(0xF3, 0xC9, 0x8F), - Rgb::new(0xD3, 0xB7, 0x99), - Rgb::new(0xE1, 0xAB, 0x91), - Rgb::new(0x82, 0x57, 0x4C), - Rgb::new(0xB9, 0x96, 0x77), - Rgb::new(0xAE, 0x8D, 0x9C), +const WALL_COLORS: [WallColor; wall_color::NUM_VARIANTS] = [ + WallColor::Wall1, + WallColor::Wall2, + WallColor::Wall3, + WallColor::Wall4, + WallColor::Wall5, + WallColor::Wall6, + WallColor::Wall7, + WallColor::Wall8, + WallColor::Wall9, ]; -const SUPPORT_COLORS: &[Rgb] = &[ - Rgb::new(60, 45, 30), - Rgb::new(0x65, 0x55, 0x56), - Rgb::new(0x53, 0x33, 0x13), - Rgb::new(0x58, 0x42, 0x33), +const SUPPORT_COLORS: [SupportColor; support_color::NUM_VARIANTS] = [ + SupportColor::Support1, + SupportColor::Support2, + SupportColor::Support3, + SupportColor::Support4, ]; pub struct House { @@ -210,6 +249,7 @@ impl Archetype for House { #[allow(clippy::int_plus_one)] // TODO: Pending review in #587 fn draw( &self, + index: IndexRef, _pos: Vec3, dist: i32, bound_offset: Vec2, @@ -220,6 +260,11 @@ impl Archetype for House { _len: i32, attr: &Self::Attr, ) -> BlockMask { + let colors = &index.colors.site.settlement.building.archetype.house; + let roof_color = *self.colors.roof.elim_case_pure(&colors.roof); + let wall_color = *self.colors.wall.elim_case_pure(&colors.wall); + let support_color = *self.colors.support.elim_case_pure(&colors.support); + let profile = Vec2::new(bound_offset.x, z); let make_meta = |ori| { @@ -240,6 +285,7 @@ impl Archetype for House { BlockMask::new( Block::new( BlockKind::Normal, + // TODO: Clarify exactly how this affects the color. Rgb::new(r, g, b) .map(|e: u8| e.saturating_add((nz & 0x0F) as u8).saturating_sub(8)), ), @@ -253,11 +299,11 @@ impl Archetype for House { let foundation_layer = internal_layer + 1; let floor_layer = foundation_layer + 1; - let foundation = make_block((100, 100, 100)).with_priority(foundation_layer); - let log = make_block(self.colors.support.into_tuple()); - let floor = make_block((100, 75, 50)); - let wall = make_block(self.colors.wall.into_tuple()).with_priority(facade_layer); - let roof = make_block(self.colors.roof.into_tuple()).with_priority(facade_layer - 1); + let foundation = make_block(colors.foundation).with_priority(foundation_layer); + let log = make_block(support_color); + let floor = make_block(colors.floor); + let wall = make_block(wall_color).with_priority(facade_layer); + let roof = make_block(roof_color).with_priority(facade_layer - 1); let empty = BlockMask::nothing(); let internal = BlockMask::new(Block::empty(), internal_layer); let end_window = BlockMask::new( diff --git a/world/src/site/settlement/building/archetype/keep.rs b/world/src/site/settlement/building/archetype/keep.rs index 2028fb75d0..160772f6e4 100644 --- a/world/src/site/settlement/building/archetype/keep.rs +++ b/world/src/site/settlement/building/archetype/keep.rs @@ -2,17 +2,29 @@ use super::{super::skeleton::*, Archetype}; use crate::{ site::BlockMask, util::{RandomField, Sampler}, + IndexRef, }; use common::{ + make_case_elim, terrain::{Block, BlockKind}, vol::Vox, }; use rand::prelude::*; +use serde::{Deserialize, Serialize}; use vek::*; +#[derive(Deserialize, Serialize)] +pub struct Colors { + pub brick_base: (u8, u8, u8), + pub floor_base: (u8, u8, u8), + pub pole: (u8, u8, u8), + pub flag: flag_color::PureCases<(u8, u8, u8)>, + pub stone: stone_color::PureCases<(u8, u8, u8)>, +} + pub struct Keep { - pub flag_color: Rgb, - pub stone_color: Rgb, + pub flag_color: FlagColor, + pub stone_color: StoneColor, } pub struct Attr { @@ -24,6 +36,24 @@ pub struct Attr { pub has_doors: bool, } +make_case_elim!( + flag_color, + #[repr(u32)] + pub enum FlagColor { + Good = 0, + Evil = 1, + } +); + +make_case_elim!( + stone_color, + #[repr(u32)] + pub enum StoneColor { + Good = 0, + Evil = 1, + } +); + impl Archetype for Keep { type Attr = Attr; @@ -71,8 +101,8 @@ impl Archetype for Keep { ( Self { - flag_color: Rgb::new(200, 80, 40), - stone_color: Rgb::new(100, 100, 110), + flag_color: FlagColor::Good, + stone_color: StoneColor::Good, }, skel, ) @@ -81,6 +111,7 @@ impl Archetype for Keep { #[allow(clippy::if_same_then_else)] // TODO: Pending review in #587 fn draw( &self, + index: IndexRef, pos: Vec3, _dist: i32, bound_offset: Vec2, @@ -91,6 +122,11 @@ impl Archetype for Keep { _len: i32, attr: &Self::Attr, ) -> BlockMask { + let dungeon_stone = index.colors.site.dungeon.stone; + let colors = &index.colors.site.settlement.building.archetype.keep; + let flag_color = self.flag_color.elim_case_pure(&colors.flag); + let stone_color = self.stone_color.elim_case_pure(&colors.stone); + let profile = Vec2::new(bound_offset.x, z); let weak_layer = 1; @@ -118,30 +154,35 @@ impl Archetype for Keep { let brick_tex_pos = (pos + Vec3::new(pos.z, pos.z, 0)) / Vec3::new(2, 2, 1); let brick_tex = RandomField::new(0).get(brick_tex_pos) as u8 % 24; - let foundation = make_block(80 + brick_tex, 80 + brick_tex, 80 + brick_tex); + let foundation = make_block( + colors.brick_base.0 + brick_tex, + colors.brick_base.1 + brick_tex, + colors.brick_base.2 + brick_tex, + ); let wall = make_block( - self.stone_color.r + brick_tex, - self.stone_color.g + brick_tex, - self.stone_color.b + brick_tex, + stone_color.0 + brick_tex, + stone_color.1 + brick_tex, + stone_color.2 + brick_tex, ); let window = BlockMask::new( Block::new(BlockKind::Window1, make_meta(ori.flip())), normal_layer, ); let floor = make_block( - 80 + (pos.y.abs() % 2) as u8 * 15, - 60 + (pos.y.abs() % 2) as u8 * 15, - 10 + (pos.y.abs() % 2) as u8 * 15, + colors.floor_base.0 + (pos.y.abs() % 2) as u8 * 15, + colors.floor_base.1 + (pos.y.abs() % 2) as u8 * 15, + colors.floor_base.2 + (pos.y.abs() % 2) as u8 * 15, ) .with_priority(important_layer); - let pole = make_block(90, 70, 50).with_priority(important_layer); - let flag = make_block(self.flag_color.r, self.flag_color.g, self.flag_color.b) - .with_priority(important_layer); + let pole = + make_block(colors.pole.0, colors.pole.1, colors.pole.2).with_priority(important_layer); + let flag = + make_block(flag_color.0, flag_color.1, flag_color.2).with_priority(important_layer); let internal = BlockMask::new(Block::empty(), internal_layer); let empty = BlockMask::nothing(); let make_staircase = move |pos: Vec3, radius: f32, inner_radius: f32, stretch: f32| { - let stone = BlockMask::new(Block::new(BlockKind::Normal, Rgb::new(150, 150, 175)), 5); + let stone = BlockMask::new(Block::new(BlockKind::Normal, dungeon_stone.into()), 5); if (pos.xy().magnitude_squared() as f32) < inner_radius.powf(2.0) { stone diff --git a/world/src/site/settlement/building/archetype/mod.rs b/world/src/site/settlement/building/archetype/mod.rs index d20e943ef2..a8e67d5aff 100644 --- a/world/src/site/settlement/building/archetype/mod.rs +++ b/world/src/site/settlement/building/archetype/mod.rs @@ -2,10 +2,17 @@ pub mod house; pub mod keep; use super::skeleton::*; -use crate::site::BlockMask; +use crate::{site::BlockMask, IndexRef}; use rand::prelude::*; +use serde::{Deserialize, Serialize}; use vek::*; +#[derive(Deserialize, Serialize)] +pub struct Colors { + pub house: house::Colors, + pub keep: keep::Colors, +} + pub trait Archetype { type Attr; @@ -16,6 +23,7 @@ pub trait Archetype { #[allow(clippy::too_many_arguments)] fn draw( &self, + index: IndexRef, pos: Vec3, dist: i32, bound_offset: Vec2, diff --git a/world/src/site/settlement/building/mod.rs b/world/src/site/settlement/building/mod.rs index 0f2e6096cd..d58ee8ad5c 100644 --- a/world/src/site/settlement/building/mod.rs +++ b/world/src/site/settlement/building/mod.rs @@ -7,10 +7,17 @@ pub use self::{ skeleton::*, }; +use crate::IndexRef; use common::terrain::Block; use rand::prelude::*; +use serde::{Deserialize, Serialize}; use vek::*; +#[derive(Deserialize, Serialize)] +pub struct Colors { + pub archetype: archetype::Colors, +} + pub struct Building { skel: Skeleton, archetype: A, @@ -46,13 +53,14 @@ impl Building { } } - pub fn sample(&self, pos: Vec3) -> Option { + pub fn sample(&self, index: IndexRef, pos: Vec3) -> Option { let rpos = pos - self.origin; self.skel .sample_closest( rpos, |pos, dist, bound_offset, center_offset, ori, branch| { self.archetype.draw( + index, pos, dist, bound_offset, diff --git a/world/src/site/settlement/mod.rs b/world/src/site/settlement/mod.rs index 4ead8da0bc..50d22b3c52 100644 --- a/world/src/site/settlement/mod.rs +++ b/world/src/site/settlement/mod.rs @@ -10,6 +10,7 @@ use crate::{ column::ColumnSample, sim::WorldSim, util::{RandomField, Sampler, StructureGen2d}, + IndexRef, }; use common::{ assets, @@ -25,9 +26,30 @@ use common::{ use fxhash::FxHasher64; use hashbrown::{HashMap, HashSet}; use rand::prelude::*; +use serde::{Deserialize, Serialize}; use std::{collections::VecDeque, f32, hash::BuildHasherDefault}; use vek::*; +#[derive(Deserialize, Serialize)] +pub struct Colors { + pub building: building::Colors, + + pub plot_town_path: (u8, u8, u8), + + pub plot_field_dirt: (u8, u8, u8), + pub plot_field_mound: (u8, u8, u8), + + pub wall_low: (u8, u8, u8), + pub wall_high: (u8, u8, u8), + + pub tower_color: (u8, u8, u8), + + pub plot_dirt: (u8, u8, u8), + pub plot_grass: (u8, u8, u8), + pub plot_water: (u8, u8, u8), + pub plot_town: (u8, u8, u8), +} + #[allow(dead_code)] pub fn gradient(line: [Vec2; 2]) -> f32 { let r = (line[0].y - line[1].y) / (line[0].x - line[1].x); @@ -109,10 +131,10 @@ impl Structure { } } - pub fn sample(&self, rpos: Vec3) -> Option { + pub fn sample(&self, index: IndexRef, rpos: Vec3) -> Option { match &self.kind { - StructureKind::House(house) => house.sample(rpos), - StructureKind::Keep(keep) => keep.sample(rpos), + StructureKind::House(house) => house.sample(index, rpos), + StructureKind::Keep(keep) => keep.sample(index, rpos), } } } @@ -527,10 +549,13 @@ impl Settlement { #[allow(clippy::modulo_one)] // TODO: Pending review in #587 pub fn apply_to<'a>( &'a self, + index: IndexRef, wpos2d: Vec2, mut get_column: impl FnMut(Vec2) -> Option<&'a ColumnSample<'a>>, vol: &mut (impl BaseVol + RectSizedVol + ReadVol + WriteVol), ) { + let colors = &index.colors.site.settlement; + for y in 0..vol.size_xy().y as i32 { for x in 0..vol.size_xy().x as i32 { let offs = Vec2::new(x, y); @@ -586,47 +611,6 @@ impl Settlement { } } - // Paths - // if let Some((WayKind::Path, dist, nearest)) = sample.way { - // let inset = -1; - - // // Try to use the column at the centre of the path for sampling to make - // them // flatter - // let col = get_column(offs + (nearest.floor().map(|e| e as i32) - rpos)) - // .unwrap_or(col_sample); - // let (bridge_offset, depth) = if let Some(water_dist) = col.water_dist { - // ( - // ((water_dist.max(0.0) * 0.2).min(f32::consts::PI).cos() + 1.0) * - // 5.0, ((1.0 - ((water_dist + 2.0) * - // 0.3).min(0.0).cos().abs()) - // * (col.riverless_alt + 5.0 - col.alt).max(0.0) - // * 1.75 - // + 3.0) as i32, - // ) - // } else { - // (0.0, 3) - // }; - // let surface_z = (col.riverless_alt + bridge_offset).floor() as i32; - - // for z in inset - depth..inset { - // let _ = vol.set( - // Vec3::new(offs.x, offs.y, surface_z + z), - // if bridge_offset >= 2.0 && dist >= 3.0 || z < inset - 1 { - // Block::new(BlockKind::Normal, noisy_color(Rgb::new(80, 80, - // 100), 8)) } else { - // Block::new(BlockKind::Normal, noisy_color(Rgb::new(80, 50, - // 30), 8)) }, - // ); - // } - // let head_space = (8 - (dist * 0.25).powf(6.0).round() as i32).max(1); - // for z in inset..inset + head_space { - // let pos = Vec3::new(offs.x, offs.y, surface_z + z); - // if vol.get(pos).unwrap().kind() != BlockKind::Water { - // let _ = vol.set(pos, Block::empty()); - // } - // } - // // Ground colour - // } else { let mut surface_block = None; @@ -634,9 +618,9 @@ impl Settlement { |seed, n| self.noise.get(Vec3::new(wpos2d.x, wpos2d.y, seed * 5)) % n; let color = match sample.plot { - 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::Dirt) => Some(colors.plot_dirt.into()), + Some(Plot::Grass) => Some(colors.plot_grass.into()), + Some(Plot::Water) => Some(colors.plot_water.into()), //Some(Plot::Town { district }) => None, Some(Plot::Town { .. }) => { if let Some((_, path_nearest, _, _)) = col_sample.path { @@ -659,13 +643,16 @@ impl Settlement { } } - Some(Rgb::new(100, 95, 65).map2(Rgb::iota(), |e: u8, i: i32| { - e.saturating_add( - (self.noise.get(Vec3::new(wpos2d.x, wpos2d.y, i * 5)) % 1) - as u8, - ) - .saturating_sub(8) - })) + Some(Rgb::from(colors.plot_town_path).map2( + Rgb::iota(), + |e: u8, i: i32| { + e.saturating_add( + (self.noise.get(Vec3::new(wpos2d.x, wpos2d.y, i * 5)) % 1) + as u8, + ) + .saturating_sub(8) + }, + )) }, Some(Plot::Field { seed, crop, .. }) => { let furrow_dirs = [ @@ -677,12 +664,13 @@ impl Settlement { let furrow_dir = furrow_dirs[*seed as usize % furrow_dirs.len()]; let in_furrow = (wpos2d * furrow_dir).sum().rem_euclid(5) < 2; - let dirt = Rgb::new(80, 55, 35).map(|e| { + let dirt = Rgb::::from(colors.plot_field_dirt).map(|e| { e + (self.noise.get(Vec3::broadcast((seed % 4096 + 0) as i32)) % 32) as u8 }); - let mound = - Rgb::new(70, 80, 30).map(|e| e + roll(0, 8) as u8).map(|e| { + let mound = Rgb::::from(colors.plot_field_mound) + .map(|e| e + roll(0, 8) as u8) + .map(|e| { e + (self.noise.get(Vec3::broadcast((seed % 4096 + 1) as i32)) % 32) as u8 }); @@ -769,8 +757,8 @@ impl Settlement { // Walls if let Some((WayKind::Wall, dist, _)) = sample.way { let color = Lerp::lerp( - Rgb::new(130i32, 100, 0), - Rgb::new(90, 70, 50), + Rgb::::from(colors.wall_low).map(i32::from), + Rgb::::from(colors.wall_high).map(i32::from), (RandomField::new(0).get(wpos2d.into()) % 256) as f32 / 256.0, ) .map(|e| (e % 256) as u8); @@ -797,7 +785,7 @@ impl Settlement { for z in -2..16 { let _ = vol.set( Vec3::new(offs.x, offs.y, surface_z + z), - Block::new(BlockKind::Normal, Rgb::new(50, 50, 50)), + Block::new(BlockKind::Normal, colors.tower_color.into()), ); } } @@ -832,7 +820,7 @@ impl Settlement { let wpos = Vec3::from(self.origin) + rpos; let coffs = wpos - Vec3::from(wpos2d); - if let Some(block) = structure.sample(rpos) { + if let Some(block) = structure.sample(index, rpos) { let _ = vol.set(coffs, block); } } @@ -938,30 +926,24 @@ impl Settlement { } } - pub fn get_color(&self, pos: Vec2) -> Option> { + pub fn get_color(&self, index: IndexRef, pos: Vec2) -> Option> { + let colors = &index.colors.site.settlement; + let sample = self.land.get_at_block(pos); - // match sample.tower { - // Some((Tower::Wall, _)) => return Some(Rgb::new(50, 50, 50)), - // _ => {}, - // } - - // match sample.way { - // Some((WayKind::Path, _, _)) => return Some(Rgb::new(90, 70, 50)), - // Some((WayKind::Hedge, _, _)) => return Some(Rgb::new(0, 150, 0)), - // Some((WayKind::Wall, _, _)) => return Some(Rgb::new(60, 60, 60)), - // _ => {}, - // } - match sample.plot { - 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::Dirt) => return Some(colors.plot_dirt.into()), + Some(Plot::Grass) => return Some(colors.plot_grass.into()), + Some(Plot::Water) => return Some(colors.plot_water.into()), 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) + return Some( + Rgb::from(colors.plot_town).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) - })); + }), + ); }, Some(Plot::Field { seed, .. }) => { let furrow_dirs = [ @@ -972,6 +954,12 @@ impl Settlement { ]; let furrow_dir = furrow_dirs[*seed as usize % furrow_dirs.len()]; let furrow = (pos * furrow_dir).sum().rem_euclid(6) < 3; + // NOTE: Very hard to understand how to make this dynamically configurable. The + // base values can easily cause the others to go out of range, and there's some + // weird scaling going on. For now, we just let these remain hardcoded. + // + // FIXME: Rewrite this so that validity is not so heavily dependent on the exact + // color values. return Some(Rgb::new( if furrow { 100 @@ -1003,6 +991,8 @@ pub enum Crop { Sunflower, } +// NOTE: No support for struct variants in make_case_elim yet, unfortunately, so +// we can't use it. #[derive(Copy, Clone, PartialEq)] pub enum Plot { Hazard, diff --git a/world/src/util/mod.rs b/world/src/util/mod.rs index e42abc03a3..de31f42c71 100644 --- a/world/src/util/mod.rs +++ b/world/src/util/mod.rs @@ -1,4 +1,3 @@ -// pub mod boruvka; pub mod fast_noise; pub mod grid; pub mod map_vec;