veloren_server/sys/
terrain.rs

1#[cfg(feature = "persistent_world")]
2use crate::TerrainPersistence;
3#[cfg(not(feature = "worldgen"))]
4use crate::test_world::{IndexOwned, World};
5use tracing::error;
6#[cfg(feature = "worldgen")]
7use world::{IndexOwned, World};
8
9#[cfg(feature = "worldgen")] use crate::rtsim;
10use crate::{
11    ChunkRequest, Tick, chunk_generator::ChunkGenerator, chunk_serialize::ChunkSendEntry,
12    client::Client, presence::RepositionOnChunkLoad, settings::Settings,
13};
14use common::{
15    SkillSetBuilder,
16    calendar::Calendar,
17    combat::{DeathEffects, RiderEffects},
18    comp::{
19        self, BehaviorCapability, ForceUpdate, Pos, Presence, Waypoint, agent, biped_small,
20        bird_medium,
21    },
22    event::{CreateNpcEvent, CreateSpecialEntityEvent, EmitExt, EventBus, NpcBuilder},
23    event_emitters,
24    generation::{EntityInfo, SpecialEntity},
25    lottery::LootSpec,
26    resources::{Time, TimeOfDay},
27    slowjob::SlowJobPool,
28    terrain::TerrainGrid,
29    util::Dir,
30};
31
32use common_ecs::{Job, Origin, Phase, System};
33use common_net::msg::ServerGeneral;
34use common_state::TerrainChanges;
35use comp::Behavior;
36use core::cmp::Reverse;
37use itertools::Itertools;
38use rayon::{iter::Either, prelude::*};
39use specs::{
40    Entities, Entity, Join, LendJoin, ParJoin, Read, ReadExpect, ReadStorage, SystemData, Write,
41    WriteExpect, WriteStorage, shred, storage::GenericReadStorage,
42};
43use std::{f32::consts::TAU, sync::Arc};
44use vek::*;
45
46#[cfg(feature = "persistent_world")]
47pub type TerrainPersistenceData<'a> = Option<Write<'a, TerrainPersistence>>;
48#[cfg(not(feature = "persistent_world"))]
49pub type TerrainPersistenceData<'a> = ();
50
51pub const SAFE_ZONE_RADIUS: f32 = 200.0;
52
53#[cfg(feature = "worldgen")]
54type RtSimData<'a> = WriteExpect<'a, rtsim::RtSim>;
55#[cfg(not(feature = "worldgen"))]
56type RtSimData<'a> = ();
57
58event_emitters! {
59    struct Events[Emitters] {
60        create_npc: CreateNpcEvent,
61        create_waypoint: CreateSpecialEntityEvent,
62    }
63}
64
65#[derive(SystemData)]
66pub struct Data<'a> {
67    events: Events<'a>,
68    tick: Read<'a, Tick>,
69    server_settings: Read<'a, Settings>,
70    time_of_day: Read<'a, TimeOfDay>,
71    calendar: Read<'a, Calendar>,
72    slow_jobs: ReadExpect<'a, SlowJobPool>,
73    index: ReadExpect<'a, IndexOwned>,
74    world: ReadExpect<'a, Arc<World>>,
75    chunk_send_bus: ReadExpect<'a, EventBus<ChunkSendEntry>>,
76    chunk_generator: WriteExpect<'a, ChunkGenerator>,
77    terrain: WriteExpect<'a, TerrainGrid>,
78    terrain_changes: Write<'a, TerrainChanges>,
79    chunk_requests: Write<'a, Vec<ChunkRequest>>,
80    rtsim: RtSimData<'a>,
81    #[cfg(feature = "persistent_world")]
82    terrain_persistence: TerrainPersistenceData<'a>,
83    positions: WriteStorage<'a, Pos>,
84    presences: ReadStorage<'a, Presence>,
85    clients: ReadStorage<'a, Client>,
86    entities: Entities<'a>,
87    reposition_on_load: WriteStorage<'a, RepositionOnChunkLoad>,
88    forced_updates: WriteStorage<'a, ForceUpdate>,
89    waypoints: WriteStorage<'a, Waypoint>,
90    time: ReadExpect<'a, Time>,
91}
92
93/// This system will handle loading generated chunks and unloading
94/// unneeded chunks.
95///     1. Inserts newly generated chunks into the TerrainGrid
96///     2. Sends new chunks to nearby clients
97///     3. Handles the chunk's supplement (e.g. npcs)
98///     4. Removes chunks outside the range of players
99#[derive(Default)]
100pub struct Sys;
101impl<'a> System<'a> for Sys {
102    type SystemData = Data<'a>;
103
104    const NAME: &'static str = "terrain";
105    const ORIGIN: Origin = Origin::Server;
106    const PHASE: Phase = Phase::Create;
107
108    fn run(_job: &mut Job<Self>, mut data: Self::SystemData) {
109        let mut emitters = data.events.get_emitters();
110
111        // Generate requested chunks
112        //
113        // Submit requests for chunks right before receiving finished chunks so that we
114        // don't create duplicate work for chunks that just finished but are not
115        // yet added to the terrain.
116        data.chunk_requests.drain(..).for_each(|request| {
117            data.chunk_generator.generate_chunk(
118                Some(request.entity),
119                request.key,
120                &data.slow_jobs,
121                Arc::clone(&data.world),
122                &data.rtsim,
123                data.index.clone(),
124                (*data.time_of_day, data.calendar.clone()),
125            )
126        });
127
128        let mut rng = rand::thread_rng();
129        // Fetch any generated `TerrainChunk`s and insert them into the terrain.
130        // Also, send the chunk data to anybody that is close by.
131        let mut new_chunks = Vec::new();
132        'insert_terrain_chunks: while let Some((key, res)) = data.chunk_generator.recv_new_chunk() {
133            #[cfg_attr(not(feature = "persistent_world"), expect(unused_mut))]
134            let (mut chunk, supplement) = match res {
135                Ok((chunk, supplement)) => (chunk, supplement),
136                Err(Some(entity)) => {
137                    if let Some(client) = data.clients.get(entity) {
138                        client.send_fallible(ServerGeneral::TerrainChunkUpdate {
139                            key,
140                            chunk: Err(()),
141                        });
142                    }
143                    continue 'insert_terrain_chunks;
144                },
145                Err(None) => {
146                    continue 'insert_terrain_chunks;
147                },
148            };
149
150            // Apply changes from terrain persistence to this chunk
151            #[cfg(feature = "persistent_world")]
152            if let Some(terrain_persistence) = data.terrain_persistence.as_mut() {
153                terrain_persistence.apply_changes(key, &mut chunk);
154            }
155
156            // Arcify the chunk
157            let chunk = Arc::new(chunk);
158
159            // Add to list of chunks to send to nearby players.
160            new_chunks.push(key);
161
162            // TODO: code duplication for chunk insertion between here and state.rs
163            // Insert the chunk into terrain changes
164            if data.terrain.insert(key, chunk).is_some() {
165                data.terrain_changes.modified_chunks.insert(key);
166            } else {
167                data.terrain_changes.new_chunks.insert(key);
168                #[cfg(feature = "worldgen")]
169                data.rtsim
170                    .hook_load_chunk(key, supplement.rtsim_max_resources);
171            }
172
173            // Handle chunk supplement
174            for entity in supplement.entities {
175                // Check this because it's a common source of weird bugs
176                assert!(
177                    data.terrain
178                        .pos_key(entity.pos.map(|e| e.floor() as i32))
179                        .map2(key, |e, tgt| (e - tgt).abs() <= 1)
180                        .reduce_and(),
181                    "Chunk spawned entity that wasn't nearby",
182                );
183
184                let data = SpawnEntityData::from_entity_info(entity);
185                match data {
186                    SpawnEntityData::Special(pos, entity) => {
187                        emitters.emit(CreateSpecialEntityEvent { pos, entity });
188                    },
189                    SpawnEntityData::Npc(data) => {
190                        let (npc_builder, pos) = data.to_npc_builder();
191
192                        emitters.emit(CreateNpcEvent {
193                            pos,
194                            ori: comp::Ori::from(Dir::random_2d(&mut rng)),
195                            npc: npc_builder.with_anchor(comp::Anchor::Chunk(key)),
196                        });
197                    },
198                }
199            }
200        }
201
202        // TODO: Consider putting this in another system since this forces us to take
203        // positions by write rather than read access.
204        let repositioned = (&data.entities, &mut data.positions, (&mut data.forced_updates).maybe(), &data.reposition_on_load)
205            // TODO: Consider using par_bridge() because Rayon has very poor work splitting for
206            // sparse joins.
207            .par_join()
208            .filter_map(|(entity, pos, force_update, reposition)| {
209                // NOTE: We use regular as casts rather than as_ because we want to saturate on
210                // overflow.
211                let entity_pos = pos.0.map(|x| x as i32);
212                // If an entity is marked as needing repositioning once the chunk loads (e.g.
213                // from having just logged in), reposition them.
214                let chunk_pos = TerrainGrid::chunk_key(entity_pos);
215                let chunk = data.terrain.get_key(chunk_pos)?;
216                let new_pos = if reposition.needs_ground {
217                    data.terrain.try_find_ground(entity_pos)
218                } else {
219                    data.terrain.try_find_space(entity_pos)
220                }.map(|x| x.as_::<f32>()).unwrap_or_else(|| chunk.find_accessible_pos(entity_pos.xy(), false));
221                pos.0 = new_pos;
222                force_update.map(|force_update| force_update.update());
223                Some((entity, new_pos))
224            })
225            .collect::<Vec<_>>();
226
227        for (entity, new_pos) in repositioned {
228            if let Some(waypoint) = data.waypoints.get_mut(entity) {
229                *waypoint = Waypoint::new(new_pos, *data.time);
230            }
231            data.reposition_on_load.remove(entity);
232        }
233
234        let max_view_distance = data.server_settings.max_view_distance.unwrap_or(u32::MAX);
235        #[cfg(feature = "worldgen")]
236        let world_size = data.world.sim().get_size();
237        #[cfg(not(feature = "worldgen"))]
238        let world_size = data.world.map_size_lg().chunks().map(u32::from);
239        let (presences_position_entities, presences_positions) = prepare_player_presences(
240            world_size,
241            max_view_distance,
242            &data.entities,
243            &data.positions,
244            &data.presences,
245            &data.clients,
246        );
247        let real_max_view_distance = convert_to_loaded_vd(u32::MAX, max_view_distance);
248
249        // Send the chunks to all nearby players.
250        new_chunks.par_iter().for_each_init(
251            || data.chunk_send_bus.emitter(),
252            |chunk_send_emitter, chunk_key| {
253                // We only have to check players inside the maximum view distance of the server
254                // of our own position.
255                //
256                // We start by partitioning by X, finding only entities in chunks within the X
257                // range of us.  These are guaranteed in bounds due to restrictions on max view
258                // distance (namely: the square of any chunk coordinate plus the max view
259                // distance along both axes must fit in an i32).
260                let min_chunk_x = chunk_key.x - real_max_view_distance;
261                let max_chunk_x = chunk_key.x + real_max_view_distance;
262                let start = presences_position_entities
263                    .partition_point(|((pos, _), _)| i32::from(pos.x) < min_chunk_x);
264                // NOTE: We *could* just scan forward until we hit the end, but this way we save
265                // a comparison in the inner loop, since also needs to check the
266                // list length.  We could also save some time by starting from
267                // start rather than end, but the hope is that this way the
268                // compiler (and machine) can reorder things so both ends are
269                // fetched in parallel; since the vast majority of the time both fetched
270                // elements should already be in cache, this should not use any
271                // extra memory bandwidth.
272                //
273                // TODO: Benchmark and figure out whether this is better in practice than just
274                // scanning forward.
275                let end = presences_position_entities
276                    .partition_point(|((pos, _), _)| i32::from(pos.x) < max_chunk_x);
277                let interior = &presences_position_entities[start..end];
278                interior
279                    .iter()
280                    .filter(|((player_chunk_pos, player_vd_sqr), _)| {
281                        chunk_in_vd(*player_chunk_pos, *player_vd_sqr, *chunk_key)
282                    })
283                    .for_each(|(_, entity)| {
284                        chunk_send_emitter.emit(ChunkSendEntry {
285                            entity: *entity,
286                            chunk_key: *chunk_key,
287                        });
288                    });
289            },
290        );
291
292        let tick = (data.tick.0 % 16) as i32;
293
294        // Remove chunks that are too far from players.
295        //
296        // Note that all chunks involved here (both terrain chunks and pending chunks)
297        // are guaranteed in bounds.  This simplifies the rest of the logic
298        // here.
299        let chunks_to_remove = data.terrain
300            .par_keys()
301            .copied()
302            // There may be lots of pending chunks, so don't check them all.  This should be okay
303            // as long as we're maintaining a reasonable tick rate.
304            .chain(data.chunk_generator.par_pending_chunks())
305            // Don't check every chunk every tick (spread over 16 ticks)
306            //
307            // TODO: Investigate whether we can add support for performing this filtering directly
308            // within hashbrown (basically, specify we want to iterate through just buckets with
309            // hashes in a particular range).  This could provide significiant speedups since we
310            // could avoid having to iterate through a bunch of buckets we don't care about.
311            //
312            // TODO: Make the percentage of the buckets that we go through adjust dynamically
313            // depending on the current number of chunks.  In the worst case, we might want to scan
314            // just 1/256 of the chunks each tick, for example.
315            .filter(|k| k.x % 4 + (k.y % 4) * 4 == tick)
316            .filter(|&chunk_key| {
317                // We only have to check players inside the maximum view distance of the server of
318                // our own position.
319                //
320                // We start by partitioning by X, finding only entities in chunks within the X
321                // range of us.  These are guaranteed in bounds due to restrictions on max view
322                // distance (namely: the square of any chunk coordinate plus the max view distance
323                // along both axes must fit in an i32).
324                let min_chunk_x = chunk_key.x - real_max_view_distance;
325                let max_chunk_x = chunk_key.x + real_max_view_distance;
326                let start = presences_positions
327                    .partition_point(|(pos, _)| i32::from(pos.x) < min_chunk_x);
328                // NOTE: We *could* just scan forward until we hit the end, but this way we save a
329                // comparison in the inner loop, since also needs to check the list length.  We
330                // could also save some time by starting from start rather than end, but the hope
331                // is that this way the compiler (and machine) can reorder things so both ends are
332                // fetched in parallel; since the vast majority of the time both fetched elements
333                // should already be in cache, this should not use any extra memory bandwidth.
334                //
335                // TODO: Benchmark and figure out whether this is better in practice than just
336                // scanning forward.
337                let end = presences_positions
338                    .partition_point(|(pos, _)| i32::from(pos.x) < max_chunk_x);
339                let interior = &presences_positions[start..end];
340                !interior.iter().any(|&(player_chunk_pos, player_vd_sqr)| {
341                    chunk_in_vd(player_chunk_pos, player_vd_sqr, chunk_key)
342                })
343            })
344            .collect::<Vec<_>>();
345
346        let chunks_to_remove = chunks_to_remove
347            .into_iter()
348            .filter_map(|key| {
349                // Register the unloading of this chunk from terrain persistence
350                #[cfg(feature = "persistent_world")]
351                if let Some(terrain_persistence) = data.terrain_persistence.as_mut() {
352                    terrain_persistence.unload_chunk(key);
353                }
354
355                data.chunk_generator.cancel_if_pending(key);
356
357                // If you want to trigger any behaivour on unload, do it in `Server::tick` by
358                // reading `TerrainChanges::removed_chunks` since chunks can also be removed
359                // using eg. /reload_chunks
360
361                // TODO: code duplication for chunk insertion between here and state.rs
362                data.terrain.remove(key).inspect(|_| {
363                    data.terrain_changes.removed_chunks.insert(key);
364                })
365            })
366            .collect::<Vec<_>>();
367        if !chunks_to_remove.is_empty() {
368            // Drop chunks in a background thread.
369            data.slow_jobs.spawn("CHUNK_DROP", move || {
370                drop(chunks_to_remove);
371            });
372        }
373    }
374}
375
376// TODO: better name
377#[derive(Debug)]
378pub struct NpcData {
379    pub pos: Pos,
380    pub stats: comp::Stats,
381    pub skill_set: comp::SkillSet,
382    pub health: Option<comp::Health>,
383    pub poise: comp::Poise,
384    pub inventory: comp::inventory::Inventory,
385    pub agent: Option<comp::Agent>,
386    pub body: comp::Body,
387    pub alignment: comp::Alignment,
388    pub scale: comp::Scale,
389    pub loot: LootSpec<String>,
390    pub pets: Vec<(NpcData, Vec3<f32>)>,
391    pub death_effects: Option<DeathEffects>,
392    pub rider_effects: Option<RiderEffects>,
393    pub rider: Option<Box<NpcData>>,
394}
395
396/// Convinient structure to use when you need to create new npc
397/// from EntityInfo
398// TODO: better name?
399// TODO: if this is send around network, optimize the large_enum_variant
400#[derive(Debug)]
401pub enum SpawnEntityData {
402    Npc(NpcData),
403    Special(Vec3<f32>, SpecialEntity),
404}
405
406impl SpawnEntityData {
407    pub fn from_entity_info(entity: EntityInfo) -> Self {
408        let EntityInfo {
409            // flags
410            special_entity,
411            has_agency,
412            agent_mark,
413            alignment,
414            no_flee,
415            idle_wander_factor,
416            aggro_range_multiplier,
417            // stats
418            body,
419            name,
420            scale,
421            pos,
422            loot,
423            // tools and skills
424            skillset_asset,
425            loadout: mut loadout_builder,
426            inventory: items,
427            make_loadout,
428            trading_information: economy,
429            pets,
430            rider,
431            death_effects,
432            rider_effects,
433        } = entity;
434
435        if let Some(special) = special_entity {
436            return Self::Special(pos, special);
437        }
438
439        let name = name.unwrap_or_else(|| "Unnamed".to_string());
440        let stats = comp::Stats::new(name, body);
441
442        let skill_set = {
443            let skillset_builder = SkillSetBuilder::default();
444            if let Some(skillset_asset) = skillset_asset {
445                skillset_builder.with_asset_expect(&skillset_asset).build()
446            } else {
447                skillset_builder.build()
448            }
449        };
450
451        let inventory = {
452            // Evaluate lazy function for loadout creation
453            if let Some(make_loadout) = make_loadout {
454                loadout_builder =
455                    loadout_builder.with_creator(make_loadout, economy.as_ref(), None);
456            }
457            let loadout = loadout_builder.build();
458            let mut inventory = comp::inventory::Inventory::with_loadout(loadout, body);
459            for (num, mut item) in items {
460                if let Err(e) = item.set_amount(num) {
461                    tracing::warn!(
462                        "error during creating inventory for {name} at {pos}: {e:?}",
463                        name = &stats.name,
464                    );
465                }
466                if let Err(e) = inventory.push(item) {
467                    tracing::warn!(
468                        "error during creating inventory for {name} at {pos}: {e:?}",
469                        name = &stats.name,
470                    );
471                }
472            }
473
474            inventory
475        };
476
477        let health = Some(comp::Health::new(body));
478        let poise = comp::Poise::new(body);
479
480        // Allow Humanoid, BirdMedium, and Parrot to speak
481        let can_speak = match body {
482            comp::Body::Humanoid(_) => true,
483            comp::Body::BipedSmall(biped_small) => {
484                matches!(biped_small.species, biped_small::Species::Flamekeeper)
485            },
486            comp::Body::BirdMedium(bird_medium) => match bird_medium.species {
487                bird_medium::Species::Parrot => alignment == comp::Alignment::Npc,
488                _ => false,
489            },
490            _ => false,
491        };
492
493        let trade_for_site = if matches!(agent_mark, Some(agent::Mark::Merchant)) {
494            economy.map(|e| e.id)
495        } else {
496            None
497        };
498
499        let agent = has_agency.then(|| {
500            let mut agent = comp::Agent::from_body(&body).with_behavior(
501                Behavior::default()
502                    .maybe_with_capabilities(can_speak.then_some(BehaviorCapability::SPEAK))
503                    .maybe_with_capabilities(trade_for_site.map(|_| BehaviorCapability::TRADE))
504                    .with_trade_site(trade_for_site),
505            );
506
507            // Non-humanoids get a patrol origin to stop them moving too far
508            if !matches!(body, comp::Body::Humanoid(_)) {
509                agent = agent.with_patrol_origin(pos);
510            }
511
512            agent
513                .with_no_flee_if(matches!(agent_mark, Some(agent::Mark::Guard)) || no_flee)
514                .with_idle_wander_factor(idle_wander_factor)
515                .with_aggro_range_multiplier(aggro_range_multiplier)
516        });
517
518        let agent = if matches!(alignment, comp::Alignment::Enemy)
519            && matches!(body, comp::Body::Humanoid(_))
520        {
521            agent.map(|a| a.with_aggro_no_warn().with_no_flee_if(true))
522        } else {
523            agent
524        };
525
526        SpawnEntityData::Npc(NpcData {
527            pos: Pos(pos),
528            stats,
529            skill_set,
530            health,
531            poise,
532            inventory,
533            agent,
534            body,
535            alignment,
536            scale: comp::Scale(scale),
537            loot,
538            pets: {
539                let pet_count = pets.len() as f32;
540                pets.into_iter()
541                    .enumerate()
542                    .flat_map(|(i, pet)| {
543                        Some((
544                            SpawnEntityData::from_entity_info(pet)
545                                .into_npc_data_inner()
546                                .inspect_err(|data| {
547                                    error!("Pets must be SpawnEntityData::Npc, but found: {data:?}")
548                                })
549                                .ok()?,
550                            Vec2::one()
551                                .rotated_z(TAU * (i as f32 / pet_count))
552                                .with_z(0.0)
553                                * ((pet_count * 3.0) / TAU),
554                        ))
555                    })
556                    .collect()
557            },
558            rider: rider.and_then(|e| {
559                Some(Box::new(
560                    SpawnEntityData::from_entity_info(*e)
561                        .into_npc_data_inner()
562                        .ok()?,
563                ))
564            }),
565            death_effects,
566            rider_effects,
567        })
568    }
569
570    pub fn into_npc_data_inner(self) -> Result<NpcData, Self> {
571        match self {
572            SpawnEntityData::Npc(inner) => Ok(inner),
573            other => Err(other),
574        }
575    }
576}
577
578impl NpcData {
579    pub fn to_npc_builder(self) -> (NpcBuilder, comp::Pos) {
580        let NpcData {
581            pos,
582            stats,
583            skill_set,
584            health,
585            poise,
586            inventory,
587            agent,
588            body,
589            alignment,
590            scale,
591            loot,
592            pets,
593            death_effects,
594            rider_effects,
595            rider,
596        } = self;
597
598        (
599            NpcBuilder::new(stats, body, alignment)
600                .with_skill_set(skill_set)
601                .with_health(health)
602                .with_poise(poise)
603                .with_inventory(inventory)
604                .with_agent(agent)
605                .with_scale(scale)
606                .with_loot(loot)
607                .with_pets(
608                    pets.into_iter()
609                        .map(|(pet, offset)| (pet.to_npc_builder().0, offset))
610                        .collect::<Vec<_>>(),
611                )
612                .with_rider(rider.map(|rider| rider.to_npc_builder().0))
613                .with_death_effects(death_effects)
614                .with_rider_effects(rider_effects),
615            pos,
616        )
617    }
618}
619
620pub fn convert_to_loaded_vd(vd: u32, max_view_distance: u32) -> i32 {
621    // Hardcoded max VD to prevent stupid view distances from creating overflows.
622    // This must be a value ≤
623    // √(i32::MAX - 2 * ((1 << (MAX_WORLD_BLOCKS_LG - TERRAIN_CHUNK_BLOCKS_LG) - 1)²
624    // - 1)) / 2
625    //
626    // since otherwise we could end up overflowing.  Since it is a requirement that
627    // each dimension (in chunks) has to fit in a i16, we can derive √((1<<31)-1
628    // - 2*((1<<15)-1)^2) / 2 ≥ 1 << 7 as the absolute limit.
629    //
630    // TODO: Make this more official and use it elsewhere.
631    const MAX_VD: u32 = 1 << 7;
632
633    // This fuzzy threshold prevents chunks rapidly unloading and reloading when
634    // players move over a chunk border.
635    const UNLOAD_THRESHOLD: u32 = 2;
636
637    // NOTE: This cast is safe for the reasons mentioned above.
638    (vd.clamp(crate::MIN_VD, max_view_distance)
639        .saturating_add(UNLOAD_THRESHOLD))
640    .min(MAX_VD) as i32
641}
642
643/// Returns: ((player_chunk_pos, player_vd_squared), entity, is_client)
644fn prepare_for_vd_check(
645    world_aabr_in_chunks: &Aabr<i32>,
646    max_view_distance: u32,
647    entity: Entity,
648    presence: &Presence,
649    pos: &Pos,
650    client: Option<u32>,
651) -> Option<((Vec2<i16>, i32), Entity, bool)> {
652    let is_client = client.is_some();
653    let pos = pos.0;
654    let vd = presence.terrain_view_distance.current();
655
656    // NOTE: We use regular as casts rather than as_ because we want to saturate on
657    // overflow.
658    let player_pos = pos.map(|x| x as i32);
659    let player_chunk_pos = TerrainGrid::chunk_key(player_pos);
660    let player_vd = convert_to_loaded_vd(vd, max_view_distance);
661
662    // We filter out positions that are *clearly* way out of range from
663    // consideration. This is pretty easy to do, and means we don't have to
664    // perform expensive overflow checks elsewhere (otherwise, a player
665    // sufficiently far off the map could cause chunks they were nowhere near to
666    // stay loaded, parallel universes style).
667    //
668    // One could also imagine snapping a player to the part of the map nearest to
669    // them. We don't currently do this in case we rely elsewhere on players
670    // always being near the chunks they're keeping loaded, but it would allow
671    // us to use u32 exclusively so it's tempting.
672    let player_aabr_in_chunks = Aabr {
673        min: player_chunk_pos - player_vd,
674        max: player_chunk_pos + player_vd,
675    };
676
677    (world_aabr_in_chunks.max.x >= player_aabr_in_chunks.min.x &&
678     world_aabr_in_chunks.min.x <= player_aabr_in_chunks.max.x &&
679     world_aabr_in_chunks.max.y >= player_aabr_in_chunks.min.y &&
680     world_aabr_in_chunks.min.y <= player_aabr_in_chunks.max.y)
681        // The cast to i32 here is definitely safe thanks to MAX_VD limiting us to fit
682        // within i32^2.
683        //
684        // The cast from each coordinate to i16 should also be correct here.  This is because valid
685        // world chunk coordinates are no greater than 1 << 14 - 1; since we verified that the
686        // player is within world bounds modulo player_vd, which is guaranteed to never let us
687        // overflow an i16 when added to a u14, safety of the cast follows.
688        .then(|| ((player_chunk_pos.as_::<i16>(), player_vd.pow(2)), entity, is_client))
689}
690
691pub fn prepare_player_presences<'a, P>(
692    world_size: Vec2<u32>,
693    max_view_distance: u32,
694    entities: &Entities<'a>,
695    positions: P,
696    presences: &ReadStorage<'a, Presence>,
697    clients: &ReadStorage<'a, Client>,
698) -> (Vec<((Vec2<i16>, i32), Entity)>, Vec<(Vec2<i16>, i32)>)
699where
700    P: GenericReadStorage<Component = Pos> + Join<Type = &'a Pos>,
701{
702    // We start by collecting presences and positions from players, because they are
703    // very sparse in the entity list and therefore iterating over them for each
704    // chunk can be quite slow.
705    let world_aabr_in_chunks = Aabr {
706        min: Vec2::zero(),
707        // NOTE: Cast is correct because chunk coordinates must fit in an i32 (actually, i16).
708        max: world_size.map(|x| x.saturating_sub(1)).as_::<i32>(),
709    };
710
711    let (mut presences_positions_entities, mut presences_positions): (Vec<_>, Vec<_>) =
712        (entities, presences, positions, clients.mask().maybe())
713            .join()
714            .filter_map(|(entity, presence, position, client)| {
715                prepare_for_vd_check(
716                    &world_aabr_in_chunks,
717                    max_view_distance,
718                    entity,
719                    presence,
720                    position,
721                    client,
722                )
723            })
724            .partition_map(|(player_data, entity, is_client)| {
725                // For chunks with clients, we need to record their entity, because they might
726                // be used for insertion.  These elements fit in 8 bytes, so
727                // this should be pretty cache-friendly.
728                if is_client {
729                    Either::Left((player_data, entity))
730                } else {
731                    // For chunks without clients, we only need to record the position and view
732                    // distance.  These elements fit in 4 bytes, which is even cache-friendlier.
733                    Either::Right(player_data)
734                }
735            });
736
737    // We sort the presence lists by X position, so we can efficiently filter out
738    // players nowhere near the chunk.  This is basically a poor substitute for
739    // the effects of a proper KDTree, but a proper KDTree has too much overhead
740    // to be worth using for such a short list (~ 1000 players at most).  We
741    // also sort by y and reverse view distance; this will become important later.
742    presences_positions_entities
743        .sort_unstable_by_key(|&((pos, vd2), _)| (pos.x, pos.y, Reverse(vd2)));
744    presences_positions.sort_unstable_by_key(|&(pos, vd2)| (pos.x, pos.y, Reverse(vd2)));
745    // For the vast majority of chunks (present and pending ones), we'll only ever
746    // need the position and view distance.  So we extend it with these from the
747    // list of client chunks, and then do some further work to improve
748    // performance (taking advantage of the fact that they don't require
749    // entities).
750    presences_positions.extend(
751        presences_positions_entities
752            .iter()
753            .map(|&(player_data, _)| player_data),
754    );
755    // Since both lists were previously sorted, we use stable sort over unstable
756    // sort, as it's faster in that case (theoretically a proper merge operation
757    // would be ideal, but it's not worth pulling in a library for).
758    presences_positions.sort_by_key(|&(pos, vd2)| (pos.x, pos.y, Reverse(vd2)));
759    // Now that the list is sorted, we deduplicate players in the same chunk (this
760    // is why we need to sort y as well as x; dedup only works if the list is
761    // sorted by the element we use to dedup).  Importantly, we can then use
762    // only the *first* element as a substitute for all the players in the
763    // chunk, because we *also* sorted from greatest to lowest view
764    // distance, and dedup_by removes all but the first matching element.  In the
765    // common case where a few chunks are very crowded, this further reduces the
766    // work required per chunk.
767    presences_positions.dedup_by_key(|&mut (pos, _)| pos);
768
769    (presences_positions_entities, presences_positions)
770}
771
772pub fn chunk_in_vd(player_chunk_pos: Vec2<i16>, player_vd_sqr: i32, chunk_pos: Vec2<i32>) -> bool {
773    // NOTE: Guaranteed in bounds as long as prepare_player_presences prepared the
774    // player_chunk_pos and player_vd_sqr.
775    let adjusted_dist_sqr = (player_chunk_pos.as_::<i32>() - chunk_pos).magnitude_squared();
776
777    adjusted_dist_sqr <= player_vd_sqr
778}