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