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