From 9e5978b34b1825475666698e95283734f0cbe47b Mon Sep 17 00:00:00 2001
From: Joshua Barretto <joshua.s.barretto@gmail.com>
Date: Wed, 17 Jun 2020 19:05:47 +0100
Subject: [PATCH] Added forts to towns, began better economy sim

---
 common/src/store.rs                           |  17 +-
 server-cli/.gitignore                         |   2 +-
 server-cli/.~lock.economy.csv#                |   1 +
 server/src/cmd.rs                             |   2 +-
 server/src/lib.rs                             |   7 +-
 world/src/block/mod.rs                        |  26 +-
 world/src/block/natural.rs                    |   4 +-
 world/src/civ/mod.rs                          | 126 ++-------
 world/src/column/mod.rs                       |  13 +-
 world/src/index.rs                            |   8 +
 world/src/lib.rs                              |  39 ++-
 world/src/sim/map.rs                          |   6 +-
 world/src/sim/mod.rs                          |   8 +-
 world/src/sim2/mod.rs                         | 250 ++++++++++++++++++
 world/src/site/block_mask.rs                  |  39 +++
 world/src/site/dungeon/mod.rs                 |   4 +-
 world/src/site/economy.rs                     | 145 ++++++++++
 world/src/site/mod.rs                         | 117 +++-----
 .../settlement/building/archetype/house.rs    |  13 +-
 .../settlement/building/archetype/keep.rs     |  80 ++++--
 world/src/site/settlement/building/mod.rs     |   3 +-
 world/src/site/settlement/mod.rs              | 150 +++++++----
 world/src/site/settlement/town.rs             |  82 ++++++
 world/src/util/map_vec.rs                     |  79 ++++++
 world/src/util/mod.rs                         |   9 +
 25 files changed, 929 insertions(+), 301 deletions(-)
 create mode 100644 server-cli/.~lock.economy.csv#
 create mode 100644 world/src/index.rs
 create mode 100644 world/src/sim2/mod.rs
 create mode 100644 world/src/site/block_mask.rs
 create mode 100644 world/src/site/economy.rs
 create mode 100644 world/src/site/settlement/town.rs
 create mode 100644 world/src/util/map_vec.rs

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<T> Store<T> {
     }
 
     pub fn ids(&self) -> impl Iterator<Item = Id<T>> {
-        // 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<Item = &T> { self.items.iter() }
+    pub fn values(&self) -> impl Iterator<Item = &T> { self.items.iter() }
 
-    pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut T> { self.items.iter_mut() }
+    pub fn values_mut(&mut self) -> impl Iterator<Item = &mut T> { self.items.iter_mut() }
 
-    pub fn iter_ids(&self) -> impl Iterator<Item = (Id<T>, &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<Item = (Id<T>, &T)> { self.ids().zip(self.values()) }
+
+    pub fn iter_mut(&mut self) -> impl Iterator<Item = (Id<T>, &mut T)> {
+        self.ids().zip(self.values_mut())
     }
 
     pub fn insert(&mut self, item: T) -> Id<T> {
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<Option<ColumnSample<'a>>>,
         wpos: Vec2<i32>,
+        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<i32>, 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<i32>) -> Option<ZCache<'a>> {
+    pub fn get_z_cache(&mut self, wpos: Vec2<i32>, index: &'a Index) -> Option<ZCache<'a>> {
         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<i32>,
         z_cache: Option<&ZCache>,
         only_structures: bool,
+        index: &Index,
     ) -> Option<Block> {
         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<i32>,
     st_seed: u32,
     st_sample: &ColumnSample,
+    index: &'a Index,
 ) -> Option<StructureInfo> {
     // 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>) -> &Place { self.places.get(id) }
 
-    pub fn sites(&self) -> impl Iterator<Item = &Site> + '_ { self.sites.iter() }
+    pub fn sites(&self) -> impl Iterator<Item = &Site> + '_ { 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: {}", "<unnamed>");
             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::<Vec<_>>();
@@ -440,7 +440,7 @@ impl Civs {
     }
 
     fn tick(&mut self, _ctx: &mut GenCtx<impl Rng>, 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<f32>,
     export_targets: Stocks<f32>,
-    trade_states: Stocks<TradeState>,
+    //trade_states: Stocks<TradeState>,
     coin: f32,
 }
 
@@ -996,79 +996,3 @@ impl Default for TradeState {
 }
 
 pub type Stocks<T> = MapVec<Stock, T>;
-
-#[derive(Clone, Debug)]
-pub struct MapVec<K, T> {
-    /// 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<K, T, BuildHasherDefault<FxHasher32>>,
-    default: T,
-}
-
-/// Need manual implementation of Default since K doesn't need that bound.
-impl<K, T: Default> Default for MapVec<K, T> {
-    fn default() -> Self {
-        Self {
-            entries: Default::default(),
-            default: Default::default(),
-        }
-    }
-}
-
-impl<K: Copy + Eq + Hash, T: Clone> MapVec<K, T> {
-    pub fn from_list<'a>(i: impl IntoIterator<Item = &'a (K, T)>, 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<U: Default>(self, mut f: impl FnMut(K, T) -> U) -> MapVec<K, U> {
-        MapVec {
-            entries: self
-                .entries
-                .into_iter()
-                .map(|(s, v)| (s, f(s, v)))
-                .collect(),
-            default: U::default(),
-        }
-    }
-
-    pub fn iter(&self) -> impl Iterator<Item = (K, &T)> + '_ {
-        self.entries.iter().map(|(s, v)| (*s, v))
-    }
-
-    pub fn iter_mut(&mut self) -> impl Iterator<Item = (K, &mut T)> + '_ {
-        self.entries.iter_mut().map(|(s, v)| (*s, v))
-    }
-}
-
-impl<K: Copy + Eq + Hash, T: Clone> std::ops::Index<K> for MapVec<K, T> {
-    type Output = T;
-
-    fn index(&self, entry: K) -> &Self::Output { self.get(entry) }
-}
-
-impl<K: Copy + Eq + Hash, T: Clone> std::ops::IndexMut<K> for MapVec<K, T> {
-    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<i32>;
+impl<'a, 'b> Sampler<'b> for ColumnGen<'a>
+where
+    'a: 'b,
+{
+    type Index = (Vec2<i32>, &'b Index);
     type Sample = Option<ColumnSample<'a>>;
 
     #[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<i32>) -> Option<ColumnSample<'a>> {
+    fn get(&self, (wpos, index): Self::Index) -> Option<ColumnSample<'a>> {
         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<Site>,
+}
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<u32> { self.sim.get_map(&self.index) }
+
     pub fn sample_columns(
         &self,
-    ) -> impl Sampler<Index = Vec2<i32>, Sample = Option<ColumnSample>> + '_ {
+    ) -> impl Sampler<Index = (Vec2<i32>, &Index), Sample = Option<ColumnSample>> + '_ {
         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<usize>, (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<u32> {
+    pub fn get_map(&self, index: &Index) -> Vec<u32> {
         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<Site>,
+    pub sites: Vec<Id<Site>>,
     pub place: Option<Id<Place>>,
     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<Site>, 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::<f32>()
+        .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::<f32>().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<Block> {
+        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::<Vec<_>>();
+        let room_areas = this.rooms.values().map(|r| r.area).collect::<Vec<_>>();
         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<Good, f32>,
+    pub surplus: MapVec<Good, f32>,
+    pub marginal_surplus: MapVec<Good, f32>,
+    pub values: MapVec<Good, Option<f32>>,
+    pub prices: MapVec<Good, f32>,
+
+    pub labors: MapVec<Labor, f32>,
+    pub yields: MapVec<Labor, f32>,
+    pub productivity: MapVec<Labor, f32>,
+}
+
+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<Option<Labor>, 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<Labor, (Good, f32)> {
+        // 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<Block> {
-        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<Settlement>),
-    Dungeon(Arc<Dungeon>),
+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<i32> {
-        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<i32>) -> 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<i32>) -> Option<&'a ColumnSample<'a>>,
         vol: &mut (impl BaseVol<Vox = Block> + 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<i32>) -> 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<Settlement> for Site {
-    fn from(settlement: Settlement) -> Self { Site::Settlement(Arc::new(settlement)) }
-}
-
-impl From<Dungeon> 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<u8>; 11] = [
+const COLOR_THEMES: [Rgb<u8>; 17] = [
     Rgb::new(0x1D, 0x4D, 0x45),
     Rgb::new(0xB3, 0x7D, 0x60),
     Rgb::new(0xAC, 0x5D, 0x26),
@@ -24,6 +24,12 @@ const COLOR_THEMES: [Rgb<u8>; 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<R: Rng>(rng: &mut R) -> (Self, Skeleton<Self::Attr>) {
-        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<i32>,
         dist: i32,
         bound_offset: Vec2<i32>,
-        _center_offset: Vec2<i32>,
+        center_offset: Vec2<i32>,
         z: i32,
         ori: Ori,
         branch: &Branch<Self::Attr>,
@@ -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<archetype::house::House>;
 pub type KeepBuilding = Building<archetype::keep::Keep>;
 
-
 pub struct Building<A: Archetype> {
     skel: Skeleton<A::Attr>,
     archetype: A,
@@ -44,7 +43,7 @@ impl<A: Archetype> Building<A> {
         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<i32> {
         match &self.kind {
             StructureKind::House(house) => house.bounds(),
             StructureKind::Keep(keep) => keep.bounds(),
-
         }
     }
-    
+
     pub fn sample(&self, rpos: Vec3<i32>) -> Option<Block> {
         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<i32>,
-}
-
 pub struct Farm {
     #[allow(dead_code)]
     base_tile: Vec2<i32>,
@@ -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::<i32>::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<u8>, factor: u32| {
+                let noisy_color = move |col: Rgb<u8>, 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<Id<District>>,
+    },
     Field {
         farm: Id<Farm>,
         seed: u32,
@@ -1015,6 +1054,8 @@ pub struct Sample<'a> {
     plot: Option<&'a Plot>,
     way: Option<(&'a WayKind, f32, Vec2<f32>)>,
     tower: Option<(&'a Tower, Vec2<i32>)>,
+    edge_dist: f32,
+    second_closest: Vec2<i32>,
 }
 
 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<i32>,
+    radius: i32,
+    districts: Store<District>,
+}
+
+impl Town {
+    pub fn districts(&self) -> &Store<District> { &self.districts }
+
+    pub fn generate(origin: Vec2<i32>, base_tile: Vec2<i32>, ctx: &mut GenCtx<impl Rng>) -> 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<i32>, ctx: &mut GenCtx<impl Rng>) {
+        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<i32>,
+    pub alt: i32,
+}
+
+enum Plot {
+    District,
+    Parent(Vec<(Aabr<i32>, Plot)>),
+}
+
+impl Plot {
+    fn for_each(&self, aabr: Aabr<i32>, f: &mut impl FnMut(Aabr<i32>)) {
+        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<i32>, ctx: &mut GenCtx<impl Rng>) -> 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<K, T> {
+    /// 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<K, T>,
+    default: T,
+}
+
+/// Need manual implementation of Default since K doesn't need that bound.
+impl<K, T: Default> Default for MapVec<K, T> {
+    fn default() -> Self {
+        Self {
+            entries: Default::default(),
+            default: Default::default(),
+        }
+    }
+}
+
+impl<K: Copy + Eq + Hash, T: Clone> MapVec<K, T> {
+    pub fn from_list<'a>(i: impl IntoIterator<Item = &'a (K, T)>, 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<U: Default>(self, mut f: impl FnMut(K, T) -> U) -> MapVec<K, U> {
+        MapVec {
+            entries: self
+                .entries
+                .into_iter()
+                .map(|(s, v)| (s.clone(), f(s, v)))
+                .collect(),
+            default: U::default(),
+        }
+    }
+
+    pub fn iter(&self) -> impl Iterator<Item = (K, &T)> + '_ {
+        self.entries.iter().map(|(s, v)| (*s, v))
+    }
+
+    pub fn iter_mut(&mut self) -> impl Iterator<Item = (K, &mut T)> + '_ {
+        self.entries.iter_mut().map(|(s, v)| (*s, v))
+    }
+}
+
+impl<K: Copy + Eq + Hash, T: Clone> std::ops::Index<K> for MapVec<K, T> {
+    type Output = T;
+
+    fn index(&self, entry: K) -> &Self::Output { self.get(entry) }
+}
+
+impl<K: Copy + Eq + Hash, T: Clone> std::ops::IndexMut<K> for MapVec<K, T> {
+    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<K, V> = HashMap<K, V, BuildHasherDefault<FxHasher32>>;
+pub type DHashSet<T> = HashSet<T, BuildHasherDefault<FxHasher32>>;
+
 pub fn attempt<T>(max_iters: usize, mut f: impl FnMut() -> Option<T>) -> Option<T> {
     (0..max_iters).find_map(|_| f())
 }