From 9998e53f663e6d523efda8b579df75b713f2d939 Mon Sep 17 00:00:00 2001 From: Avi Weinstock Date: Tue, 4 May 2021 22:04:50 -0400 Subject: [PATCH] Plant growth simulation, not yet attached to anything. --- assets/common/sprite_behavior_manifest.ron | 17 ++++ common/src/terrain/block.rs | 21 ++++ common/src/terrain/sprite.rs | 106 ++++++++++++++++++++- common/state/src/lib.rs | 2 +- common/state/src/state.rs | 38 ++++---- server/src/sys/terrain.rs | 10 +- 6 files changed, 169 insertions(+), 25 deletions(-) diff --git a/assets/common/sprite_behavior_manifest.ron b/assets/common/sprite_behavior_manifest.ron index b818cc4c2e..733f4a79d4 100644 --- a/assets/common/sprite_behavior_manifest.ron +++ b/assets/common/sprite_behavior_manifest.ron @@ -116,4 +116,21 @@ SpriteBehaviorManifest( Mud: LootTable("common.loot_tables.sprite.mud"), Crate: LootTable("common.loot_tables.sprite.crate"), }, + growth_specs: { + RedFlower: GrowthSpec(start_growth: 0, max_growth: 16, days_per_growth: 0.0625), + Sunflower: GrowthSpec(start_growth: 0, max_growth: 16, days_per_growth: 0.0625), + Tomato: GrowthSpec(start_growth: 0, max_growth: 16, days_per_growth: 0.0625), + Cabbage: GrowthSpec(start_growth: 0, max_growth: 16, days_per_growth: 0.0625), + Carrot: GrowthSpec(start_growth: 0, max_growth: 16, days_per_growth: 0.0625), + Corn: GrowthSpec(start_growth: 0, max_growth: 16, days_per_growth: 0.0625), + Garlic: GrowthSpec(start_growth: 0, max_growth: 16, days_per_growth: 0.0625), + Onion: GrowthSpec(start_growth: 0, max_growth: 16, days_per_growth: 0.0625), + Turnip: GrowthSpec(start_growth: 0, max_growth: 16, days_per_growth: 0.0625), + Radish: GrowthSpec(start_growth: 0, max_growth: 16, days_per_growth: 0.0625), + Blueberry: GrowthSpec(start_growth: 0, max_growth: 16, days_per_growth: 0.0625), + Pumpkin: GrowthSpec(start_growth: 0, max_growth: 16, days_per_growth: 0.0625), + WheatYellow: GrowthSpec(start_growth: 0, max_growth: 16, days_per_growth: 0.0625), + WheatGreen: GrowthSpec(start_growth: 0, max_growth: 16, days_per_growth: 0.0625), + Flax: GrowthSpec(start_growth: 0, max_growth: 16, days_per_growth: 0.0625), + }, ) diff --git a/common/src/terrain/block.rs b/common/src/terrain/block.rs index 6c6d370c3f..55aab54009 100644 --- a/common/src/terrain/block.rs +++ b/common/src/terrain/block.rs @@ -166,6 +166,15 @@ impl Block { } } + #[inline] + pub fn get_growth(&self) -> Option { + if self.get_sprite()?.get_growth_spec().is_some() { + Some((self.attr[1] >> 3) & ((1 << 5) - 1)) + } else { + None + } + } + #[inline] pub fn get_glow(&self) -> Option { match self.get_sprite()? { @@ -270,6 +279,18 @@ impl Block { self } + /// If this block is a growable sprite, set its growth + #[inline] + pub fn with_sprite_growth(mut self, growth: u8) -> Self { + if self + .get_sprite() + .map_or(false, |s| s.get_growth_spec().is_some()) + { + self.attr[1] |= (growth & ((1 << 5) - 1)) << 3; + } + self + } + /// If this block can have orientation, give it a new orientation. #[inline] pub fn with_ori(mut self, ori: u8) -> Option { diff --git a/common/src/terrain/sprite.rs b/common/src/terrain/sprite.rs index 47a72f9a14..306a34612f 100644 --- a/common/src/terrain/sprite.rs +++ b/common/src/terrain/sprite.rs @@ -3,6 +3,8 @@ use crate::{ comp::tool::ToolKind, lottery::LootSpec, make_case_elim, + terrain::TerrainChunk, + vol::{IntoVolIterator, ReadVol, RectRasterableVol, WriteVol}, }; use enum_iterator::IntoEnumIterator; use hashbrown::HashMap; @@ -10,6 +12,7 @@ use lazy_static::lazy_static; use num_derive::FromPrimitive; use serde::{Deserialize, Serialize}; use std::{convert::TryFrom, fmt}; +use vek::Vec3; make_case_elim!( sprite_kind, @@ -162,6 +165,7 @@ make_case_elim!( pub struct SpriteBehaviorManifest { pub solid_height: HashMap, pub collectible_id: HashMap, + pub growth_specs: HashMap, } impl Asset for SpriteBehaviorManifest { @@ -175,6 +179,13 @@ lazy_static! { AssetExt::load_expect("common.sprite_behavior_manifest"); } +#[derive(Copy, Clone, Debug, Deserialize)] +pub struct GrowthSpec { + pub start_growth: u8, + pub max_growth: u8, + pub days_per_growth: f32, +} + impl SpriteKind { pub fn solid_height(&self) -> Option { SPRITE_BEHAVIOR_MANIFEST @@ -190,7 +201,15 @@ impl SpriteKind { .collectible_id .get(self) .is_some() - && !self.mine_tool().is_some() + && self.mine_tool().is_none() + } + + pub fn get_growth_spec(&self) -> Option { + SPRITE_BEHAVIOR_MANIFEST + .read() + .growth_specs + .get(self) + .copied() } /// Is the sprite a container that will emit a mystery item? @@ -286,3 +305,88 @@ impl<'a> TryFrom<&'a str> for SpriteKind { fn try_from(s: &'a str) -> Result { SPRITE_KINDS.get(s).copied().ok_or(()) } } + +/// Per-chunk index of plant data, for efficiently growing plants, and +/// persisting plant growth when the chunk is unloaded with low memory footprint +pub struct PlantGrowthData { + data: HashMap, +} + +struct PlantGrowthPerKind { + // TODO: if we made use of the assumption that chunks are 32x32xk voxels, we could pack + // positions into 10+log_2(k) bits instead of using the whole 12 bytes that a Vec3 uses + positions: Vec>, + growth_amounts: Vec, + last_growth_tick: f32, +} + +impl PlantGrowthPerKind { + fn new(time: f32) -> Self { + Self { + positions: Vec::new(), + growth_amounts: Vec::new(), + last_growth_tick: time, + } + } + + fn calculate_growth(&mut self, sprite: SpriteKind, time: f32) { + if let Some(growth_spec) = sprite.get_growth_spec() { + let dt = self.last_growth_tick - time; + let dt_days = dt / (60.0 * 60.0 * 24.0); + if dt_days > growth_spec.days_per_growth { + self.last_growth_tick += dt; + for growth in self.growth_amounts.iter_mut() { + *growth += (dt_days / growth_spec.days_per_growth) as u8; + *growth = (*growth).min(growth_spec.max_growth); + } + } + } + } +} + +impl PlantGrowthData { + pub fn from_fresh_chunk(chunk: &TerrainChunk, time: f32) -> Self { + let mut ret = Self { + data: HashMap::new(), + }; + for (pos, block) in chunk.vol_iter( + Vec3::new(0, 0, chunk.get_min_z()), + ::RECT_SIZE + .as_() + .with_z(chunk.get_max_z()), + ) { + if let Some(sprite) = block.get_sprite() { + if let Some(growth_spec) = sprite.get_growth_spec() { + let entry = ret + .data + .entry(sprite) + .or_insert_with(|| PlantGrowthPerKind::new(time)); + entry.positions.push(pos); + entry.growth_amounts.push(growth_spec.start_growth); + } + } + } + ret + } + + pub fn calculate_growth(&mut self, time: f32) { + for (sprite, per_kind_growth) in self.data.iter_mut() { + per_kind_growth.calculate_growth(*sprite, time); + } + } + + pub fn overwrite_plants(&self, chunk: &mut TerrainChunk) { + for (sprite, per_kind_growth) in self.data.iter() { + for (pos, growth) in per_kind_growth + .positions + .iter() + .zip(per_kind_growth.growth_amounts.iter()) + { + if let Ok(block) = chunk.get(*pos) { + let block = *block; + let _ = chunk.set(*pos, block.with_sprite(*sprite).with_sprite_growth(*growth)); + } + } + } + } +} diff --git a/common/state/src/lib.rs b/common/state/src/lib.rs index d0047fe6de..9f4ca9eac9 100644 --- a/common/state/src/lib.rs +++ b/common/state/src/lib.rs @@ -6,4 +6,4 @@ mod build_areas; mod state; // TODO: breakup state module and remove glob pub use build_areas::{BuildAreaError, BuildAreas}; -pub use state::{BlockChange, State, TerrainChanges}; +pub use state::{insert_terrain_chunk, BlockChange, State, TerrainChanges}; diff --git a/common/state/src/state.rs b/common/state/src/state.rs index 9031c57b55..8f92ab0b56 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -78,6 +78,21 @@ impl TerrainChanges { } } +pub fn insert_terrain_chunk<'a>( + terrain_grid: &mut specs::WriteExpect<'a, TerrainGrid>, + terrain_changes: &mut specs::Write<'a, TerrainChanges>, + key: Vec2, + chunk: Arc, + new_hook: impl FnOnce(), +) { + if terrain_grid.insert(key, chunk).is_some() { + terrain_changes.modified_chunks.insert(key); + } else { + terrain_changes.new_chunks.insert(key); + new_hook(); + } +} + /// A type used to represent game state stored on both the client and the /// server. This includes things like entity components, terrain data, and /// global states like weather, time of day, etc. @@ -384,22 +399,13 @@ impl State { /// Insert the provided chunk into this state's terrain. pub fn insert_chunk(&mut self, key: Vec2, chunk: Arc) { - if self - .ecs - .write_resource::() - .insert(key, chunk) - .is_some() - { - self.ecs - .write_resource::() - .modified_chunks - .insert(key); - } else { - self.ecs - .write_resource::() - .new_chunks - .insert(key); - } + insert_terrain_chunk( + &mut self.ecs.write_resource::().into(), + &mut self.ecs.write_resource::().into(), + key, + chunk, + || (), + ); } /// Remove the chunk with the given key from this state's terrain, if it diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index b352de9438..e99095d562 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -15,7 +15,7 @@ use common::{ }; use common_ecs::{Job, Origin, Phase, System}; use common_net::msg::{SerializedTerrainChunk, ServerGeneral}; -use common_state::TerrainChanges; +use common_state::{insert_terrain_chunk, TerrainChanges}; use comp::Behavior; use specs::{Join, Read, ReadExpect, ReadStorage, Write, WriteExpect}; use std::sync::Arc; @@ -150,14 +150,10 @@ impl<'a> System<'a> for Sys { // Add to list of chunks to send to nearby players. new_chunks.push((key, Arc::clone(&chunk))); - // TODO: code duplication for chunk insertion between here and state.rs // Insert the chunk into terrain changes - if terrain.insert(key, chunk).is_some() { - terrain_changes.modified_chunks.insert(key); - } else { - terrain_changes.new_chunks.insert(key); + insert_terrain_chunk(&mut terrain, &mut terrain_changes, key, chunk, || { rtsim.hook_load_chunk(key); - } + }); // Handle chunk supplement for entity in supplement.entities {