1use 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
21pub trait LevelReader {
23 fn get_block_state(&self, pos: BlockPos) -> BlockStateId;
25
26 #[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 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 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 fn raw_brightness(&self, pos: BlockPos, sky_darkening: u8) -> u8;
57
58 fn can_see_sky(&self, pos: BlockPos) -> bool {
60 self.raw_brightness(pos, 0) >= 15
61 }
62
63 fn ambient_light(&self) -> f32 {
65 0.0
66 }
67
68 fn min_y(&self) -> i32;
70
71 fn height(&self) -> i32;
73
74 fn max_y_exclusive(&self) -> i32 {
76 self.min_y() + self.height()
77 }
78
79 fn is_outside_build_height(&self, y: i32) -> bool {
81 y < self.min_y() || y >= self.max_y_exclusive()
82 }
83
84 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 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 fn pathfinding_cost_from_light_levels(&self, pos: BlockPos) -> f32 {
106 self.light_level_dependent_magic_value(pos) - 0.5
107 }
108}
109
110pub trait ScheduledTickAccess: LevelReader {
116 fn fluid_tick_delay(&self, fluid: FluidRef) -> i32;
118
119 fn schedule_block_tick_default(&self, pos: BlockPos, block: BlockRef, delay: i32) -> bool;
121
122 #[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 #[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 fn schedule_fluid_tick_default(&self, pos: BlockPos, fluid: FluidRef, delay: i32) -> bool;
142
143 #[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
153pub trait LevelAccessor: ScheduledTickAccess {
155 fn set_block_state(&self, pos: BlockPos, state: BlockStateId, flags: UpdateFlags) -> bool;
157
158 #[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 #[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}