Skip to main content

steel_core/world/
mod.rs

1//! This module contains the `World` struct, which represents a world.
2
3use std::{
4    io, mem,
5    path::Path,
6    sync::{
7        Arc, LazyLock, Weak,
8        atomic::{AtomicBool, AtomicI8, Ordering},
9    },
10    time::Duration,
11};
12
13use crate::chunk::chunk_ticket_storage::ChunkTicketStorage;
14use crate::chunk::full_chunk::{FullChunkBlockSetResult, FullChunkRef};
15use crate::chunk::gameplay_chunk_lookup_cache::GameplayChunkLookupCacheScope;
16use crate::chunk::light::{
17    LightLayer, LightSectionEmptinessChange, MAX_LIGHT_LEVEL, has_different_light_properties,
18};
19use crate::chunk::status::ChunkStatus;
20use crate::entity::damage::DamageHistory;
21use crate::poi::OccupationStatus;
22use crate::portal::WorldChangeRequest;
23use crate::world::game_event::{
24    GameEventContext, GameEventDispatcher, GameEventListenerCount, GameEventListenerStorage,
25    SharedGameEventListener,
26};
27use crate::{chunk::chunk_map::ChunkMapGameTickTimings, world::weather::Weather};
28use steel_utils::saved_data::{SavedDataManager, names as saved_data_names};
29
30use glam::DVec3;
31use sha2::{Digest, Sha256};
32use steel_protocol::packets::game::{
33    CBlockDestruction, CChangeDifficulty, CGameEvent, CInitializeBorder, CLevelEvent,
34    CLevelParticles, CPlayerChat, CSetBorderCenter, CSetBorderLerpSize, CSetBorderSize,
35    CSetBorderWarningDelay, CSetBorderWarningDistance, CSetEntityData, CSetEntityLink,
36    CSetEquipment, CSound, CSystemChat, CUpdateAttributes, GameEventType, RandomizationType,
37    SoundSource,
38};
39use steel_protocol::utils::ConnectionProtocol;
40use steel_protocol::{
41    packet_traits::{ClientPacket, CompressionInfo, EncodedPacket},
42    packets::game::CSetTime,
43};
44
45use rustc_hash::FxHashSet;
46use simdnbt::owned::NbtCompound;
47use steel_registry::biome::{BiomeRef, TemperatureModifier};
48use steel_registry::blocks::block_state_ext::BlockStateExt;
49use steel_registry::blocks::properties::{Axis, BlockStateProperties, Direction};
50use steel_registry::blocks::shapes::{
51    BooleanOp, OffsetVoxelShape, SupportType, VoxelShape, is_offset_face_full, is_shape_full_block,
52    join_is_not_empty,
53};
54use steel_registry::fluid::{FluidRef, FluidState};
55use steel_registry::game_events::GameEventRef;
56use steel_registry::game_rules::{ErasedGameRuleRef, GameRule, GameRuleValue, GameRuleValueType};
57use steel_registry::item_stack::ItemStack;
58use steel_registry::level_events;
59use steel_registry::loot_table::LootContext;
60use steel_registry::particle_type::ParticleData;
61use steel_registry::sound_event::SoundEventRef;
62use steel_registry::vanilla_block_tags::BlockTag;
63use steel_registry::vanilla_game_rules::{
64    BLOCK_DROPS, GLOBAL_SOUND_EVENTS, PLAYERS_NETHER_PORTAL_DEFAULT_DELAY, RANDOM_TICK_SPEED,
65};
66use steel_registry::{REGISTRY, RegistryEntry, RegistryExt, dimension_type::DimensionTypeRef};
67use steel_registry::{block_entity_type::BlockEntityTypeRef, vanilla_dimension_types};
68use steel_registry::{
69    blocks::BlockRef, vanilla_game_rules::ADVANCE_TIME, vanilla_game_rules::ADVANCE_WEATHER,
70};
71use steel_registry::{vanilla_blocks, vanilla_entities, vanilla_game_events, vanilla_poi_types};
72use steel_utils::block_util::FoundRectangle;
73use steel_utils::{
74    Downcast as _,
75    locks::{SyncMutex, SyncRwLock},
76    random::{Random as _, RandomSource, legacy_random::LegacyRandom},
77};
78use steel_worldgen::{biomes::obfuscate_biome_seed, noise::PerlinSimplexNoise};
79
80use steel_utils::{
81    BlockLocalAabb, BlockPos, BlockStateId, ChunkPos, Identifier, PackedBlockPos, SectionPos,
82    WorldAabb,
83    types::{Difficulty, GameType, UpdateFlags},
84};
85use tokio::{runtime::Runtime, time::Instant};
86
87use crate::{
88    ChunkMap,
89    behavior::{BLOCK_BEHAVIORS, BlockCollisionContext, BlockLootContext, FLUID_BEHAVIORS},
90    block_entity::{BlockEntity, SharedBlockEntity, entities::EndGatewayBlockEntity},
91    chunk::{heightmap::HeightmapType, player_chunk_view::PlayerChunkView},
92    chunk_saver::{ChunkStorage, RamOnlyStorage, RegionManager},
93    entity::{
94        AddEntityError, Entity, EntityChangeSenders, EntityChunkCallback, EntityLifecycleChanges,
95        EntityMovementSyncPacket, EntityOwnership, EntityTracker, EntityVisibility,
96        InactiveEntityCallback, MobEffectSyncPacket, RemovalReason, SharedEntity,
97        WorldEntityManager,
98        entities::{ExperienceOrbEntity, ItemEntity},
99        entity_loot_ref,
100    },
101    fluid::{FluidStateExt as _, fluid_state_to_block},
102    level_data::{
103        GameTime, GameTimeSource, LevelDataManager, RespawnData, WorldGenerationSettings,
104    },
105    player::{LastSeen, Player, connection::NetworkConnection},
106    poi::PointOfInterestStorage,
107};
108
109mod block_entity_ticker;
110mod block_event;
111mod block_region;
112mod block_updates;
113mod border;
114mod broadcasts;
115pub(crate) mod clock;
116mod domain_entity_directory;
117mod entity_management;
118mod environment;
119mod events;
120pub mod explosion;
121/// Vanilla game-event contexts, listeners, and dispatch storage.
122pub mod game_event;
123mod level_effects;
124mod level_reader;
125mod player_index;
126pub(crate) mod player_spawn_finder;
127mod portals;
128mod properties;
129mod raycast;
130mod redstone;
131mod signal_getter;
132mod sleep;
133mod sleep_status;
134mod spawn;
135pub mod tick_scheduler;
136mod weather;
137mod world_entities;
138
139#[cfg(test)]
140mod tests;
141
142pub use crate::config::WorldStorageConfig;
143use crate::worldgen::generators::vanilla::fuzzed_biome_at_block;
144use crate::worldgen::{ChunkGenerator, ChunkGeneratorType};
145use block_event::BlockEventQueue;
146pub(crate) use block_region::{BlockRegionBounds, BlockRegionRead, MAX_BLOCK_REGION_WORKSET_SLOTS};
147use block_updates::CollectingNeighborUpdater;
148pub use border::WorldBorderError;
149pub(crate) use border::{MAX_CENTER_COORDINATE, MAX_SIZE};
150use border::{WorldBorder, WorldBorderSnapshot};
151pub(crate) use domain_entity_directory::DomainEntityDirectory;
152use entity_management::NavigatingMobTracker;
153#[cfg(test)]
154use entity_management::nearest_player_distance_in_range;
155pub use explosion::{
156    BlockInteraction, DefaultExplosionDamageCalculator, EntityBasedExplosionDamageCalculator,
157    Explosion, ExplosionDamageCalculator, ExplosionInteraction, ExplosionOptions, ExplosionOutcome,
158};
159pub(crate) use explosion::{ExplosionBlockReader, ImmutableExplosionBlockCalculator};
160pub use level_reader::{LevelAccessor, LevelReader, ScheduledTickAccess};
161pub use player_index::{PlayerAreaMap, PlayerMap};
162pub use raycast::{ClipBlockShape, ClipFluid, ClipHitResult, RaytraceAction};
163#[cfg(test)]
164pub(crate) use signal_getter::get_best_neighbor_signal;
165pub use signal_getter::{SignalGetter, SignalQueryContext};
166pub(crate) use signal_getter::{get_control_input_signal, get_signal, is_redstone_conductor};
167pub use tick_scheduler::ScheduledTick;
168
169#[cfg(test)]
170use level_effects::sound_is_within_range;
171#[cfg(test)]
172use portals::{
173    closest_portal_candidate, nether_portal_creation_scan_origin, nether_portal_frame_offset_pos,
174};
175
176const fn initialize_border_packet(snapshot: WorldBorderSnapshot) -> CInitializeBorder {
177    CInitializeBorder {
178        new_center_x: snapshot.center_x,
179        new_center_z: snapshot.center_z,
180        old_size: snapshot.old_size,
181        new_size: snapshot.new_size,
182        lerp_time: snapshot.lerp_time,
183        new_absolute_max_size: snapshot.absolute_max_size,
184        warning_blocks: snapshot.warning_blocks,
185        warning_time: snapshot.warning_time,
186    }
187}
188
189/// Timing information for a world game tick.
190#[derive(Debug)]
191pub struct WorldGameTickTimings {
192    /// Total time for this world's tick.
193    pub elapsed: Duration,
194    /// Chunk map game tick timings.
195    pub chunk_map: ChunkMapGameTickTimings,
196    /// Time spent ticking entities.
197    pub entity_tick: Duration,
198}
199
200/// Result of replacing a block only when its current state still matches a prior read.
201#[must_use]
202#[derive(Clone, Copy, Debug, Eq, PartialEq)]
203pub enum ConditionalBlockSetResult {
204    /// The expected state was claimed and replaced.
205    Changed,
206    /// The expected state already equals the requested state, so no callbacks ran.
207    Unchanged,
208    /// The current state did not match the caller's expected state.
209    Stale(BlockStateId),
210    /// The position is invalid, the chunk is unavailable, or the update limit was exhausted.
211    Unavailable,
212}
213
214/// Configuration for creating a new world.
215#[derive(Clone)]
216pub struct WorldConfig {
217    /// Server-owned history; the caller retains it while the world runs.
218    pub damage_history: Arc<DamageHistory>,
219    /// Domain game-time authority, bound during construction.
220    pub game_time_source: GameTimeSource,
221    /// Storage configuration for chunk persistence.
222    pub storage: WorldStorageConfig,
223    /// Directory for level data. `None` means level data is ephemeral.
224    pub level_data_path: Option<String>,
225    /// World generator.
226    pub generator: Arc<ChunkGeneratorType>,
227    /// Generator metadata persisted for startup compatibility checks.
228    pub generation_settings: WorldGenerationSettings,
229    /// Server view distance (maximum chunk radius).
230    pub view_distance: u8,
231    /// Server simulation distance.
232    pub simulation_distance: u8,
233    /// Maximum queued neighbor-update tasks in one chained run; negative means unlimited.
234    pub max_chained_neighbor_updates: i32,
235    /// Compression settings for encoding broadcast packets.
236    pub compression: Option<CompressionInfo>,
237    /// Whether the world should be marked as flat in login/respawn packets.
238    pub is_flat: bool,
239    /// Sea level sent in login/respawn packets.
240    pub sea_level: i32,
241    /// Default game mode for first-visit player data.
242    pub default_gamemode: GameType,
243    /// Difficulty used when creating new level data.
244    pub difficulty: Difficulty,
245}
246
247/// A struct that represents a world.
248pub struct World {
249    pub(crate) damage_history: Weak<DamageHistory>,
250    /// The chunk map of the world.
251    pub chunk_map: Arc<ChunkMap>,
252    /// All players in the world with dual indexing by UUID and entity ID.
253    pub players: PlayerMap,
254    /// Spatial index for player proximity queries.
255    pub player_area_map: PlayerAreaMap,
256    /// Loaded world identifier (`domain:world`).
257    pub key: Identifier,
258    /// Vanilla dimension type for this loaded world.
259    ///
260    /// Vanilla often calls loaded worlds "dimensions". In Steel, `World` is the
261    /// loaded world instance and `dimension_type` is the vanilla registry entry
262    /// controlling height, skylight, ceiling, water evaporation, etc.
263    pub dimension_type: DimensionTypeRef,
264    /// Level data manager for persistent world state.
265    pub level_data: SyncRwLock<LevelDataManager>,
266    pub(crate) game_time: Arc<GameTime>,
267    /// Per-world saved data storage.
268    pub(crate) saved_data: SavedDataManager,
269    /// Runtime world border state.
270    world_border: SyncMutex<WorldBorder>,
271    /// Vanilla sleeping player counts for night-skip checks.
272    sleep_status: SyncMutex<sleep_status::SleepStatus>,
273    /// Server view distance (maximum chunk radius).
274    pub view_distance: u8,
275    /// Server simulation distance.
276    pub simulation_distance: u8,
277    /// Compression settings for encoding broadcast packets.
278    pub compression: Option<CompressionInfo>,
279    /// Whether the world should be marked as flat in login/respawn packets.
280    pub is_flat: bool,
281    /// Sea level sent in login/respawn packets.
282    pub sea_level: i32,
283    /// Default game mode for first-visit player data.
284    default_gamemode: AtomicI8,
285    /// Whether the tick rate is running normally (not frozen/paused).
286    /// When false, movement validation checks are skipped.
287    tick_runs_normally: AtomicBool,
288    /// Whether vanilla's scheduled/chunk/block-event tick phase is active.
289    handling_tick: AtomicBool,
290    /// Ordered, duplicate-suppressing server block events awaiting execution.
291    block_events: SyncMutex<BlockEventQueue>,
292    /// Vanilla collecting neighbor updater shared by all live block mutations.
293    neighbor_updater: CollectingNeighborUpdater,
294    /// Central runtime entity ownership and lookup.
295    entity_manager: WorldEntityManager,
296    domain_entity_directory: SyncRwLock<Option<Arc<DomainEntityDirectory>>>,
297    /// World-global ordered block-entity ticker phase.
298    block_entity_tickers: block_entity_ticker::WorldBlockEntityTickers,
299    /// Physical entries retained by this world's chunk-owned game-event registries.
300    game_event_listener_count: Arc<GameEventListenerCount>,
301    /// Entity tracker for managing which players can see which entities.
302    entity_tracker: EntityTracker,
303    /// Runtime IDs for pathfinder mobs currently visible to the active world.
304    navigating_mobs: NavigatingMobTracker,
305    /// Weather Data needed for animating starting and stopping of rain clientside
306    pub weather: SyncMutex<Weather>,
307    /// Per-level recent toggle history used by vanilla redstone-torch burnout.
308    redstone_torch_toggles: SyncMutex<redstone::RedstoneTorchToggleTracker>,
309    /// World registration and sparse head index for chunk-owned scheduled ticks.
310    scheduled_ticks: tick_scheduler::WorldTickScheduler,
311    /// Published block batch used by `willTickThisTick` queries during callbacks.
312    scheduled_block_ticks_this_tick:
313        SyncMutex<Option<Arc<tick_scheduler::ScheduledTickRunBatch<BlockRef>>>>,
314    /// Published fluid batch used by `willTickThisTick` queries during callbacks.
315    scheduled_fluid_ticks_this_tick:
316        SyncMutex<Option<Arc<tick_scheduler::ScheduledTickRunBatch<FluidRef>>>>,
317    /// Point of interest storage for efficient spatial queries of special blocks.
318    pub poi_storage: SyncMutex<PointOfInterestStorage>,
319    /// World-change requests queued by world-local ticks for server safe-point processing.
320    pending_world_changes: SyncMutex<Vec<(SharedEntity, WorldChangeRequest)>>,
321}
322
323impl World {
324    /// Creates a new world with custom configuration.
325    ///
326    /// This allows specifying storage backend (disk or RAM-only) and other options.
327    /// Uses `Arc::new_cyclic` to create a cyclic reference between
328    /// the World and its `ChunkMap`'s `WorldGenContext`.
329    ///
330    /// # Arguments
331    /// * `chunk_runtime` - The Tokio runtime for chunk operations
332    /// * `dimension_type` - Vanilla dimension type (overworld, nether, end)
333    /// * `seed` - The world seed
334    /// * `config` - World configuration including storage options
335    pub async fn new_with_config(
336        chunk_runtime: Arc<Runtime>,
337        key: Identifier,
338        dimension_type: DimensionTypeRef,
339        seed: i64,
340        config: WorldConfig,
341        generation_pool: Arc<rayon::ThreadPool>,
342    ) -> io::Result<Arc<Self>> {
343        let chunk_encoding_pool = Arc::clone(&generation_pool);
344        Self::new_with_config_and_encoding_pool(
345            chunk_runtime,
346            key,
347            dimension_type,
348            seed,
349            config,
350            generation_pool,
351            chunk_encoding_pool,
352        )
353        .await
354    }
355
356    pub(crate) async fn new_with_config_and_encoding_pool(
357        chunk_runtime: Arc<Runtime>,
358        key: Identifier,
359        dimension_type: DimensionTypeRef,
360        seed: i64,
361        config: WorldConfig,
362        generation_pool: Arc<rayon::ThreadPool>,
363        chunk_encoding_pool: Arc<rayon::ThreadPool>,
364    ) -> io::Result<Arc<Self>> {
365        let view_distance = config.view_distance;
366        let simulation_distance = config.simulation_distance;
367        let max_chained_neighbor_updates = config.max_chained_neighbor_updates;
368        let compression = config.compression;
369        let is_flat = config.is_flat;
370        let sea_level = config.sea_level;
371        let default_gamemode = config.default_gamemode;
372        // Create storage backend based on config
373        let storage: Arc<ChunkStorage> = match &config.storage {
374            WorldStorageConfig::Disk { path } => {
375                Arc::new(ChunkStorage::Disk(RegionManager::new(path.clone())))
376            }
377            WorldStorageConfig::RamOnly => {
378                Arc::new(ChunkStorage::RamOnly(RamOnlyStorage::empty_world()))
379            }
380        };
381
382        // Create or skip level data based on config
383
384        let path = config.level_data_path.as_deref().map(Path::new);
385        let saved_data = SavedDataManager::new(path);
386        let mut level_data = LevelDataManager::new(
387            path,
388            seed,
389            config.difficulty,
390            config.generation_settings,
391            config.game_time_source,
392        )
393        .await?;
394        if level_data.is_dirty() {
395            level_data.save().await?;
396        }
397        let ticket_storage = ChunkTicketStorage::load(&saved_data, &key).await;
398        let world_border = WorldBorder::new(level_data.data().world_border)
399            .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
400        // let generator = Arc::new(ChunkGeneratorType::Flat(FlatChunkGenerator::new(
401        //     REGISTRY
402        //         .blocks
403        //         .get_default_state_id(vanilla_blocks::BEDROCK), // Bedrock
404        //     REGISTRY.blocks.get_default_state_id(vanilla_blocks::DIRT), // Dirt
405        //     REGISTRY
406        //         .blocks
407        //         .get_default_state_id(vanilla_blocks::GRASS_BLOCK), // Grass Block
408        // )));
409
410        let mut weather = Weather::default();
411        if level_data.is_raining() {
412            weather.rain_level = 1.0;
413            if level_data.is_thundering() {
414                weather.thunder_level = 1.0;
415            }
416        }
417
418        Ok(Arc::new_cyclic(|weak_self: &Weak<World>| {
419            let chunk_map = Arc::new(ChunkMap::new_with_storage_and_ticket_storage(
420                chunk_runtime,
421                Weak::clone(weak_self),
422                dimension_type,
423                sea_level,
424                storage,
425                config.generator,
426                generation_pool,
427                chunk_encoding_pool,
428                view_distance,
429                simulation_distance,
430                ticket_storage,
431            ));
432            chunk_map.start_generation_refill_loop();
433
434            Self {
435                damage_history: Arc::downgrade(&config.damage_history),
436                chunk_map,
437                players: PlayerMap::new(),
438                player_area_map: PlayerAreaMap::new(),
439                key,
440                dimension_type,
441                game_time: level_data.game_time_handle(),
442                level_data: SyncRwLock::new(level_data),
443                saved_data,
444                world_border: SyncMutex::new(world_border),
445                sleep_status: SyncMutex::new(sleep_status::SleepStatus::default()),
446                view_distance,
447                simulation_distance,
448                compression,
449                is_flat,
450                sea_level,
451                default_gamemode: AtomicI8::new(default_gamemode.into()),
452                tick_runs_normally: AtomicBool::new(true),
453                handling_tick: AtomicBool::new(false),
454                block_events: SyncMutex::new(BlockEventQueue::default()),
455                neighbor_updater: CollectingNeighborUpdater::new(max_chained_neighbor_updates),
456                entity_manager: WorldEntityManager::new(),
457                domain_entity_directory: SyncRwLock::new(None),
458                block_entity_tickers: block_entity_ticker::WorldBlockEntityTickers::new(),
459                game_event_listener_count: GameEventListenerCount::shared(),
460                entity_tracker: EntityTracker::new(),
461                navigating_mobs: NavigatingMobTracker::new(),
462                weather: SyncMutex::new(weather),
463                redstone_torch_toggles: SyncMutex::new(
464                    redstone::RedstoneTorchToggleTracker::default(),
465                ),
466                scheduled_ticks: tick_scheduler::WorldTickScheduler::new(),
467                scheduled_block_ticks_this_tick: SyncMutex::new(None),
468                scheduled_fluid_ticks_this_tick: SyncMutex::new(None),
469                poi_storage: SyncMutex::new(PointOfInterestStorage::new()),
470                pending_world_changes: SyncMutex::new(Vec::new()),
471            }
472        }))
473    }
474
475    /// Cleans up the world by saving all chunks.
476    #[expect(
477        clippy::await_holding_lock,
478        reason = "holding the write lock across await is safe here because it only happens during shutdown"
479    )]
480    pub async fn cleanup(&self, total_saved: &mut usize) {
481        self.sync_world_border_to_level_data();
482        match self.level_data.write().save().await {
483            Ok(()) => log::info!("World {} level data saved successfully", self.key),
484            Err(e) => log::error!("Failed to save world level data: {e}"),
485        }
486
487        let chunk_tickets = self.chunk_map.persistent_chunk_tickets();
488        match self
489            .saved_data
490            .save(saved_data_names::CHUNK_TICKETS, &chunk_tickets)
491            .await
492        {
493            Ok(()) => log::info!("World {} saved chunk ticket data successfully", self.key),
494            Err(e) => log::error!("Failed to save world chunk ticket data: {e}"),
495        }
496
497        match self.save_all_chunks().await {
498            Ok(count) => *total_saved += count,
499            Err(e) => log::error!("Failed to save world chunks: {e}"),
500        }
501    }
502
503    /// Returns the domain this loaded world belongs to.
504    #[must_use]
505    pub fn domain(&self) -> &str {
506        self.key.namespace.as_ref()
507    }
508
509    pub(crate) fn set_domain_entity_directory(&self, directory: Arc<DomainEntityDirectory>) {
510        *self.domain_entity_directory.write() = Some(directory);
511    }
512
513    /// Gets an entity by UUID, checking this world before other loaded worlds in its domain.
514    #[must_use]
515    pub fn get_entity_in_domain_by_uuid(&self, uuid: &uuid::Uuid) -> Option<SharedEntity> {
516        self.get_entity_by_uuid(uuid).or_else(|| {
517            self.domain_entity_directory
518                .read()
519                .as_ref()
520                .and_then(|directory| directory.get_entity_by_uuid(uuid))
521        })
522    }
523
524    /// Vanilla keeps ticking removed players from their connection. Steel ticks
525    /// players through the entity manager, which skips removed entities, so tick
526    /// them here instead. This must run before the entity tick so the removal
527    /// tick itself is not processed twice. Unlike the entity tick, this skips
528    /// `commonTick`, so `tickCount` stays frozen like vanilla.
529    fn tick_removed_players(&self) {
530        self.players.iter_players(|_, player| {
531            if player.is_removed() {
532                Arc::clone(player).tick();
533            }
534            true
535        });
536    }
537
538    /// Game tick: weather, time, chunk game tick (broadcasts + random/scheduled ticks),
539    /// and player logic (without chunk sending).
540    ///
541    /// * `tick_count` - The current tick number
542    /// * `runs_normally` - Whether game elements (random ticks, entities) should run.
543    ///   When false (frozen), only essential operations like chunk loading run.
544    #[tracing::instrument(level = "trace", skip(self), name = "world_game_tick")]
545    #[expect(
546        clippy::too_many_lines,
547        reason = "world tick orchestration keeps vanilla subsystem order explicit"
548    )]
549    pub fn tick_game(
550        self: &Arc<Self>,
551        tick_count: u64,
552        runs_normally: bool,
553    ) -> WorldGameTickTimings {
554        let world_start = Instant::now();
555        let lookup_cache_scope = GameplayChunkLookupCacheScope::enter(&self.chunk_map);
556        self.handling_tick.store(true, Ordering::Relaxed);
557        self.set_tick_runs_normally(runs_normally);
558        if runs_normally {
559            self.tick_world_border();
560            self.tick_weather();
561        }
562        self.tick_sleeping_players();
563        if runs_normally {
564            self.tick_time();
565        }
566
567        let random_tick_speed = self.get_game_rule(&RANDOM_TICK_SPEED) as u32;
568
569        let early_lookup_stats = lookup_cache_scope.finish();
570        let mut chunk_map_timings =
571            self.chunk_map
572                .tick_game(self, tick_count, random_tick_speed, runs_normally);
573        chunk_map_timings.lookup_cache.merge(early_lookup_stats);
574        let lookup_cache_scope = GameplayChunkLookupCacheScope::enter(&self.chunk_map);
575
576        if runs_normally {
577            let _span = tracing::trace_span!("block_events").entered();
578            self.run_block_events();
579        }
580
581        // Vanilla clears this before ticking entities and block entities.
582        self.handling_tick.store(false, Ordering::Relaxed);
583
584        self.tick_removed_players();
585
586        let entity_tick = {
587            let _span = tracing::trace_span!("entity_tick").entered();
588            let start = Instant::now();
589            let dirty_chunks = self
590                .entity_manager
591                .tick_entities(tick_count as i32, runs_normally);
592            for chunk in dirty_chunks {
593                self.mark_chunk_dirty(chunk);
594            }
595            start.elapsed()
596        };
597
598        {
599            let _span = tracing::trace_span!("block_entities").entered();
600            let start = Instant::now();
601            self.block_entity_tickers.tick(self, runs_normally);
602            chunk_map_timings.tick_block_entities = start.elapsed();
603        }
604
605        {
606            let _span = tracing::trace_span!("entity_tracker_send_changes").entered();
607            self.entity_tracker.send_changes(
608                |chunk| self.get_packet_tracking_players(chunk),
609                |player_id| self.players.get_by_entity_id(player_id),
610                EntityChangeSenders {
611                    movement: |entity_id, packet| {
612                        self.broadcast_movement_sync_to_entity_trackers(entity_id, packet, None);
613                    },
614                    self_movement: |player_id, packet| {
615                        let Some(encoded) = self.encode_movement_sync_packet(packet) else {
616                            return;
617                        };
618                        let Some(player) = self.players.get_by_entity_id(player_id) else {
619                            return;
620                        };
621                        player.connection.send_encoded(encoded);
622                    },
623                    entity_data: |entity_id, dirty_entity_data| {
624                        let packet = CSetEntityData::new(entity_id, dirty_entity_data);
625                        let Ok(encoded) = EncodedPacket::from_bare(
626                            packet,
627                            self.compression,
628                            ConnectionProtocol::Play,
629                        ) else {
630                            return;
631                        };
632                        self.broadcast_to_entity_trackers_encoded(entity_id, encoded.clone(), None);
633                        if let Some(player) = self.players.get_by_entity_id(entity_id) {
634                            player.connection.send_encoded(encoded);
635                        }
636                    },
637                    attributes: |entity_id, dirty_attributes| {
638                        let packet = CUpdateAttributes::new(entity_id, dirty_attributes);
639                        let Ok(encoded) = EncodedPacket::from_bare(
640                            packet,
641                            self.compression,
642                            ConnectionProtocol::Play,
643                        ) else {
644                            return;
645                        };
646                        self.broadcast_to_entity_trackers_encoded(entity_id, encoded.clone(), None);
647                        if let Some(player) = self.players.get_by_entity_id(entity_id) {
648                            player.connection.send_encoded(encoded);
649                        }
650                    },
651                    mob_effects: |player_id, packet| {
652                        let Some(player) = self.players.get_by_entity_id(player_id) else {
653                            return;
654                        };
655                        match packet {
656                            MobEffectSyncPacket::Update(packet) => player.send_packet(packet),
657                            MobEffectSyncPacket::Remove(packet) => player.send_packet(packet),
658                        }
659                    },
660                    equipment: |entity_id, packet: CSetEquipment| {
661                        let Ok(encoded) = EncodedPacket::from_bare(
662                            packet,
663                            self.compression,
664                            ConnectionProtocol::Play,
665                        ) else {
666                            return;
667                        };
668                        self.broadcast_to_entity_trackers_encoded(entity_id, encoded, None);
669                    },
670                    passengers: |player_id, packet| {
671                        if let Some(player) = self.players.get_by_entity_id(player_id) {
672                            player.send_packet(packet);
673                        }
674                    },
675                    entity_link: |entity_id, packet: CSetEntityLink| {
676                        let Ok(encoded) = EncodedPacket::from_bare(
677                            packet,
678                            self.compression,
679                            ConnectionProtocol::Play,
680                        ) else {
681                            return;
682                        };
683                        self.broadcast_to_entity_trackers_encoded(entity_id, encoded, None);
684                    },
685                },
686            );
687        }
688
689        chunk_map_timings
690            .lookup_cache
691            .merge(lookup_cache_scope.finish());
692        WorldGameTickTimings {
693            elapsed: world_start.elapsed(),
694            chunk_map: chunk_map_timings,
695            entity_tick,
696        }
697    }
698
699    /// Saves all dirty chunks in this world to disk.
700    ///
701    /// This should be called during graceful shutdown.
702    /// Returns the number of chunks saved.
703    pub async fn save_all_chunks(&self) -> io::Result<usize> {
704        self.chunk_map.save_all_chunks().await
705    }
706}
707
708impl LevelReader for World {
709    fn get_block_state(&self, pos: BlockPos) -> BlockStateId {
710        Self::get_block_state(self, pos)
711    }
712
713    fn get_block_entity(&self, pos: BlockPos) -> Option<SharedBlockEntity> {
714        Self::get_block_entity(self, pos)
715    }
716
717    fn is_face_sturdy_for(
718        &self,
719        state: BlockStateId,
720        pos: BlockPos,
721        direction: Direction,
722        support_type: SupportType,
723    ) -> bool {
724        BLOCK_BEHAVIORS
725            .get_behavior(state.get_block())
726            .is_face_sturdy(state, self, pos, direction, support_type)
727    }
728
729    fn sky_darkening(&self) -> u8 {
730        World::sky_darkening(self)
731    }
732
733    fn raw_brightness(&self, pos: BlockPos, sky_darkening: u8) -> u8 {
734        let sky_light = if self.dimension_type.has_skylight {
735            self.light_value_at(LightLayer::Sky, pos)
736                .saturating_sub(sky_darkening)
737        } else {
738            0
739        };
740
741        if sky_light == MAX_LIGHT_LEVEL {
742            return MAX_LIGHT_LEVEL;
743        }
744
745        sky_light.max(self.light_value_at(LightLayer::Block, pos))
746    }
747
748    fn can_see_sky(&self, pos: BlockPos) -> bool {
749        Self::can_see_sky(self, pos)
750    }
751
752    fn ambient_light(&self) -> f32 {
753        self.dimension_type.ambient_light
754    }
755
756    fn min_y(&self) -> i32 {
757        self.get_min_y()
758    }
759
760    fn height(&self) -> i32 {
761        self.get_height()
762    }
763
764    fn sea_level(&self) -> i32 {
765        self.sea_level
766    }
767}
768
769impl LevelReader for Arc<World> {
770    fn get_block_state(&self, pos: BlockPos) -> BlockStateId {
771        self.as_ref().get_block_state(pos)
772    }
773
774    fn get_block_entity(&self, pos: BlockPos) -> Option<SharedBlockEntity> {
775        self.as_ref().get_block_entity(pos)
776    }
777
778    fn is_face_sturdy_for(
779        &self,
780        state: BlockStateId,
781        pos: BlockPos,
782        direction: Direction,
783        support_type: SupportType,
784    ) -> bool {
785        self.as_ref()
786            .is_face_sturdy_for(state, pos, direction, support_type)
787    }
788
789    fn sky_darkening(&self) -> u8 {
790        self.as_ref().sky_darkening()
791    }
792
793    fn raw_brightness(&self, pos: BlockPos, sky_darkening: u8) -> u8 {
794        self.as_ref().raw_brightness(pos, sky_darkening)
795    }
796
797    fn can_see_sky(&self, pos: BlockPos) -> bool {
798        self.as_ref().can_see_sky(pos)
799    }
800
801    fn ambient_light(&self) -> f32 {
802        self.as_ref().ambient_light()
803    }
804
805    fn height_at(&self, heightmap_type: HeightmapType, x: i32, z: i32) -> i32 {
806        let mapped_type = match heightmap_type {
807            HeightmapType::WorldSurfaceWg => HeightmapType::WorldSurface,
808            HeightmapType::OceanFloorWg => HeightmapType::OceanFloor,
809            other => other,
810        };
811        self.as_ref()
812            .height_at(mapped_type, x, z)
813            .unwrap_or_else(|| self.min_y())
814    }
815
816    fn min_y(&self) -> i32 {
817        self.as_ref().get_min_y()
818    }
819
820    fn height(&self) -> i32 {
821        self.as_ref().get_height()
822    }
823
824    fn sea_level(&self) -> i32 {
825        self.as_ref().sea_level
826    }
827}
828
829impl ScheduledTickAccess for Arc<World> {
830    fn fluid_tick_delay(&self, fluid: FluidRef) -> i32 {
831        FLUID_BEHAVIORS.get_behavior(fluid).tick_delay(self)
832    }
833
834    fn schedule_block_tick_default(&self, pos: BlockPos, block: BlockRef, delay: i32) -> bool {
835        self.as_ref().schedule_block_tick_default(pos, block, delay);
836        true
837    }
838
839    fn has_scheduled_block_tick(&self, pos: BlockPos, block: BlockRef) -> bool {
840        self.as_ref().has_scheduled_block_tick(pos, block)
841    }
842
843    fn will_tick_block_this_tick(&self, pos: BlockPos, block: BlockRef) -> bool {
844        self.as_ref().will_tick_block_this_tick(pos, block)
845    }
846
847    fn schedule_fluid_tick_default(&self, pos: BlockPos, fluid: FluidRef, delay: i32) -> bool {
848        self.as_ref().schedule_fluid_tick_default(pos, fluid, delay);
849        true
850    }
851
852    fn will_tick_fluid_this_tick(&self, pos: BlockPos, fluid: FluidRef) -> bool {
853        self.as_ref().will_tick_fluid_this_tick(pos, fluid)
854    }
855}
856
857impl LevelAccessor for Arc<World> {
858    fn set_block_state(&self, pos: BlockPos, state: BlockStateId, flags: UpdateFlags) -> bool {
859        self.set_block(pos, state, flags)
860    }
861
862    fn can_write_to_chunk(&self, chunk_x: i32, chunk_z: i32) -> bool {
863        self.chunk_map
864            .with_full_chunk(ChunkPos::new(chunk_x, chunk_z), |_| ())
865            .is_some()
866    }
867
868    fn requires_live_write_preflight(&self) -> bool {
869        true
870    }
871
872    fn destroy_block(&self, pos: BlockPos, drop_items: bool) -> bool {
873        World::destroy_block(self, pos, drop_items)
874    }
875
876    fn play_block_sound(
877        &self,
878        sound: SoundEventRef,
879        pos: BlockPos,
880        volume: f32,
881        pitch: f32,
882        exclude: Option<i32>,
883    ) {
884        self.as_ref()
885            .play_block_sound(sound, pos, volume, pitch, exclude);
886    }
887
888    fn game_event(&self, event: GameEventRef, pos: BlockPos, context: &GameEventContext<'_>) {
889        World::game_event(self, event, pos, context);
890    }
891}