Skip to main content

steel_core/chunk_saver/storage/
entities.rs

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