steel_core/block_entity/entities/
end_gateway.rs1use std::sync::{Arc, Weak};
4
5use simdnbt::borrow::{BaseNbtCompound as BorrowedNbtCompound, NbtCompound as NbtCompoundView};
6use simdnbt::owned::{NbtCompound, NbtTag};
7use steel_registry::blocks::block_state_ext::BlockStateExt as _;
8use steel_registry::{vanilla_block_entity_types, vanilla_blocks};
9use steel_utils::{BlockPos, BlockStateId, DowncastType, DowncastTypeKey, locks::SyncMutex};
10
11use crate::block_entity::{BlockEntity, BlockEntityBase, BlockEntityLifecycleExt as _};
12use crate::world::World;
13
14const SPAWN_TIME: i64 = 200;
15const COOLDOWN_TIME: i32 = 40;
16const ATTENTION_INTERVAL: i64 = 2400;
17const EVENT_COOLDOWN: i32 = 1;
18
19pub struct EndGatewayBlockEntity {
21 base: BlockEntityBase,
22 gateway: SyncMutex<EndGatewayState>,
23}
24
25#[derive(Clone, Copy)]
26struct EndGatewayState {
27 age: i64,
28 teleport_cooldown: i32,
29 exit_portal: Option<BlockPos>,
30 exact_teleport: bool,
31}
32
33unsafe impl DowncastType for EndGatewayBlockEntity {
35 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:block_entity/end_gateway");
36}
37
38impl EndGatewayBlockEntity {
39 #[must_use]
41 pub fn new(world: Weak<World>, pos: BlockPos, state: BlockStateId) -> Self {
42 Self {
43 base: BlockEntityBase::new(&vanilla_block_entity_types::END_GATEWAY, world, pos, state),
44 gateway: SyncMutex::new(EndGatewayState {
45 age: 0,
46 teleport_cooldown: 0,
47 exit_portal: None,
48 exact_teleport: false,
49 }),
50 }
51 }
52
53 #[must_use]
55 pub fn is_spawning(&self) -> bool {
56 self.gateway.lock().age < SPAWN_TIME
57 }
58
59 #[must_use]
61 pub fn is_cooling_down(&self) -> bool {
62 self.gateway.lock().teleport_cooldown > 0
63 }
64
65 #[must_use]
67 pub fn exit_portal(&self) -> Option<BlockPos> {
68 self.gateway.lock().exit_portal
69 }
70
71 #[must_use]
73 pub fn exact_teleport(&self) -> bool {
74 self.gateway.lock().exact_teleport
75 }
76
77 pub fn set_exit_position(&self, exact_position: BlockPos, exact: bool) {
79 {
80 let mut gateway = self.gateway.lock();
81 gateway.exact_teleport = exact;
82 gateway.exit_portal = Some(exact_position);
83 }
84 self.set_changed();
85 }
86
87 pub fn trigger_cooldown(&self, world: &World) {
89 self.gateway.lock().teleport_cooldown = COOLDOWN_TIME;
90 world.block_event(
91 self.get_block_pos(),
92 self.get_block_state().get_block(),
93 EVENT_COOLDOWN,
94 0,
95 );
96 self.set_changed();
97 }
98
99 const fn nbt_bool(value: bool) -> i8 {
100 value as i8
101 }
102
103 fn load_exit_portal(nbt: &NbtCompoundView<'_, '_>) -> Option<BlockPos> {
104 let exit = nbt.int_array("exit_portal")?;
105 if exit.len() != 3 {
106 return None;
107 }
108 let pos = BlockPos::new(exit[0], exit[1], exit[2]);
109 World::is_in_spawnable_bounds(pos).then_some(pos)
110 }
111}
112
113impl BlockEntity for EndGatewayBlockEntity {
114 fn base(&self) -> &BlockEntityBase {
115 &self.base
116 }
117
118 fn trigger_event(&self, param_a: i32, _param_b: i32) -> bool {
119 if param_a != EVENT_COOLDOWN {
120 return false;
121 }
122 self.gateway.lock().teleport_cooldown = COOLDOWN_TIME;
123 true
124 }
125
126 fn load_additional(&self, nbt: &BorrowedNbtCompound<'_>) {
127 let nbt: NbtCompoundView<'_, '_> = nbt.into();
128 let mut gateway = self.gateway.lock();
129 gateway.age = nbt.long("Age").unwrap_or(0);
130 gateway.exit_portal = Self::load_exit_portal(&nbt);
131 gateway.exact_teleport = nbt.byte("ExactTeleport").is_some_and(|value| value != 0);
132 }
133
134 fn save_additional(&self, nbt: &mut NbtCompound) {
135 let gateway = self.gateway.lock();
136 nbt.insert("Age", gateway.age);
137 if let Some(exit) = gateway.exit_portal {
138 nbt.insert(
139 "exit_portal",
140 NbtTag::IntArray(vec![exit.x(), exit.y(), exit.z()]),
141 );
142 }
143 if gateway.exact_teleport {
144 nbt.insert("ExactTeleport", Self::nbt_bool(true));
145 }
146 }
147
148 fn get_update_tag(&self) -> Option<NbtCompound> {
149 let mut nbt = NbtCompound::new();
150 self.save_additional(&mut nbt);
151 Some(nbt)
152 }
153
154 fn tick(&self, world: &Arc<World>) {
155 let pos = self.get_block_pos();
156 let state = world.get_block_state(pos);
157 if state.get_block() != &vanilla_blocks::END_GATEWAY {
158 self.set_removed();
159 return;
160 }
161
162 self.set_block_state(state);
163 let (trigger_cooldown, lifecycle_changed) = {
164 let mut gateway = self.gateway.lock();
165 let was_spawning = gateway.age < SPAWN_TIME;
166 let was_cooling_down = gateway.teleport_cooldown > 0;
167 gateway.age += 1;
168 let trigger_cooldown = if was_cooling_down {
169 gateway.teleport_cooldown -= 1;
170 false
171 } else if gateway.age % ATTENTION_INTERVAL == 0 {
172 gateway.teleport_cooldown = COOLDOWN_TIME;
173 true
174 } else {
175 false
176 };
177 let lifecycle_changed = was_spawning != (gateway.age < SPAWN_TIME)
178 || was_cooling_down != (gateway.teleport_cooldown > 0);
179 (trigger_cooldown, lifecycle_changed)
180 };
181
182 if trigger_cooldown {
183 world.block_event(pos, state.get_block(), EVENT_COOLDOWN, 0);
184 self.set_changed();
185 }
186 if lifecycle_changed {
187 self.set_changed();
188 }
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use std::io::Cursor;
195 use std::sync::Weak;
196
197 use simdnbt::borrow::read_compound as read_borrowed_compound;
198 use steel_registry::{init_vanilla_registry, vanilla_blocks};
199
200 use super::*;
201
202 fn load_from_owned_nbt(gateway: &EndGatewayBlockEntity, nbt: &NbtCompound) {
203 let mut bytes = Vec::new();
204 nbt.write(&mut bytes);
205 let borrowed = read_borrowed_compound(&mut Cursor::new(bytes.as_slice()))
206 .expect("test nbt should reborrow");
207 gateway.load_additional(&borrowed);
208 }
209
210 fn gateway() -> EndGatewayBlockEntity {
211 init_vanilla_registry();
212 EndGatewayBlockEntity::new(
213 Weak::new(),
214 BlockPos::new(4, 65, -9),
215 vanilla_blocks::END_GATEWAY.default_state(),
216 )
217 }
218
219 #[test]
220 fn end_gateway_saves_vanilla_nbt_keys() {
221 let gateway = gateway();
222 gateway.gateway.lock().age = 12;
223 gateway.set_exit_position(BlockPos::new(100, 72, -32), true);
224
225 let mut nbt = NbtCompound::new();
226 gateway.save_additional(&mut nbt);
227
228 assert_eq!(nbt.long("Age"), Some(12));
229 assert_eq!(
230 nbt.int_array("exit_portal").map(<[i32]>::to_vec),
231 Some(vec![100, 72, -32])
232 );
233 assert_eq!(nbt.byte("ExactTeleport"), Some(1));
234 }
235
236 #[test]
237 fn full_metadata_includes_type_and_position_after_additional_data() {
238 let gateway = gateway();
239 let nbt = gateway.save_with_full_metadata();
240
241 assert_eq!(
242 nbt.string("id").map(ToString::to_string),
243 Some("minecraft:end_gateway".to_owned())
244 );
245 assert_eq!(nbt.int("x"), Some(4));
246 assert_eq!(nbt.int("y"), Some(65));
247 assert_eq!(nbt.int("z"), Some(-9));
248 assert_eq!(nbt.long("Age"), Some(0));
249 }
250
251 #[test]
252 fn end_gateway_loads_vanilla_nbt_keys() {
253 let mut nbt = NbtCompound::new();
254 nbt.insert("Age", 44_i64);
255 nbt.insert("exit_portal", NbtTag::IntArray(vec![8, 70, 12]));
256 nbt.insert("ExactTeleport", 1_i8);
257
258 let gateway = gateway();
259 load_from_owned_nbt(&gateway, &nbt);
260
261 assert_eq!(gateway.gateway.lock().age, 44);
262 assert_eq!(gateway.exit_portal(), Some(BlockPos::new(8, 70, 12)));
263 assert!(gateway.exact_teleport());
264 }
265
266 #[test]
267 fn end_gateway_rejects_exit_outside_spawnable_bounds() {
268 let mut nbt = NbtCompound::new();
269 nbt.insert("exit_portal", NbtTag::IntArray(vec![0, 20_000_000, 0]));
270
271 let gateway = gateway();
272 load_from_owned_nbt(&gateway, &nbt);
273
274 assert_eq!(gateway.exit_portal(), None);
275 }
276}