Skip to main content

steel_core/chunk_saver/storage/
entities.rs

1use super::*;
2use crate::entity::clamp_loaded_entity_position;
3
4impl ChunkStorage {
5    pub(super) fn entities_to_persistent(entities: &[SharedEntity]) -> Vec<PersistentEntity> {
6        let mut visited = FxHashSet::default();
7        entities
8            .iter()
9            .filter(|entity| !entity.is_passenger())
10            .filter_map(|entity| {
11                Self::entity_to_persistent(entity, &mut visited, EntityPersistenceMode::ChunkSave)
12            })
13            .collect()
14    }
15
16    pub(crate) fn entity_tree_to_persistent(entity: &SharedEntity) -> Option<PersistentEntity> {
17        let mut visited = FxHashSet::default();
18        Self::entity_to_persistent(entity, &mut visited, EntityPersistenceMode::ChunkSave)
19    }
20
21    pub(crate) fn entity_to_dimension_transition_persistent(
22        entity: &SharedEntity,
23    ) -> Option<PersistentEntity> {
24        let mut visited = FxHashSet::default();
25        Self::entity_to_persistent(
26            entity,
27            &mut visited,
28            EntityPersistenceMode::DimensionTransition,
29        )
30    }
31
32    pub(super) fn custom_name_to_persistent(custom_name: Option<&TextComponent>) -> Vec<u8> {
33        let Some(custom_name) = custom_name else {
34            return Vec::new();
35        };
36
37        let mut root = NbtCompound::new();
38        root.insert("CustomName", custom_name.to_codec_nbt());
39        let mut bytes = Vec::new();
40        root.write(&mut bytes);
41        bytes
42    }
43
44    pub(super) fn custom_name_from_persistent(
45        bytes: &[u8],
46        uuid: uuid::Uuid,
47    ) -> Option<TextComponent> {
48        if bytes.is_empty() {
49            return None;
50        }
51
52        let Ok(root) = read_borrowed_compound(&mut Cursor::new(bytes)) else {
53            tracing::warn!(
54                ?uuid,
55                "Failed to parse entity custom name NBT, defaulting to no custom name"
56            );
57            return None;
58        };
59        let root = simdnbt::borrow::NbtCompound::from(&root);
60        let tag = root.get("CustomName")?;
61        let custom_name = TextComponent::from_nbt(&tag.to_owned());
62        if custom_name.is_none() {
63            tracing::warn!(
64                ?uuid,
65                "Failed to decode entity custom name, defaulting to no custom name"
66            );
67            return None;
68        }
69        custom_name
70    }
71
72    pub(super) fn compound_to_persistent(compound: &NbtCompound) -> Vec<u8> {
73        if compound.is_empty() {
74            return Vec::new();
75        }
76
77        let mut bytes = Vec::new();
78        compound.write(&mut bytes);
79        bytes
80    }
81
82    pub(super) fn compound_from_persistent(bytes: &[u8], uuid: uuid::Uuid) -> NbtCompound {
83        if bytes.is_empty() {
84            return NbtCompound::new();
85        }
86
87        let Ok(compound) = read_borrowed_compound(&mut Cursor::new(bytes)) else {
88            tracing::warn!(
89                ?uuid,
90                "Failed to parse entity custom data NBT, defaulting to empty custom data"
91            );
92            return NbtCompound::new();
93        };
94        simdnbt::borrow::NbtCompound::from(&compound).to_owned()
95    }
96
97    pub(super) fn save_data_from_persistent(
98        persistent: &PersistentEntity,
99        uuid: uuid::Uuid,
100    ) -> EntityBaseSaveData {
101        EntityBaseSaveData {
102            air_supply: persistent.air_supply,
103            portal_cooldown: persistent.portal_cooldown,
104            no_gravity: persistent.no_gravity,
105            invulnerable: persistent.invulnerable,
106            custom_name: Self::custom_name_from_persistent(&persistent.custom_name_nbt, uuid),
107            custom_name_visible: persistent.custom_name_visible,
108            silent: persistent.silent,
109            glowing: persistent.glowing,
110            tags: persistent
111                .tags
112                .iter()
113                .take(MAX_ENTITY_TAGS)
114                .cloned()
115                .collect(),
116            custom_data: Self::compound_from_persistent(&persistent.custom_data_nbt, uuid),
117        }
118    }
119
120    pub(super) fn entity_to_persistent(
121        entity: &SharedEntity,
122        visited: &mut FxHashSet<i32>,
123        mode: EntityPersistenceMode,
124    ) -> Option<PersistentEntity> {
125        if !Self::entity_should_persist(entity.as_ref(), mode) {
126            return None;
127        }
128
129        if !visited.insert(entity.id()) {
130            tracing::warn!(
131                uuid = ?entity.uuid(),
132                "Entity passenger tree contains duplicate entity id {}, skipping duplicate save",
133                entity.id()
134            );
135            return None;
136        }
137
138        let pos = entity.position();
139        let stored_pos = if let Some(vehicle) = entity.vehicle() {
140            let vehicle_pos = vehicle.position();
141            DVec3::new(vehicle_pos.x, pos.y, vehicle_pos.z)
142        } else {
143            pos
144        };
145        let vel = entity.velocity();
146        let (yaw, pitch) = entity.rotation();
147        let fire_freeze = entity.fire_freeze_state();
148        let save_data = entity.base().save_data();
149
150        if !stored_pos.x.is_finite() || !stored_pos.y.is_finite() || !stored_pos.z.is_finite() {
151            tracing::warn!(
152                uuid = ?entity.uuid(),
153                "Entity has non-finite position {:?}, skipping save",
154                stored_pos
155            );
156            return None;
157        }
158
159        let mut nbt = NbtCompound::new();
160        entity.save_additional(&mut nbt);
161        let mut nbt_bytes = Vec::new();
162        nbt.write(&mut nbt_bytes);
163
164        let passengers = entity
165            .passengers()
166            .iter()
167            .filter_map(|passenger| Self::entity_to_persistent(passenger, visited, mode))
168            .collect();
169
170        Some(PersistentEntity {
171            entity_type: entity.entity_type().key.clone(),
172            uuid: *entity.uuid().as_bytes(),
173            pos: [stored_pos.x, stored_pos.y, stored_pos.z],
174            motion: [vel.x, vel.y, vel.z],
175            rotation: [yaw, pitch],
176            fall_distance: entity.fall_distance(),
177            remaining_fire_ticks: fire_freeze.remaining_fire_ticks(),
178            ticks_frozen: fire_freeze.ticks_frozen(),
179            is_in_powder_snow: fire_freeze.is_in_powder_snow(),
180            was_in_powder_snow: fire_freeze.was_in_powder_snow(),
181            has_visual_fire: fire_freeze.has_visual_fire(),
182            on_ground: entity.on_ground(),
183            no_gravity: save_data.no_gravity,
184            invulnerable: save_data.invulnerable,
185            air_supply: save_data.air_supply,
186            portal_cooldown: save_data.portal_cooldown,
187            custom_name_nbt: Self::custom_name_to_persistent(save_data.custom_name.as_ref()),
188            custom_name_visible: save_data.custom_name_visible,
189            silent: save_data.silent,
190            glowing: save_data.glowing,
191            tags: save_data.tags.iter().cloned().collect(),
192            custom_data_nbt: Self::compound_to_persistent(&save_data.custom_data),
193            nbt_data: nbt_bytes,
194            passengers,
195        })
196    }
197
198    pub(super) fn entity_should_save(entity: &dyn Entity) -> bool {
199        (!entity.is_removed()
200            || entity
201                .removal_reason()
202                .is_some_and(RemovalReason::should_save))
203            && entity.entity_type().can_serialize
204    }
205
206    pub(super) fn entity_should_persist(entity: &dyn Entity, mode: EntityPersistenceMode) -> bool {
207        match mode {
208            EntityPersistenceMode::ChunkSave => Self::entity_should_save(entity),
209            EntityPersistenceMode::DimensionTransition => !entity.is_removed(),
210        }
211    }
212
213    /// Converts a runtime section to persistent format.
214    pub(super) fn persistent_block_entity_pos(
215        persistent: &PersistentBlockEntity,
216        chunk_pos: ChunkPos,
217    ) -> BlockPos {
218        let abs_x = chunk_pos.0.x * 16 + i32::from(persistent.x);
219        let abs_z = chunk_pos.0.y * 16 + i32::from(persistent.z);
220        BlockPos::new(abs_x, i32::from(persistent.y), abs_z)
221    }
222
223    /// Converts a persistent block entity to runtime format.
224    pub(super) fn persistent_to_block_entity(
225        persistent: &PersistentBlockEntity,
226        chunk_pos: ChunkPos,
227        chunk: FullChunkRef<'_>,
228    ) -> Option<SharedBlockEntity> {
229        let pos = Self::persistent_block_entity_pos(persistent, chunk_pos);
230        let state = chunk.get_block_state(pos);
231        Self::persistent_to_block_entity_at(persistent, pos, chunk.level_weak(), state)
232    }
233
234    pub(super) fn persistent_to_block_entity_at(
235        persistent: &PersistentBlockEntity,
236        pos: BlockPos,
237        level: Weak<World>,
238        state: BlockStateId,
239    ) -> Option<SharedBlockEntity> {
240        // Look up the block entity type
241        let block_entity_type_key = persistent.entity_type.as_ref()?;
242        let block_entity_type = REGISTRY.block_entity_types.by_key(block_entity_type_key)?;
243        if !block_entity_type.is_valid(state.get_block()) {
244            log::warn!(
245                "Skipping block entity {} at {pos:?}: block {} does not accept that type",
246                block_entity_type.key,
247                state.get_block().key,
248            );
249            return None;
250        }
251
252        // Parse and load NBT data
253        if persistent.nbt_data.is_empty() {
254            // No NBT data, just create the entity without loading
255            Some(BLOCK_ENTITIES.create_or_unimplemented(block_entity_type, level, pos, state))
256        } else {
257            // Parse NBT from bytes as borrowed
258            let Ok(nbt) = read_borrowed_compound(&mut Cursor::new(&persistent.nbt_data)) else {
259                log::warn!(
260                    "Skipping block entity {} at {pos:?}: malformed NBT",
261                    block_entity_type.key,
262                );
263                return None;
264            };
265
266            // Create the block entity and load NBT
267            Some(BLOCK_ENTITIES.create_and_load_or_unimplemented(
268                block_entity_type,
269                level,
270                pos,
271                state,
272                &nbt,
273            ))
274        }
275    }
276
277    /// Converts a persistent entity tree to runtime format.
278    pub(crate) fn persistent_to_entity_tree_at_level(
279        persistent: &PersistentEntity,
280        chunk_pos: ChunkPos,
281        level: &Weak<World>,
282    ) -> Vec<SharedEntity> {
283        let mut entities = Vec::new();
284        let Some(entity) = Self::persistent_to_entity_at_level(persistent, chunk_pos, level) else {
285            return entities;
286        };
287
288        entities.push(Arc::clone(&entity));
289        for persistent_passenger in &persistent.passengers {
290            Self::load_persistent_passenger_tree(
291                persistent_passenger,
292                chunk_pos,
293                level,
294                &entity,
295                &mut entities,
296            );
297        }
298        entities
299    }
300
301    pub(super) fn load_persistent_passenger_tree(
302        persistent: &PersistentEntity,
303        chunk_pos: ChunkPos,
304        level: &Weak<World>,
305        vehicle: &SharedEntity,
306        entities: &mut Vec<SharedEntity>,
307    ) {
308        let Some(passenger) = Self::persistent_to_entity_at_level(persistent, chunk_pos, level)
309        else {
310            return;
311        };
312
313        EntityBase::restore_passenger_relationship(vehicle, &passenger);
314        entities.push(Arc::clone(&passenger));
315        for persistent_passenger in &persistent.passengers {
316            Self::load_persistent_passenger_tree(
317                persistent_passenger,
318                chunk_pos,
319                level,
320                &passenger,
321                entities,
322            );
323        }
324    }
325
326    /// Converts one persistent entity to runtime format without loading passengers.
327    pub(super) fn persistent_to_entity_at_level(
328        persistent: &PersistentEntity,
329        chunk_pos: ChunkPos,
330        level: &Weak<World>,
331    ) -> Option<SharedEntity> {
332        use uuid::Uuid;
333
334        // Reconstruct base fields
335        let stored_pos = DVec3::new(persistent.pos[0], persistent.pos[1], persistent.pos[2]);
336        let mut velocity = DVec3::new(
337            persistent.motion[0],
338            persistent.motion[1],
339            persistent.motion[2],
340        );
341        let rotation = (persistent.rotation[0], persistent.rotation[1]);
342        let uuid = Uuid::from_bytes(persistent.uuid);
343
344        // Validate position is finite
345        if !stored_pos.x.is_finite() || !stored_pos.y.is_finite() || !stored_pos.z.is_finite() {
346            tracing::warn!(
347                ?uuid,
348                "Entity has non-finite position {:?}, skipping load",
349                stored_pos
350            );
351            return None;
352        }
353
354        if !rotation.0.is_finite() || !rotation.1.is_finite() {
355            tracing::warn!(
356                ?uuid,
357                "Entity has non-finite rotation {rotation:?}, skipping load"
358            );
359            return None;
360        }
361
362        let pos = clamp_loaded_entity_position(stored_pos);
363
364        // Validate position is within expected chunk (sanity check)
365        let expected_chunk = ChunkPos::from_entity_pos(pos);
366        if chunk_pos != expected_chunk {
367            tracing::warn!(
368                ?uuid,
369                "Entity position {:?} doesn't match chunk {:?}, loading anyway",
370                pos,
371                chunk_pos
372            );
373        }
374
375        // Clamp motion values > 10.0 to 0 (vanilla behavior to prevent corruption)
376        if velocity.x.abs() > 10.0 {
377            velocity.x = 0.0;
378        }
379        if velocity.y.abs() > 10.0 {
380            velocity.y = 0.0;
381        }
382        if velocity.z.abs() > 10.0 {
383            velocity.z = 0.0;
384        }
385
386        // Look up entity type
387        let entity_type = REGISTRY.entity_types.by_key(&persistent.entity_type)?;
388        let save_data = Self::save_data_from_persistent(persistent, uuid);
389
390        // Parse NBT from bytes (or use empty compound data)
391        let nbt_bytes = if persistent.nbt_data.is_empty() {
392            // Empty compound body for `simdnbt::borrow::read_compound`.
393            &[0x00][..]
394        } else {
395            &persistent.nbt_data[..]
396        };
397
398        let Ok(nbt) = read_borrowed_compound(&mut Cursor::new(nbt_bytes)) else {
399            tracing::warn!(?uuid, "Failed to parse entity NBT, skipping");
400            return None;
401        };
402
403        ENTITIES.create_and_load(
404            EntityLoadRequest {
405                entity_type,
406                position: pos,
407                uuid,
408                velocity,
409                rotation,
410                fall_distance: persistent.fall_distance,
411                fire_freeze: EntityFireFreezeState::from_parts(
412                    persistent.remaining_fire_ticks,
413                    persistent.ticks_frozen,
414                    persistent.is_in_powder_snow,
415                    persistent.was_in_powder_snow,
416                    persistent.has_visual_fire,
417                ),
418                on_ground: persistent.on_ground,
419                save_data,
420                world: Weak::clone(level),
421            },
422            &nbt,
423        )
424    }
425}