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::chunk::heightmap::HeightmapType;
18use crate::chunk::light::MAX_LIGHT_LEVEL;
19use crate::world::game_event::GameEventContext;
20
21use super::World;
22
23pub trait LevelReader {
25 fn get_block_state(&self, pos: BlockPos) -> BlockStateId;
27
28 #[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 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 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 fn raw_brightness(&self, pos: BlockPos, sky_darkening: u8) -> u8;
59
60 fn sky_darkening(&self) -> u8 {
63 0
64 }
65
66 fn can_see_sky(&self, pos: BlockPos) -> bool {
69 self.raw_brightness(pos, 0) >= MAX_LIGHT_LEVEL
70 }
71
72 fn ambient_light(&self) -> f32 {
74 0.0
75 }
76
77 fn min_y(&self) -> i32;
79
80 fn height(&self) -> i32;
82
83 fn height_at(&self, _heightmap_type: HeightmapType, _x: i32, _z: i32) -> i32 {
85 self.min_y()
86 }
87
88 fn sea_level(&self) -> i32;
90
91 fn max_y_exclusive(&self) -> i32 {
93 self.min_y() + self.height()
94 }
95
96 fn is_outside_build_height(&self, y: i32) -> bool {
98 y < self.min_y() || y >= self.max_y_exclusive()
99 }
100
101 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 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 fn pathfinding_cost_from_light_levels(&self, pos: BlockPos) -> f32 {
122 self.light_level_dependent_magic_value(pos) - 0.5
123 }
124}
125
126pub trait ScheduledTickAccess: LevelReader {
132 fn fluid_tick_delay(&self, fluid: FluidRef) -> i32;
134
135 fn schedule_block_tick_default(&self, pos: BlockPos, block: BlockRef, delay: i32) -> bool;
137
138 #[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 #[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 fn schedule_fluid_tick_default(&self, pos: BlockPos, fluid: FluidRef, delay: i32) -> bool;
158
159 #[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
169pub trait LevelAccessor: ScheduledTickAccess {
171 fn set_block_state(&self, pos: BlockPos, state: BlockStateId, flags: UpdateFlags) -> bool;
173
174 fn destroy_block(&self, pos: BlockPos, drop_items: bool) -> bool;
176
177 fn can_write_to_chunk(&self, _chunk_x: i32, _chunk_z: i32) -> bool {
179 true
180 }
181
182 fn requires_live_write_preflight(&self) -> bool {
184 false
185 }
186
187 #[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 #[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 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}