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::{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
19/// Vanilla `TheEndGatewayBlockEntity`.
20pub 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
33// SAFETY: This key is owned by Steel and uniquely identifies `EndGatewayBlockEntity`.
34unsafe impl DowncastType for EndGatewayBlockEntity {
35    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:block_entity/end_gateway");
36}
37
38impl EndGatewayBlockEntity {
39    /// Creates an End gateway block entity with vanilla default state.
40    #[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    /// Returns vanilla `TheEndGatewayBlockEntity.isSpawning`.
54    #[must_use]
55    pub fn is_spawning(&self) -> bool {
56        self.gateway.lock().age < SPAWN_TIME
57    }
58
59    /// Returns vanilla `TheEndGatewayBlockEntity.isCoolingDown`.
60    #[must_use]
61    pub fn is_cooling_down(&self) -> bool {
62        self.gateway.lock().teleport_cooldown > 0
63    }
64
65    /// Returns the stored gateway exit position.
66    #[must_use]
67    pub fn exit_portal(&self) -> Option<BlockPos> {
68        self.gateway.lock().exit_portal
69    }
70
71    /// Returns whether the stored exit is used exactly.
72    #[must_use]
73    pub fn exact_teleport(&self) -> bool {
74        self.gateway.lock().exact_teleport
75    }
76
77    /// Sets the stored gateway exit position.
78    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    /// Triggers vanilla gateway cooldown and broadcasts the block event.
88    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}