1#![expect(
2 clippy::option_map_unit_fn,
3 clippy::blocks_in_conditions,
4 clippy::identity_op,
5 clippy::needless_pass_by_ref_mut )]
7#![expect(clippy::branches_sharing_code)] #![deny(clippy::clone_on_ref_ptr)]
9#![feature(option_zip, let_chains)]
10#![cfg_attr(feature = "simd", feature(portable_simd))]
11
12mod all;
13mod block;
14pub mod canvas;
15pub mod civ;
16mod column;
17pub mod config;
18pub mod index;
19pub mod land;
20pub mod layer;
21pub mod pathfinding;
22pub mod sim;
23pub mod sim2;
24pub mod site;
25pub mod site2;
26pub mod util;
27
28pub use crate::{
30 canvas::{Canvas, CanvasInfo},
31 config::{CONFIG, Features},
32 land::Land,
33 layer::PathLocals,
34};
35pub use block::BlockGen;
36use civ::WorldCivStage;
37pub use column::ColumnSample;
38pub use common::terrain::site::{DungeonKindMeta, SettlementKindMeta};
39use common::{spiral::Spiral2d, terrain::CoordinateConversions};
40pub use index::{IndexOwned, IndexRef};
41use sim::WorldSimStage;
42
43use crate::{
44 column::ColumnGen,
45 index::Index,
46 layer::spot::SpotGenerate,
47 site::{SiteKind, SpawnRules},
48 util::{Grid, Sampler},
49};
50use common::{
51 assets,
52 calendar::Calendar,
53 generation::{ChunkSupplement, EntityInfo, SpecialEntity},
54 lod,
55 resources::TimeOfDay,
56 rtsim::ChunkResource,
57 spot::Spot,
58 terrain::{
59 Block, BlockKind, SpriteKind, TerrainChunk, TerrainChunkMeta, TerrainChunkSize, TerrainGrid,
60 },
61 vol::{ReadVol, RectVolSize, WriteVol},
62};
63use common_base::prof_span;
64use common_net::msg::{WorldMapMsg, world_msg};
65use enum_map::EnumMap;
66use rand::{Rng, prelude::*};
67use rand_chacha::ChaCha8Rng;
68use serde::Deserialize;
69use std::time::Duration;
70use vek::*;
71
72#[cfg(all(feature = "be-dyn-lib", feature = "use-dyn-lib"))]
73compile_error!("Can't use both \"be-dyn-lib\" and \"use-dyn-lib\" features at once");
74
75#[cfg(feature = "use-dyn-lib")]
76use {common_dynlib::LoadedLib, lazy_static::lazy_static, std::sync::Arc, std::sync::Mutex};
77
78#[cfg(feature = "use-dyn-lib")]
79lazy_static! {
80 pub static ref LIB: Arc<Mutex<Option<LoadedLib>>> =
81 common_dynlib::init("veloren-world", "world", &[]);
82}
83
84#[cfg(feature = "use-dyn-lib")]
85pub fn init() { lazy_static::initialize(&LIB); }
86
87#[derive(Debug)]
88pub enum Error {
89 Other(String),
90}
91
92#[derive(Debug)]
93pub enum WorldGenerateStage {
94 WorldSimGenerate(WorldSimStage),
95 WorldCivGenerate(WorldCivStage),
96 EconomySimulation,
97 SpotGeneration,
98}
99
100pub struct World {
101 sim: sim::WorldSim,
102 civs: civ::Civs,
103}
104
105#[derive(Deserialize)]
106pub struct Colors {
107 pub deep_stone_color: (u8, u8, u8),
108 pub block: block::Colors,
109 pub column: column::Colors,
110 pub layer: layer::Colors,
111 pub site: site::Colors,
112}
113
114impl assets::Asset for Colors {
115 type Loader = assets::RonLoader;
116
117 const EXTENSION: &'static str = "ron";
118}
119
120impl World {
121 pub fn empty() -> (Self, IndexOwned) {
122 let index = Index::new(0);
123 (
124 Self {
125 sim: sim::WorldSim::empty(),
126 civs: civ::Civs::default(),
127 },
128 IndexOwned::new(index),
129 )
130 }
131
132 pub fn generate(
133 seed: u32,
134 opts: sim::WorldOpts,
135 threadpool: &rayon::ThreadPool,
136 report_stage: &(dyn Fn(WorldGenerateStage) + Send + Sync),
137 ) -> (Self, IndexOwned) {
138 prof_span!("World::generate");
139 threadpool.install(|| {
142 let mut index = Index::new(seed);
143 let calendar = opts.calendar.clone();
144
145 let mut sim = sim::WorldSim::generate(seed, opts, threadpool, &|stage| {
146 report_stage(WorldGenerateStage::WorldSimGenerate(stage))
147 });
148
149 let civs =
150 civ::Civs::generate(seed, &mut sim, &mut index, calendar.as_ref(), &|stage| {
151 report_stage(WorldGenerateStage::WorldCivGenerate(stage))
152 });
153
154 report_stage(WorldGenerateStage::EconomySimulation);
155 sim2::simulate(&mut index, &mut sim);
156
157 report_stage(WorldGenerateStage::SpotGeneration);
158 Spot::generate(&mut sim);
159
160 (Self { sim, civs }, IndexOwned::new(index))
161 })
162 }
163
164 pub fn sim(&self) -> &sim::WorldSim { &self.sim }
165
166 pub fn civs(&self) -> &civ::Civs { &self.civs }
167
168 pub fn tick(&self, _dt: Duration) {
169 }
171
172 pub fn get_map_data(&self, index: IndexRef, threadpool: &rayon::ThreadPool) -> WorldMapMsg {
173 prof_span!("World::get_map_data");
174 threadpool.install(|| {
175 WorldMapMsg {
176 pois: self
177 .civs()
178 .pois
179 .iter()
180 .map(|(_, poi)| world_msg::PoiInfo {
181 name: poi.name.clone(),
182 kind: match &poi.kind {
183 civ::PoiKind::Peak(alt) => world_msg::PoiKind::Peak(*alt),
184 civ::PoiKind::Biome(size) => world_msg::PoiKind::Lake(*size),
185 },
186 wpos: poi.loc * TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
187 })
188 .collect(),
189 sites: self
190 .civs()
191 .sites
192 .iter()
193 .filter(|(_, site)| {
194 !matches!(
195 &site.kind,
196 civ::SiteKind::PirateHideout
197 | civ::SiteKind::JungleRuin
198 | civ::SiteKind::RockCircle
199 | civ::SiteKind::TrollCave
200 | civ::SiteKind::Camp
201 )
202 })
203 .map(|(_, site)| {
204 world_msg::SiteInfo {
205 id: site.site_tmp.map(|i| i.id()).unwrap_or_default(),
206 name: site.site_tmp.map(|id| index.sites[id].name().to_string()),
207 kind: match &site.kind {
209 civ::SiteKind::Settlement
210 | civ::SiteKind::Refactor
211 | civ::SiteKind::CliffTown
212 | civ::SiteKind::SavannahTown
213 | civ::SiteKind::CoastalTown
214 | civ::SiteKind::DesertCity
215 | civ::SiteKind::PirateHideout
216 | civ::SiteKind::JungleRuin
217 | civ::SiteKind::RockCircle
218 | civ::SiteKind::TrollCave
219 | civ::SiteKind::Camp => world_msg::SiteKind::Town,
220 civ::SiteKind::Castle => world_msg::SiteKind::Castle,
221 civ::SiteKind::Tree | civ::SiteKind::GiantTree => {
222 world_msg::SiteKind::Tree
223 },
224 civ::SiteKind::Gnarling => world_msg::SiteKind::Gnarling,
226 civ::SiteKind::DwarvenMine => world_msg::SiteKind::DwarvenMine,
227 civ::SiteKind::ChapelSite => world_msg::SiteKind::ChapelSite,
228 civ::SiteKind::Terracotta => world_msg::SiteKind::Terracotta,
229 civ::SiteKind::Citadel => world_msg::SiteKind::Castle,
230 civ::SiteKind::Bridge(_, _) => world_msg::SiteKind::Bridge,
231 civ::SiteKind::GliderCourse => world_msg::SiteKind::GliderCourse,
232 civ::SiteKind::Cultist => world_msg::SiteKind::Cultist,
233 civ::SiteKind::Sahagin => world_msg::SiteKind::Sahagin,
234 civ::SiteKind::Myrmidon => world_msg::SiteKind::Myrmidon,
235 civ::SiteKind::Adlet => world_msg::SiteKind::Adlet,
236 civ::SiteKind::Haniwa => world_msg::SiteKind::Haniwa,
237 civ::SiteKind::VampireCastle => world_msg::SiteKind::VampireCastle,
238 },
239 wpos: site.center * TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
240 }
241 })
242 .chain(
243 layer::cave::surface_entrances(&Land::from_sim(self.sim()))
244 .enumerate()
245 .map(|(i, wpos)| world_msg::SiteInfo {
246 id: 65536 + i as u64, name: None,
248 kind: world_msg::SiteKind::Cave,
249 wpos,
250 }),
251 )
252 .collect(),
253 possible_starting_sites: {
254 const STARTING_SITE_COUNT: usize = 5;
255
256 let mut candidates = self
257 .civs()
258 .sites
259 .iter()
260 .filter_map(|(_, civ_site)| Some((civ_site, civ_site.site_tmp?)))
261 .map(|(civ_site, site_id)| {
262 let (site2, mut score) = match &index.sites[site_id].kind {
265 SiteKind::Refactor(site2) => (site2, 2.0),
266 _ => return (site_id.id(), 0.0),
269 };
270
271 const OPTIMAL_STARTER_TOWN_SIZE: f32 = 30.0;
273
274 let plots = site2.plots().len() as f32;
276 let size_score = if plots > OPTIMAL_STARTER_TOWN_SIZE {
277 1.0 + (1.0
278 / (1.0 + ((plots - OPTIMAL_STARTER_TOWN_SIZE) / 15.0).powi(3)))
279 } else {
280 (2.05
281 / (1.0 + ((OPTIMAL_STARTER_TOWN_SIZE - plots) / 15.0).powi(5)))
282 - 0.05
283 }
284 .max(0.01);
285
286 score *= size_score;
287
288 let pos_score = (10.0
290 / (1.0
291 + (civ_site
292 .center
293 .map2(self.sim().get_size(), |e, sz| {
294 (e as f32 / sz as f32 - 0.5).abs() * 2.0
295 })
296 .reduce_partial_max())
297 .powi(6)
298 * 25.0))
299 .max(0.02);
300 score *= pos_score;
301
302 let mut chunk_scores = 2.0;
304 for (chunk, distance) in
305 Spiral2d::with_radius(10).filter_map(|rel_pos| {
306 let chunk_pos = civ_site.center + rel_pos * 2;
307 self.sim()
308 .get(chunk_pos)
309 .zip(Some(rel_pos.as_::<f32>().magnitude()))
310 })
311 {
312 let weight = 1.0 / (distance * std::f32::consts::TAU + 1.0);
313 let chunk_difficulty = 20.0
314 / (20.0 + chunk.get_biome().difficulty().pow(4) as f32 / 5.0);
315 chunk_scores *= 1.0 - weight + chunk_difficulty * weight;
319 }
320
321 score *= chunk_scores;
322
323 (site_id.id(), score)
324 })
325 .collect::<Vec<_>>();
326 candidates.sort_by_key(|(_, score)| -(*score * 1000.0) as i32);
327 candidates
328 .into_iter()
329 .map(|(site_id, _)| site_id)
330 .take(STARTING_SITE_COUNT)
331 .collect()
332 },
333 ..self.sim.get_map(index, self.sim().calendar.as_ref())
334 }
335 })
336 }
337
338 pub fn sample_columns(
339 &self,
340 ) -> impl Sampler<
341 Index = (Vec2<i32>, IndexRef, Option<&'_ Calendar>),
342 Sample = Option<ColumnSample>,
343 > + '_ {
344 ColumnGen::new(&self.sim)
345 }
346
347 pub fn sample_blocks(&self) -> BlockGen { BlockGen::new(ColumnGen::new(&self.sim)) }
348
349 pub fn find_accessible_pos(
355 &self,
356 index: IndexRef,
357 spawn_wpos: Vec2<i32>,
358 ascending: bool,
359 ) -> Vec3<f32> {
360 let chunk_pos = TerrainGrid::chunk_key(spawn_wpos);
361
362 let (tc, _cs) = self
365 .generate_chunk(index, chunk_pos, None, || false, None)
366 .unwrap();
367
368 tc.find_accessible_pos(spawn_wpos, ascending)
369 }
370
371 #[expect(clippy::result_unit_err)]
372 pub fn generate_chunk(
373 &self,
374 index: IndexRef,
375 chunk_pos: Vec2<i32>,
376 rtsim_resources: Option<EnumMap<ChunkResource, f32>>,
377 mut should_continue: impl FnMut() -> bool,
379 time: Option<(TimeOfDay, Calendar)>,
380 ) -> Result<(TerrainChunk, ChunkSupplement), ()> {
381 let calendar = time.as_ref().map(|(_, cal)| cal);
382
383 let mut sampler = self.sample_blocks();
384
385 let chunk_wpos2d = chunk_pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32);
386 let chunk_center_wpos2d = chunk_wpos2d + TerrainChunkSize::RECT_SIZE.map(|e| e as i32 / 2);
387 let grid_border = 4;
388 let zcache_grid = Grid::populate_from(
389 TerrainChunkSize::RECT_SIZE.map(|e| e as i32) + grid_border * 2,
390 |offs| sampler.get_z_cache(chunk_wpos2d - grid_border + offs, index, calendar),
391 );
392
393 let air = Block::air(SpriteKind::Empty);
394 let stone = Block::new(
395 BlockKind::Rock,
396 zcache_grid
397 .get(grid_border + TerrainChunkSize::RECT_SIZE.map(|e| e as i32) / 2)
398 .and_then(|zcache| zcache.as_ref())
399 .map(|zcache| zcache.sample.stone_col)
400 .unwrap_or_else(|| index.colors.deep_stone_color.into()),
401 );
402
403 let (base_z, sim_chunk) = match self
404 .sim
405 .get_base_z(chunk_pos)
411 {
412 Some(base_z) => (base_z as i32, self.sim.get(chunk_pos).unwrap()),
413 None => {
415 return Ok((self.sim().generate_oob_chunk(), ChunkSupplement::default()));
418 },
419 };
420 let meta = TerrainChunkMeta::new(
421 sim_chunk.get_location_name(&index.sites, &self.civs.pois, chunk_center_wpos2d),
422 sim_chunk.get_biome(),
423 sim_chunk.alt,
424 sim_chunk.tree_density,
425 sim_chunk.river.is_river(),
426 sim_chunk.river.near_water(),
427 sim_chunk.river.velocity,
428 sim_chunk.temp,
429 sim_chunk.humidity,
430 sim_chunk
431 .sites
432 .iter()
433 .filter(|id| {
434 index.sites[**id]
435 .get_origin()
436 .distance_squared(chunk_center_wpos2d) as f32
437 <= index.sites[**id].radius().powi(2)
438 })
439 .min_by_key(|id| {
440 index.sites[**id]
441 .get_origin()
442 .distance_squared(chunk_center_wpos2d)
443 })
444 .map(|id| index.sites[*id].kind.convert_to_meta().unwrap_or_default()),
445 self.sim.approx_chunk_terrain_normal(chunk_pos),
446 sim_chunk.rockiness,
447 sim_chunk.cliff_height,
448 );
449
450 let mut chunk = TerrainChunk::new(base_z, stone, air, meta);
451
452 for y in 0..TerrainChunkSize::RECT_SIZE.y as i32 {
453 for x in 0..TerrainChunkSize::RECT_SIZE.x as i32 {
454 if should_continue() {
455 return Err(());
456 };
457
458 let offs = Vec2::new(x, y);
459
460 let z_cache = match zcache_grid.get(grid_border + offs) {
461 Some(Some(z_cache)) => z_cache,
462 _ => continue,
463 };
464
465 let (min_z, max_z) = z_cache.get_z_limits();
466
467 (base_z..min_z as i32).for_each(|z| {
468 let _ = chunk.set(Vec3::new(x, y, z), stone);
469 });
470
471 (min_z as i32..max_z as i32).for_each(|z| {
472 let lpos = Vec3::new(x, y, z);
473 let wpos = Vec3::from(chunk_wpos2d) + lpos;
474
475 if let Some(block) = sampler.get_with_z_cache(wpos, Some(z_cache)) {
476 let _ = chunk.set(lpos, block);
477 }
478 });
479 }
480 }
481
482 let sample_get = |offs| {
483 zcache_grid
484 .get(grid_border + offs)
485 .and_then(Option::as_ref)
486 .map(|zc| &zc.sample)
487 };
488
489 let mut dynamic_rng = ChaCha8Rng::from_seed(thread_rng().gen());
491
492 let mut canvas = Canvas {
494 info: CanvasInfo {
495 chunk_pos,
496 wpos: chunk_pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
497 column_grid: &zcache_grid,
498 column_grid_border: grid_border,
499 chunks: &self.sim,
500 index,
501 chunk: sim_chunk,
502 calendar,
503 },
504 chunk: &mut chunk,
505 entities: Vec::new(),
506 rtsim_resource_blocks: Vec::new(),
507 };
508
509 if index.features.train_tracks {
510 layer::apply_trains_to(&mut canvas, &self.sim, sim_chunk, chunk_center_wpos2d);
511 }
512
513 if index.features.caverns {
514 layer::apply_caverns_to(&mut canvas, &mut dynamic_rng);
515 }
516 if index.features.caves {
517 layer::apply_caves_to(&mut canvas, &mut dynamic_rng);
518 }
519 if index.features.rocks {
520 layer::apply_rocks_to(&mut canvas, &mut dynamic_rng);
521 }
522 if index.features.shrubs {
523 layer::apply_shrubs_to(&mut canvas, &mut dynamic_rng);
524 }
525 if index.features.trees {
526 layer::apply_trees_to(&mut canvas, &mut dynamic_rng, calendar);
527 }
528 if index.features.scatter {
529 layer::apply_scatter_to(&mut canvas, &mut dynamic_rng, calendar);
530 }
531 if index.features.paths {
532 layer::apply_paths_to(&mut canvas);
533 }
534 if index.features.spots {
535 layer::apply_spots_to(&mut canvas, &mut dynamic_rng);
536 }
537 sim_chunk
541 .sites
542 .iter()
543 .for_each(|site| index.sites[*site].apply_to(&mut canvas, &mut dynamic_rng));
544
545 let mut rtsim_resource_blocks = std::mem::take(&mut canvas.rtsim_resource_blocks);
546 let mut supplement = ChunkSupplement {
547 entities: std::mem::take(&mut canvas.entities),
548 rtsim_max_resources: Default::default(),
549 };
550 drop(canvas);
551
552 let gen_entity_pos = |dynamic_rng: &mut ChaCha8Rng| {
553 let lpos2d = TerrainChunkSize::RECT_SIZE
554 .map(|sz| dynamic_rng.gen::<u32>().rem_euclid(sz) as i32);
555 let mut lpos = Vec3::new(
556 lpos2d.x,
557 lpos2d.y,
558 sample_get(lpos2d).map(|s| s.alt as i32 - 32).unwrap_or(0),
559 );
560
561 while let Some(block) = chunk.get(lpos).ok().copied().filter(Block::is_solid) {
562 lpos.z += block.solid_height().ceil() as i32;
563 }
564
565 (Vec3::from(chunk_wpos2d) + lpos).map(|e: i32| e as f32) + 0.5
566 };
567
568 if sim_chunk.contains_waypoint {
569 let waypoint_pos = gen_entity_pos(&mut dynamic_rng);
570 if sim_chunk
571 .sites
572 .iter()
573 .map(|site| index.sites[*site].spawn_rules(waypoint_pos.xy().as_()))
574 .fold(SpawnRules::default(), |a, b| a.combine(b))
575 .waypoints
576 {
577 supplement
578 .add_entity(EntityInfo::at(waypoint_pos).into_special(SpecialEntity::Waypoint));
579 }
580 }
581
582 layer::wildlife::apply_wildlife_supplement(
584 &mut dynamic_rng,
585 chunk_wpos2d,
586 sample_get,
587 &chunk,
588 index,
589 sim_chunk,
590 &mut supplement,
591 time.as_ref(),
592 );
593
594 sim_chunk.sites.iter().for_each(|site| {
596 index.sites[*site].apply_supplement(
597 &mut dynamic_rng,
598 chunk_wpos2d,
599 sample_get,
600 &mut supplement,
601 site.id(),
602 time.as_ref(),
603 )
604 });
605
606 chunk.defragment();
608
609 if let Some(rtsim_resources) = rtsim_resources {
614 rtsim_resource_blocks.sort_unstable_by_key(|pos| pos.into_array());
615 rtsim_resource_blocks.dedup();
616 for wpos in rtsim_resource_blocks {
617 let _ = chunk.map(wpos - chunk_wpos2d.with_z(0), |block| {
618 if let Some(res) = block.get_rtsim_resource() {
619 supplement.rtsim_max_resources[res] += 1;
623 if dynamic_rng.gen_bool(rtsim_resources[res] as f64) {
626 block
627 } else {
628 block.into_vacant()
629 }
630 } else {
631 block
632 }
633 });
634 }
635 }
636
637 Ok((chunk, supplement))
638 }
639
640 pub fn get_lod_zone(&self, pos: Vec2<i32>, index: IndexRef) -> lod::Zone {
642 let min_wpos = pos.map(lod::to_wpos);
643 let max_wpos = (pos + 1).map(lod::to_wpos);
644
645 let mut objects = Vec::new();
646
647 prof_span!(guard, "add trees");
649 objects.extend(
650 &mut self
651 .sim()
652 .get_area_trees(min_wpos, max_wpos)
653 .filter_map(|attr| {
654 ColumnGen::new(self.sim())
655 .get((attr.pos, index, self.sim().calendar.as_ref()))
656 .filter(|col| layer::tree::tree_valid_at(attr.pos, col, None, attr.seed))
657 .zip(Some(attr))
658 })
659 .filter_map(|(col, tree)| {
660 Some(lod::Object {
661 kind: match tree.forest_kind {
662 all::ForestKind::Dead => lod::ObjectKind::Dead,
663 all::ForestKind::Pine => lod::ObjectKind::Pine,
664 all::ForestKind::Mangrove => lod::ObjectKind::Mangrove,
665 all::ForestKind::Acacia => lod::ObjectKind::Acacia,
666 all::ForestKind::Birch => lod::ObjectKind::Birch,
667 all::ForestKind::Redwood => lod::ObjectKind::Redwood,
668 all::ForestKind::Baobab => lod::ObjectKind::Baobab,
669 all::ForestKind::Frostpine => lod::ObjectKind::Frostpine,
670 all::ForestKind::Palm => lod::ObjectKind::Palm,
671 _ => lod::ObjectKind::GenericTree,
672 },
673 pos: {
674 let rpos = tree.pos - min_wpos;
675 if rpos.is_any_negative() {
676 return None;
677 } else {
678 rpos.map(|e| e as i16).with_z(col.alt as i16)
679 }
680 },
681 flags: lod::InstFlags::empty()
682 | if col.snow_cover {
683 lod::InstFlags::SNOW_COVERED
684 } else {
685 lod::InstFlags::empty()
686 }
687 | lod::InstFlags::from_bits(((tree.seed % 4) as u8) << 2).expect("This shouldn't set unknown bits"),
689 color: {
690 let field = crate::util::RandomField::new(tree.seed);
691 let lerp = field.get_f32(Vec3::from(tree.pos)) * 0.8 + 0.1;
692 let sblock = tree.forest_kind.leaf_block();
693
694 crate::all::leaf_color(index, tree.seed, lerp, &sblock)
695 .unwrap_or(Rgb::black())
696 },
697 })
698 }),
699 );
700 drop(guard);
701
702 objects.extend(
704 index
705 .sites
706 .iter()
707 .filter(|(_, site)| {
708 site.get_origin()
709 .map2(min_wpos.zip(max_wpos), |e, (min, max)| e >= min && e < max)
710 .reduce_and()
711 })
712 .filter_map(|(_, site)| {
713 site.site2().map(|site| {
714 site.plots().filter_map(|plot| match &plot.kind {
715 site2::plot::PlotKind::House(h) => Some((
716 site.tile_wpos(plot.root_tile),
717 h.roof_color(),
718 lod::ObjectKind::House,
719 )),
720 site2::plot::PlotKind::GiantTree(t) => Some((
721 site.tile_wpos(plot.root_tile),
722 t.leaf_color(),
723 lod::ObjectKind::GiantTree,
724 )),
725 site2::plot::PlotKind::Haniwa(_) => Some((
726 site.tile_wpos(plot.root_tile),
727 Rgb::black(),
728 lod::ObjectKind::Haniwa,
729 )),
730 site2::plot::PlotKind::DesertCityMultiPlot(_) => Some((
731 site.tile_wpos(plot.root_tile),
732 Rgb::black(),
733 lod::ObjectKind::Desert,
734 )),
735 site2::plot::PlotKind::DesertCityArena(_) => Some((
736 site.tile_wpos(plot.root_tile),
737 Rgb::black(),
738 lod::ObjectKind::Arena,
739 )),
740 site2::plot::PlotKind::SavannahHut(_)
741 | site2::plot::PlotKind::SavannahWorkshop(_) => Some((
742 site.tile_wpos(plot.root_tile),
743 Rgb::black(),
744 lod::ObjectKind::SavannahHut,
745 )),
746 site2::plot::PlotKind::SavannahAirshipDock(_) => Some((
747 site.tile_wpos(plot.root_tile),
748 Rgb::black(),
749 lod::ObjectKind::SavannahAirshipDock,
750 )),
751 site2::plot::PlotKind::TerracottaPalace(_) => Some((
752 site.tile_wpos(plot.root_tile),
753 Rgb::black(),
754 lod::ObjectKind::TerracottaPalace,
755 )),
756 site2::plot::PlotKind::TerracottaHouse(_) => Some((
757 site.tile_wpos(plot.root_tile),
758 Rgb::black(),
759 lod::ObjectKind::TerracottaHouse,
760 )),
761 site2::plot::PlotKind::TerracottaYard(_) => Some((
762 site.tile_wpos(plot.root_tile),
763 Rgb::black(),
764 lod::ObjectKind::TerracottaYard,
765 )),
766 site2::plot::PlotKind::AirshipDock(_) => Some((
767 site.tile_wpos(plot.root_tile),
768 Rgb::black(),
769 lod::ObjectKind::AirshipDock,
770 )),
771 site2::plot::PlotKind::CoastalHouse(_) => Some((
772 site.tile_wpos(plot.root_tile),
773 Rgb::black(),
774 lod::ObjectKind::CoastalHouse,
775 )),
776 site2::plot::PlotKind::CoastalWorkshop(_) => Some((
777 site.tile_wpos(plot.root_tile),
778 Rgb::black(),
779 lod::ObjectKind::CoastalWorkshop,
780 )),
781 _ => None,
782 })
783 })
784 })
785 .flatten()
786 .filter_map(|(wpos2d, color, model)| {
787 ColumnGen::new(self.sim())
788 .get((wpos2d, index, self.sim().calendar.as_ref()))
789 .zip(Some((wpos2d, color, model)))
790 })
791 .map(|(column, (wpos2d, color, model))| lod::Object {
792 kind: model,
793 pos: (wpos2d - min_wpos)
794 .map(|e| e as i16)
795 .with_z(self.sim().get_alt_approx(wpos2d).unwrap_or(0.0) as i16),
796 flags: if column.snow_cover {
797 lod::InstFlags::SNOW_COVERED
798 } else {
799 lod::InstFlags::empty()
800 },
801 color,
802 }),
803 );
804
805 lod::Zone { objects }
806 }
807
808 pub fn get_location_name(&self, index: IndexRef, wpos2d: Vec2<i32>) -> Option<String> {
810 let chunk_pos = wpos2d.wpos_to_cpos();
811 let sim_chunk = self.sim.get(chunk_pos)?;
812 sim_chunk.get_location_name(&index.sites, &self.civs.pois, wpos2d)
813 }
814}