1use std::sync::Arc;
4
5use glam::DVec3;
6use steel_math::DEGREE_90;
7use steel_protocol::packets::game::RelativeMovement;
8use steel_registry::{
9 blocks::{block_state_ext::BlockStateExt, properties::BlockStateProperties},
10 dimension_type::DimensionType,
11};
12use steel_utils::{
13 BlockPos, ChunkPos, SectionPos,
14 axis::Axis,
15 block_util::{FoundRectangle, get_largest_rectangle_around},
16};
17
18use crate::{
19 entity::Entity,
20 portal::{
21 PortalTicketTarget, TeleportPostTransition, TeleportTransition, portal_shape::PortalShape,
22 },
23 world::World,
24};
25
26const NETHER_TARGET_PORTAL_SEARCH_RADIUS: i32 = 16;
27const OVERWORLD_TARGET_PORTAL_SEARCH_RADIUS: i32 = 128;
28const PORTAL_RECTANGLE_SCAN_LIMIT: i32 = 21;
29
30#[must_use]
32pub(crate) const fn search_radius(to_nether: bool) -> i32 {
33 if to_nether {
34 NETHER_TARGET_PORTAL_SEARCH_RADIUS
35 } else {
36 OVERWORLD_TARGET_PORTAL_SEARCH_RADIUS
37 }
38}
39
40#[must_use]
42pub(crate) const fn prewarm_chunk_radius(to_nether: bool) -> u8 {
43 let chunk_radius = search_radius(to_nether) / 16 + 2;
44 chunk_radius as u8
45}
46
47#[must_use]
49pub(crate) const fn prewarm_center(pos: BlockPos) -> ChunkPos {
50 ChunkPos::new(
51 SectionPos::block_to_section_coord(pos.x()),
52 SectionPos::block_to_section_coord(pos.z()),
53 )
54}
55
56#[must_use]
58pub(crate) fn approximate_exit_position(
59 source_world: &World,
60 target_world: &World,
61 entity_position: DVec3,
62) -> BlockPos {
63 let scale = DimensionType::get_teleportation_scale(
64 source_world.dimension_type,
65 target_world.dimension_type,
66 );
67 target_world.clamp_to_world_border(
68 entity_position.x * scale,
69 entity_position.y,
70 entity_position.z * scale,
71 )
72}
73
74#[must_use]
76pub(crate) fn calculate_transition(
77 source_world: &Arc<World>,
78 target_world: &Arc<World>,
79 entity: &dyn Entity,
80 portal_entry_pos: BlockPos,
81 approximate_exit_pos: BlockPos,
82 to_nether: bool,
83) -> Option<TeleportTransition> {
84 let exit_portal_pos =
85 target_world.find_closest_nether_portal_position(approximate_exit_pos, to_nether);
86 let (exit_portal, ticket_target) = if let Some(pos) = exit_portal_pos {
87 (
88 largest_portal_rectangle_at(target_world, pos)?,
89 PortalTicketTarget::Block(pos),
90 )
91 } else {
92 let source_portal_axis = source_world
93 .get_block_state(portal_entry_pos)
94 .try_get_value(&BlockStateProperties::HORIZONTAL_AXIS)
95 .unwrap_or(Axis::X);
96 let Some(created) =
97 target_world.create_nether_portal(approximate_exit_pos, source_portal_axis)
98 else {
99 log::error!("Unable to create a portal, likely target out of world border");
100 return None;
101 };
102 (created, PortalTicketTarget::Destination)
103 };
104 let post_transition = TeleportPostTransition::play_portal_sound()
105 .then(TeleportPostTransition::place_portal_ticket(ticket_target));
106
107 Some(dimension_transition_from_exit(
108 source_world,
109 target_world,
110 entity,
111 portal_entry_pos,
112 exit_portal,
113 post_transition,
114 ))
115}
116
117fn largest_portal_rectangle_at(world: &World, pos: BlockPos) -> Option<FoundRectangle> {
118 let portal_state = world.get_block_state(pos);
119 let axis = portal_state.try_get_value(&BlockStateProperties::HORIZONTAL_AXIS)?;
120 Some(get_largest_rectangle_around(
121 pos,
122 axis,
123 PORTAL_RECTANGLE_SCAN_LIMIT,
124 Axis::Y,
125 PORTAL_RECTANGLE_SCAN_LIMIT,
126 |block_pos| world.get_block_state(block_pos) == portal_state,
127 ))
128}
129
130fn dimension_transition_from_exit(
131 source_world: &World,
132 target_world: &Arc<World>,
133 entity: &dyn Entity,
134 portal_entry_pos: BlockPos,
135 exit_portal: FoundRectangle,
136 post_transition: TeleportPostTransition,
137) -> TeleportTransition {
138 let source_portal_state = source_world.get_block_state(portal_entry_pos);
139 let (source_axis, offset) = if let Some(axis) =
140 source_portal_state.try_get_value(&BlockStateProperties::HORIZONTAL_AXIS)
141 {
142 let portal_area = get_largest_rectangle_around(
143 portal_entry_pos,
144 axis,
145 PORTAL_RECTANGLE_SCAN_LIMIT,
146 Axis::Y,
147 PORTAL_RECTANGLE_SCAN_LIMIT,
148 |pos| source_world.get_block_state(pos) == source_portal_state,
149 );
150 (axis, entity.get_relative_portal_position(axis, portal_area))
151 } else {
152 (Axis::X, DVec3::new(0.5, 0.0, 0.0))
153 };
154
155 create_dimension_transition(
156 target_world,
157 exit_portal,
158 source_axis,
159 offset,
160 entity,
161 post_transition,
162 )
163}
164
165fn create_dimension_transition(
166 target_world: &Arc<World>,
167 found_rectangle: FoundRectangle,
168 portal_axis: Axis,
169 offset: DVec3,
170 entity: &dyn Entity,
171 post_transition: TeleportPostTransition,
172) -> TeleportTransition {
173 let bottom_left = found_rectangle.min_corner;
174 let target_axis = target_world
175 .get_block_state(bottom_left)
176 .try_get_value(&BlockStateProperties::HORIZONTAL_AXIS)
177 .unwrap_or(Axis::X);
178 let width = f64::from(found_rectangle.axis1_size);
179 let height = f64::from(found_rectangle.axis2_size);
180 let dimensions = entity.dimensions_for_pose(entity.pose());
181 let entity_width = f64::from(dimensions.width);
182 let entity_height = f64::from(dimensions.height);
183 let output_rotation = if portal_axis == target_axis {
184 0.0
185 } else {
186 DEGREE_90
187 };
188 let offset_right = entity_width / 2.0 + (width - entity_width) * offset.x;
189 let offset_up = (height - entity_height) * offset.y;
190 let offset_forward = 0.5 + offset.z;
191 let x_aligned = target_axis == Axis::X;
192 let target_pos = DVec3::new(
193 f64::from(bottom_left.x())
194 + if x_aligned {
195 offset_right
196 } else {
197 offset_forward
198 },
199 f64::from(bottom_left.y()) + offset_up,
200 f64::from(bottom_left.z())
201 + if x_aligned {
202 offset_forward
203 } else {
204 offset_right
205 },
206 );
207 let collision_free_pos =
208 PortalShape::find_collision_free_position(target_pos, target_world, entity, dimensions);
209
210 TeleportTransition {
211 target_world: target_world.clone(),
212 position: collision_free_pos,
213 rotation: (output_rotation, 0.0),
214 velocity: DVec3::ZERO,
215 relatives: RelativeMovement::DELTA.union(RelativeMovement::ROTATION),
216 portal_cooldown: entity.dimension_changing_delay(),
217 as_passenger: false,
218 post_transition,
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::{prewarm_chunk_radius, search_radius};
225
226 #[test]
227 fn search_radius_matches_vanilla_portal_forcer_targets() {
228 assert_eq!(search_radius(true), 16);
229 assert_eq!(search_radius(false), 128);
230 }
231
232 #[test]
233 fn prewarm_radius_covers_poi_search_and_exit_rectangle_edges() {
234 assert_eq!(prewarm_chunk_radius(true), 3);
235 assert_eq!(prewarm_chunk_radius(false), 10);
236 }
237}