1use glam::DVec3;
2use rustc_hash::FxHashMap;
3use steel_utils::Identifier;
4
5#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum EntityAttachment {
21 Passenger,
22 Vehicle,
23 NameTag,
24 WardenChest,
25}
26
27#[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#[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#[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 #[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 #[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 #[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 #[must_use]
197 pub fn half_width(&self) -> f32 {
198 self.width / 2.0
199 }
200}
201
202#[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 pub track_deltas: bool,
224
225 pub dimensions: EntityDimensions,
227 pub fixed: bool,
229
230 pub mob_category: MobCategory,
232 pub fire_immune: bool,
234 pub summonable: bool,
236 pub can_spawn_far_from_player: bool,
238 pub can_serialize: bool,
241 pub is_abstract_boat: bool,
243 pub is_abstract_minecart: bool,
245
246 pub flags: EntityFlags,
248
249 pub default_attributes: &'static [(&'static str, f64)],
252}
253
254pub type EntityTypeRef = &'static EntityType;
255
256impl PartialEq for EntityTypeRef {
257 #[expect(clippy::disallowed_methods)] 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 #[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 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}