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