Skip to main content

steel_core/world/
properties.rs

1use super::{
2    ADVANCE_TIME, BlockPos, CChangeDifficulty, ChunkPos, Difficulty, Digest, ErasedGameRuleRef,
3    GameRule, GameRuleValue, GameRuleValueType, LevelDataManager, OffsetVoxelShape, Ordering,
4    Player, REGISTRY, SectionPos, Sha256, World, vanilla_dimension_types,
5};
6
7impl World {
8    /// Vanilla `Level.MAX_LEVEL_SIZE`.
9    pub const MAX_LEVEL_SIZE: i32 = 30_000_000;
10    /// Vanilla `Level.MAX_ENTITY_SPAWN_Y`.
11    pub const MAX_ENTITY_SPAWN_Y: i32 = 20_000_000;
12    /// Vanilla `Level.MIN_ENTITY_SPAWN_Y`.
13    pub const MIN_ENTITY_SPAWN_Y: i32 = -20_000_000;
14
15    /// Returns whether this world uses the vanilla End dimension type.
16    ///
17    /// Steel world keys are domain-scoped, so End gameplay semantics cannot rely on the
18    /// vanilla `minecraft:the_end` level key.
19    #[must_use]
20    pub fn is_end_dimension_type(&self) -> bool {
21        self.dimension_type == &vanilla_dimension_types::THE_END
22    }
23
24    /// Returns vanilla level difficulty.
25    pub fn difficulty(&self) -> Difficulty {
26        self.level_data.read().data().difficulty
27    }
28
29    /// Sets the level difficulty and broadcasts the new value to its players.
30    pub(crate) fn set_difficulty(&self, difficulty: Difficulty) {
31        let locked = {
32            let mut level_data = self.level_data.write();
33            level_data.data_mut().difficulty = difficulty;
34            level_data.data().difficulty_locked
35        };
36        self.broadcast_to_all(CChangeDifficulty { difficulty, locked });
37    }
38
39    /// Returns the total height of the world in blocks.
40    pub const fn get_height(&self) -> i32 {
41        self.dimension_type.height
42    }
43
44    /// Returns the minimum Y coordinate of the world.
45    pub const fn get_min_y(&self) -> i32 {
46        self.dimension_type.min_y
47    }
48
49    /// Returns the maximum Y coordinate of the world.
50    pub const fn get_max_y(&self) -> i32 {
51        self.get_min_y() + self.get_height() - 1
52    }
53
54    /// Returns whether the given Y coordinate is outside the build height.
55    pub const fn is_outside_build_height(&self, block_y: i32) -> bool {
56        block_y < self.get_min_y() || block_y > self.get_max_y()
57    }
58
59    /// Returns whether the block position is within valid horizontal bounds.
60    #[expect(clippy::unused_self, reason = "this is an api function")]
61    pub const fn is_in_valid_bounds_horizontal(&self, block_pos: BlockPos) -> bool {
62        let chunk_x = SectionPos::block_to_section_coord(block_pos.0.x);
63        let chunk_z = SectionPos::block_to_section_coord(block_pos.0.z);
64        ChunkPos::is_valid(chunk_x, chunk_z)
65    }
66
67    /// Returns whether the block position is within valid world bounds.
68    pub const fn is_in_valid_bounds(&self, block_pos: BlockPos) -> bool {
69        !self.is_outside_build_height(block_pos.0.y)
70            && self.is_in_valid_bounds_horizontal(block_pos)
71    }
72
73    /// Returns whether a block position is within vanilla's absolute world bounds.
74    pub const fn is_in_world_bounds(&self, block_pos: BlockPos) -> bool {
75        !self.is_outside_build_height(block_pos.0.y)
76            && Self::is_in_world_bounds_horizontal(block_pos)
77    }
78
79    /// Returns whether the block position is within vanilla spawnable bounds.
80    #[must_use]
81    pub const fn is_in_spawnable_bounds(block_pos: BlockPos) -> bool {
82        !Self::is_outside_spawnable_height(block_pos.0.y)
83            && Self::is_in_world_bounds_horizontal(block_pos)
84    }
85
86    pub(super) const fn is_in_world_bounds_horizontal(block_pos: BlockPos) -> bool {
87        block_pos.0.x >= -Self::MAX_LEVEL_SIZE
88            && block_pos.0.z >= -Self::MAX_LEVEL_SIZE
89            && block_pos.0.x < Self::MAX_LEVEL_SIZE
90            && block_pos.0.z < Self::MAX_LEVEL_SIZE
91    }
92
93    pub(super) const fn is_outside_spawnable_height(y: i32) -> bool {
94        y < Self::MIN_ENTITY_SPAWN_Y || y >= Self::MAX_ENTITY_SPAWN_Y
95    }
96
97    /// Returns the maximum build height (one above the highest placeable block).
98    /// This is `min_y + height`.
99    #[must_use]
100    pub const fn max_build_height(&self) -> i32 {
101        self.get_min_y() + self.get_height()
102    }
103
104    /// Checks if a player may interact with the world at the given position.
105    /// Currently only checks if position is within world bounds.
106    #[must_use]
107    pub const fn may_interact(&self, _player: &Player, pos: BlockPos) -> bool {
108        self.is_in_valid_bounds(pos)
109    }
110
111    /// Checks if a block's collision shape at the given position is unobstructed by entities.
112    ///
113    /// This is the Rust equivalent of vanilla's `Level.isUnobstructed(BlockState, BlockPos, CollisionContext)`.
114    /// In vanilla, this checks all entities with `blocksBuilding=true` (players, mobs, boats, etc.).
115    ///
116    /// Returns `true` if the position is clear, `false` if an entity would obstruct placement.
117    #[must_use]
118    pub fn is_unobstructed(&self, collision_shape: OffsetVoxelShape, pos: BlockPos) -> bool {
119        if collision_shape.is_empty() {
120            return true;
121        }
122
123        for block_aabb in collision_shape.iter() {
124            let world_aabb = block_aabb.at_block(pos);
125            for entity in self.get_entities_in_aabb(&world_aabb) {
126                if entity.blocks_building() && entity.bounding_box().intersects(world_aabb) {
127                    return false;
128                }
129            }
130        }
131
132        true
133    }
134
135    /// Returns whether the tick rate is running normally.
136    ///
137    /// When false (frozen/paused), movement validation checks should be skipped.
138    /// Matches vanilla's `level.tickRateManager().runsNormally()`.
139    #[must_use]
140    pub fn tick_runs_normally(&self) -> bool {
141        self.tick_runs_normally.load(Ordering::Relaxed)
142    }
143
144    /// Sets whether the tick rate is running normally.
145    ///
146    /// Set to false to freeze/pause the world (e.g., via `/tick freeze` command).
147    pub fn set_tick_runs_normally(&self, runs_normally: bool) {
148        self.tick_runs_normally
149            .store(runs_normally, Ordering::Relaxed);
150    }
151
152    /// Mirrors `ServerLevel.isHandlingTick` for piston early-retraction rules.
153    #[must_use]
154    pub(crate) fn is_handling_tick(&self) -> bool {
155        self.handling_tick.load(Ordering::Relaxed)
156    }
157
158    /// Gets the value of a game rule.
159    /// WARNING: this function acquires a read lock on the level data.
160    /// if you already have a write lock on level data, this will DEADLOCK
161    #[must_use]
162    pub fn get_game_rule<T: GameRuleValueType>(&self, rule: &GameRule<T>) -> T {
163        let guard = self.level_data.read();
164        self.get_game_rule_with_guard(rule, &guard)
165    }
166
167    /// Gets the value of a game rule on the `LevelDataManager` guard being passed in.
168    #[expect(clippy::unused_self, reason = "this is an api function")]
169    #[must_use]
170    pub fn get_game_rule_with_guard<T: GameRuleValueType>(
171        &self,
172        rule: &GameRule<T>,
173        guard: &LevelDataManager,
174    ) -> T {
175        guard
176            .data()
177            .game_rules_values
178            .get(rule, &REGISTRY.game_rules)
179    }
180
181    /// Gets a type-erased value for a dynamically selected game rule.
182    #[must_use]
183    pub fn get_erased_game_rule(&self, rule: ErasedGameRuleRef) -> GameRuleValue {
184        self.level_data
185            .read()
186            .data()
187            .game_rules_values
188            .get_erased(rule, &REGISTRY.game_rules)
189            .clone()
190    }
191
192    /// Sets the value of a game rule.
193    /// WARNING: this function acquires a write lock on the level data.
194    /// if you already have a read or write lock on level data, this will DEADLOCK
195    pub fn set_game_rule<T: GameRuleValueType>(&self, rule: &GameRule<T>, value: T) -> bool {
196        let updated = {
197            let mut guard = self.level_data.write();
198            self.set_game_rule_with_guard(rule, value, &mut guard)
199        };
200        if updated && rule.key() == ADVANCE_TIME.key() {
201            self.broadcast_time_sync();
202        }
203        updated
204    }
205
206    /// Sets the value of a game rule on the `LevelDataManager` guard being passed in.
207    #[expect(clippy::unused_self, reason = "this is an api function")]
208    pub fn set_game_rule_with_guard<T: GameRuleValueType>(
209        &self,
210        rule: &GameRule<T>,
211        value: T,
212        guard: &mut LevelDataManager,
213    ) -> bool {
214        guard
215            .data_mut()
216            .game_rules_values
217            .set(rule, value, &REGISTRY.game_rules)
218    }
219
220    /// Sets a type-erased value for a dynamically selected game rule.
221    pub fn set_erased_game_rule(&self, rule: ErasedGameRuleRef, value: GameRuleValue) -> bool {
222        let updated = self
223            .level_data
224            .write()
225            .data_mut()
226            .game_rules_values
227            .set_erased(rule, value, &REGISTRY.game_rules);
228        if updated && rule.key() == ADVANCE_TIME.key() {
229            self.broadcast_time_sync();
230        }
231        updated
232    }
233
234    pub(super) fn advance_time_with_guard(&self, guard: &LevelDataManager) -> bool {
235        self.get_game_rule_with_guard(&ADVANCE_TIME, guard)
236    }
237
238    /// Gets the world seed.
239    #[must_use]
240    pub fn seed(&self) -> i64 {
241        self.level_data.read().data().seed
242    }
243
244    /// Gets the obfuscated seed for sending to clients.
245    ///
246    /// This uses SHA-256 hashing to prevent clients from easily extracting
247    /// the actual world seed, matching vanilla's `BiomeManager.obfuscateSeed()`.
248    #[must_use]
249    #[expect(
250        clippy::missing_panics_doc,
251        reason = "panic is unreachable: SHA-256 always produces 32 bytes"
252    )]
253    pub fn obfuscated_seed(&self) -> i64 {
254        let seed = self.seed();
255        let mut hasher = Sha256::new();
256        hasher.update(seed.to_be_bytes());
257        let result = hasher.finalize();
258        // SHA-256 always produces 32 bytes, so taking 8 bytes always succeeds
259        let bytes: [u8; 8] = result[0..8].try_into().expect("SHA-256 produces 32 bytes");
260        i64::from_be_bytes(bytes)
261    }
262}