Skip to main content

steel_core/block_entity/entities/
end_gateway.rs

1//! End gateway block entity.
2
3use 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
20/// Block entity for end gateways, tracking beam age, teleport cooldown, and
21/// the linked exit portal position.
22pub 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
35// SAFETY: This key is owned by Steel and uniquely identifies `EndGatewayBlockEntity`.
36unsafe impl DowncastType for EndGatewayBlockEntity {
37    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:block_entity/end_gateway");
38}
39
40impl EndGatewayBlockEntity {
41    /// Creates an End gateway block entity with vanilla default state.
42    #[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    /// Returns whether the gateway is still in its initial spawning phase,
56    /// during which it isn't yet usable for teleportation.
57    #[must_use]
58    pub fn is_spawning(&self) -> bool {
59        self.gateway.lock().age < SPAWN_TIME
60    }
61
62    /// Returns whether the gateway is on cooldown from a recent teleport.
63    #[must_use]
64    pub fn is_cooling_down(&self) -> bool {
65        self.gateway.lock().teleport_cooldown > 0
66    }
67
68    /// Returns the stored gateway exit position.
69    #[must_use]
70    pub fn exit_portal(&self) -> Option<BlockPos> {
71        self.gateway.lock().exit_portal
72    }
73
74    /// Returns whether the stored exit is used exactly.
75    #[must_use]
76    pub fn exact_teleport(&self) -> bool {
77        self.gateway.lock().exact_teleport
78    }
79
80    /// Sets the stored gateway exit position.
81    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    /// Triggers vanilla gateway cooldown and broadcasts the block event.
91    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}