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 can_see_sky(&self, pos: BlockPos) -> bool {
62 self.raw_brightness(pos, 0) >= MAX_LIGHT_LEVEL
63 }
64
65 fn ambient_light(&self) -> f32 {
67 0.0
68 }
69
70 fn min_y(&self) -> i32;
72
73 fn height(&self) -> i32;
75
76 fn height_at(&self, _heightmap_type: HeightmapType, _x: i32, _z: i32) -> i32 {
78 self.min_y()
79 }
80
81 fn max_y_exclusive(&self) -> i32 {
83 self.min_y() + self.height()
84 }
85
86 fn is_outside_build_height(&self, y: i32) -> bool {
88 y < self.min_y() || y >= self.max_y_exclusive()
89 }
90
91 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 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 fn pathfinding_cost_from_light_levels(&self, pos: BlockPos) -> f32 {
109 self.light_level_dependent_magic_value(pos) - 0.5
110 }
111}
112
113pub trait ScheduledTickAccess: LevelReader {
119 fn fluid_tick_delay(&self, fluid: FluidRef) -> i32;
121
122 fn schedule_block_tick_default(&self, pos: BlockPos, block: BlockRef, delay: i32) -> bool;
124
125 #[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 #[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 fn schedule_fluid_tick_default(&self, pos: BlockPos, fluid: FluidRef, delay: i32) -> bool;
145
146 #[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
156pub trait LevelAccessor: ScheduledTickAccess {
158 fn set_block_state(&self, pos: BlockPos, state: BlockStateId, flags: UpdateFlags) -> bool;
160
161 fn destroy_block(&self, pos: BlockPos, drop_items: bool) -> bool;
163
164 fn can_write_to_chunk(&self, _chunk_x: i32, _chunk_z: i32) -> bool {
166 true
167 }
168
169 fn requires_live_write_preflight(&self) -> bool {
171 false
172 }
173
174 #[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 #[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}