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::world::game_event::GameEventContext;
18
19const VANILLA_HORIZONTAL_LIMIT: i32 = 30_000_000;
20
21/// Read-only level access needed by block behavior and worldgen predicates.
22pub trait LevelReader {
23    /// Gets the block state at a position.
24    fn get_block_state(&self, pos: BlockPos) -> BlockStateId;
25
26    /// Gets the block entity at a position when this level surface supports it
27    #[expect(
28        unused_variables,
29        reason = "default trait implementation ignores position"
30    )]
31    fn get_block_entity(&self, pos: BlockPos) -> Option<SharedBlockEntity> {
32        None
33    }
34
35    /// Mirrors vanilla `BlockState.isFaceSturdy` with full-face support.
36    fn is_face_sturdy(&self, state: BlockStateId, pos: BlockPos, direction: Direction) -> bool {
37        self.is_face_sturdy_for(state, pos, direction, SupportType::Full)
38    }
39
40    /// Mirrors vanilla `BlockState.isFaceSturdy` for a specific support type.
41    ///
42    /// Lightweight and worldgen views default to extracted support shapes.
43    /// Live views override this to dispatch through block behavior for dynamic
44    /// world-dependent shapes.
45    fn is_face_sturdy_for(
46        &self,
47        state: BlockStateId,
48        pos: BlockPos,
49        direction: Direction,
50        support_type: SupportType,
51    ) -> bool {
52        state.is_face_sturdy_for_at(pos, direction, support_type)
53    }
54
55    /// Returns vanilla raw brightness at a position after sky darkening.
56    fn raw_brightness(&self, pos: BlockPos, sky_darkening: u8) -> u8;
57
58    /// Returns vanilla `BlockAndLightGetter.canSeeSky`.
59    fn can_see_sky(&self, pos: BlockPos) -> bool {
60        self.raw_brightness(pos, 0) >= 15
61    }
62
63    /// Returns this dimension's vanilla ambient light factor.
64    fn ambient_light(&self) -> f32 {
65        0.0
66    }
67
68    /// Returns the minimum build height.
69    fn min_y(&self) -> i32;
70
71    /// Returns the build height.
72    fn height(&self) -> i32;
73
74    /// Returns the exclusive maximum build height.
75    fn max_y_exclusive(&self) -> i32 {
76        self.min_y() + self.height()
77    }
78
79    /// Checks if a Y coordinate is outside build height.
80    fn is_outside_build_height(&self, y: i32) -> bool {
81        y < self.min_y() || y >= self.max_y_exclusive()
82    }
83
84    /// Returns vanilla `LevelReader.getMaxLocalRawBrightness`.
85    fn max_local_raw_brightness(&self, pos: BlockPos, sky_darkening: u8) -> u8 {
86        if pos.x() < -VANILLA_HORIZONTAL_LIMIT
87            || pos.z() < -VANILLA_HORIZONTAL_LIMIT
88            || pos.x() >= VANILLA_HORIZONTAL_LIMIT
89            || pos.z() >= VANILLA_HORIZONTAL_LIMIT
90        {
91            return 15;
92        }
93
94        self.raw_brightness(pos, sky_darkening)
95    }
96
97    /// Returns vanilla `LevelReader.getLightLevelDependentMagicValue`.
98    fn light_level_dependent_magic_value(&self, pos: BlockPos) -> f32 {
99        let value = f32::from(self.max_local_raw_brightness(pos, 0)) / 15.0;
100        let curved_value = value / value.mul_add(-3.0, 4.0);
101        curved_value + self.ambient_light() * (1.0 - curved_value)
102    }
103
104    /// Returns vanilla `LevelReader.getPathfindingCostFromLightLevels`.
105    fn pathfinding_cost_from_light_levels(&self, pos: BlockPos) -> f32 {
106        self.light_level_dependent_magic_value(pos) - 0.5
107    }
108}
109
110/// Level access needed by vanilla block `updateShape` logic.
111///
112/// Vanilla passes both `LevelReader` and `ScheduledTickAccess` to block shape updates.
113/// Steel combines those surfaces so the same block behavior can run against a live
114/// `World` and a `WorldGenRegion`.
115pub trait ScheduledTickAccess: LevelReader {
116    /// Returns the fluid tick delay in this level.
117    fn fluid_tick_delay(&self, fluid: FluidRef) -> i32;
118
119    /// Schedules a block tick using vanilla's default priority.
120    fn schedule_block_tick_default(&self, pos: BlockPos, block: BlockRef, delay: i32) -> bool;
121
122    /// Returns whether a tick is already scheduled for the same `(pos, block)`.
123    #[expect(
124        unused_variables,
125        reason = "most test/worldgen level surfaces do not track scheduled tick presence"
126    )]
127    fn has_scheduled_block_tick(&self, pos: BlockPos, block: BlockRef) -> bool {
128        false
129    }
130
131    /// Returns whether the same `(pos, block)` was selected for this tick and has not started.
132    #[expect(
133        unused_variables,
134        reason = "worldgen and most test level surfaces do not execute scheduled tick batches"
135    )]
136    fn will_tick_block_this_tick(&self, pos: BlockPos, block: BlockRef) -> bool {
137        false
138    }
139
140    /// Schedules a fluid tick using vanilla's default priority.
141    fn schedule_fluid_tick_default(&self, pos: BlockPos, fluid: FluidRef, delay: i32) -> bool;
142
143    /// Returns whether the same `(pos, fluid)` was selected for this tick and has not started.
144    #[expect(
145        unused_variables,
146        reason = "worldgen and most test level surfaces do not execute scheduled tick batches"
147    )]
148    fn will_tick_fluid_this_tick(&self, pos: BlockPos, fluid: FluidRef) -> bool {
149        false
150    }
151}
152
153/// Mutable level access needed by vanilla `LevelAccessor` block hooks.
154pub trait LevelAccessor: ScheduledTickAccess {
155    /// Sets a block state with vanilla update flags.
156    fn set_block_state(&self, pos: BlockPos, state: BlockStateId, flags: UpdateFlags) -> bool;
157
158    /// Plays a block sound when this level surface supports runtime side effects.
159    #[expect(
160        unused_variables,
161        reason = "worldgen and test level surfaces do not emit sounds"
162    )]
163    fn play_block_sound(
164        &self,
165        sound: SoundEventRef,
166        pos: BlockPos,
167        volume: f32,
168        pitch: f32,
169        exclude: Option<i32>,
170    ) {
171    }
172
173    /// Dispatches a game event when this level surface supports runtime listeners.
174    #[expect(
175        unused_variables,
176        reason = "worldgen and test level surfaces do not emit game events"
177    )]
178    fn game_event(&self, event: GameEventRef, pos: BlockPos, context: &GameEventContext<'_>) {}
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    struct TestLevel {
186        raw_brightness: u8,
187        ambient_light: f32,
188    }
189
190    impl LevelReader for TestLevel {
191        fn get_block_state(&self, _pos: BlockPos) -> BlockStateId {
192            BlockStateId(0)
193        }
194
195        fn raw_brightness(&self, _pos: BlockPos, _sky_darkening: u8) -> u8 {
196            self.raw_brightness
197        }
198
199        fn ambient_light(&self) -> f32 {
200            self.ambient_light
201        }
202
203        fn min_y(&self) -> i32 {
204            -64
205        }
206
207        fn height(&self) -> i32 {
208            384
209        }
210    }
211
212    fn assert_f32_close(left: f32, right: f32) {
213        assert!(
214            (left - right).abs() < 0.000_001,
215            "left={left}, right={right}"
216        );
217    }
218
219    #[test]
220    fn pathfinding_cost_uses_vanilla_curved_light_value() {
221        let level = TestLevel {
222            raw_brightness: 6,
223            ambient_light: 0.0,
224        };
225
226        assert_f32_close(
227            level.pathfinding_cost_from_light_levels(BlockPos::ZERO),
228            -0.357_142_87,
229        );
230    }
231
232    #[test]
233    fn pathfinding_cost_lerps_toward_full_light_with_ambient_light() {
234        let level = TestLevel {
235            raw_brightness: 6,
236            ambient_light: 0.2,
237        };
238
239        assert_f32_close(
240            level.pathfinding_cost_from_light_levels(BlockPos::ZERO),
241            -0.185_714_3,
242        );
243    }
244
245    #[test]
246    fn max_local_raw_brightness_matches_vanilla_horizontal_limit() {
247        let level = TestLevel {
248            raw_brightness: 0,
249            ambient_light: 0.0,
250        };
251
252        assert_eq!(
253            level.max_local_raw_brightness(BlockPos::new(29_999_999, 64, 0), 0),
254            0
255        );
256        assert_eq!(
257            level.max_local_raw_brightness(BlockPos::new(30_000_000, 64, 0), 0),
258            15
259        );
260    }
261
262    #[test]
263    fn can_see_sky_uses_vanilla_sky_light_threshold() {
264        assert!(
265            TestLevel {
266                raw_brightness: 15,
267                ambient_light: 0.0,
268            }
269            .can_see_sky(BlockPos::ZERO)
270        );
271        assert!(
272            !TestLevel {
273                raw_brightness: 14,
274                ambient_light: 0.0,
275            }
276            .can_see_sky(BlockPos::ZERO)
277        );
278    }
279}