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 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,
50 calendar::Calendar,
51 comp::Content,
52 generation::{ChunkSupplement, EntityInfo, SpecialEntity},
53 lod,
54 resources::TimeOfDay,
55 rtsim::ChunkResource,
56 spiral::Spiral2d,
57 spot::Spot,
58 terrain::{
59 Block, BlockKind, CoordinateConversions, SpriteKind, TerrainChunk, TerrainChunkMeta,
60 TerrainChunkSize, TerrainGrid,
61 },
62 vol::{ReadVol, RectVolSize, WriteVol},
63};
64use common_base::prof_span;
65use common_net::msg::{WorldMapMsg, world_msg};
66use enum_map::EnumMap;
67use rand::{Rng, prelude::*};
68use rand_chacha::ChaCha8Rng;
69use serde::Deserialize;
70use std::time::Duration;
71use vek::*;
72
73#[cfg(all(feature = "be-dyn-lib", feature = "use-dyn-lib"))]
74compile_error!("Can't use both \"be-dyn-lib\" and \"use-dyn-lib\" features at once");
75
76#[cfg(feature = "use-dyn-lib")]
77use {common_dynlib::LoadedLib, lazy_static::lazy_static, std::sync::Arc, std::sync::Mutex};
78
79#[cfg(feature = "use-dyn-lib")]
80lazy_static! {
81 pub static ref LIB: Arc<Mutex<Option<LoadedLib>>> =
82 common_dynlib::init("veloren-world", "world", &[]);
83}
84
85#[cfg(feature = "use-dyn-lib")]
86pub fn init() { lazy_static::initialize(&LIB); }
87
88#[derive(Debug)]
89pub enum Error {
90 Other(String),
91}
92
93#[derive(Debug)]
94pub enum WorldGenerateStage {
95 WorldSimGenerate(WorldSimStage),
96 WorldCivGenerate(WorldCivStage),
97 EconomySimulation,
98 SpotGeneration,
99}
100
101pub struct World {
102 sim: sim::WorldSim,
103 civs: civ::Civs,
104}
105
106#[derive(Deserialize)]
107pub struct Colors {
108 pub deep_stone_color: (u8, u8, u8),
109 pub block: block::Colors,
110 pub column: column::Colors,
111 pub layer: layer::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 .values()
193 .filter_map(|site| Some((site.kind.marker()?, site)))
194 .map(|(marker, site)| {
195 world_msg::Marker {
196 id: site.site_tmp.map(|i| i.id()),
197 name: site
198 .site_tmp
199 .map(|id| Content::Plain(index.sites[id].name().to_string())),
200 kind: marker,
202 wpos: site.center * TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
203 }
204 })
205 .chain(
206 layer::cave::surface_entrances(&Land::from_sim(self.sim()), index).map(
207 |wpos| world_msg::Marker {
208 id: None,
209 name: None,
210 kind: world_msg::MarkerKind::Cave,
211 wpos,
212 },
213 ),
214 )
215 .collect(),
216 possible_starting_sites: {
217 const STARTING_SITE_COUNT: usize = 5;
218
219 let mut candidates = self
220 .civs()
221 .sites
222 .iter()
223 .filter_map(|(_, civ_site)| Some((civ_site, civ_site.site_tmp?)))
224 .map(|(civ_site, site_id)| {
225 let site = &index.sites[site_id];
228 let mut score = match site.kind {
229 Some(SiteKind::Refactor) => 2.0,
230 Some(kind)
231 if matches!(
232 kind.meta(),
233 Some(common::terrain::SiteKindMeta::Settlement(_))
234 ) =>
235 {
236 1.0
237 },
238 _ => return (site_id.id(), 0.0),
241 };
242
243 const OPTIMAL_STARTER_TOWN_SIZE: f32 = 30.0;
245
246 let plots = site.plots().len() as f32;
248 let size_score = if plots > OPTIMAL_STARTER_TOWN_SIZE {
249 1.0 + (1.0
250 / (1.0 + ((plots - OPTIMAL_STARTER_TOWN_SIZE) / 15.0).powi(3)))
251 } else {
252 (2.05
253 / (1.0 + ((OPTIMAL_STARTER_TOWN_SIZE - plots) / 15.0).powi(5)))
254 - 0.05
255 }
256 .max(0.01);
257
258 score *= size_score;
259
260 let pos_score = (10.0
262 / (1.0
263 + (civ_site
264 .center
265 .map2(self.sim().get_size(), |e, sz| {
266 (e as f32 / sz as f32 - 0.5).abs() * 2.0
267 })
268 .reduce_partial_max())
269 .powi(6)
270 * 25.0))
271 .max(0.02);
272 score *= pos_score;
273
274 let mut chunk_scores = 2.0;
276 for (chunk, distance) in
277 Spiral2d::with_radius(10).filter_map(|rel_pos| {
278 let chunk_pos = civ_site.center + rel_pos * 2;
279 self.sim()
280 .get(chunk_pos)
281 .zip(Some(rel_pos.as_::<f32>().magnitude()))
282 })
283 {
284 let weight = 1.0 / (distance * std::f32::consts::TAU + 1.0);
285 let chunk_difficulty = 20.0
286 / (20.0 + chunk.get_biome().difficulty().pow(4) as f32 / 5.0);
287 chunk_scores *= 1.0 - weight + chunk_difficulty * weight;
291 }
292
293 score *= chunk_scores;
294
295 (site_id.id(), score)
296 })
297 .collect::<Vec<_>>();
298 candidates.sort_by_key(|(_, score)| -(*score * 1000.0) as i32);
299 candidates
300 .into_iter()
301 .map(|(site_id, _)| site_id)
302 .take(STARTING_SITE_COUNT)
303 .collect()
304 },
305 ..self.sim.get_map(index, self.sim().calendar.as_ref())
306 }
307 })
308 }
309
310 pub fn sample_columns(
311 &self,
312 ) -> impl Sampler<
313 Index = (Vec2<i32>, IndexRef, Option<&'_ Calendar>),
314 Sample = Option<ColumnSample>,
315 > + '_ {
316 ColumnGen::new(&self.sim)
317 }
318
319 pub fn sample_blocks(&self) -> BlockGen { BlockGen::new(ColumnGen::new(&self.sim)) }
320
321 pub fn find_accessible_pos(
327 &self,
328 index: IndexRef,
329 spawn_wpos: Vec2<i32>,
330 ascending: bool,
331 ) -> Vec3<f32> {
332 let chunk_pos = TerrainGrid::chunk_key(spawn_wpos);
333
334 let (tc, _cs) = self
337 .generate_chunk(index, chunk_pos, None, || false, None)
338 .unwrap();
339
340 tc.find_accessible_pos(spawn_wpos, ascending)
341 }
342
343 #[expect(clippy::result_unit_err)]
344 pub fn generate_chunk(
345 &self,
346 index: IndexRef,
347 chunk_pos: Vec2<i32>,
348 rtsim_resources: Option<EnumMap<ChunkResource, f32>>,
349 mut should_continue: impl FnMut() -> bool,
351 time: Option<(TimeOfDay, Calendar)>,
352 ) -> Result<(TerrainChunk, ChunkSupplement), ()> {
353 let calendar = time.as_ref().map(|(_, cal)| cal);
354
355 let mut sampler = self.sample_blocks();
356
357 let chunk_wpos2d = chunk_pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32);
358 let chunk_center_wpos2d = chunk_wpos2d + TerrainChunkSize::RECT_SIZE.map(|e| e as i32 / 2);
359 let grid_border = 4;
360 let zcache_grid = Grid::populate_from(
361 TerrainChunkSize::RECT_SIZE.map(|e| e as i32) + grid_border * 2,
362 |offs| sampler.get_z_cache(chunk_wpos2d - grid_border + offs, index, calendar),
363 );
364
365 let air = Block::air(SpriteKind::Empty);
366 let stone = Block::new(
367 BlockKind::Rock,
368 zcache_grid
369 .get(grid_border + TerrainChunkSize::RECT_SIZE.map(|e| e as i32) / 2)
370 .and_then(|zcache| zcache.as_ref())
371 .map(|zcache| zcache.sample.stone_col)
372 .unwrap_or_else(|| index.colors.deep_stone_color.into()),
373 );
374
375 let (base_z, sim_chunk) = match self
376 .sim
377 .get_base_z(chunk_pos)
383 {
384 Some(base_z) => (base_z as i32, self.sim.get(chunk_pos).unwrap()),
385 None => {
387 return Ok((self.sim().generate_oob_chunk(), ChunkSupplement::default()));
390 },
391 };
392 let meta = TerrainChunkMeta::new(
393 sim_chunk.get_location_name(&index.sites, &self.civs.pois, chunk_center_wpos2d),
394 sim_chunk.get_biome(),
395 sim_chunk.alt,
396 sim_chunk.tree_density,
397 sim_chunk.river.is_river(),
398 sim_chunk.river.near_water(),
399 sim_chunk.river.velocity,
400 sim_chunk.temp,
401 sim_chunk.humidity,
402 sim_chunk
403 .sites
404 .iter()
405 .filter(|id| {
406 index.sites[**id]
407 .origin
408 .as_::<f32>()
409 .distance_squared(chunk_center_wpos2d.as_::<f32>())
410 <= index.sites[**id].radius().powi(2)
411 })
412 .min_by_key(|id| {
413 index.sites[**id]
414 .origin
415 .as_::<i64>()
416 .distance_squared(chunk_center_wpos2d.as_::<i64>())
417 })
418 .map(|id| index.sites[*id].meta().unwrap_or_default()),
419 self.sim.approx_chunk_terrain_normal(chunk_pos),
420 sim_chunk.rockiness,
421 sim_chunk.cliff_height,
422 );
423
424 let mut chunk = TerrainChunk::new(base_z, stone, air, meta);
425
426 for y in 0..TerrainChunkSize::RECT_SIZE.y as i32 {
427 for x in 0..TerrainChunkSize::RECT_SIZE.x as i32 {
428 if should_continue() {
429 return Err(());
430 };
431
432 let offs = Vec2::new(x, y);
433
434 let z_cache = match zcache_grid.get(grid_border + offs) {
435 Some(Some(z_cache)) => z_cache,
436 _ => continue,
437 };
438
439 let (min_z, max_z) = z_cache.get_z_limits();
440
441 (base_z..min_z as i32).for_each(|z| {
442 let _ = chunk.set(Vec3::new(x, y, z), stone);
443 });
444
445 (min_z as i32..max_z as i32).for_each(|z| {
446 let lpos = Vec3::new(x, y, z);
447 let wpos = Vec3::from(chunk_wpos2d) + lpos;
448
449 if let Some(block) = sampler.get_with_z_cache(wpos, Some(z_cache)) {
450 let _ = chunk.set(lpos, block);
451 }
452 });
453 }
454 }
455
456 let sample_get = |offs| {
457 zcache_grid
458 .get(grid_border + offs)
459 .and_then(Option::as_ref)
460 .map(|zc| &zc.sample)
461 };
462
463 let mut dynamic_rng = ChaCha8Rng::from_seed(thread_rng().gen());
465
466 let mut canvas = Canvas {
468 info: CanvasInfo {
469 chunk_pos,
470 wpos: chunk_pos * TerrainChunkSize::RECT_SIZE.map(|e| e as i32),
471 column_grid: &zcache_grid,
472 column_grid_border: grid_border,
473 chunks: &self.sim,
474 index,
475 chunk: sim_chunk,
476 calendar,
477 },
478 chunk: &mut chunk,
479 entities: Vec::new(),
480 rtsim_resource_blocks: Vec::new(),
481 };
482
483 if index.features.train_tracks {
484 layer::apply_trains_to(&mut canvas, &self.sim, sim_chunk, chunk_center_wpos2d);
485 }
486
487 if index.features.caverns {
488 layer::apply_caverns_to(&mut canvas, &mut dynamic_rng);
489 }
490 if index.features.caves {
491 layer::apply_caves_to(&mut canvas, &mut dynamic_rng);
492 }
493 if index.features.rocks {
494 layer::apply_rocks_to(&mut canvas, &mut dynamic_rng);
495 }
496 if index.features.shrubs {
497 layer::apply_shrubs_to(&mut canvas, &mut dynamic_rng);
498 }
499 if index.features.trees {
500 layer::apply_trees_to(&mut canvas, &mut dynamic_rng, calendar);
501 }
502 if index.features.scatter {
503 layer::apply_scatter_to(&mut canvas, &mut dynamic_rng, calendar);
504 }
505 if index.features.paths {
506 layer::apply_paths_to(&mut canvas);
507 }
508 if index.features.spots {
509 layer::apply_spots_to(&mut canvas, &mut dynamic_rng);
510 }
511 sim_chunk
515 .sites
516 .iter()
517 .for_each(|site| index.sites[*site].render(&mut canvas, &mut dynamic_rng));
518
519 let mut rtsim_resource_blocks = std::mem::take(&mut canvas.rtsim_resource_blocks);
520 let mut supplement = ChunkSupplement {
521 entities: std::mem::take(&mut canvas.entities),
522 rtsim_max_resources: Default::default(),
523 };
524 drop(canvas);
525
526 let gen_entity_pos = |dynamic_rng: &mut ChaCha8Rng| {
527 let lpos2d = TerrainChunkSize::RECT_SIZE
528 .map(|sz| dynamic_rng.gen::<u32>().rem_euclid(sz) as i32);
529 let mut lpos = Vec3::new(
530 lpos2d.x,
531 lpos2d.y,
532 sample_get(lpos2d).map(|s| s.alt as i32 - 32).unwrap_or(0),
533 );
534
535 while let Some(block) = chunk.get(lpos).ok().copied().filter(Block::is_solid) {
536 lpos.z += block.solid_height().ceil() as i32;
537 }
538
539 (Vec3::from(chunk_wpos2d) + lpos).map(|e: i32| e as f32) + 0.5
540 };
541
542 if sim_chunk.contains_waypoint {
543 let waypoint_pos = gen_entity_pos(&mut dynamic_rng);
544 if sim_chunk
545 .sites
546 .iter()
547 .map(|site| index.sites[*site].spawn_rules(waypoint_pos.xy().as_()))
548 .fold(SpawnRules::default(), |a, b| a.combine(b))
549 .waypoints
550 {
551 supplement
552 .add_entity(EntityInfo::at(waypoint_pos).into_special(SpecialEntity::Waypoint));
553 }
554 }
555
556 layer::wildlife::apply_wildlife_supplement(
558 &mut dynamic_rng,
559 chunk_wpos2d,
560 sample_get,
561 &chunk,
562 index,
563 sim_chunk,
564 &mut supplement,
565 time.as_ref(),
566 );
567
568 sim_chunk.sites.iter().for_each(|site| {
570 index.sites[*site].apply_supplement(&mut dynamic_rng, chunk_wpos2d, &mut supplement)
571 });
572
573 chunk.defragment();
575
576 if let Some(rtsim_resources) = rtsim_resources {
581 rtsim_resource_blocks.sort_unstable_by_key(|pos| pos.into_array());
582 rtsim_resource_blocks.dedup();
583 for wpos in rtsim_resource_blocks {
584 let _ = chunk.map(wpos - chunk_wpos2d.with_z(0), |block| {
585 if let Some(res) = block.get_rtsim_resource() {
586 supplement.rtsim_max_resources[res] += 1;
590 if dynamic_rng.gen_bool(rtsim_resources[res] as f64) {
593 block
594 } else {
595 block.into_vacant()
596 }
597 } else {
598 block
599 }
600 });
601 }
602 }
603
604 Ok((chunk, supplement))
605 }
606
607 pub fn get_lod_zone(&self, pos: Vec2<i32>, index: IndexRef) -> lod::Zone {
609 let min_wpos = pos.map(lod::to_wpos);
610 let max_wpos = (pos + 1).map(lod::to_wpos);
611
612 let mut objects = Vec::new();
613
614 prof_span!(guard, "add trees");
616 objects.extend(
617 &mut self
618 .sim()
619 .get_area_trees(min_wpos, max_wpos)
620 .filter_map(|attr| {
621 ColumnGen::new(self.sim())
622 .get((attr.pos, index, self.sim().calendar.as_ref()))
623 .filter(|col| layer::tree::tree_valid_at(attr.pos, col, None, attr.seed))
624 .zip(Some(attr))
625 })
626 .filter_map(|(col, tree)| {
627 Some(lod::Object {
628 kind: match tree.forest_kind {
629 all::ForestKind::Dead => lod::ObjectKind::Dead,
630 all::ForestKind::Pine => lod::ObjectKind::Pine,
631 all::ForestKind::Mangrove => lod::ObjectKind::Mangrove,
632 all::ForestKind::Acacia => lod::ObjectKind::Acacia,
633 all::ForestKind::Birch => lod::ObjectKind::Birch,
634 all::ForestKind::Redwood => lod::ObjectKind::Redwood,
635 all::ForestKind::Baobab => lod::ObjectKind::Baobab,
636 all::ForestKind::Frostpine => lod::ObjectKind::Frostpine,
637 all::ForestKind::Palm => lod::ObjectKind::Palm,
638 _ => lod::ObjectKind::GenericTree,
639 },
640 pos: {
641 let rpos = tree.pos - min_wpos;
642 if rpos.is_any_negative() {
643 return None;
644 } else {
645 rpos.map(|e| e as i16).with_z(col.alt as i16)
646 }
647 },
648 flags: lod::InstFlags::empty()
649 | if col.snow_cover {
650 lod::InstFlags::SNOW_COVERED
651 } else {
652 lod::InstFlags::empty()
653 }
654 | lod::InstFlags::from_bits(((tree.seed % 4) as u8) << 2).expect("This shouldn't set unknown bits"),
656 color: {
657 let field = crate::util::RandomField::new(tree.seed);
658 let lerp = field.get_f32(Vec3::from(tree.pos)) * 0.8 + 0.1;
659 let sblock = tree.forest_kind.leaf_block();
660
661 crate::all::leaf_color(index, tree.seed, lerp, &sblock)
662 .unwrap_or(Rgb::black())
663 },
664 })
665 }),
666 );
667 drop(guard);
668
669 objects.extend(
671 index
672 .sites
673 .iter()
674 .filter(|(_, site)| {
675 site.origin
676 .map2(min_wpos.zip(max_wpos), |e, (min, max)| e >= min && e < max)
677 .reduce_and()
678 })
679 .flat_map(|(_, site)| {
680 site.plots().filter_map(|plot| match &plot.kind {
681 site::plot::PlotKind::House(h) => Some((
682 site.tile_wpos(plot.root_tile),
683 h.roof_color(),
684 lod::ObjectKind::House,
685 )),
686 site::plot::PlotKind::GiantTree(t) => Some((
687 site.tile_wpos(plot.root_tile),
688 t.leaf_color(),
689 lod::ObjectKind::GiantTree,
690 )),
691 site::plot::PlotKind::Haniwa(_) => Some((
692 site.tile_wpos(plot.root_tile),
693 Rgb::black(),
694 lod::ObjectKind::Haniwa,
695 )),
696 site::plot::PlotKind::DesertCityMultiPlot(_) => Some((
697 site.tile_wpos(plot.root_tile),
698 Rgb::black(),
699 lod::ObjectKind::Desert,
700 )),
701 site::plot::PlotKind::DesertCityArena(_) => Some((
702 site.tile_wpos(plot.root_tile),
703 Rgb::black(),
704 lod::ObjectKind::Arena,
705 )),
706 site::plot::PlotKind::SavannahHut(_)
707 | site::plot::PlotKind::SavannahWorkshop(_) => Some((
708 site.tile_wpos(plot.root_tile),
709 Rgb::black(),
710 lod::ObjectKind::SavannahHut,
711 )),
712 site::plot::PlotKind::SavannahAirshipDock(_) => Some((
713 site.tile_wpos(plot.root_tile),
714 Rgb::black(),
715 lod::ObjectKind::SavannahAirshipDock,
716 )),
717 site::plot::PlotKind::TerracottaPalace(_) => Some((
718 site.tile_wpos(plot.root_tile),
719 Rgb::black(),
720 lod::ObjectKind::TerracottaPalace,
721 )),
722 site::plot::PlotKind::TerracottaHouse(_) => Some((
723 site.tile_wpos(plot.root_tile),
724 Rgb::black(),
725 lod::ObjectKind::TerracottaHouse,
726 )),
727 site::plot::PlotKind::TerracottaYard(_) => Some((
728 site.tile_wpos(plot.root_tile),
729 Rgb::black(),
730 lod::ObjectKind::TerracottaYard,
731 )),
732 site::plot::PlotKind::AirshipDock(_) => Some((
733 site.tile_wpos(plot.root_tile),
734 Rgb::black(),
735 lod::ObjectKind::AirshipDock,
736 )),
737 site::plot::PlotKind::CoastalHouse(_) => Some((
738 site.tile_wpos(plot.root_tile),
739 Rgb::black(),
740 lod::ObjectKind::CoastalHouse,
741 )),
742 site::plot::PlotKind::CoastalWorkshop(_) => Some((
743 site.tile_wpos(plot.root_tile),
744 Rgb::black(),
745 lod::ObjectKind::CoastalWorkshop,
746 )),
747 _ => None,
748 })
749 })
750 .filter_map(|(wpos2d, color, model)| {
751 ColumnGen::new(self.sim())
752 .get((wpos2d, index, self.sim().calendar.as_ref()))
753 .zip(Some((wpos2d, color, model)))
754 })
755 .map(|(column, (wpos2d, color, model))| lod::Object {
756 kind: model,
757 pos: (wpos2d - min_wpos)
758 .map(|e| e as i16)
759 .with_z(self.sim().get_alt_approx(wpos2d).unwrap_or(0.0) as i16),
760 flags: if column.snow_cover {
761 lod::InstFlags::SNOW_COVERED
762 } else {
763 lod::InstFlags::empty()
764 },
765 color,
766 }),
767 );
768
769 lod::Zone { objects }
770 }
771
772 pub fn get_location_name(&self, index: IndexRef, wpos2d: Vec2<i32>) -> Option<String> {
774 let chunk_pos = wpos2d.wpos_to_cpos();
775 let sim_chunk = self.sim.get(chunk_pos)?;
776 sim_chunk.get_location_name(&index.sites, &self.civs.pois, wpos2d)
777 }
778}