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