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