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    /// Returns vanilla `BlockAndLightGetter.canSeeSky`.
61    fn can_see_sky(&self, pos: BlockPos) -> bool {
62        self.raw_brightness(pos, 0) >= MAX_LIGHT_LEVEL
63    }
64
65    /// Returns this dimension's vanilla ambient light factor.
66    fn ambient_light(&self) -> f32 {
67        0.0
68    }
69
70    /// Returns the minimum build height.
71    fn min_y(&self) -> i32;
72
73    /// Returns the build height.
74    fn height(&self) -> i32;
75
76    /// Returns the vanilla heightmap value at a column.
77    fn height_at(&self, _heightmap_type: HeightmapType, _x: i32, _z: i32) -> i32 {
78        self.min_y()
79    }
80
81    /// Returns the exclusive maximum build height.
82    fn max_y_exclusive(&self) -> i32 {
83        self.min_y() + self.height()
84    }
85
86    /// Checks if a Y coordinate is outside build height.
87    fn is_outside_build_height(&self, y: i32) -> bool {
88        y < self.min_y() || y >= self.max_y_exclusive()
89    }
90
91    /// Returns vanilla `LevelReader.getMaxLocalRawBrightness`.
92    fn max_local_raw_brightness(&self, pos: BlockPos, sky_darkening: u8) -> u8 {
93        if !World::is_in_world_bounds_horizontal(pos) {
94            return MAX_LIGHT_LEVEL;
95        }
96
97        self.raw_brightness(pos, sky_darkening)
98    }
99
100    /// Returns vanilla `LevelReader.getLightLevelDependentMagicValue`.
101    fn light_level_dependent_magic_value(&self, pos: BlockPos) -> f32 {
102        let value = f32::from(self.max_local_raw_brightness(pos, 0)) / f32::from(MAX_LIGHT_LEVEL);
103        let curved_value = value / value.mul_add(-3.0, 4.0);
104        curved_value + self.ambient_light() * (1.0 - curved_value)
105    }
106
107    /// Returns vanilla `LevelReader.getPathfindingCostFromLightLevels`.
108    fn pathfinding_cost_from_light_levels(&self, pos: BlockPos) -> f32 {
109        self.light_level_dependent_magic_value(pos) - 0.5
110    }
111}
112
113/// Level access needed by vanilla block `updateShape` logic.
114///
115/// Vanilla passes both `LevelReader` and `ScheduledTickAccess` to block shape updates.
116/// Steel combines those surfaces so the same block behavior can run against a live
117/// `World` and a `WorldGenRegion`.
118pub trait ScheduledTickAccess: LevelReader {
119    /// Returns the fluid tick delay in this level.
120    fn fluid_tick_delay(&self, fluid: FluidRef) -> i32;
121
122    /// Schedules a block tick using vanilla's default priority.
123    fn schedule_block_tick_default(&self, pos: BlockPos, block: BlockRef, delay: i32) -> bool;
124
125    /// Returns whether a tick is already scheduled for the same `(pos, block)`.
126    #[expect(
127        unused_variables,
128        reason = "most test/worldgen level surfaces do not track scheduled tick presence"
129    )]
130    fn has_scheduled_block_tick(&self, pos: BlockPos, block: BlockRef) -> bool {
131        false
132    }
133
134    /// Returns whether the same `(pos, block)` was selected for this tick and has not started.
135    #[expect(
136        unused_variables,
137        reason = "worldgen and most test level surfaces do not execute scheduled tick batches"
138    )]
139    fn will_tick_block_this_tick(&self, pos: BlockPos, block: BlockRef) -> bool {
140        false
141    }
142
143    /// Schedules a fluid tick using vanilla's default priority.
144    fn schedule_fluid_tick_default(&self, pos: BlockPos, fluid: FluidRef, delay: i32) -> bool;
145
146    /// Returns whether the same `(pos, fluid)` was selected for this tick and has not started.
147    #[expect(
148        unused_variables,
149        reason = "worldgen and most test level surfaces do not execute scheduled tick batches"
150    )]
151    fn will_tick_fluid_this_tick(&self, pos: BlockPos, fluid: FluidRef) -> bool {
152        false
153    }
154}
155
156/// Mutable level access needed by vanilla `LevelAccessor` block hooks.
157pub trait LevelAccessor: ScheduledTickAccess {
158    /// Sets a block state with vanilla update flags.
159    fn set_block_state(&self, pos: BlockPos, state: BlockStateId, flags: UpdateFlags) -> bool;
160
161    /// Destroys a block and optionally drops its resources.
162    fn destroy_block(&self, pos: BlockPos, drop_items: bool) -> bool;
163
164    /// Returns whether this surface accepts writes into the given chunk.
165    fn can_write_to_chunk(&self, _chunk_x: i32, _chunk_z: i32) -> bool {
166        true
167    }
168
169    /// Returns whether multi-block placements must be dry-run before committing.
170    fn requires_live_write_preflight(&self) -> bool {
171        false
172    }
173
174    /// Plays a block sound when this level surface supports runtime side effects.
175    #[expect(
176        unused_variables,
177        reason = "worldgen and test level surfaces do not emit sounds"
178    )]
179    fn play_block_sound(
180        &self,
181        sound: SoundEventRef,
182        pos: BlockPos,
183        volume: f32,
184        pitch: f32,
185        exclude: Option<i32>,
186    ) {
187    }
188
189    /// Dispatches a game event when this level surface supports runtime listeners.
190    #[expect(
191        unused_variables,
192        reason = "worldgen and test level surfaces do not emit game events"
193    )]
194    fn game_event(&self, event: GameEventRef, pos: BlockPos, context: &GameEventContext<'_>) {}
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    struct TestLevel {
202        raw_brightness: u8,
203        ambient_light: f32,
204    }
205
206    impl LevelReader for TestLevel {
207        fn get_block_state(&self, _pos: BlockPos) -> BlockStateId {
208            BlockStateId(0)
209        }
210
211        fn raw_brightness(&self, _pos: BlockPos, _sky_darkening: u8) -> u8 {
212            self.raw_brightness
213        }
214
215        fn ambient_light(&self) -> f32 {
216            self.ambient_light
217        }
218
219        fn min_y(&self) -> i32 {
220            -64
221        }
222
223        fn height(&self) -> i32 {
224            384
225        }
226    }
227
228    fn assert_f32_close(left: f32, right: f32) {
229        assert!(
230            (left - right).abs() < 0.000_001,
231            "left={left}, right={right}"
232        );
233    }
234
235    #[test]
236    fn pathfinding_cost_uses_vanilla_curved_light_value() {
237        let level = TestLevel {
238            raw_brightness: 6,
239            ambient_light: 0.0,
240        };
241
242        assert_f32_close(
243            level.pathfinding_cost_from_light_levels(BlockPos::ZERO),
244            -0.357_142_87,
245        );
246    }
247
248    #[test]
249    fn pathfinding_cost_lerps_toward_full_light_with_ambient_light() {
250        let level = TestLevel {
251            raw_brightness: 6,
252            ambient_light: 0.2,
253        };
254
255        assert_f32_close(
256            level.pathfinding_cost_from_light_levels(BlockPos::ZERO),
257            -0.185_714_3,
258        );
259    }
260
261    #[test]
262    fn max_local_raw_brightness_matches_vanilla_horizontal_limit() {
263        let level = TestLevel {
264            raw_brightness: 0,
265            ambient_light: 0.0,
266        };
267
268        assert_eq!(
269            level.max_local_raw_brightness(BlockPos::new(29_999_999, 64, 0), 0),
270            0
271        );
272        assert_eq!(
273            level.max_local_raw_brightness(BlockPos::new(30_000_000, 64, 0), 0),
274            15
275        );
276    }
277
278    #[test]
279    fn can_see_sky_uses_vanilla_sky_light_threshold() {
280        assert!(
281            TestLevel {
282                raw_brightness: 15,
283                ambient_light: 0.0,
284            }
285            .can_see_sky(BlockPos::ZERO)
286        );
287        assert!(
288            !TestLevel {
289                raw_brightness: 14,
290                ambient_light: 0.0,
291            }
292            .can_see_sky(BlockPos::ZERO)
293        );
294    }
295}