Skip to main content

steel_registry/
entity_type.rs

1use glam::DVec3;
2use rustc_hash::FxHashMap;
3use steel_utils::Identifier;
4
5/// Mob category for spawn classification.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum MobCategory {
8    Monster,
9    Creature,
10    Ambient,
11    Axolotls,
12    UndergroundWaterCreature,
13    WaterCreature,
14    WaterAmbient,
15    Misc,
16}
17
18/// Vanilla attachment point kind used by `EntityDimensions`.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum EntityAttachment {
21    Passenger,
22    Vehicle,
23    NameTag,
24    WardenChest,
25}
26
27/// A vanilla entity attachment point before yaw rotation is applied.
28#[derive(Debug, Clone, Copy, PartialEq)]
29pub struct EntityAttachmentPoint {
30    pub x: f64,
31    pub y: f64,
32    pub z: f64,
33}
34
35impl EntityAttachmentPoint {
36    #[must_use]
37    pub const fn new(x: f64, y: f64, z: f64) -> Self {
38        Self { x, y, z }
39    }
40
41    #[must_use]
42    fn scaled(self, scale_x: f32, scale_y: f32, scale_z: f32) -> DVec3 {
43        DVec3::new(
44            self.x * f64::from(scale_x),
45            self.y * f64::from(scale_y),
46            self.z * f64::from(scale_z),
47        )
48    }
49}
50
51/// Vanilla `EntityAttachments`.
52#[derive(Debug, Clone, Copy, PartialEq)]
53pub struct EntityAttachments {
54    pub passenger: &'static [EntityAttachmentPoint],
55    pub vehicle: &'static [EntityAttachmentPoint],
56    pub name_tag: &'static [EntityAttachmentPoint],
57    pub warden_chest: &'static [EntityAttachmentPoint],
58    scale_x: f32,
59    scale_y: f32,
60    scale_z: f32,
61}
62
63impl EntityAttachments {
64    #[must_use]
65    pub const fn new(
66        passenger: &'static [EntityAttachmentPoint],
67        vehicle: &'static [EntityAttachmentPoint],
68        name_tag: &'static [EntityAttachmentPoint],
69        warden_chest: &'static [EntityAttachmentPoint],
70    ) -> Self {
71        Self {
72            passenger,
73            vehicle,
74            name_tag,
75            warden_chest,
76            scale_x: 1.0,
77            scale_y: 1.0,
78            scale_z: 1.0,
79        }
80    }
81
82    #[must_use]
83    pub const fn fallback() -> Self {
84        Self::new(&[], &[], &[], &[])
85    }
86
87    #[must_use]
88    pub fn scale(self, width_factor: f32, height_factor: f32) -> Self {
89        Self {
90            scale_x: self.scale_x * width_factor,
91            scale_y: self.scale_y * height_factor,
92            scale_z: self.scale_z * width_factor,
93            ..self
94        }
95    }
96
97    #[must_use]
98    pub fn get_clamped(
99        self,
100        attachment: EntityAttachment,
101        index: usize,
102        yaw_degrees: f32,
103        dimensions: EntityDimensions,
104    ) -> DVec3 {
105        let point = self.points(attachment).map_or_else(
106            || fallback_point(attachment, dimensions),
107            |points| {
108                points[index.min(points.len() - 1)].scaled(self.scale_x, self.scale_y, self.scale_z)
109            },
110        );
111        rotate_attachment_point(point, yaw_degrees)
112    }
113
114    fn points(self, attachment: EntityAttachment) -> Option<&'static [EntityAttachmentPoint]> {
115        let points = match attachment {
116            EntityAttachment::Passenger => self.passenger,
117            EntityAttachment::Vehicle => self.vehicle,
118            EntityAttachment::NameTag => self.name_tag,
119            EntityAttachment::WardenChest => self.warden_chest,
120        };
121        (!points.is_empty()).then_some(points)
122    }
123}
124
125fn fallback_point(attachment: EntityAttachment, dimensions: EntityDimensions) -> DVec3 {
126    match attachment {
127        EntityAttachment::Passenger | EntityAttachment::NameTag => {
128            DVec3::new(0.0, f64::from(dimensions.height), 0.0)
129        }
130        EntityAttachment::Vehicle => DVec3::ZERO,
131        EntityAttachment::WardenChest => DVec3::new(0.0, f64::from(dimensions.height) / 2.0, 0.0),
132    }
133}
134
135fn rotate_attachment_point(point: DVec3, yaw_degrees: f32) -> DVec3 {
136    let radians = f64::from(-yaw_degrees).to_radians();
137    let cos = radians.cos();
138    let sin = radians.sin();
139    DVec3::new(
140        point.x.mul_add(cos, point.z * sin),
141        point.y,
142        point.z.mul_add(cos, -(point.x * sin)),
143    )
144}
145
146/// Entity dimensions used for bounding box calculation.
147/// Bounding box is centered on X/Z with Y at entity feet.
148#[derive(Debug, Clone, Copy, PartialEq)]
149pub struct EntityDimensions {
150    pub width: f32,
151    pub height: f32,
152    pub eye_height: f32,
153    pub attachments: EntityAttachments,
154}
155
156impl EntityDimensions {
157    /// Creates new entity dimensions.
158    #[must_use]
159    pub const fn new(width: f32, height: f32, eye_height: f32) -> Self {
160        Self {
161            width,
162            height,
163            eye_height,
164            attachments: EntityAttachments::fallback(),
165        }
166    }
167
168    /// Creates new entity dimensions with vanilla attachment points.
169    #[must_use]
170    pub const fn new_with_attachments(
171        width: f32,
172        height: f32,
173        eye_height: f32,
174        attachments: EntityAttachments,
175    ) -> Self {
176        Self {
177            width,
178            height,
179            eye_height,
180            attachments,
181        }
182    }
183
184    /// Scale dimensions by a factor (for baby entities, etc.)
185    #[must_use]
186    pub fn scale(&self, factor: f32) -> Self {
187        Self {
188            width: self.width * factor,
189            height: self.height * factor,
190            eye_height: self.eye_height * factor,
191            attachments: self.attachments.scale(factor, factor),
192        }
193    }
194
195    /// Get the half-width for bounding box calculation.
196    #[must_use]
197    pub fn half_width(&self) -> f32 {
198        self.width / 2.0
199    }
200}
201
202/// Behavioral flags for entity collision and interaction.
203#[derive(Debug, Clone, Copy, PartialEq)]
204pub struct EntityFlags {
205    pub is_pushable: bool,
206    pub is_attackable: bool,
207    pub is_pickable: bool,
208    pub can_be_collided_with: bool,
209    pub is_pushed_by_fluid: bool,
210    pub can_freeze: bool,
211    pub can_be_hit_by_projectile: bool,
212    pub is_sensitive_to_water: bool,
213    pub can_breathe_underwater: bool,
214    pub can_be_seen_as_enemy: bool,
215}
216
217#[derive(Debug)]
218pub struct EntityType {
219    pub key: Identifier,
220    pub client_tracking_range: i32,
221    pub update_interval: i32,
222    /// Whether vanilla `ServerEntity` tracks velocity deltas for this type.
223    pub track_deltas: bool,
224
225    /// Default entity dimensions.
226    pub dimensions: EntityDimensions,
227    /// If true, dimensions cannot be scaled.
228    pub fixed: bool,
229
230    /// Mob category for spawn classification.
231    pub mob_category: MobCategory,
232    /// Whether this entity is immune to fire damage.
233    pub fire_immune: bool,
234    /// Whether this entity can be summoned via commands.
235    pub summonable: bool,
236    /// Whether this entity can spawn far from players.
237    pub can_spawn_far_from_player: bool,
238    /// Whether this entity type can be serialized to disk.
239    /// Set to false for transient entities (lightning, fishing hooks, players).
240    pub can_serialize: bool,
241    /// Whether vanilla class hierarchy makes this entity an `AbstractBoat`.
242    pub is_abstract_boat: bool,
243    /// Whether vanilla class hierarchy makes this entity an `AbstractMinecart`.
244    pub is_abstract_minecart: bool,
245
246    /// Behavioral flags for collision and interaction.
247    pub flags: EntityFlags,
248
249    /// Default attribute base values for this entity type
250    /// Empty for entities that don't have attributes (projectiles, items, displays, etc.)
251    pub default_attributes: &'static [(&'static str, f64)],
252}
253
254pub type EntityTypeRef = &'static EntityType;
255
256impl PartialEq for EntityTypeRef {
257    #[expect(clippy::disallowed_methods)] // This IS the PartialEq impl; ptr::eq is correct here
258    fn eq(&self, other: &Self) -> bool {
259        std::ptr::eq(*self, *other)
260    }
261}
262
263pub struct EntityTypeRegistry {
264    types_by_id: Vec<EntityTypeRef>,
265    types_by_key: FxHashMap<Identifier, usize>,
266    tags: FxHashMap<Identifier, Vec<Identifier>>,
267    allows_registering: bool,
268}
269
270impl Default for EntityTypeRegistry {
271    fn default() -> Self {
272        Self::new()
273    }
274}
275
276impl EntityTypeRegistry {
277    // Creates a new, empty registry.
278    #[must_use]
279    pub fn new() -> Self {
280        Self {
281            types_by_id: Vec::new(),
282            types_by_key: FxHashMap::default(),
283            tags: FxHashMap::default(),
284            allows_registering: true,
285        }
286    }
287
288    /// Registers a new entity type
289    pub fn register(&mut self, entity_type: EntityTypeRef) {
290        assert!(
291            self.allows_registering,
292            "Cannot register entity types after the registry has been frozen"
293        );
294        let idx = self.types_by_id.len();
295        self.types_by_key.insert(entity_type.key.clone(), idx);
296        self.types_by_id.push(entity_type);
297    }
298
299    pub fn iter(&self) -> impl Iterator<Item = (usize, EntityTypeRef)> + '_ {
300        self.types_by_id
301            .iter()
302            .enumerate()
303            .map(|(id, &et)| (id, et))
304    }
305}
306
307crate::impl_registry!(
308    EntityTypeRegistry,
309    EntityType,
310    types_by_id,
311    types_by_key,
312    entity_types
313);
314
315crate::impl_tagged_registry!(EntityTypeRegistry, types_by_key, "entity type");
316
317#[cfg(test)]
318mod tests {
319    use crate::vanilla_entities;
320
321    use super::{EntityAttachment, EntityAttachmentPoint, EntityAttachments, EntityDimensions};
322
323    fn assert_vec3_close(left: glam::DVec3, right: glam::DVec3) {
324        let diff = left - right;
325        assert!(
326            diff.length_squared() < 1.0e-12,
327            "expected {left:?} to equal {right:?}"
328        );
329    }
330
331    #[test]
332    fn attachment_points_clamp_index_and_rotate_like_vanilla() {
333        const PASSENGERS: [EntityAttachmentPoint; 2] = [
334            EntityAttachmentPoint::new(0.0, 0.5, 0.0),
335            EntityAttachmentPoint::new(1.0, 0.75, 0.0),
336        ];
337        const ZERO: [EntityAttachmentPoint; 1] = [EntityAttachmentPoint::new(0.0, 0.0, 0.0)];
338        let dimensions = EntityDimensions::new_with_attachments(
339            1.0,
340            2.0,
341            1.7,
342            EntityAttachments::new(&PASSENGERS, &ZERO, &ZERO, &ZERO),
343        );
344
345        let point =
346            dimensions
347                .attachments
348                .get_clamped(EntityAttachment::Passenger, 99, 90.0, dimensions);
349
350        assert_vec3_close(point, glam::DVec3::new(0.0, 0.75, 1.0));
351    }
352
353    #[test]
354    fn fallback_attachment_points_match_vanilla_defaults() {
355        let dimensions = EntityDimensions::new(0.6, 1.8, 1.62);
356
357        assert_vec3_close(
358            dimensions
359                .attachments
360                .get_clamped(EntityAttachment::Passenger, 0, 0.0, dimensions),
361            glam::DVec3::new(0.0, 1.8, 0.0),
362        );
363        assert_vec3_close(
364            dimensions
365                .attachments
366                .get_clamped(EntityAttachment::Vehicle, 0, 0.0, dimensions),
367            glam::DVec3::ZERO,
368        );
369        assert_vec3_close(
370            dimensions
371                .attachments
372                .get_clamped(EntityAttachment::WardenChest, 0, 0.0, dimensions),
373            glam::DVec3::new(0.0, 0.9, 0.0),
374        );
375    }
376
377    #[test]
378    fn vanilla_track_deltas_exclusions_match_entity_type_method() {
379        assert!(!vanilla_entities::PLAYER.track_deltas);
380        assert!(!vanilla_entities::BAT.track_deltas);
381        assert!(!vanilla_entities::ITEM_FRAME.track_deltas);
382        assert!(!vanilla_entities::EVOKER_FANGS.track_deltas);
383
384        assert!(vanilla_entities::ITEM.track_deltas);
385        assert!(vanilla_entities::ARROW.track_deltas);
386    }
387
388    #[test]
389    fn vanilla_class_hierarchy_flags_match_representative_entities() {
390        assert!(vanilla_entities::OAK_BOAT.is_abstract_boat);
391        assert!(vanilla_entities::OAK_CHEST_BOAT.is_abstract_boat);
392        assert!(!vanilla_entities::ITEM.is_abstract_boat);
393
394        assert!(vanilla_entities::MINECART.is_abstract_minecart);
395        assert!(vanilla_entities::CHEST_MINECART.is_abstract_minecart);
396        assert!(vanilla_entities::TNT_MINECART.is_abstract_minecart);
397        assert!(!vanilla_entities::ITEM.is_abstract_minecart);
398    }
399}