1#![expect(
2 clippy::option_map_unit_fn,
3 clippy::blocks_in_conditions,
4 clippy::identity_op,
5 clippy::needless_pass_by_ref_mut )]
7#![expect(clippy::branches_sharing_code)] #![deny(clippy::clone_on_ref_ptr)]
9#![feature(option_zip, let_chains)]
10#![cfg_attr(feature = "simd", feature(portable_simd))]
11
12mod all;
13mod block;
14pub mod canvas;
15pub mod civ;
16mod column;
17pub mod config;
18pub mod index;
19pub mod land;
20pub mod layer;
21pub mod pathfinding;
22pub mod sim;
23pub mod sim2;
24pub mod site;
25pub mod site2;
26pub mod util;
27
28pub 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 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 }
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 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 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 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 std::iter::once((id.id() + num_sites, info.name.clone(), info.location.0))
242 .chain(std::iter::once((id.id() + num_sites + num_caves, info.name.clone(), info.location.1)))
244 }) .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, 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 let (site2, mut score) = match &index.sites[site_id].kind {
273 SiteKind::Refactor(site2) => (site2, 2.0),
274 _ => return (site_id.id(), 0.0)
276 };
277
278 const OPTIMAL_STARTER_TOWN_SIZE: f32 = 30.0;
280
281 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 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 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 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 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 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 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_base_z(chunk_pos)
404 {
405 Some(base_z) => (base_z as i32, self.sim.get(chunk_pos).unwrap()),
406 None => {
408 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 let mut dynamic_rng = ChaCha8Rng::from_seed(thread_rng().gen());
486
487 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 }
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 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 layer::apply_caves_supplement(
580 &mut dynamic_rng,
581 chunk_wpos2d,
582 sample_get,
583 &chunk,
584 index,
585 &mut supplement,
586 );
587
588 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 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 chunk.defragment();
614
615 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 supplement.rtsim_max_resources[res] += 1;
629 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 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 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 | 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 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 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}