Skip to main content

steel_core/world/
level_reader.rs

1//! Read-only world view shared by live worlds and world-generation regions.
2//!
3//! This mirrors vanilla's `LevelReader` role: block behavior such as
4//! `canSurvive` should depend on the world-reading surface, not on the concrete
5//! `World` type. `World` and `WorldGenRegion` both implement this trait.
6
7use steel_registry::blocks::BlockRef;
8use steel_registry::blocks::block_state_ext::BlockStateExt as _;
9use steel_registry::blocks::properties::Direction;
10use steel_registry::blocks::shapes::SupportType;
11use steel_registry::fluid::FluidRef;
12use steel_registry::game_events::GameEventRef;
13use steel_registry::sound_event::SoundEventRef;
14use steel_utils::{BlockPos, BlockStateId, types::UpdateFlags};
15
16use crate::block_entity::SharedBlockEntity;
17use crate::chunk::heightmap::HeightmapType;
18use crate::chunk::light::MAX_LIGHT_LEVEL;
19use crate::world::game_event::GameEventContext;
20
21use super::World;
22
23/// Read-only level access needed by block behavior and worldgen predicates.
24pub trait LevelReader {
25    /// Gets the block state at a position.
26    fn get_block_state(&self, pos: BlockPos) -> BlockStateId;
27
28    /// Gets the block entity at a position when this level surface supports it
29    #[expect(
30        unused_variables,
31        reason = "default trait implementation ignores position"
32    )]
33    fn get_block_entity(&self, pos: BlockPos) -> Option<SharedBlockEntity> {
34        None
35    }
36
37    /// Mirrors vanilla `BlockState.isFaceSturdy` with full-face support.
38    fn is_face_sturdy(&self, state: BlockStateId, pos: BlockPos, direction: Direction) -> bool {
39        self.is_face_sturdy_for(state, pos, direction, SupportType::Full)
40    }
41
42    /// Mirrors vanilla `BlockState.isFaceSturdy` for a specific support type.
43    ///
44    /// Lightweight and worldgen views default to extracted support shapes.
45    /// Live views override this to dispatch through block behavior for dynamic
46    /// world-dependent shapes.
47    fn is_face_sturdy_for(
48        &self,
49        state: BlockStateId,
50        pos: BlockPos,
51        direction: Direction,
52        support_type: SupportType,
53    ) -> bool {
54        state.is_face_sturdy_for_at(pos, direction, support_type)
55    }
56
57    /// Returns vanilla raw brightness at a position after sky darkening.
58    fn raw_brightness(&self, pos: BlockPos, sky_darkening: u8) -> u8;
59
60    /// Sky darkening (`15 - SKY_LIGHT_LEVEL`). Worldgen defaults to zero;
61    /// live worlds use the environment attribute.
62    fn sky_darkening(&self) -> u8 {
63        0
64    }
65
66    /// Whether sky light is full. Lightweight views approximate from raw
67    /// brightness; live worlds read the sky-light layer directly.
68    fn can_see_sky(&self, pos: BlockPos) -> bool {
69        self.raw_brightness(pos, 0) >= MAX_LIGHT_LEVEL
70    }
71
72    /// Returns this dimension's vanilla ambient light factor.
73    fn ambient_light(&self) -> f32 {
74        0.0
75    }
76
77    /// Returns the minimum build height.
78    fn min_y(&self) -> i32;
79
80    /// Returns the build height.
81    fn height(&self) -> i32;
82
83    /// Returns the vanilla heightmap value at a column.
84    fn height_at(&self, _heightmap_type: HeightmapType, _x: i32, _z: i32) -> i32 {
85        self.min_y()
86    }
87
88    /// Returns the dimension's sea level.
89    fn sea_level(&self) -> i32;
90
91    /// Returns the exclusive maximum build height.
92    fn max_y_exclusive(&self) -> i32 {
93        self.min_y() + self.height()
94    }
95
96    /// Checks if a Y coordinate is outside build height.
97    fn is_outside_build_height(&self, y: i32) -> bool {
98        y < self.min_y() || y >= self.max_y_exclusive()
99    }
100
101    /// Returns raw brightness at a position, treating positions outside the
102    /// world's horizontal bounds as fully lit.
103    fn max_local_raw_brightness(&self, pos: BlockPos, sky_darkening: u8) -> u8 {
104        if !World::is_in_world_bounds_horizontal(pos) {
105            return MAX_LIGHT_LEVEL;
106        }
107
108        self.raw_brightness(pos, sky_darkening)
109    }
110
111    /// Converts brightness after current sky darkening to the curved light value.
112    fn light_level_dependent_magic_value(&self, pos: BlockPos) -> f32 {
113        let value = f32::from(self.max_local_raw_brightness(pos, self.sky_darkening()))
114            / f32::from(MAX_LIGHT_LEVEL);
115        let curved_value = value / value.mul_add(-3.0, 4.0);
116        curved_value + self.ambient_light() * (1.0 - curved_value)
117    }
118
119    /// Returns the light-based cost modifier used by mob pathfinding, derived
120    /// from the light-curved brightness at a position.
121    fn pathfinding_cost_from_light_levels(&self, pos: BlockPos) -> f32 {
122        self.light_level_dependent_magic_value(pos) - 0.5
123    }
124}
125
126/// Level access needed by vanilla block `updateShape` logic.
127///
128/// Vanilla passes both `LevelReader` and `ScheduledTickAccess` to block shape updates.
129/// Steel combines those surfaces so the same block behavior can run against a live
130/// `World` and a `WorldGenRegion`.
131pub trait ScheduledTickAccess: LevelReader {
132    /// Returns the fluid tick delay in this level.
133    fn fluid_tick_delay(&self, fluid: FluidRef) -> i32;
134
135    /// Schedules a block tick using vanilla's default priority.
136    fn schedule_block_tick_default(&self, pos: BlockPos, block: BlockRef, delay: i32) -> bool;
137
138    /// Returns whether a tick is already scheduled for the same `(pos, block)`.
139    #[expect(
140        unused_variables,
141        reason = "most test/worldgen level surfaces do not track scheduled tick presence"
142    )]
143    fn has_scheduled_block_tick(&self, pos: BlockPos, block: BlockRef) -> bool {
144        false
145    }
146
147    /// Returns whether the same `(pos, block)` was selected for this tick and has not started.
148    #[expect(
149        unused_variables,
150        reason = "worldgen and most test level surfaces do not execute scheduled tick batches"
151    )]
152    fn will_tick_block_this_tick(&self, pos: BlockPos, block: BlockRef) -> bool {
153        false
154    }
155
156    /// Schedules a fluid tick using vanilla's default priority.
157    fn schedule_fluid_tick_default(&self, pos: BlockPos, fluid: FluidRef, delay: i32) -> bool;
158
159    /// Returns whether the same `(pos, fluid)` was selected for this tick and has not started.
160    #[expect(
161        unused_variables,
162        reason = "worldgen and most test level surfaces do not execute scheduled tick batches"
163    )]
164    fn will_tick_fluid_this_tick(&self, pos: BlockPos, fluid: FluidRef) -> bool {
165        false
166    }
167}
168
169/// Mutable level access needed by vanilla `LevelAccessor` block hooks.
170pub trait LevelAccessor: ScheduledTickAccess {
171    /// Sets a block state with vanilla update flags.
172    fn set_block_state(&self, pos: BlockPos, state: BlockStateId, flags: UpdateFlags) -> bool;
173
174    /// Destroys a block and optionally drops its resources.
175    fn destroy_block(&self, pos: BlockPos, drop_items: bool) -> bool;
176
177    /// Returns whether this surface accepts writes into the given chunk.
178    fn can_write_to_chunk(&self, _chunk_x: i32, _chunk_z: i32) -> bool {
179        true
180    }
181
182    /// Returns whether multi-block placements must be dry-run before committing.
183    fn requires_live_write_preflight(&self) -> bool {
184        false
185    }
186
187    /// Plays a block sound when this level surface supports runtime side effects.
188    #[expect(
189        unused_variables,
190        reason = "worldgen and test level surfaces do not emit sounds"
191    )]
192    fn play_block_sound(
193        &self,
194        sound: SoundEventRef,
195        pos: BlockPos,
196        volume: f32,
197        pitch: f32,
198        exclude: Option<i32>,
199    ) {
200    }
201
202    /// Dispatches a game event when this level surface supports runtime listeners.
203    #[expect(
204        unused_variables,
205        reason = "worldgen and test level surfaces do not emit game events"
206    )]
207    fn game_event(&self, event: GameEventRef, pos: BlockPos, context: &GameEventContext<'_>) {}
208}
209
210#[cfg(test)]
211mod tests {
212    use steel_worldgen::density_functions::overworld::OverworldNoiseSettings;
213
214    use super::*;
215
216    struct TestLevel {
217        raw_brightness: u8,
218        ambient_light: f32,
219        sky_darkening: u8,
220    }
221
222    impl LevelReader for TestLevel {
223        fn get_block_state(&self, _pos: BlockPos) -> BlockStateId {
224            BlockStateId(0)
225        }
226
227        fn raw_brightness(&self, _pos: BlockPos, sky_darkening: u8) -> u8 {
228            self.raw_brightness.saturating_sub(sky_darkening)
229        }
230
231        fn sky_darkening(&self) -> u8 {
232            self.sky_darkening
233        }
234
235        fn ambient_light(&self) -> f32 {
236            self.ambient_light
237        }
238
239        fn min_y(&self) -> i32 {
240            OverworldNoiseSettings::MIN_Y
241        }
242
243        fn height(&self) -> i32 {
244            OverworldNoiseSettings::HEIGHT
245        }
246
247        fn sea_level(&self) -> i32 {
248            OverworldNoiseSettings::SEA_LEVEL
249        }
250    }
251
252    fn assert_f32_close(left: f32, right: f32) {
253        assert!(
254            (left - right).abs() < 0.000_001,
255            "left={left}, right={right}"
256        );
257    }
258
259    #[test]
260    fn pathfinding_cost_uses_vanilla_curved_light_value() {
261        let level = TestLevel {
262            raw_brightness: 6,
263            ambient_light: 0.0,
264            sky_darkening: 0,
265        };
266
267        assert_f32_close(
268            level.pathfinding_cost_from_light_levels(BlockPos::ZERO),
269            -0.357_142_87,
270        );
271    }
272
273    #[test]
274    fn pathfinding_cost_lerps_toward_full_light_with_ambient_light() {
275        let level = TestLevel {
276            raw_brightness: 6,
277            ambient_light: 0.2,
278            sky_darkening: 0,
279        };
280
281        assert_f32_close(
282            level.pathfinding_cost_from_light_levels(BlockPos::ZERO),
283            -0.185_714_3,
284        );
285    }
286
287    #[test]
288    fn max_local_raw_brightness_matches_vanilla_horizontal_limit() {
289        let level = TestLevel {
290            raw_brightness: 0,
291            ambient_light: 0.0,
292            sky_darkening: 0,
293        };
294
295        assert_eq!(
296            level.max_local_raw_brightness(BlockPos::new(29_999_999, 64, 0), 0),
297            0
298        );
299        assert_eq!(
300            level.max_local_raw_brightness(BlockPos::new(30_000_000, 64, 0), 0),
301            15
302        );
303    }
304
305    #[test]
306    fn light_level_dependent_magic_value_applies_current_sky_darkening() {
307        let level = TestLevel {
308            raw_brightness: 15,
309            ambient_light: 0.0,
310            sky_darkening: 11,
311        };
312
313        // Brightness is max(block, sky - darkening); raw 15 with darkening 11
314        // leaves 4, curving to 4/15 -> ~0.083 instead of the full 1.0.
315        assert_f32_close(
316            level.light_level_dependent_magic_value(BlockPos::ZERO),
317            0.083_333_33,
318        );
319    }
320
321    #[test]
322    fn can_see_sky_uses_vanilla_sky_light_threshold() {
323        assert!(
324            TestLevel {
325                raw_brightness: 15,
326                ambient_light: 0.0,
327                sky_darkening: 0,
328            }
329            .can_see_sky(BlockPos::ZERO)
330        );
331        assert!(
332            !TestLevel {
333                raw_brightness: 14,
334                ambient_light: 0.0,
335                sky_darkening: 0,
336            }
337            .can_see_sky(BlockPos::ZERO)
338        );
339    }
340}