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