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