Skip to main content

steel_core/chunk_saver/storage/
entities.rs

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