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