Skip to main content

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)]
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::{self, BoxedError, FileAsset, load_ron},
50    calendar::Calendar,
51    comp::Content,
52    generation::{ChunkSupplement, EntityInfo, EntitySpawn, 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::{RngExt, 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        // NOTE: Generating index first in order to quickly fail if the color manifest
141        // is broken.
142        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        // TODO
171    }
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                            // Score the site according to how suitable it is to be a starting site
220
221                            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                                // Non-town sites should not be chosen as starting sites and get a
233                                // score of 0
234                                _ => return (site_id.id(), 0.0),
235                            };
236
237                            /// Optimal number of plots in a starter town
238                            const OPTIMAL_STARTER_TOWN_SIZE: f32 = 30.0;
239
240                            // Prefer sites of a medium size
241                            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                            // Prefer sites that are close to the centre of the world
255                            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                            // Check if neighboring biomes are beginner friendly
269                            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                                // let chunk_difficulty = 1.0 / chunk.get_biome().difficulty() as
282                                // f32;
283
284                                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    /// Find a position that's accessible to a player at the given world
317    /// position by searching blocks vertically.
318    ///
319    /// If `ascending` is `true`, we try to find the highest accessible position
320    /// instead of the lowest.
321    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        // Unwrapping because generate_chunk only returns err when should_continue evals
330        // to true
331        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        // TODO: misleading name
345        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_interpolated(
373                chunk_pos.map2(chunk_size2d, |e, sz: u32| e * sz as i32 + sz as i32 / 2),
374                |chunk| chunk.get_base_z(),
375            )
376            .and_then(|base_z| self.sim.get(chunk_pos).map(|sim_chunk| (base_z, sim_chunk))) */
377            .get_base_z(chunk_pos)
378        {
379            Some(base_z) => (base_z as i32, self.sim.get(chunk_pos).unwrap()),
380            // Some((base_z, sim_chunk)) => (base_z as i32, sim_chunk),
381            None => {
382                // NOTE: This is necessary in order to generate a handful of chunks at the edges
383                // of the map.
384                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        // Only use for rng affecting dynamic elements like chests and entities!
459        let mut dynamic_rng = ChaCha8Rng::from_seed(rand::rng().random());
460
461        // Apply layers (paths, caves, etc.)
462        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            entity_spawns: 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        // layer::apply_coral_to(&mut canvas);
507
508        // Apply site generation
509        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            entity_spawns: std::mem::take(&mut canvas.entity_spawns),
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            let mut spawn_rules = SpawnRules::default();
540            for site in sim_chunk.sites.iter().map(|site| &index.sites[*site]) {
541                site.spawn_rules(
542                    &mut spawn_rules,
543                    &Land::from_sim(&self.sim),
544                    waypoint_pos.xy().as_(),
545                );
546            }
547            if spawn_rules.waypoints {
548                supplement.add_entity_spawn(EntitySpawn::Entity(Box::new(
549                    EntityInfo::at(waypoint_pos).into_special(SpecialEntity::Waypoint),
550                )));
551            }
552        }
553
554        // Apply layer supplement
555        layer::wildlife::apply_wildlife_supplement(
556            &mut dynamic_rng,
557            chunk_wpos2d,
558            sample_get,
559            &chunk,
560            index,
561            sim_chunk,
562            &mut supplement,
563            time.as_ref(),
564        );
565
566        // Apply site supplementary information
567        sim_chunk.sites.iter().for_each(|site| {
568            index.sites[*site].apply_supplement(&mut dynamic_rng, chunk_wpos2d, &mut supplement)
569        });
570
571        // Finally, defragment to minimize space consumption.
572        chunk.defragment();
573
574        // Before we finish, we check candidate rtsim resource blocks, deduplicating
575        // positions and only keeping those that actually do have resources.
576        // Although this looks potentially very expensive, only blocks that are rtsim
577        // resources (i.e: a relatively small number of sprites) are processed here.
578        if let Some(rtsim_resources) = rtsim_resources {
579            rtsim_resource_blocks.sort_unstable_by_key(|pos| pos.into_array());
580            rtsim_resource_blocks.dedup();
581            for wpos in rtsim_resource_blocks {
582                let _ = chunk.map(wpos - chunk_wpos2d.with_z(0), |block| {
583                    if let Some(res) = block.get_rtsim_resource() {
584                        // Note: this represents the upper limit, not the actual number spanwed, so
585                        // we increment this before deciding whether we're going to spawn the
586                        // resource.
587                        supplement.rtsim_max_resources[res] += 1;
588
589                        debug_assert!(
590                            0.0 <= rtsim_resources[res] && rtsim_resources[res] <= 1.0,
591                            "The rtsim resource {res:?} has the value '{}', which is not in the \
592                             expected range of 0.0..=1.0. When registering a block with the \
593                             sprite `{:?}`, with the damage `{:?}`.",
594                            rtsim_resources[res],
595                            block.get_sprite(),
596                            block.get_attr::<common::terrain::sprite::Damage>().ok(),
597                        );
598
599                        // Throw a dice to determine whether this resource should actually spawn
600                        // TODO: Don't throw a dice, try to generate the *exact* correct number
601                        if dynamic_rng.random_bool(rtsim_resources[res].clamp(0.0, 1.0) as f64) {
602                            block
603                        } else {
604                            block.into_vacant()
605                        }
606                    } else {
607                        block
608                    }
609                });
610            }
611        }
612
613        Ok((chunk, supplement))
614    }
615
616    // Zone coordinates
617    pub fn get_lod_zone(&self, pos: Vec2<i32>, index: IndexRef) -> lod::Zone {
618        let min_wpos = pos.map(lod::to_wpos);
619        let max_wpos = (pos + 1).map(lod::to_wpos);
620
621        let mut objects = Vec::new();
622
623        // Add trees
624        prof_span!(guard, "add trees");
625        objects.extend(
626            &mut self
627                .sim()
628                .get_area_trees(min_wpos, max_wpos)
629                .filter_map(|attr| {
630                    ColumnGen::new(self.sim())
631                        .get((attr.pos, index, self.sim().calendar.as_ref()))
632                        .filter(|col| layer::tree::tree_valid_at(attr.pos, col, None, attr.seed))
633                        .zip(Some(attr))
634                })
635                .filter_map(|(col, tree)| {
636                    Some(lod::Object {
637                        kind: match tree.forest_kind {
638                            all::ForestKind::Dead => lod::ObjectKind::Dead,
639                            all::ForestKind::Pine => lod::ObjectKind::Pine,
640                            all::ForestKind::Mangrove => lod::ObjectKind::Mangrove,
641                            all::ForestKind::Acacia => lod::ObjectKind::Acacia,
642                            all::ForestKind::Birch => lod::ObjectKind::Birch,
643                            all::ForestKind::Redwood => lod::ObjectKind::Redwood,
644                            all::ForestKind::Baobab => lod::ObjectKind::Baobab,
645                            all::ForestKind::Frostpine => lod::ObjectKind::Frostpine,
646                            all::ForestKind::Palm => lod::ObjectKind::Palm,
647                            _ => lod::ObjectKind::GenericTree,
648                        },
649                        pos: {
650                            let rpos = tree.pos - min_wpos;
651                            if rpos.is_any_negative() {
652                                return None;
653                            } else {
654                                rpos.map(|e| e as i16).with_z(col.alt as i16)
655                            }
656                        },
657                        flags: lod::InstFlags::empty()
658                            | if col.snow_cover {
659                                lod::InstFlags::SNOW_COVERED
660                            } else {
661                                lod::InstFlags::empty()
662                            }
663                            // Apply random rotation
664                            | lod::InstFlags::from_bits(((tree.seed % 4) as u8) << 2).expect("This shouldn't set unknown bits"),
665                        color: {
666                            let field = crate::util::RandomField::new(tree.seed);
667                            let lerp = field.get_f32(Vec3::from(tree.pos)) * 0.8 + 0.1;
668                            let sblock = tree.forest_kind.leaf_block();
669
670                            crate::all::leaf_color(index, tree.seed, lerp, &sblock)
671                                .unwrap_or(Rgb::black())
672                        },
673                    })
674                }),
675        );
676        drop(guard);
677
678        // Add structures
679        objects.extend(
680            index
681                .sites
682                .iter()
683                .filter(|(_, site)| {
684                    site.origin
685                        .map2(min_wpos.zip(max_wpos), |e, (min, max)| e >= min && e < max)
686                        .reduce_and()
687                })
688                .flat_map(|(_, site)| {
689                    site.plots().filter_map(|plot| match &plot.kind {
690                        site::plot::PlotKind::House(h) => Some((
691                            site.tile_wpos(plot.root_tile),
692                            h.roof_color(),
693                            lod::ObjectKind::House,
694                        )),
695                        site::plot::PlotKind::GiantTree(t) => Some((
696                            site.tile_wpos(plot.root_tile),
697                            t.leaf_color(),
698                            lod::ObjectKind::GiantTree,
699                        )),
700                        site::plot::PlotKind::Haniwa(_) => Some((
701                            site.tile_wpos(plot.root_tile),
702                            Rgb::black(),
703                            lod::ObjectKind::Haniwa,
704                        )),
705                        site::plot::PlotKind::DesertCityMultiPlot(_) => Some((
706                            site.tile_wpos(plot.root_tile),
707                            Rgb::black(),
708                            lod::ObjectKind::Desert,
709                        )),
710                        site::plot::PlotKind::DesertCityArena(_) => Some((
711                            site.tile_wpos(plot.root_tile),
712                            Rgb::black(),
713                            lod::ObjectKind::Arena,
714                        )),
715                        site::plot::PlotKind::SavannahHut(_)
716                        | site::plot::PlotKind::SavannahWorkshop(_) => Some((
717                            site.tile_wpos(plot.root_tile),
718                            Rgb::black(),
719                            lod::ObjectKind::SavannahHut,
720                        )),
721                        site::plot::PlotKind::SavannahAirshipDock(_) => Some((
722                            site.tile_wpos(plot.root_tile),
723                            Rgb::black(),
724                            lod::ObjectKind::SavannahAirshipDock,
725                        )),
726                        site::plot::PlotKind::TerracottaPalace(_) => Some((
727                            site.tile_wpos(plot.root_tile),
728                            Rgb::black(),
729                            lod::ObjectKind::TerracottaPalace,
730                        )),
731                        site::plot::PlotKind::TerracottaHouse(_) => Some((
732                            site.tile_wpos(plot.root_tile),
733                            Rgb::black(),
734                            lod::ObjectKind::TerracottaHouse,
735                        )),
736                        site::plot::PlotKind::TerracottaYard(_) => Some((
737                            site.tile_wpos(plot.root_tile),
738                            Rgb::black(),
739                            lod::ObjectKind::TerracottaYard,
740                        )),
741                        site::plot::PlotKind::AirshipDock(_) => Some((
742                            site.tile_wpos(plot.root_tile),
743                            Rgb::black(),
744                            lod::ObjectKind::AirshipDock,
745                        )),
746                        site::plot::PlotKind::CoastalHouse(_) => Some((
747                            site.tile_wpos(plot.root_tile),
748                            Rgb::black(),
749                            lod::ObjectKind::CoastalHouse,
750                        )),
751                        site::plot::PlotKind::CoastalWorkshop(_) => Some((
752                            site.tile_wpos(plot.root_tile),
753                            Rgb::black(),
754                            lod::ObjectKind::CoastalWorkshop,
755                        )),
756                        site::plot::PlotKind::CoastalAirshipDock(_) => Some((
757                            site.tile_wpos(plot.root_tile),
758                            Rgb::black(),
759                            lod::ObjectKind::CoastalAirshipDock,
760                        )),
761                        site::plot::PlotKind::DesertCityAirshipDock(_) => Some((
762                            site.tile_wpos(plot.root_tile),
763                            Rgb::black(),
764                            lod::ObjectKind::DesertCityAirshipDock,
765                        )),
766                        site::plot::PlotKind::CliffTownAirshipDock(_) => Some((
767                            site.tile_wpos(plot.root_tile),
768                            Rgb::black(),
769                            lod::ObjectKind::CliffTownAirshipDock,
770                        )),
771                        _ => None,
772                    })
773                })
774                .filter_map(|(wpos2d, color, model)| {
775                    ColumnGen::new(self.sim())
776                        .get((wpos2d, index, self.sim().calendar.as_ref()))
777                        .zip(Some((wpos2d, color, model)))
778                })
779                .map(|(column, (wpos2d, color, model))| lod::Object {
780                    kind: model,
781                    pos: (wpos2d - min_wpos)
782                        .map(|e| e as i16)
783                        .with_z(self.sim().get_alt_approx(wpos2d).unwrap_or(0.0) as i16),
784                    flags: if column.snow_cover {
785                        lod::InstFlags::SNOW_COVERED
786                    } else {
787                        lod::InstFlags::empty()
788                    },
789                    color,
790                }),
791        );
792
793        lod::Zone { objects }
794    }
795
796    // determine waypoint name
797    pub fn get_location_name(&self, index: IndexRef, wpos2d: Vec2<i32>) -> Option<String> {
798        let chunk_pos = wpos2d.wpos_to_cpos();
799        let sim_chunk = self.sim.get(chunk_pos)?;
800        sim_chunk.get_location_name(&index.sites, &self.civs.pois, wpos2d)
801    }
802}