1use glam::DVec3;
4use std::sync::Arc;
5use steel_math::inverse_lerp;
6use steel_registry::blocks::BlockRef;
7use steel_registry::blocks::block_state_ext::BlockStateExt;
8use steel_registry::blocks::properties::BlockStateProperties;
9use steel_registry::entity_type::EntityDimensions;
10use steel_registry::vanilla_block_tags::BlockTag;
11use steel_registry::vanilla_blocks;
12use steel_utils::axis::Axis;
13use steel_utils::block_util::FoundRectangle;
14use steel_utils::types::UpdateFlags;
15use steel_utils::{BlockPos, Direction, WorldAabb};
16
17use crate::entity::Entity;
18use crate::physics::WorldCollisionProvider;
19use crate::world::{LevelReader, World};
20
21pub struct PortalShape {
23 pub axis: Axis,
25 pub bottom_left: BlockPos,
27 pub width: u32,
29 pub height: u32,
31 pub right_dir: Direction,
33 pub portal: BlockRef,
35 num_portal_blocks: u32,
38}
39
40pub struct PortalFrameConfig {
42 pub min_width: u32,
44 pub max_width: u32,
46 pub min_height: u32,
48 pub max_height: u32,
50 pub frame: BlockRef,
52 pub portal: BlockRef,
54}
55
56#[must_use]
58pub fn nether_portal_config() -> PortalFrameConfig {
59 PortalFrameConfig {
60 min_width: 2,
61 max_width: 21,
62 min_height: 3,
63 max_height: 21,
64 frame: &vanilla_blocks::OBSIDIAN,
65 portal: &vanilla_blocks::NETHER_PORTAL,
66 }
67}
68
69fn is_empty(
72 world: &(impl LevelReader + ?Sized),
73 pos: BlockPos,
74 config: &PortalFrameConfig,
75) -> bool {
76 let state = world.get_block_state(pos);
77 if state.is_air() {
78 return true;
79 }
80 let block = state.get_block();
81 block.has_tag(&BlockTag::FIRE) || block == config.portal
82}
83
84const fn block_pos_axis(pos: BlockPos, axis: Axis) -> i32 {
85 match axis {
86 Axis::X => pos.x(),
87 Axis::Y => pos.y(),
88 Axis::Z => pos.z(),
89 }
90}
91
92const fn vec_axis(pos: DVec3, axis: Axis) -> f64 {
93 match axis {
94 Axis::X => pos.x,
95 Axis::Y => pos.y,
96 Axis::Z => pos.z,
97 }
98}
99
100impl PortalShape {
101 pub fn find_empty_portal_shape(
104 world: &(impl LevelReader + ?Sized),
105 fire_pos: BlockPos,
106 config: &PortalFrameConfig,
107 ) -> Option<Self> {
108 Self::find_empty_portal_shape_with_axis(world, fire_pos, Axis::X, config)
109 }
110
111 pub fn find_empty_portal_shape_with_axis(
114 world: &(impl LevelReader + ?Sized),
115 fire_pos: BlockPos,
116 preferred_axis: Axis,
117 config: &PortalFrameConfig,
118 ) -> Option<Self> {
119 let other_axis = if preferred_axis == Axis::X {
120 Axis::Z
121 } else {
122 Axis::X
123 };
124 Self::try_axis(world, fire_pos, preferred_axis, config)
125 .filter(|s| s.num_portal_blocks == 0)
126 .or_else(|| {
127 Self::try_axis(world, fire_pos, other_axis, config)
128 .filter(|s| s.num_portal_blocks == 0)
129 })
130 }
131
132 pub fn find_any_shape(
135 world: &(impl LevelReader + ?Sized),
136 pos: BlockPos,
137 axis: Axis,
138 config: &PortalFrameConfig,
139 ) -> Option<Self> {
140 Self::try_axis(world, pos, axis, config)
141 }
142
143 fn try_axis(
145 world: &(impl LevelReader + ?Sized),
146 pos: BlockPos,
147 axis: Axis,
148 config: &PortalFrameConfig,
149 ) -> Option<Self> {
150 let right_dir: Direction = match axis {
152 Axis::X => Direction::West,
153 Axis::Z => Direction::South,
154 Axis::Y => return None,
155 };
156
157 let bottom_left = Self::calculate_bottom_left(world, pos, right_dir, config)?;
158
159 let width = Self::calculate_width(world, bottom_left, right_dir, config);
160 if width == 0 {
161 return None;
162 }
163
164 let mut num_portal_blocks = 0;
165 let height = Self::calculate_height(
166 world,
167 bottom_left,
168 width,
169 right_dir,
170 config,
171 &mut num_portal_blocks,
172 );
173 if height < config.min_height {
174 return None;
175 }
176
177 if !Self::has_top_frame(world, bottom_left, height, width, right_dir, config) {
178 return None;
179 }
180
181 Some(Self {
182 axis,
183 bottom_left,
184 width,
185 height,
186 right_dir,
187 portal: config.portal,
188 num_portal_blocks,
189 })
190 }
191
192 fn get_distance_until_edge(
196 world: &(impl LevelReader + ?Sized),
197 pos: BlockPos,
198 direction: Direction,
199 config: &PortalFrameConfig,
200 ) -> u32 {
201 for i in 0..=config.max_width {
202 let next = pos.relative_n(direction, i as i32);
203 if !is_empty(world, next, config) {
204 return if Self::is_frame_block(world, next, config) {
206 i
207 } else {
208 0
209 };
210 }
211 if !Self::is_frame_block(world, next.below(), config) {
212 return 0;
213 }
214 }
215 0
216 }
217
218 fn calculate_bottom_left(
220 world: &(impl LevelReader + ?Sized),
221 pos: BlockPos,
222 right_dir: Direction,
223 config: &PortalFrameConfig,
224 ) -> Option<BlockPos> {
225 let mut cur = pos;
227 for _ in 0..config.max_height {
228 let next = cur.below();
229 if !is_empty(world, next, config) {
230 break;
231 }
232 cur = next;
233 }
234
235 let left_dir = right_dir.opposite();
237 let dist = Self::get_distance_until_edge(world, cur, left_dir, config);
238 if dist == 0 {
239 return None;
240 }
241 Some(cur.relative_n(left_dir, (dist - 1) as i32))
242 }
243
244 fn calculate_width(
246 world: &(impl LevelReader + ?Sized),
247 bottom_left: BlockPos,
248 right_dir: Direction,
249 config: &PortalFrameConfig,
250 ) -> u32 {
251 let dist = Self::get_distance_until_edge(world, bottom_left, right_dir, config);
252 if dist < config.min_width || dist > config.max_width {
253 return 0;
254 }
255 dist
256 }
257
258 fn calculate_height(
264 world: &(impl LevelReader + ?Sized),
265 bottom_left: BlockPos,
266 width: u32,
267 right_dir: Direction,
268 config: &PortalFrameConfig,
269 portal_block_count: &mut u32,
270 ) -> u32 {
271 let mut height = 0;
272 'outer: for h in 0..config.max_height {
273 let row_start = bottom_left.above_n(h as i32);
274
275 if !Self::is_frame_block(world, row_start.relative(right_dir.opposite()), config) {
277 break;
278 }
279 if !Self::is_frame_block(world, row_start.relative_n(right_dir, width as i32), config) {
281 break;
282 }
283
284 for w in 0..width {
286 let interior_pos = row_start.relative_n(right_dir, w as i32);
287 if !is_empty(world, interior_pos, config) {
288 break 'outer;
289 }
290 if world.get_block_state(interior_pos).get_block() == config.portal {
291 *portal_block_count += 1;
292 }
293 }
294 height = h + 1;
295 }
296 height
297 }
298
299 fn has_top_frame(
301 world: &(impl LevelReader + ?Sized),
302 bottom_left: BlockPos,
303 height: u32,
304 width: u32,
305 right_dir: Direction,
306 config: &PortalFrameConfig,
307 ) -> bool {
308 let top_row = bottom_left.above_n(height as i32);
309 for w in 0..width {
310 if !Self::is_frame_block(world, top_row.relative_n(right_dir, w as i32), config) {
311 return false;
312 }
313 }
314 true
315 }
316
317 fn is_frame_block(
318 world: &(impl LevelReader + ?Sized),
319 pos: BlockPos,
320 config: &PortalFrameConfig,
321 ) -> bool {
322 world.get_block_state(pos).get_block() == config.frame
323 }
324
325 #[must_use]
327 pub const fn is_complete(&self) -> bool {
328 self.num_portal_blocks == self.width * self.height
329 }
330
331 pub fn place_portal_blocks(&self, world: &Arc<World>) {
335 let portal_state = self
336 .portal
337 .default_state()
338 .set_value(&BlockStateProperties::HORIZONTAL_AXIS, self.axis);
339 let flags = UpdateFlags::UPDATE_CLIENTS.union(UpdateFlags::UPDATE_KNOWN_SHAPE);
340 for w in 0..self.width {
341 for h in 0..self.height {
342 world.set_block(
343 self.bottom_left
344 .above_n(h as i32)
345 .relative_n(self.right_dir, w as i32),
346 portal_state,
347 flags,
348 );
349 }
350 }
351 }
352
353 #[must_use]
357 pub fn get_relative_position(
358 largest_rectangle_around: FoundRectangle,
359 axis: Axis,
360 position: DVec3,
361 dimensions: EntityDimensions,
362 ) -> DVec3 {
363 let width = f64::from(largest_rectangle_around.axis1_size) - f64::from(dimensions.width);
364 let height = f64::from(largest_rectangle_around.axis2_size) - f64::from(dimensions.height);
365 let bottom_min = largest_rectangle_around.min_corner;
366 let relative_right = if width > 0.0 {
367 let bottom_start =
368 f64::from(block_pos_axis(bottom_min, axis)) + f64::from(dimensions.width) / 2.0;
369 inverse_lerp(vec_axis(position, axis) - bottom_start, 0.0, width).clamp(0.0, 1.0)
370 } else {
371 0.5
372 };
373
374 let relative_up = if height > 0.0 {
375 inverse_lerp(
376 vec_axis(position, Axis::Y) - f64::from(block_pos_axis(bottom_min, Axis::Y)),
377 0.0,
378 height,
379 )
380 .clamp(0.0, 1.0)
381 } else {
382 0.0
383 };
384
385 let forward_axis = if axis == Axis::X { Axis::Z } else { Axis::X };
386 let relative_forward = vec_axis(position, forward_axis)
387 - (f64::from(block_pos_axis(bottom_min, forward_axis)) + 0.5);
388 DVec3::new(relative_right, relative_up, relative_forward)
389 }
390
391 #[must_use]
394 pub fn find_collision_free_position(
395 bottom_center: DVec3,
396 world: &Arc<World>,
397 entity: &dyn Entity,
398 dimensions: EntityDimensions,
399 ) -> DVec3 {
400 if dimensions.width > 4.0 || dimensions.height > 4.0 {
401 return bottom_center;
402 }
403
404 let width = f64::from(dimensions.width);
405 let height = f64::from(dimensions.height);
406 let half_height = height / 2.0;
407 let center = bottom_center + DVec3::new(0.0, half_height, 0.0);
408 let allowed_centers = [WorldAabb::of_size(center, width, 0.0, width)
409 .expand_towards(DVec3::Y)
410 .inflate(1.0E-6)];
411
412 WorldCollisionProvider::for_entity(world, entity)
413 .find_free_position(&allowed_centers, center, width, height, width)
414 .map_or(bottom_center, |pos| pos - DVec3::new(0.0, half_height, 0.0))
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use glam::DVec3;
421 use steel_registry::entity_type::EntityDimensions;
422 use steel_utils::BlockPos;
423 use steel_utils::axis::Axis;
424 use steel_utils::block_util::FoundRectangle;
425
426 use super::PortalShape;
427
428 #[test]
429 fn relative_portal_position_matches_vanilla_axis_math() {
430 let rectangle = FoundRectangle {
431 min_corner: BlockPos::new(10, 64, 20),
432 axis1_size: 4,
433 axis2_size: 5,
434 };
435 let position = DVec3::new(12.0, 66.0, 20.75);
436 let dimensions = EntityDimensions::new(1.0, 2.0, 1.62);
437
438 let relative = PortalShape::get_relative_position(rectangle, Axis::X, position, dimensions);
439
440 assert!((relative.x - 0.5).abs() < f64::EPSILON);
441 assert!((relative.y - (2.0 / 3.0)).abs() < f64::EPSILON);
442 assert!((relative.z - 0.25).abs() < f64::EPSILON);
443 }
444
445 #[test]
446 fn relative_portal_position_clamps_right_and_up_offsets() {
447 let rectangle = FoundRectangle {
448 min_corner: BlockPos::new(10, 64, 20),
449 axis1_size: 4,
450 axis2_size: 5,
451 };
452 let position = DVec3::new(8.0, 80.0, 20.5);
453 let dimensions = EntityDimensions::new(1.0, 2.0, 1.62);
454
455 let relative = PortalShape::get_relative_position(rectangle, Axis::X, position, dimensions);
456
457 assert!((relative.x - 0.0).abs() < f64::EPSILON);
458 assert!((relative.y - 1.0).abs() < f64::EPSILON);
459 }
460
461 #[test]
462 fn relative_portal_position_uses_vanilla_fallbacks_when_entity_fills_rectangle() {
463 let rectangle = FoundRectangle {
464 min_corner: BlockPos::new(10, 64, 20),
465 axis1_size: 2,
466 axis2_size: 3,
467 };
468 let position = DVec3::new(11.0, 65.0, 20.25);
469 let dimensions = EntityDimensions::new(3.0, 4.0, 1.62);
470
471 let relative = PortalShape::get_relative_position(rectangle, Axis::Z, position, dimensions);
472
473 assert!((relative.x - 0.5).abs() < f64::EPSILON);
474 assert!((relative.y - 0.0).abs() < f64::EPSILON);
475 assert!((relative.z - 0.5).abs() < f64::EPSILON);
476 }
477}