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