Skip to main content

steel_registry/entity_data/
mod.rs

1//! Entity data synchronization system.
2//!
3//! This module provides the infrastructure for syncing entity state (health, pose, flags, etc.)
4//! between server and client via the `SetEntityData` packet.
5//!
6//! # Architecture
7//!
8//! - [`SyncedValue<T>`] - Wrapper that tracks per-field dirty state
9//! - [`EntityData`] - Type-erased enum for network serialization
10//! - [`DataValue`] - Network-ready value with index and serializer info
11//! - [`EntityPose`] - Entity pose states (standing, sneaking, swimming, etc.)
12//! - [`EntityDataSerializerRegistry`] - Registry of serializers with writers
13//!
14//! # Serialization Flow
15//!
16//! Serialization happens through the registry, not directly on `EntityData`:
17//! ```ignore
18//! let writer = REGISTRY.entity_data_serializers.get_writer(serializer_id).unwrap();
19//! writer(&entity_data_value, &mut buf)?;
20//! ```
21//!
22//! # Generated Code
23//!
24//! Per-entity data structs (e.g., `PlayerEntityData`) are generated by the build script
25//! from `entities.json` and live in `steel-registry/src/generated/entity_data.rs`.
26
27mod serializer;
28mod vanilla_serializers;
29
30pub use serializer::{
31    EntityDataSerializerEntry, EntityDataSerializerEntryRef, EntityDataSerializerRegistry,
32    EntityDataWriter,
33};
34pub use vanilla_serializers::register_vanilla_entity_data_serializers;
35
36use std::{io, str::FromStr};
37
38use steel_utils::{
39    BlockStateId, Identifier,
40    codec::VarInt,
41    serial::{ReadFrom, WriteTo},
42};
43use text_components::TextComponent;
44use uuid::Uuid;
45
46use crate::RegistryReference;
47use crate::cat_sound_variant::CatSoundVariant;
48use crate::cat_variant::CatVariant;
49use crate::chicken_sound_variant::ChickenSoundVariant;
50use crate::chicken_variant::ChickenVariant;
51use crate::cow_sound_variant::CowSoundVariant;
52use crate::cow_variant::CowVariant;
53use crate::frog_variant::FrogVariant;
54use crate::item_stack::ItemStack;
55use crate::painting_variant::PaintingVariant;
56pub use crate::particle_type::{ColorParticleOption, ParticleData};
57use crate::pig_sound_variant::PigSoundVariant;
58use crate::pig_variant::PigVariant;
59use crate::wolf_sound_variant::WolfSoundVariant;
60use crate::wolf_variant::WolfVariant;
61use crate::zombie_nautilus_variant::ZombieNautilusVariant;
62
63// Re-export types used in generated code
64pub use crate::blocks::properties::Direction;
65pub use steel_utils::BlockPos;
66
67/// Wrapper that tracks modifications per-field.
68///
69/// Each field in an entity data struct is wrapped in `SyncedValue` to track
70/// whether it has been modified and needs to be synced to clients.
71#[derive(Debug, Clone)]
72pub struct SyncedValue<T> {
73    value: T,
74    default: T,
75    dirty: bool,
76}
77
78impl<T: Clone + PartialEq> SyncedValue<T> {
79    /// Create a new synced value with the given default.
80    pub fn new(default: T) -> Self {
81        Self {
82            value: default.clone(),
83            default,
84            dirty: false,
85        }
86    }
87
88    /// Get a reference to the current value.
89    #[inline]
90    pub const fn get(&self) -> &T {
91        &self.value
92    }
93
94    /// Set the value. Only marks as dirty if the value actually changed.
95    #[inline]
96    pub fn set(&mut self, value: T) {
97        if self.value != value {
98            self.value = value;
99            self.dirty = true;
100        }
101    }
102
103    /// Returns true if the value has been modified since last sync.
104    #[inline]
105    pub const fn is_dirty(&self) -> bool {
106        self.dirty
107    }
108
109    /// Clear the dirty flag after syncing.
110    #[inline]
111    pub const fn clear_dirty(&mut self) {
112        self.dirty = false;
113    }
114
115    /// Returns true if the current value equals the default.
116    #[inline]
117    pub fn is_default(&self) -> bool {
118        self.value == self.default
119    }
120}
121
122/// Entity pose states.
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
124#[repr(u8)]
125pub enum EntityPose {
126    #[default]
127    Standing = 0,
128    FallFlying = 1,
129    Sleeping = 2,
130    Swimming = 3,
131    SpinAttack = 4,
132    Sneaking = 5,
133    LongJumping = 6,
134    Dying = 7,
135    Croaking = 8,
136    UsingTongue = 9,
137    Sitting = 10,
138    Roaring = 11,
139    Sniffing = 12,
140    Emerging = 13,
141    Digging = 14,
142    Sliding = 15,
143    Shooting = 16,
144    Inhaling = 17,
145}
146
147impl FromStr for EntityPose {
148    type Err = ();
149
150    fn from_str(s: &str) -> Result<Self, Self::Err> {
151        match s {
152            "STANDING" => Ok(Self::Standing),
153            "FALL_FLYING" => Ok(Self::FallFlying),
154            "SLEEPING" => Ok(Self::Sleeping),
155            "SWIMMING" => Ok(Self::Swimming),
156            "SPIN_ATTACK" => Ok(Self::SpinAttack),
157            "SNEAKING" | "CROUCHING" => Ok(Self::Sneaking),
158            "LONG_JUMPING" => Ok(Self::LongJumping),
159            "DYING" => Ok(Self::Dying),
160            "CROAKING" => Ok(Self::Croaking),
161            "USING_TONGUE" => Ok(Self::UsingTongue),
162            "SITTING" => Ok(Self::Sitting),
163            "ROARING" => Ok(Self::Roaring),
164            "SNIFFING" => Ok(Self::Sniffing),
165            "EMERGING" => Ok(Self::Emerging),
166            "DIGGING" => Ok(Self::Digging),
167            "SLIDING" => Ok(Self::Sliding),
168            "SHOOTING" => Ok(Self::Shooting),
169            "INHALING" => Ok(Self::Inhaling),
170            _ => Err(()),
171        }
172    }
173}
174
175/// Rotations for armor stands and similar entities (pitch, yaw, roll in degrees).
176#[derive(Debug, Clone, Copy, PartialEq)]
177pub struct Rotations {
178    pub x: f32,
179    pub y: f32,
180    pub z: f32,
181}
182
183impl Rotations {
184    pub const ZERO: Self = Self {
185        x: 0.0,
186        y: 0.0,
187        z: 0.0,
188    };
189
190    #[must_use]
191    pub const fn new(x: f32, y: f32, z: f32) -> Self {
192        Self { x, y, z }
193    }
194}
195
196/// Main hand preference (left or right).
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
198#[repr(u8)]
199pub enum HumanoidArm {
200    Left = 0,
201    #[default]
202    Right = 1,
203}
204
205impl ReadFrom for HumanoidArm {
206    fn read(data: &mut std::io::Cursor<&[u8]>) -> io::Result<Self> {
207        match VarInt::read(data)?.0 {
208            0 => Ok(Self::Left),
209            1 => Ok(Self::Right),
210            value => Err(io::Error::new(
211                io::ErrorKind::InvalidData,
212                format!("invalid humanoid arm id {value}"),
213            )),
214        }
215    }
216}
217
218/// Villager profession, type, and level data.
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
220pub struct VillagerData {
221    /// Villager type (biome variant) - registry ID.
222    pub villager_type: i32,
223    /// Profession - registry ID.
224    pub profession: i32,
225    /// Trading level (1-5).
226    pub level: i32,
227}
228
229impl VillagerData {
230    #[must_use]
231    pub const fn new(villager_type: i32, profession: i32, level: i32) -> Self {
232        Self {
233            villager_type,
234            profession,
235            level,
236        }
237    }
238}
239
240/// Selects a villager profession by numeric registry order, preserving the fallback when empty.
241pub(crate) fn random_villager_profession_id(
242    random: &mut impl steel_utils::random::Random,
243    profession_count: usize,
244    fallback: i32,
245) -> i32 {
246    if profession_count == 0 {
247        return fallback;
248    }
249
250    let Ok(profession_count) = i32::try_from(profession_count) else {
251        panic!("villager profession registry exceeds Vanilla's signed integer range");
252    };
253    random.next_i32_bounded(profession_count)
254}
255
256/// A global position (dimension + block position).
257#[derive(Debug, Clone, PartialEq, Eq, Hash)]
258pub struct GlobalPos {
259    pub dimension: Identifier,
260    pub pos: BlockPos,
261}
262
263impl GlobalPos {
264    #[must_use]
265    pub const fn new(dimension: Identifier, pos: BlockPos) -> Self {
266        Self { dimension, pos }
267    }
268}
269
270/// A 3D vector (for display entities).
271#[derive(Debug, Clone, Copy, PartialEq)]
272pub struct Vector3f {
273    pub x: f32,
274    pub y: f32,
275    pub z: f32,
276}
277
278impl Vector3f {
279    pub const ZERO: Self = Self {
280        x: 0.0,
281        y: 0.0,
282        z: 0.0,
283    };
284
285    #[must_use]
286    pub const fn new(x: f32, y: f32, z: f32) -> Self {
287        Self { x, y, z }
288    }
289}
290
291/// A quaternion rotation (for display entities).
292#[derive(Debug, Clone, Copy, PartialEq)]
293pub struct Quaternionf {
294    pub x: f32,
295    pub y: f32,
296    pub z: f32,
297    pub w: f32,
298}
299
300impl Quaternionf {
301    pub const IDENTITY: Self = Self {
302        x: 0.0,
303        y: 0.0,
304        z: 0.0,
305        w: 1.0,
306    };
307
308    #[must_use]
309    pub const fn new(x: f32, y: f32, z: f32, w: f32) -> Self {
310        Self { x, y, z, w }
311    }
312}
313
314/// A list of particle effects.
315#[derive(Debug, Clone, PartialEq, Default)]
316pub struct ParticleList {
317    pub particles: Vec<ParticleData>,
318}
319
320pub use crate::resolvable_profile::ResolvableProfile;
321
322/// Sniffer entity state.
323#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
324#[repr(u8)]
325pub enum SnifferState {
326    #[default]
327    Idling = 0,
328    FeelingHappy = 1,
329    Scenting = 2,
330    Sniffing = 3,
331    Searching = 4,
332    Digging = 5,
333    Rising = 6,
334}
335
336/// Armadillo entity state.
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
338#[repr(u8)]
339pub enum ArmadilloState {
340    #[default]
341    Idle = 0,
342    RollingUp = 1,
343    Rolled = 2,
344    Unrolling = 3,
345    Scared = 4,
346}
347
348/// Type-erased entity data for network encoding.
349///
350/// This enum holds the actual value to be sent over the network.
351/// Serialization happens through the [`EntityDataSerializerRegistry`], not directly
352/// on this type. Use `REGISTRY.entity_data_serializers.get_writer(id)` to get the
353/// writer function for a specific serializer ID.
354#[derive(Debug, Clone)]
355pub enum EntityData {
356    // Primitive types
357    Byte(i8),
358    Int(i32),
359    Long(i64),
360    Float(f32),
361    String(String),
362    Boolean(bool),
363
364    // Text components
365    Component(Box<TextComponent>),
366    OptionalComponent(Option<Box<TextComponent>>),
367
368    // Item
369    ItemStack(ItemStack),
370
371    // Spatial types
372    Rotations(Rotations),
373    BlockPos(BlockPos),
374    OptionalBlockPos(Option<BlockPos>),
375    Direction(Direction),
376    OptionalLivingEntityRef(Option<Uuid>),
377    BlockState(BlockStateId),
378    OptionalBlockState(Option<BlockStateId>),
379    Vector3(Vector3f),
380    Quaternion(Quaternionf),
381
382    // Entity states
383    Pose(EntityPose),
384    SnifferState(SnifferState),
385    ArmadilloState(ArmadilloState),
386
387    // Particles
388    Particle(ParticleData),
389    Particles(ParticleList),
390
391    // Villager
392    VillagerData(VillagerData),
393
394    // Optional numeric
395    OptionalUnsignedInt(Option<u32>),
396
397    // Holder/registry reference variants (encoded as VarInt registry IDs)
398    CatVariant(RegistryReference<CatVariant>),
399    CatSoundVariant(RegistryReference<CatSoundVariant>),
400    CowVariant(RegistryReference<CowVariant>),
401    CowSoundVariant(RegistryReference<CowSoundVariant>),
402    WolfVariant(RegistryReference<WolfVariant>),
403    WolfSoundVariant(RegistryReference<WolfSoundVariant>),
404    FrogVariant(RegistryReference<FrogVariant>),
405    PigVariant(RegistryReference<PigVariant>),
406    PigSoundVariant(RegistryReference<PigSoundVariant>),
407    ChickenVariant(RegistryReference<ChickenVariant>),
408    ChickenSoundVariant(RegistryReference<ChickenSoundVariant>),
409    ZombieNautilusVariant(RegistryReference<ZombieNautilusVariant>),
410    PaintingVariant(RegistryReference<PaintingVariant>),
411
412    // Global position
413    OptionalGlobalPos(Option<GlobalPos>),
414
415    // Copper golem / weathering states (future entities)
416    CopperGolemState(i32),
417    WeatheringCopperState(i32),
418
419    // Profile
420    ResolvableProfile(ResolvableProfile),
421
422    // Player
423    HumanoidArm(HumanoidArm),
424}
425
426/// Network-ready data value with index and serializer info.
427///
428/// This is what gets written to the `SetEntityData` packet.
429#[derive(Debug, Clone)]
430pub struct DataValue {
431    /// The index of this data field (0-254, 255 is terminator).
432    pub index: u8,
433    /// The serializer ID (from registration order in `EntityDataSerializerRegistry`).
434    pub serializer_id: i32,
435    /// The actual value to write.
436    pub value: EntityData,
437}
438
439impl DataValue {
440    /// Write this data value to the buffer (without terminator).
441    ///
442    /// Uses the global registry to look up the writer function for this serializer.
443    pub fn write_to(&self, buf: &mut Vec<u8>) -> io::Result<()> {
444        use crate::REGISTRY;
445
446        self.index.write(buf)?;
447        VarInt(self.serializer_id).write(buf)?;
448
449        let writer = REGISTRY
450            .entity_data_serializers
451            .get_writer(self.serializer_id)
452            .ok_or_else(|| {
453                io::Error::other(format!(
454                    "Unknown entity data serializer ID: {}",
455                    self.serializer_id
456                ))
457            })?;
458
459        writer(&self.value, buf)
460    }
461}
462
463/// Write a list of data values to a buffer, with the 0xFF terminator.
464pub fn write_data_values(values: &[DataValue], buf: &mut Vec<u8>) -> io::Result<()> {
465    for value in values {
466        value.write_to(buf)?;
467    }
468    // Write terminator
469    0xFFu8.write(buf)
470}
471
472#[cfg(test)]
473mod tests {
474    use steel_utils::{
475        ArgbColor, Identifier,
476        codec::VarInt,
477        random::{Random as _, legacy_random::LegacyRandom},
478        serial::WriteTo,
479    };
480
481    use crate::vanilla_entity_data::{EggEntityData, ItemEntityData, ZombieVillagerEntityData};
482    use crate::{REGISTRY, RegistryExt, init_vanilla_registry};
483
484    use super::{
485        ColorParticleOption, EntityData, EntityDataSerializerRegistry, ParticleData,
486        random_villager_profession_id, register_vanilla_entity_data_serializers,
487    };
488
489    #[test]
490    fn zombie_villager_default_selects_profession_from_registry() {
491        init_vanilla_registry();
492
493        let profession_count = REGISTRY.villager_professions.len();
494        let Ok(profession_bound) = i32::try_from(profession_count) else {
495            panic!("test villager profession registry must fit in i32");
496        };
497        let Some(nitwit_id) = REGISTRY
498            .villager_professions
499            .id_from_key(&Identifier::vanilla_static("nitwit"))
500        else {
501            panic!("nitwit villager profession must be registered");
502        };
503        let Some(plains_id) = REGISTRY
504            .villager_types
505            .id_from_key(&Identifier::vanilla_static("plains"))
506        else {
507            panic!("plains villager type must be registered");
508        };
509
510        let mut expected_random = LegacyRandom::from_seed(25);
511        let expected_profession = expected_random.next_i32_bounded(profession_bound);
512        assert_eq!(usize::try_from(expected_profession), Ok(nitwit_id));
513
514        let mut random = LegacyRandom::from_seed(25);
515        let data = ZombieVillagerEntityData::new(&mut random);
516        let villager_data = data.villager_data.get();
517
518        assert_eq!(usize::try_from(villager_data.villager_type), Ok(plains_id));
519        assert_eq!(villager_data.profession, expected_profession);
520        assert_eq!(villager_data.level, 1);
521        assert!(data.pack_all().is_empty());
522    }
523
524    #[test]
525    fn empty_villager_profession_registry_preserves_fallback_without_rng_draw() {
526        let mut random = LegacyRandom::from_seed(25);
527        let seed_before = random.get_seed();
528
529        assert_eq!(random_villager_profession_id(&mut random, 0, 7), 7);
530        assert_eq!(random.get_seed(), seed_before);
531    }
532
533    #[test]
534    fn projectile_item_stack_defaults_use_extracted_item() {
535        let data = EggEntityData::new();
536        let stack = data.throwable_item_projectile().item_stack.get();
537
538        assert_eq!(&stack.item().key, &Identifier::vanilla_static("egg"));
539        assert_eq!(stack.count(), 1);
540        assert!(data.pack_all().is_empty());
541    }
542
543    #[test]
544    fn empty_item_stack_defaults_remain_empty() {
545        let data = ItemEntityData::new();
546
547        assert!(data.item.get().is_empty());
548        assert!(data.pack_all().is_empty());
549    }
550
551    #[test]
552    fn entity_effect_particle_color_options_encode_payload() {
553        crate::init_vanilla_registry();
554        let registry = &*crate::REGISTRY;
555        let entity_effect = Identifier::vanilla_static("entity_effect");
556        let Some(particle_type_id) = registry.particle_types.id_from_key(&entity_effect) else {
557            panic!("entity_effect particle type must be registered");
558        };
559
560        let mut serializers = EntityDataSerializerRegistry::new();
561        register_vanilla_entity_data_serializers(&mut serializers);
562
563        let Some(serializer_id) = serializers.id_from_key(&Identifier::vanilla_static("particle"))
564        else {
565            panic!("particle entity-data serializer must be registered");
566        };
567
568        let Some(writer) = serializers.get_writer(serializer_id as i32) else {
569            panic!("particle entity-data serializer must have a writer");
570        };
571
572        let particle = ParticleData::new(
573            &crate::vanilla_particle_types::ENTITY_EFFECT,
574            ColorParticleOption::new(ArgbColor::new(-1)),
575        );
576        let value = EntityData::Particle(particle);
577        let mut encoded = Vec::new();
578        let result = writer(&value, &mut encoded);
579        assert!(result.is_ok(), "{result:?}");
580
581        let mut expected = Vec::new();
582        let result = VarInt(particle_type_id as i32).write(&mut expected);
583        assert!(result.is_ok(), "{result:?}");
584        let result = (-1i32).write(&mut expected);
585        assert!(result.is_ok(), "{result:?}");
586
587        assert_eq!(encoded, expected);
588    }
589}