Skip to main content

steel_registry/
entity_type.rs

1use glam::DVec3;
2use rustc_hash::FxHashMap;
3use steel_utils::{DowncastType, DowncastTypeKey, Identifier};
4
5use crate::{RegistryTags, blocks::behavior::PushReaction};
6
7const DEFAULT_EYE_HEIGHT_FACTOR: f32 = 0.85;
8
9/// Mob category for spawn classification.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub enum MobCategory {
12    Monster,
13    Creature,
14    Ambient,
15    Axolotls,
16    UndergroundWaterCreature,
17    WaterCreature,
18    WaterAmbient,
19    Misc,
20}
21
22impl MobCategory {
23    #[must_use]
24    pub const fn despawn_distance(self) -> i32 {
25        match self {
26            Self::WaterAmbient => 64,
27            Self::Monster
28            | Self::Creature
29            | Self::Ambient
30            | Self::Axolotls
31            | Self::UndergroundWaterCreature
32            | Self::WaterCreature
33            | Self::Misc => 128,
34        }
35    }
36
37    #[must_use]
38    pub const fn no_despawn_distance(self) -> i32 {
39        32
40    }
41}
42
43/// Vanilla attachment point kind used by `EntityDimensions`.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub enum EntityAttachment {
46    Passenger,
47    Vehicle,
48    NameTag,
49    WardenChest,
50}
51
52/// A vanilla entity attachment point before yaw rotation is applied.
53#[derive(Debug, Clone, Copy, PartialEq)]
54pub struct EntityAttachmentPoint {
55    pub x: f64,
56    pub y: f64,
57    pub z: f64,
58}
59
60impl EntityAttachmentPoint {
61    #[must_use]
62    pub const fn new(x: f64, y: f64, z: f64) -> Self {
63        Self { x, y, z }
64    }
65
66    #[must_use]
67    fn scaled(self, scale_x: f32, scale_y: f32, scale_z: f32) -> DVec3 {
68        DVec3::new(
69            self.x * f64::from(scale_x),
70            self.y * f64::from(scale_y),
71            self.z * f64::from(scale_z),
72        )
73    }
74}
75
76/// Vanilla `EntityAttachments`.
77#[derive(Debug, Clone, Copy, PartialEq)]
78pub struct EntityAttachments {
79    pub passenger: &'static [EntityAttachmentPoint],
80    pub vehicle: &'static [EntityAttachmentPoint],
81    pub name_tag: &'static [EntityAttachmentPoint],
82    pub warden_chest: &'static [EntityAttachmentPoint],
83    scale_x: f32,
84    scale_y: f32,
85    scale_z: f32,
86}
87
88impl EntityAttachments {
89    #[must_use]
90    pub const fn new(
91        passenger: &'static [EntityAttachmentPoint],
92        vehicle: &'static [EntityAttachmentPoint],
93        name_tag: &'static [EntityAttachmentPoint],
94        warden_chest: &'static [EntityAttachmentPoint],
95    ) -> Self {
96        Self {
97            passenger,
98            vehicle,
99            name_tag,
100            warden_chest,
101            scale_x: 1.0,
102            scale_y: 1.0,
103            scale_z: 1.0,
104        }
105    }
106
107    #[must_use]
108    pub const fn fallback() -> Self {
109        Self::new(&[], &[], &[], &[])
110    }
111
112    #[must_use]
113    pub fn scale(self, width_factor: f32, height_factor: f32) -> Self {
114        Self {
115            scale_x: self.scale_x * width_factor,
116            scale_y: self.scale_y * height_factor,
117            scale_z: self.scale_z * width_factor,
118            ..self
119        }
120    }
121
122    #[must_use]
123    pub fn get_clamped(
124        self,
125        attachment: EntityAttachment,
126        index: usize,
127        yaw_degrees: f32,
128        dimensions: EntityDimensions,
129    ) -> DVec3 {
130        let point = self.points(attachment).map_or_else(
131            || fallback_point(attachment, dimensions),
132            |points| {
133                points[index.min(points.len() - 1)].scaled(self.scale_x, self.scale_y, self.scale_z)
134            },
135        );
136        rotate_attachment_point(point, yaw_degrees)
137    }
138
139    #[must_use]
140    pub fn get_average(self, attachment: EntityAttachment, dimensions: EntityDimensions) -> DVec3 {
141        let Some(points) = self.points(attachment) else {
142            return fallback_point(attachment, dimensions);
143        };
144
145        points.iter().fold(DVec3::ZERO, |sum, point| {
146            sum + point.scaled(self.scale_x, self.scale_y, self.scale_z)
147        }) / points.len() as f64
148    }
149
150    fn points(self, attachment: EntityAttachment) -> Option<&'static [EntityAttachmentPoint]> {
151        let points = match attachment {
152            EntityAttachment::Passenger => self.passenger,
153            EntityAttachment::Vehicle => self.vehicle,
154            EntityAttachment::NameTag => self.name_tag,
155            EntityAttachment::WardenChest => self.warden_chest,
156        };
157        (!points.is_empty()).then_some(points)
158    }
159}
160
161fn fallback_point(attachment: EntityAttachment, dimensions: EntityDimensions) -> DVec3 {
162    match attachment {
163        EntityAttachment::Passenger | EntityAttachment::NameTag => {
164            DVec3::new(0.0, f64::from(dimensions.height), 0.0)
165        }
166        EntityAttachment::Vehicle => DVec3::ZERO,
167        EntityAttachment::WardenChest => DVec3::new(0.0, f64::from(dimensions.height) / 2.0, 0.0),
168    }
169}
170
171fn rotate_attachment_point(point: DVec3, yaw_degrees: f32) -> DVec3 {
172    let radians = f64::from(-yaw_degrees).to_radians();
173    let cos = radians.cos();
174    let sin = radians.sin();
175    DVec3::new(
176        point.x.mul_add(cos, point.z * sin),
177        point.y,
178        point.z.mul_add(cos, -(point.x * sin)),
179    )
180}
181
182/// Entity dimensions used for bounding box calculation.
183/// Bounding box is centered on X/Z with Y at entity feet.
184#[derive(Debug, Clone, Copy, PartialEq)]
185pub struct EntityDimensions {
186    pub width: f32,
187    pub height: f32,
188    pub eye_height: f32,
189    pub attachments: EntityAttachments,
190}
191
192impl EntityDimensions {
193    /// Creates new entity dimensions.
194    #[must_use]
195    pub const fn new(width: f32, height: f32, eye_height: f32) -> Self {
196        Self {
197            width,
198            height,
199            eye_height,
200            attachments: EntityAttachments::fallback(),
201        }
202    }
203
204    /// Creates new entity dimensions with vanilla attachment points.
205    #[must_use]
206    pub const fn new_with_attachments(
207        width: f32,
208        height: f32,
209        eye_height: f32,
210        attachments: EntityAttachments,
211    ) -> Self {
212        Self {
213            width,
214            height,
215            eye_height,
216            attachments,
217        }
218    }
219
220    /// Creates new entity dimensions with a default eye height.
221    #[must_use]
222    pub const fn with_default_eye_height(width: f32, height: f32) -> Self {
223        Self {
224            width,
225            height,
226            eye_height: Self::default_eye_height(height),
227            attachments: EntityAttachments::fallback(),
228        }
229    }
230
231    /// Scale dimensions by a factor (for baby entities, etc.)
232    #[must_use]
233    pub fn scale(&self, factor: f32) -> Self {
234        Self {
235            width: self.width * factor,
236            height: self.height * factor,
237            eye_height: self.eye_height * factor,
238            attachments: self.attachments.scale(factor, factor),
239        }
240    }
241
242    /// Get the half-width for bounding box calculation.
243    #[must_use]
244    pub fn half_width(&self) -> f32 {
245        self.width / 2.0
246    }
247
248    /// Gets the default eye height for a given entity height.
249    #[must_use]
250    const fn default_eye_height(height: f32) -> f32 {
251        height * DEFAULT_EYE_HEIGHT_FACTOR
252    }
253}
254
255/// Behavioral flags for entity collision and interaction.
256#[derive(Debug, Clone, Copy, PartialEq)]
257pub struct EntityFlags {
258    pub is_pushable: bool,
259    pub is_attackable: bool,
260    pub is_pickable: bool,
261    pub can_be_collided_with: bool,
262    pub is_pushed_by_fluid: bool,
263    pub can_freeze: bool,
264    pub can_be_hit_by_projectile: bool,
265    pub is_sensitive_to_water: bool,
266    pub can_breathe_underwater: bool,
267    pub can_be_seen_as_enemy: bool,
268    /// Default response returned by this vanilla entity type when pushed by a piston.
269    pub piston_push_reaction: PushReaction,
270}
271
272#[derive(Debug)]
273pub struct EntityType {
274    pub key: Identifier,
275    pub client_tracking_range: i32,
276    pub update_interval: i32,
277    /// Whether vanilla `ServerEntity` tracks velocity deltas for this type.
278    pub track_deltas: bool,
279
280    /// Default entity dimensions.
281    pub dimensions: EntityDimensions,
282    /// If true, dimensions cannot be scaled.
283    pub fixed: bool,
284
285    /// Mob category for spawn classification.
286    pub mob_category: MobCategory,
287    /// Whether this entity is immune to fire damage.
288    pub fire_immune: bool,
289    /// Whether this entity can be summoned via commands.
290    pub summonable: bool,
291    /// Whether this entity type is allowed to exist in Peaceful difficulty.
292    pub allowed_in_peaceful: bool,
293    /// Whether this entity can spawn far from players.
294    pub can_spawn_far_from_player: bool,
295    /// Whether this entity type can be serialized to disk.
296    /// Set to false for transient entities (lightning, fishing hooks, players).
297    pub can_serialize: bool,
298    /// Whether only server operators may set this entity's data via a spawn
299    /// item's `ENTITY_DATA` component. Mirrors vanilla `EntityType.onlyOpCanSetNbt`.
300    pub only_op_can_set_nbt: bool,
301    /// Whether vanilla class hierarchy makes this entity an `AbstractBoat`.
302    pub is_abstract_boat: bool,
303    /// Whether vanilla class hierarchy makes this entity an `AbstractMinecart`.
304    pub is_abstract_minecart: bool,
305    /// Whether vanilla class hierarchy makes this entity a `Projectile`.
306    pub is_projectile: bool,
307    /// Whether vanilla class hierarchy makes this entity an `AbstractArrow`.
308    pub is_abstract_arrow: bool,
309    /// Whether vanilla class hierarchy makes this entity an `AbstractHorse`.
310    pub is_abstract_horse: bool,
311    /// Whether vanilla class hierarchy makes this entity an `AbstractNautilus`.
312    pub is_abstract_nautilus: bool,
313
314    /// Behavioral flags for collision and interaction.
315    pub flags: EntityFlags,
316
317    /// Default attribute base values for this entity type
318    /// Empty for entities that don't have attributes (projectiles, items, displays, etc.)
319    pub default_attributes: &'static [(&'static str, f64)],
320}
321
322pub type EntityTypeRef = &'static EntityType;
323
324pub struct EntityTypeRegistry {
325    types_by_id: Vec<EntityTypeRef>,
326    types_by_key: FxHashMap<Identifier, usize>,
327    tags: RegistryTags,
328    allows_registering: bool,
329}
330
331// SAFETY: This Steel-owned key uniquely identifies the entity type registry.
332unsafe impl DowncastType for EntityTypeRegistry {
333    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:registry/entity_type");
334}
335
336impl Default for EntityTypeRegistry {
337    fn default() -> Self {
338        Self::new()
339    }
340}
341
342impl EntityTypeRegistry {
343    // Creates a new, empty registry.
344    #[must_use]
345    pub fn new() -> Self {
346        Self {
347            types_by_id: Vec::new(),
348            types_by_key: FxHashMap::default(),
349            tags: RegistryTags::default(),
350            allows_registering: true,
351        }
352    }
353
354    /// Registers a new entity type
355    pub fn register(&mut self, entity_type: EntityTypeRef) {
356        assert!(
357            self.allows_registering,
358            "Cannot register entity types after the registry has been frozen"
359        );
360        let idx = self.types_by_id.len();
361        self.types_by_key.insert(entity_type.key.clone(), idx);
362        self.types_by_id.push(entity_type);
363    }
364
365    pub fn iter(&self) -> impl Iterator<Item = (usize, EntityTypeRef)> + '_ {
366        self.types_by_id
367            .iter()
368            .enumerate()
369            .map(|(id, &et)| (id, et))
370    }
371}
372
373crate::impl_registry!(
374    EntityTypeRegistry,
375    EntityType,
376    types_by_id,
377    types_by_key,
378    entity_types
379);
380
381crate::impl_tagged_registry!(EntityTypeRegistry, types_by_key, "entity type");
382
383#[cfg(test)]
384mod tests {
385    use crate::vanilla_entities;
386
387    use super::{EntityAttachment, EntityAttachmentPoint, EntityAttachments, EntityDimensions};
388
389    fn assert_vec3_close(left: glam::DVec3, right: glam::DVec3) {
390        let diff = left - right;
391        assert!(
392            diff.length_squared() < 1.0e-12,
393            "expected {left:?} to equal {right:?}"
394        );
395    }
396
397    #[test]
398    fn attachment_points_clamp_index_and_rotate_like_vanilla() {
399        const PASSENGERS: [EntityAttachmentPoint; 2] = [
400            EntityAttachmentPoint::new(0.0, 0.5, 0.0),
401            EntityAttachmentPoint::new(1.0, 0.75, 0.0),
402        ];
403        const ZERO: [EntityAttachmentPoint; 1] = [EntityAttachmentPoint::new(0.0, 0.0, 0.0)];
404        let dimensions = EntityDimensions::new_with_attachments(
405            1.0,
406            2.0,
407            1.7,
408            EntityAttachments::new(&PASSENGERS, &ZERO, &ZERO, &ZERO),
409        );
410
411        let point =
412            dimensions
413                .attachments
414                .get_clamped(EntityAttachment::Passenger, 99, 90.0, dimensions);
415
416        assert_vec3_close(point, glam::DVec3::new(0.0, 0.75, 1.0));
417    }
418
419    #[test]
420    fn fallback_attachment_points_match_vanilla_defaults() {
421        let dimensions = EntityDimensions::new(0.6, 1.8, 1.62);
422
423        assert_vec3_close(
424            dimensions
425                .attachments
426                .get_clamped(EntityAttachment::Passenger, 0, 0.0, dimensions),
427            glam::DVec3::new(0.0, 1.8, 0.0),
428        );
429        assert_vec3_close(
430            dimensions
431                .attachments
432                .get_clamped(EntityAttachment::Vehicle, 0, 0.0, dimensions),
433            glam::DVec3::ZERO,
434        );
435        assert_vec3_close(
436            dimensions
437                .attachments
438                .get_clamped(EntityAttachment::WardenChest, 0, 0.0, dimensions),
439            glam::DVec3::new(0.0, 0.9, 0.0),
440        );
441    }
442
443    #[test]
444    fn attachment_average_uses_unrotated_scaled_points() {
445        const PASSENGERS: [EntityAttachmentPoint; 2] = [
446            EntityAttachmentPoint::new(0.0, 0.5, 0.0),
447            EntityAttachmentPoint::new(1.0, 0.75, -0.5),
448        ];
449        const ZERO: [EntityAttachmentPoint; 1] = [EntityAttachmentPoint::new(0.0, 0.0, 0.0)];
450        let dimensions = EntityDimensions::new_with_attachments(
451            1.0,
452            2.0,
453            1.7,
454            EntityAttachments::new(&PASSENGERS, &ZERO, &ZERO, &ZERO),
455        )
456        .scale(2.0);
457
458        assert_vec3_close(
459            dimensions
460                .attachments
461                .get_average(EntityAttachment::Passenger, dimensions),
462            glam::DVec3::new(1.0, 1.25, -0.5),
463        );
464    }
465
466    #[test]
467    fn vanilla_track_deltas_exclusions_match_entity_type_method() {
468        assert!(!vanilla_entities::PLAYER.track_deltas);
469        assert!(!vanilla_entities::BAT.track_deltas);
470        assert!(!vanilla_entities::ITEM_FRAME.track_deltas);
471        assert!(!vanilla_entities::EVOKER_FANGS.track_deltas);
472
473        assert!(vanilla_entities::ITEM.track_deltas);
474        assert!(vanilla_entities::ARROW.track_deltas);
475    }
476
477    #[test]
478    fn vanilla_class_hierarchy_flags_match_representative_entities() {
479        assert!(vanilla_entities::OAK_BOAT.is_abstract_boat);
480        assert!(vanilla_entities::OAK_CHEST_BOAT.is_abstract_boat);
481        assert!(!vanilla_entities::ITEM.is_abstract_boat);
482
483        assert!(vanilla_entities::MINECART.is_abstract_minecart);
484        assert!(vanilla_entities::CHEST_MINECART.is_abstract_minecart);
485        assert!(vanilla_entities::TNT_MINECART.is_abstract_minecart);
486        assert!(!vanilla_entities::ITEM.is_abstract_minecart);
487
488        assert!(vanilla_entities::ARROW.is_projectile);
489        assert!(vanilla_entities::WIND_CHARGE.is_projectile);
490        assert!(vanilla_entities::FISHING_BOBBER.is_projectile);
491        assert!(!vanilla_entities::ITEM.is_projectile);
492
493        assert!(vanilla_entities::ARROW.is_abstract_arrow);
494        assert!(vanilla_entities::SPECTRAL_ARROW.is_abstract_arrow);
495        assert!(vanilla_entities::TRIDENT.is_abstract_arrow);
496        assert!(!vanilla_entities::ENDER_PEARL.is_abstract_arrow);
497    }
498}