veloren_world/
lib.rs

1#![expect(
2    clippy::option_map_unit_fn,
3    clippy::blocks_in_conditions,
4    clippy::identity_op,
5    clippy::needless_pass_by_ref_mut //until we find a better way for specs
6)]
7#![expect(clippy::branches_sharing_code)] // TODO: evaluate
8#![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
27// Reexports
28pub 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        // NOTE: Generating index first in order to quickly fail if the color manifest
140        // is broken.
141        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        // TODO
170    }
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                            // TODO: Probably unify these, at some point
201                            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                            // Score the site according to how suitable it is to be a starting site
226
227                            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                                // Non-town sites should not be chosen as starting sites and get a
239                                // score of 0
240                                _ => return (site_id.id(), 0.0),
241                            };
242
243                            /// Optimal number of plots in a starter town
244                            const OPTIMAL_STARTER_TOWN_SIZE: f32 = 30.0;
245
246                            // Prefer sites of a medium size
247                            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                            // Prefer sites that are close to the centre of the world
261                            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                            // Check if neighboring biomes are beginner friendly
275                            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                                // let chunk_difficulty = 1.0 / chunk.get_biome().difficulty() as
288                                // f32;
289
290                                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    /// Find a position that's accessible to a player at the given world
322    /// position by searching blocks vertically.
323    ///
324    /// If `ascending` is `true`, we try to find the highest accessible position
325    /// instead of the lowest.
326    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        // Unwrapping because generate_chunk only returns err when should_continue evals
335        // to true
336        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        // TODO: misleading name
350        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_interpolated(
378                chunk_pos.map2(chunk_size2d, |e, sz: u32| e * sz as i32 + sz as i32 / 2),
379                |chunk| chunk.get_base_z(),
380            )
381            .and_then(|base_z| self.sim.get(chunk_pos).map(|sim_chunk| (base_z, sim_chunk))) */
382            .get_base_z(chunk_pos)
383        {
384            Some(base_z) => (base_z as i32, self.sim.get(chunk_pos).unwrap()),
385            // Some((base_z, sim_chunk)) => (base_z as i32, sim_chunk),
386            None => {
387                // NOTE: This is necessary in order to generate a handful of chunks at the edges
388                // of the map.
389                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        // Only use for rng affecting dynamic elements like chests and entities!
464        let mut dynamic_rng = ChaCha8Rng::from_seed(thread_rng().gen());
465
466        // Apply layers (paths, caves, etc.)
467        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        // layer::apply_coral_to(&mut canvas);
512
513        // Apply site generation
514        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        // Apply layer supplement
557        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        // Apply site supplementary information
569        sim_chunk.sites.iter().for_each(|site| {
570            index.sites[*site].apply_supplement(&mut dynamic_rng, chunk_wpos2d, &mut supplement)
571        });
572
573        // Finally, defragment to minimize space consumption.
574        chunk.defragment();
575
576        // Before we finish, we check candidate rtsim resource blocks, deduplicating
577        // positions and only keeping those that actually do have resources.
578        // Although this looks potentially very expensive, only blocks that are rtsim
579        // resources (i.e: a relatively small number of sprites) are processed here.
580        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                        // Note: this represents the upper limit, not the actual number spanwed, so
587                        // we increment this before deciding whether we're going to spawn the
588                        // resource.
589                        supplement.rtsim_max_resources[res] += 1;
590                        // Throw a dice to determine whether this resource should actually spawn
591                        // TODO: Don't throw a dice, try to generate the *exact* correct number
592                        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    // Zone coordinates
608    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        // Add trees
615        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                            // Apply random rotation
655                            | 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        // Add structures
670        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    // determine waypoint name
773    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}