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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub enum EntityAttachment {
46 Passenger,
47 Vehicle,
48 NameTag,
49 WardenChest,
50}
51
52#[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)]
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#[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 #[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 #[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 #[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 #[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 #[must_use]
244 pub fn half_width(&self) -> f32 {
245 self.width / 2.0
246 }
247
248 #[must_use]
250 const fn default_eye_height(height: f32) -> f32 {
251 height * DEFAULT_EYE_HEIGHT_FACTOR
252 }
253}
254
255#[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 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 pub track_deltas: bool,
279
280 pub dimensions: EntityDimensions,
282 pub fixed: bool,
284
285 pub mob_category: MobCategory,
287 pub fire_immune: bool,
289 pub summonable: bool,
291 pub allowed_in_peaceful: bool,
293 pub can_spawn_far_from_player: bool,
295 pub can_serialize: bool,
298 pub only_op_can_set_nbt: bool,
301 pub is_abstract_boat: bool,
303 pub is_abstract_minecart: bool,
305 pub is_projectile: bool,
307 pub is_abstract_arrow: bool,
309 pub is_abstract_horse: bool,
311 pub is_abstract_nautilus: bool,
313
314 pub flags: EntityFlags,
316
317 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
331unsafe 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 #[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 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}