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)]
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#[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 #[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 #[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 #[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 #[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 #[must_use]
243 pub fn half_width(&self) -> f32 {
244 self.width / 2.0
245 }
246
247 #[must_use]
249 const fn default_eye_height(height: f32) -> f32 {
250 height * DEFAULT_EYE_HEIGHT_FACTOR
251 }
252}
253
254#[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 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 pub track_deltas: bool,
278
279 pub dimensions: EntityDimensions,
281 pub fixed: bool,
283
284 pub mob_category: MobCategory,
286 pub fire_immune: bool,
288 pub summonable: bool,
290 pub allowed_in_peaceful: bool,
292 pub can_spawn_far_from_player: bool,
294 pub can_serialize: bool,
297 pub only_op_can_set_nbt: bool,
300 pub is_abstract_boat: bool,
302 pub is_abstract_minecart: bool,
304 pub is_projectile: bool,
306 pub is_abstract_arrow: bool,
308 pub is_abstract_horse: bool,
310 pub is_abstract_nautilus: bool,
312
313 pub flags: EntityFlags,
315
316 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
330unsafe 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 #[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 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}