#![allow(incomplete_features)]
#![allow(
clippy::option_map_unit_fn,
clippy::blocks_in_conditions,
clippy::identity_op,
clippy::needless_pass_by_ref_mut )]
#![allow(clippy::branches_sharing_code)] #![deny(clippy::clone_on_ref_ptr)]
#![feature(option_zip, let_chains)]
#![cfg_attr(feature = "simd", feature(portable_simd))]
mod all;
mod block;
pub mod canvas;
pub mod civ;
mod column;
pub mod config;
pub mod index;
pub mod land;
pub mod layer;
pub mod pathfinding;
pub mod sim;
pub mod sim2;
pub mod site;
pub mod site2;
pub mod util;
pub use crate::{
canvas::{Canvas, CanvasInfo},
config::{Features, CONFIG},
land::Land,
layer::PathLocals,
};
pub use block::BlockGen;
use civ::WorldCivStage;
pub use column::ColumnSample;
pub use common::terrain::site::{DungeonKindMeta, SettlementKindMeta};
use common::{spiral::Spiral2d, terrain::CoordinateConversions};
pub use index::{IndexOwned, IndexRef};
use sim::WorldSimStage;
use crate::{
column::ColumnGen,
index::Index,
layer::spot::Spot,
site::{SiteKind, SpawnRules},
util::{Grid, Sampler},
};
use common::{
assets,
calendar::Calendar,
generation::{ChunkSupplement, EntityInfo, SpecialEntity},
lod,
resources::TimeOfDay,
rtsim::ChunkResource,
terrain::{
Block, BlockKind, SpriteKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize, TerrainGrid,
},
vol::{ReadVol, RectVolSize, WriteVol},
};
use common_base::prof_span;
use common_net::msg::{world_msg, WorldMapMsg};
use enum_map::EnumMap;
use rand::{prelude::*, Rng};
use rand_chacha::ChaCha8Rng;
use serde::Deserialize;
use std::time::Duration;
use vek::*;
#[cfg(all(feature = "be-dyn-lib", feature = "use-dyn-lib"))]
compile_error!("Can't use both \"be-dyn-lib\" and \"use-dyn-lib\" features at once");
#[cfg(feature = "use-dyn-lib")]
use {common_dynlib::LoadedLib, lazy_static::lazy_static, std::sync::Arc, std::sync::Mutex};
#[cfg(feature = "use-dyn-lib")]
lazy_static! {
pub static ref LIB: Arc<Mutex<Option<LoadedLib>>> =
common_dynlib::init("veloren-world", "world");
}
#[cfg(feature = "use-dyn-lib")]
pub fn init() { lazy_static::initialize(&LIB); }
#[derive(Debug)]
pub enum Error {
Other(String),
}
#[derive(Debug)]
pub enum WorldGenerateStage {
WorldSimGenerate(WorldSimStage),
WorldCivGenerate(WorldCivStage),
EconomySimulation,
SpotGeneration,
}
pub struct World {
sim: sim::WorldSim,
civs: civ::Civs,
}
#[derive(Deserialize)]
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 assets::Asset for Colors {
type Loader = assets::RonLoader;
const EXTENSION: &'static str = "ron";
}
impl World {
pub fn generate(
seed: u32,
opts: sim::WorldOpts,
threadpool: &rayon::ThreadPool,
report_stage: &(dyn Fn(WorldGenerateStage) + Send + Sync),
) -> (Self, IndexOwned) {
prof_span!("World::generate");
threadpool.install(|| {
let mut index = Index::new(seed);
let calendar = opts.calendar.clone();
let mut sim = sim::WorldSim::generate(seed, opts, threadpool, &|stage| {
report_stage(WorldGenerateStage::WorldSimGenerate(stage))
});
let civs =
civ::Civs::generate(seed, &mut sim, &mut index, calendar.as_ref(), &|stage| {
report_stage(WorldGenerateStage::WorldCivGenerate(stage))
});
report_stage(WorldGenerateStage::EconomySimulation);
sim2::simulate(&mut index, &mut sim);
report_stage(WorldGenerateStage::SpotGeneration);
Spot::generate(&mut sim);
(Self { sim, civs }, IndexOwned::new(index))
})
}
pub fn sim(&self) -> &sim::WorldSim { &self.sim }
pub fn civs(&self) -> &civ::Civs { &self.civs }
pub fn tick(&self, _dt: Duration) {
}
pub fn get_map_data(&self, index: IndexRef, threadpool: &rayon::ThreadPool) -> WorldMapMsg {
prof_span!("World::get_map_data");
threadpool.install(|| {
let num_sites = self.civs().sites().count() as u64;
let num_caves = self.civs().caves.values().count() as u64;
WorldMapMsg {
pois: self.civs().pois.iter().map(|(_, poi)| {
world_msg::PoiInfo {
name: poi.name.clone(),
kind: match &poi.kind {
civ::PoiKind::Peak(alt) => world_msg::PoiKind::Peak(*alt),
civ::PoiKind::Biome(size) => world_msg::PoiKind::Lake(*size),
},
wpos: poi.loc * TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
}
}).collect(),
sites: self
.civs()
.sites
.iter()
.filter(|(_, site)| !matches!(&site.kind,
civ::SiteKind::PirateHideout
| civ::SiteKind::JungleRuin
| civ::SiteKind::RockCircle
| civ::SiteKind::TrollCave
| civ::SiteKind::Camp))
.map(|(_, site)| {
world_msg::SiteInfo {
id: site.site_tmp.map(|i| i.id()).unwrap_or_default(),
name: site.site_tmp.map(|id| index.sites[id].name().to_string()),
kind: match &site.kind {
civ::SiteKind::Settlement
| civ::SiteKind::Refactor
| civ::SiteKind::CliffTown
| civ::SiteKind::SavannahTown
| civ::SiteKind::CoastalTown
| civ::SiteKind::DesertCity
| civ::SiteKind::PirateHideout
| civ::SiteKind::JungleRuin
| civ::SiteKind::RockCircle
| civ::SiteKind::TrollCave
| civ::SiteKind::Camp => world_msg::SiteKind::Town,
civ::SiteKind::Castle => world_msg::SiteKind::Castle,
civ::SiteKind::Tree | civ::SiteKind::GiantTree => world_msg::SiteKind::Tree,
civ::SiteKind::Gnarling => world_msg::SiteKind::Gnarling,
civ::SiteKind::DwarvenMine => world_msg::SiteKind::DwarvenMine,
civ::SiteKind::ChapelSite => world_msg::SiteKind::ChapelSite,
civ::SiteKind::Terracotta => world_msg::SiteKind::Terracotta,
civ::SiteKind::Citadel => world_msg::SiteKind::Castle,
civ::SiteKind::Bridge(_, _) => world_msg::SiteKind::Bridge,
civ::SiteKind::GliderCourse => world_msg::SiteKind::GliderCourse,
civ::SiteKind::Cultist => world_msg::SiteKind::Cultist,
civ::SiteKind::Sahagin => world_msg::SiteKind::Sahagin,
civ::SiteKind::Myrmidon => world_msg::SiteKind::Myrmidon,
civ::SiteKind::Adlet => world_msg::SiteKind::Adlet,
civ::SiteKind::Haniwa => world_msg::SiteKind::Haniwa,
civ::SiteKind::VampireCastle => world_msg::SiteKind::VampireCastle,
},
wpos: site.center * TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
}
})
.chain(
self.civs()
.caves
.iter()
.flat_map(|(id, info)| {
std::iter::once((id.id() + num_sites, info.name.clone(), info.location.0))
.chain(std::iter::once((id.id() + num_sites + num_caves, info.name.clone(), info.location.1)))
}) .map(|(id, name, pos)| world_msg::SiteInfo {
id,
name: Some(name),
kind: world_msg::SiteKind::Cave,
wpos: pos,
}),
)
.chain(layer::cave::surface_entrances(&Land::from_sim(self.sim()))
.enumerate()
.map(|(i, wpos)| world_msg::SiteInfo {
id: 65536 + i as u64, name: None,
kind: world_msg::SiteKind::Cave,
wpos,
}))
.collect(),
possible_starting_sites: {
const STARTING_SITE_COUNT: usize = 5;
let mut candidates = self
.civs()
.sites
.iter()
.filter_map(|(_, civ_site)| Some((civ_site, civ_site.site_tmp?)))
.map(|(civ_site, site_id)| {
let (site2, mut score) = match &index.sites[site_id].kind {
SiteKind::Refactor(site2) => (site2, 2.0),
_ => return (site_id.id(), 0.0)
};
const OPTIMAL_STARTER_TOWN_SIZE: f32 = 30.0;
let plots = site2.plots().len() as f32;
let size_score = if plots > OPTIMAL_STARTER_TOWN_SIZE {
1.0 + (1.0 / (1.0 + ((plots - OPTIMAL_STARTER_TOWN_SIZE) / 15.0).powi(3)))
} else {
(2.05 / (1.0 + ((OPTIMAL_STARTER_TOWN_SIZE - plots) / 15.0).powi(5))) - 0.05
}.max(0.01);
score *= size_score;
let pos_score = (
10.0 / (
1.0 + (
civ_site.center.map2(self.sim().get_size(),
|e, sz|
(e as f32 / sz as f32 - 0.5).abs() * 2.0).reduce_partial_max()
).powi(6) * 25.0
)
).max(0.02);
score *= pos_score;
let mut chunk_scores = 2.0;
for (chunk, distance) in Spiral2d::with_radius(10)
.filter_map(|rel_pos| {
let chunk_pos = civ_site.center + rel_pos * 2;
self.sim().get(chunk_pos).zip(Some(rel_pos.as_::<f32>().magnitude()))
})
{
let weight = 1.0 / (distance * std::f32::consts::TAU + 1.0);
let chunk_difficulty = 20.0 / (20.0 + chunk.get_biome().difficulty().pow(4) as f32 / 5.0);
chunk_scores *= 1.0 - weight + chunk_difficulty * weight;
}
score *= chunk_scores;
(site_id.id(), score)
})
.collect::<Vec<_>>();
candidates.sort_by_key(|(_, score)| -(*score * 1000.0) as i32);
candidates.into_iter().map(|(site_id, _)| site_id).take(STARTING_SITE_COUNT).collect()
},
..self.sim.get_map(index, self.sim().calendar.as_ref())
}
})
}
pub fn sample_columns(
&self,
) -> impl Sampler<
Index = (Vec2<i32>, IndexRef, Option<&'_ Calendar>),
Sample = Option<ColumnSample>,
> + '_ {
ColumnGen::new(&self.sim)
}
pub fn sample_blocks(&self) -> BlockGen { BlockGen::new(ColumnGen::new(&self.sim)) }
pub fn find_accessible_pos(
&self,
index: IndexRef,
spawn_wpos: Vec2<i32>,
ascending: bool,
) -> Vec3<f32> {
let chunk_pos = TerrainGrid::chunk_key(spawn_wpos);
let (tc, _cs) = self
.generate_chunk(index, chunk_pos, None, || false, None)
.unwrap();
tc.find_accessible_pos(spawn_wpos, ascending)
}
#[allow(clippy::result_unit_err)]
pub fn generate_chunk(
&self,
index: IndexRef,
chunk_pos: Vec2<i32>,
rtsim_resources: Option<EnumMap<ChunkResource, f32>>,
mut should_continue: impl FnMut() -> bool,
time: Option<(TimeOfDay, Calendar)>,
) -> Result<(TerrainChunk, ChunkSupplement), ()> {
let calendar = time.as_ref().map(|(_, cal)| cal);
let mut sampler = self.sample_blocks();
let chunk_wpos2d = chunk_pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32);
let chunk_center_wpos2d = chunk_wpos2d + TerrainChunkSize::RECT_SIZE.map(|e| e as i32 / 2);
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, index, calendar),
);
let air = Block::air(SpriteKind::Empty);
let stone = Block::new(
BlockKind::Rock,
zcache_grid
.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_else(|| index.colors.deep_stone_color.into()),
);
let (base_z, sim_chunk) = match self
.sim
.get_base_z(chunk_pos)
{
Some(base_z) => (base_z as i32, self.sim.get(chunk_pos).unwrap()),
None => {
return Ok((self.sim().generate_oob_chunk(), ChunkSupplement::default()));
},
};
let meta = TerrainChunkMeta::new(
sim_chunk.get_location_name(&index.sites, &self.civs.pois, chunk_center_wpos2d),
sim_chunk.get_biome(),
sim_chunk.alt,
sim_chunk.tree_density,
sim_chunk.cave.1.alt != 0.0,
sim_chunk.river.is_river(),
sim_chunk.river.near_water(),
sim_chunk.river.velocity,
sim_chunk.temp,
sim_chunk.humidity,
sim_chunk
.sites
.iter()
.filter(|id| {
index.sites[**id]
.get_origin()
.distance_squared(chunk_center_wpos2d) as f32
<= index.sites[**id].radius().powi(2)
})
.min_by_key(|id| {
index.sites[**id]
.get_origin()
.distance_squared(chunk_center_wpos2d)
})
.map(|id| index.sites[*id].kind.convert_to_meta().unwrap_or_default()),
self.sim.approx_chunk_terrain_normal(chunk_pos),
sim_chunk.rockiness,
sim_chunk.cliff_height,
);
let mut chunk = TerrainChunk::new(base_z, stone, air, meta);
for y in 0..TerrainChunkSize::RECT_SIZE.y as i32 {
for x in 0..TerrainChunkSize::RECT_SIZE.x as i32 {
if should_continue() {
return Err(());
};
let offs = Vec2::new(x, y);
let z_cache = match zcache_grid.get(grid_border + offs) {
Some(Some(z_cache)) => z_cache,
_ => continue,
};
let (min_z, max_z) = z_cache.get_z_limits();
(base_z..min_z as i32).for_each(|z| {
let _ = chunk.set(Vec3::new(x, y, z), stone);
});
(min_z as i32..max_z as i32).for_each(|z| {
let lpos = Vec3::new(x, y, z);
let wpos = Vec3::from(chunk_wpos2d) + lpos;
if let Some(block) = sampler.get_with_z_cache(wpos, Some(z_cache)) {
let _ = chunk.set(lpos, block);
}
});
}
}
let sample_get = |offs| {
zcache_grid
.get(grid_border + offs)
.and_then(Option::as_ref)
.map(|zc| &zc.sample)
};
let mut dynamic_rng = ChaCha8Rng::from_seed(thread_rng().gen());
let mut canvas = Canvas {
info: CanvasInfo {
chunk_pos,
wpos: chunk_pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
column_grid: &zcache_grid,
column_grid_border: grid_border,
chunks: &self.sim,
index,
chunk: sim_chunk,
calendar,
},
chunk: &mut chunk,
entities: Vec::new(),
rtsim_resource_blocks: Vec::new(),
};
if index.features.train_tracks {
layer::apply_trains_to(&mut canvas, &self.sim, sim_chunk, chunk_center_wpos2d);
}
if index.features.caverns {
layer::apply_caverns_to(&mut canvas, &mut dynamic_rng);
}
if index.features.caves {
layer::apply_caves2_to(&mut canvas, &mut dynamic_rng);
}
if index.features.rocks {
layer::apply_rocks_to(&mut canvas, &mut dynamic_rng);
}
if index.features.shrubs {
layer::apply_shrubs_to(&mut canvas, &mut dynamic_rng);
}
if index.features.trees {
layer::apply_trees_to(&mut canvas, &mut dynamic_rng, calendar);
}
if index.features.scatter {
layer::apply_scatter_to(&mut canvas, &mut dynamic_rng, calendar);
}
if index.features.paths {
layer::apply_paths_to(&mut canvas);
}
if index.features.spots {
layer::apply_spots_to(&mut canvas, &mut dynamic_rng);
}
sim_chunk
.sites
.iter()
.for_each(|site| index.sites[*site].apply_to(&mut canvas, &mut dynamic_rng));
let mut rtsim_resource_blocks = std::mem::take(&mut canvas.rtsim_resource_blocks);
let mut supplement = ChunkSupplement {
entities: std::mem::take(&mut canvas.entities),
rtsim_max_resources: Default::default(),
};
drop(canvas);
let gen_entity_pos = |dynamic_rng: &mut ChaCha8Rng| {
let lpos2d = TerrainChunkSize::RECT_SIZE
.map(|sz| dynamic_rng.gen::<u32>().rem_euclid(sz) as i32);
let mut lpos = Vec3::new(
lpos2d.x,
lpos2d.y,
sample_get(lpos2d).map(|s| s.alt as i32 - 32).unwrap_or(0),
);
while let Some(block) = chunk.get(lpos).ok().copied().filter(Block::is_solid) {
lpos.z += block.solid_height().ceil() as i32;
}
(Vec3::from(chunk_wpos2d) + lpos).map(|e: i32| e as f32) + 0.5
};
if sim_chunk.contains_waypoint {
let waypoint_pos = gen_entity_pos(&mut dynamic_rng);
if sim_chunk
.sites
.iter()
.map(|site| index.sites[*site].spawn_rules(waypoint_pos.xy().as_()))
.fold(SpawnRules::default(), |a, b| a.combine(b))
.waypoints
{
supplement
.add_entity(EntityInfo::at(waypoint_pos).into_special(SpecialEntity::Waypoint));
}
}
layer::apply_caves_supplement(
&mut dynamic_rng,
chunk_wpos2d,
sample_get,
&chunk,
index,
&mut supplement,
);
layer::wildlife::apply_wildlife_supplement(
&mut dynamic_rng,
chunk_wpos2d,
sample_get,
&chunk,
index,
sim_chunk,
&mut supplement,
time.as_ref(),
);
sim_chunk.sites.iter().for_each(|site| {
index.sites[*site].apply_supplement(
&mut dynamic_rng,
chunk_wpos2d,
sample_get,
&mut supplement,
site.id(),
time.as_ref(),
)
});
chunk.defragment();
if let Some(rtsim_resources) = rtsim_resources {
rtsim_resource_blocks.sort_unstable_by_key(|pos| pos.into_array());
rtsim_resource_blocks.dedup();
for wpos in rtsim_resource_blocks {
let _ = chunk.map(wpos - chunk_wpos2d.with_z(0), |block| {
if let Some(res) = block.get_rtsim_resource() {
supplement.rtsim_max_resources[res] += 1;
if dynamic_rng.gen_bool(rtsim_resources[res] as f64) {
block
} else {
block.into_vacant()
}
} else {
block
}
});
}
}
Ok((chunk, supplement))
}
pub fn get_lod_zone(&self, pos: Vec2<i32>, index: IndexRef) -> lod::Zone {
let min_wpos = pos.map(lod::to_wpos);
let max_wpos = (pos + 1).map(lod::to_wpos);
let mut objects = Vec::new();
prof_span!(guard, "add trees");
objects.extend(
&mut self
.sim()
.get_area_trees(min_wpos, max_wpos)
.filter_map(|attr| {
ColumnGen::new(self.sim())
.get((attr.pos, index, self.sim().calendar.as_ref()))
.filter(|col| layer::tree::tree_valid_at(attr.pos, col, None, attr.seed))
.zip(Some(attr))
})
.filter_map(|(col, tree)| {
Some(lod::Object {
kind: match tree.forest_kind {
all::ForestKind::Dead => lod::ObjectKind::Dead,
all::ForestKind::Pine => lod::ObjectKind::Pine,
all::ForestKind::Mangrove => lod::ObjectKind::Mangrove,
all::ForestKind::Acacia => lod::ObjectKind::Acacia,
all::ForestKind::Birch => lod::ObjectKind::Birch,
all::ForestKind::Redwood => lod::ObjectKind::Redwood,
all::ForestKind::Baobab => lod::ObjectKind::Baobab,
all::ForestKind::Frostpine => lod::ObjectKind::Frostpine,
all::ForestKind::Palm => lod::ObjectKind::Palm,
_ => lod::ObjectKind::GenericTree,
},
pos: {
let rpos = tree.pos - min_wpos;
if rpos.is_any_negative() {
return None;
} else {
rpos.map(|e| e as i16).with_z(col.alt as i16)
}
},
flags: lod::InstFlags::empty()
| if col.snow_cover {
lod::InstFlags::SNOW_COVERED
} else {
lod::InstFlags::empty()
}
| lod::InstFlags::from_bits(((tree.seed % 4) as u8) << 2).expect("This shouldn't set unknown bits"),
color: {
let field = crate::util::RandomField::new(tree.seed);
let lerp = field.get_f32(Vec3::from(tree.pos)) * 0.8 + 0.1;
let sblock = tree.forest_kind.leaf_block();
crate::all::leaf_color(index, tree.seed, lerp, &sblock)
.unwrap_or(Rgb::black())
},
})
}),
);
drop(guard);
objects.extend(
index
.sites
.iter()
.filter(|(_, site)| {
site.get_origin()
.map2(min_wpos.zip(max_wpos), |e, (min, max)| e >= min && e < max)
.reduce_and()
})
.filter_map(|(_, site)| {
site.site2().map(|site| {
site.plots().filter_map(|plot| match &plot.kind {
site2::plot::PlotKind::House(h) => Some((
site.tile_wpos(plot.root_tile),
h.roof_color(),
lod::ObjectKind::House,
)),
site2::plot::PlotKind::GiantTree(t) => Some((
site.tile_wpos(plot.root_tile),
t.leaf_color(),
lod::ObjectKind::GiantTree,
)),
site2::plot::PlotKind::Haniwa(_) => Some((
site.tile_wpos(plot.root_tile),
Rgb::black(),
lod::ObjectKind::Haniwa,
)),
site2::plot::PlotKind::DesertCityMultiPlot(_) => Some((
site.tile_wpos(plot.root_tile),
Rgb::black(),
lod::ObjectKind::Desert,
)),
site2::plot::PlotKind::DesertCityArena(_) => Some((
site.tile_wpos(plot.root_tile),
Rgb::black(),
lod::ObjectKind::Arena,
)),
site2::plot::PlotKind::SavannahHut(_)
| site2::plot::PlotKind::SavannahWorkshop(_) => Some((
site.tile_wpos(plot.root_tile),
Rgb::black(),
lod::ObjectKind::SavannahHut,
)),
site2::plot::PlotKind::SavannahAirshipDock(_) => Some((
site.tile_wpos(plot.root_tile),
Rgb::black(),
lod::ObjectKind::SavannahAirshipDock,
)),
site2::plot::PlotKind::TerracottaPalace(_) => Some((
site.tile_wpos(plot.root_tile),
Rgb::black(),
lod::ObjectKind::TerracottaPalace,
)),
site2::plot::PlotKind::TerracottaHouse(_) => Some((
site.tile_wpos(plot.root_tile),
Rgb::black(),
lod::ObjectKind::TerracottaHouse,
)),
site2::plot::PlotKind::TerracottaYard(_) => Some((
site.tile_wpos(plot.root_tile),
Rgb::black(),
lod::ObjectKind::TerracottaYard,
)),
site2::plot::PlotKind::AirshipDock(_) => Some((
site.tile_wpos(plot.root_tile),
Rgb::black(),
lod::ObjectKind::AirshipDock,
)),
site2::plot::PlotKind::CoastalHouse(_) => Some((
site.tile_wpos(plot.root_tile),
Rgb::black(),
lod::ObjectKind::CoastalHouse,
)),
site2::plot::PlotKind::CoastalWorkshop(_) => Some((
site.tile_wpos(plot.root_tile),
Rgb::black(),
lod::ObjectKind::CoastalWorkshop,
)),
_ => None,
})
})
})
.flatten()
.filter_map(|(wpos2d, color, model)| {
ColumnGen::new(self.sim())
.get((wpos2d, index, self.sim().calendar.as_ref()))
.zip(Some((wpos2d, color, model)))
})
.map(|(column, (wpos2d, color, model))| lod::Object {
kind: model,
pos: (wpos2d - min_wpos)
.map(|e| e as i16)
.with_z(self.sim().get_alt_approx(wpos2d).unwrap_or(0.0) as i16),
flags: if column.snow_cover {
lod::InstFlags::SNOW_COVERED
} else {
lod::InstFlags::empty()
},
color,
}),
);
lod::Zone { objects }
}
pub fn get_location_name(&self, index: IndexRef, wpos2d: Vec2<i32>) -> Option<String> {
let chunk_pos = wpos2d.wpos_to_cpos();
let sim_chunk = self.sim.get(chunk_pos)?;
sim_chunk.get_location_name(&index.sites, &self.civs.pois, wpos2d)
}
}