Skip to main content

steel_core/world/
properties.rs

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