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