Skip to main content

steel_core/entity/
attribute.rs

1//! Runtime entity attribute system.
2//!
3use core::iter;
4
5use simdnbt::owned::{NbtCompound, NbtList};
6use steel_protocol::packets::game::{AttributeModifierData, AttributeSnapshot};
7pub use steel_registry::attribute::AttributeModifierOperation;
8use steel_registry::attribute::AttributeRef;
9use steel_registry::entity_type::EntityTypeRef;
10use steel_registry::{REGISTRY, RegistryEntry, RegistryExt};
11use steel_utils::Identifier;
12
13/// Growable bitmask for tracking dirty attribute IDs.
14pub struct DirtySet {
15    chunks: Vec<u64>,
16}
17
18impl Default for DirtySet {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl DirtySet {
25    /// Creates an empty dirty set
26    #[must_use]
27    pub const fn new() -> Self {
28        Self { chunks: Vec::new() }
29    }
30
31    /// Marks an attribute ID as dirty
32    pub fn mark(&mut self, id: u16) {
33        let chunk = id as usize / 64;
34        let bit = id as usize % 64;
35        if chunk >= self.chunks.len() {
36            self.chunks.resize(chunk + 1, 0);
37        }
38        self.chunks[chunk] |= 1 << bit;
39    }
40
41    /// Returns `true` if no attributes are dirty
42    #[must_use]
43    pub fn is_empty(&self) -> bool {
44        self.chunks.iter().all(|&c| c == 0)
45    }
46
47    /// Drains all marked IDs, clearing the set
48    pub fn drain(&mut self) -> impl Iterator<Item = u16> + '_ {
49        self.chunks.iter_mut().enumerate().flat_map(|(i, chunk)| {
50            let mut bits = *chunk;
51            *chunk = 0;
52            iter::from_fn(move || {
53                if bits == 0 {
54                    return None;
55                }
56                let bit = bits.trailing_zeros() as u16;
57                bits &= bits - 1;
58                Some(i as u16 * 64 + bit)
59            })
60        })
61    }
62}
63
64/// A modifier applied to an attribute instance
65#[derive(Clone, Debug)]
66pub struct AttributeModifier {
67    /// Unique identifier (e.g. `minecraft:sprinting`)
68    pub id: Identifier,
69    /// The modifier value
70    pub amount: f64,
71    /// How the modifier is applied during calculation
72    pub operation: AttributeModifierOperation,
73}
74
75/// Runtime state for a single attribute on an entity
76pub struct AttributeInstance {
77    attribute: AttributeRef,
78    base_value: f64,
79    modifiers: Vec<AttributeModifier>,
80    /// Parallel to `modifiers` `true` means the modifier survives serialization
81    persistent: Vec<bool>,
82    cached_value: f64,
83}
84
85impl AttributeInstance {
86    const fn new(attribute: AttributeRef, base_value: f64) -> Self {
87        let cached = attribute.sanitize_value(base_value);
88        Self {
89            attribute,
90            base_value,
91            modifiers: Vec::new(),
92            persistent: Vec::new(),
93            cached_value: cached,
94        }
95    }
96
97    /// Returns the attribute definition
98    #[must_use]
99    pub const fn attribute(&self) -> AttributeRef {
100        self.attribute
101    }
102
103    /// Returns the base value before modifiers
104    #[must_use]
105    pub const fn base_value(&self) -> f64 {
106        self.base_value
107    }
108
109    /// Sets the base value and recalculates. Returns `true` if changed
110    #[expect(
111        clippy::float_cmp,
112        reason = "vanilla uses exact base value equality for dirty checks"
113    )]
114    pub fn set_base_value(&mut self, value: f64) -> bool {
115        if self.base_value == value {
116            return false;
117        }
118        self.base_value = value;
119        self.recalculate();
120        true
121    }
122
123    /// Returns the final calculated value (base + modifiers, clamped)
124    #[must_use]
125    pub const fn value(&self) -> f64 {
126        self.cached_value
127    }
128
129    /// Adds a modifier. Returns `false` if a modifier with this ID already exists
130    pub fn add_modifier(&mut self, modifier: AttributeModifier, persistent: bool) -> bool {
131        if self.modifiers.iter().any(|m| m.id == modifier.id) {
132            return false;
133        }
134        self.modifiers.push(modifier);
135        self.persistent.push(persistent);
136        self.recalculate();
137        true
138    }
139
140    /// Returns whether a modifier with the given ID exists.
141    #[must_use]
142    pub fn has_modifier(&self, id: &Identifier) -> bool {
143        self.modifiers.iter().any(|modifier| modifier.id == *id)
144    }
145
146    /// Adds or replaces a modifier. Returns `true` if the value actually changed.
147    #[expect(
148        clippy::float_cmp,
149        reason = "exact equality is intentional — we want to skip recalculation when the modifier is identical"
150    )]
151    pub fn set_modifier(&mut self, modifier: AttributeModifier, persistent: bool) -> bool {
152        if let Some(idx) = self.modifiers.iter().position(|m| m.id == modifier.id) {
153            let existing = &self.modifiers[idx];
154            if existing.amount == modifier.amount
155                && existing.operation == modifier.operation
156                && self.persistent[idx] == persistent
157            {
158                return false;
159            }
160            self.modifiers[idx] = modifier;
161            self.persistent[idx] = persistent;
162        } else {
163            self.modifiers.push(modifier);
164            self.persistent.push(persistent);
165        }
166        self.recalculate();
167        true
168    }
169
170    /// Removes a modifier by ID, Returns `true` if it existed
171    pub fn remove_modifier(&mut self, id: &Identifier) -> bool {
172        let Some(idx) = self.modifiers.iter().position(|m| m.id == *id) else {
173            return false;
174        };
175        self.modifiers.swap_remove(idx);
176        self.persistent.swap_remove(idx);
177        self.recalculate();
178        true
179    }
180
181    /// Returns an iterator over permanent modifiers (for serialization)
182    pub fn permanent_modifiers(&self) -> impl Iterator<Item = &AttributeModifier> {
183        self.modifiers
184            .iter()
185            .zip(self.persistent.iter())
186            .filter(|&(_, p)| *p)
187            .map(|(m, _)| m)
188    }
189
190    /// Removes all transient (non-persistent) modifiers. Returns `true` if any were removed
191    fn remove_transient_modifiers(&mut self) -> bool {
192        if !self.persistent.iter().any(|&p| !p) {
193            return false;
194        }
195        let mut i = 0;
196        while i < self.modifiers.len() {
197            if self.persistent[i] {
198                i += 1;
199            } else {
200                self.modifiers.swap_remove(i);
201                self.persistent.swap_remove(i);
202            }
203        }
204        self.recalculate();
205        true
206    }
207
208    /// Removes every modifier while preserving the base value
209    fn remove_modifiers(&mut self) -> bool {
210        if self.modifiers.is_empty() {
211            return false;
212        }
213
214        self.modifiers.clear();
215        self.persistent.clear();
216        self.recalculate();
217        true
218    }
219
220    /// Three-phase vanilla calculation:
221    /// 1. `ADD_VALUE`
222    /// 2. `ADD_MULTIPLIED_BASE`
223    /// 3. `ADD_MULTIPLIED_TOTAL`
224    fn recalculate(&mut self) {
225        let mut base = self.base_value;
226        for m in &self.modifiers {
227            if m.operation == AttributeModifierOperation::AddValue {
228                base += m.amount;
229            }
230        }
231
232        let mut result = base;
233        for m in &self.modifiers {
234            if m.operation == AttributeModifierOperation::AddMultipliedBase {
235                result += base * m.amount;
236            }
237        }
238        for m in &self.modifiers {
239            if m.operation == AttributeModifierOperation::AddMultipliedTotal {
240                result *= 1.0 + m.amount;
241            }
242        }
243
244        self.cached_value = self.attribute.sanitize_value(result);
245    }
246
247    /// Builds a network snapshot for `CUpdateAttributes`
248    fn to_snapshot(&self, attribute_id: i32) -> AttributeSnapshot {
249        AttributeSnapshot {
250            attribute_id,
251            base_value: self.base_value,
252            modifiers: self
253                .modifiers
254                .iter()
255                .map(|m| AttributeModifierData {
256                    id: m.id.clone(),
257                    amount: m.amount,
258                    operation: m.operation,
259                })
260                .collect(),
261        }
262    }
263}
264
265/// Per-entity container for attribute instances with dirty tracking
266///
267/// Indexed by attribute registry ID. Two dirty sets match vanilla's design:
268/// - `to_update`: all dirty attributes, drained for server-side effects
269/// - `to_sync`: syncable dirty attributes, drained for network packets
270pub struct AttributeMap {
271    instances: Vec<Option<AttributeInstance>>,
272    to_update: DirtySet,
273    to_sync: DirtySet,
274}
275
276impl AttributeMap {
277    /// Creates an `AttributeMap` from an entity type's default attributes
278    ///
279    /// # Panics
280    /// Panics if generated entity default attributes reference an attribute
281    /// that is missing from the generated vanilla registry.
282    // TODO: Add AttributeSupplier for lazy instantiation when mob entities are implemented
283    #[must_use]
284    pub fn new_for_entity(entity_type: EntityTypeRef) -> Self {
285        let attr_count = REGISTRY.attributes.len();
286        let mut instances = Vec::with_capacity(attr_count);
287        instances.resize_with(attr_count, || None);
288
289        for &(attr_name, base_value) in entity_type.default_attributes {
290            let key = Identifier::vanilla_static(attr_name);
291            let Some(id) = REGISTRY.attributes.id_from_key(&key) else {
292                panic!(
293                    "default attributes for entity type {} reference unregistered attribute {key}",
294                    entity_type.key
295                );
296            };
297            let Some(attr) = REGISTRY.attributes.by_id(id) else {
298                panic!("attribute registry id {id} for default attribute {key} is not registered");
299            };
300            instances[id] = Some(AttributeInstance::new(attr, base_value));
301        }
302
303        Self {
304            instances,
305            to_update: DirtySet::new(),
306            to_sync: DirtySet::new(),
307        }
308    }
309
310    /// Returns `true` if the entity has this attribute registered
311    #[must_use]
312    pub fn has_attribute(&self, attribute: AttributeRef) -> bool {
313        attribute
314            .try_id()
315            .and_then(|id| self.instances.get(id))
316            .is_some_and(Option::is_some)
317    }
318
319    /// Gets the calculated value of an attribute
320    #[must_use]
321    pub fn get_value(&self, attribute: AttributeRef) -> Option<f64> {
322        let id = attribute.try_id()?;
323        self.instances
324            .get(id)?
325            .as_ref()
326            .map(AttributeInstance::value)
327    }
328
329    /// Gets the calculated value of an attribute that must exist on this entity.
330    ///
331    /// # Panics
332    ///
333    /// Panics if the entity type was not constructed with the requested
334    /// attribute. Vanilla's `AttributeSupplier.getValue` is a hard failure for
335    /// missing attributes; using this keeps required living attributes from
336    /// silently falling back to unrelated defaults.
337    #[must_use]
338    pub fn required_value(&self, attribute: AttributeRef) -> f64 {
339        let Some(value) = self.get_value(attribute) else {
340            panic!("required attribute {} is missing", attribute.key);
341        };
342        value
343    }
344
345    /// Gets the base value of an attribute
346    #[must_use]
347    pub fn get_base_value(&self, attribute: AttributeRef) -> Option<f64> {
348        let id = attribute.try_id()?;
349        self.instances
350            .get(id)?
351            .as_ref()
352            .map(AttributeInstance::base_value)
353    }
354
355    /// Gets a reference to an attribute instance
356    #[must_use]
357    pub fn get_instance(&self, attribute: AttributeRef) -> Option<&AttributeInstance> {
358        let id = attribute.try_id()?;
359        self.instances.get(id)?.as_ref()
360    }
361
362    /// Serializes the vanilla `LivingEntity.attributes` list.
363    #[must_use]
364    pub(crate) fn to_vanilla_nbt(&self) -> NbtList {
365        let attributes = self
366            .instances
367            .iter()
368            .flatten()
369            .map(|instance| {
370                let mut attribute = NbtCompound::new();
371                attribute.insert("id", instance.attribute().key.to_string());
372                attribute.insert("base", instance.base_value());
373
374                let modifiers = instance
375                    .permanent_modifiers()
376                    .map(|modifier| {
377                        let mut packed = NbtCompound::new();
378                        packed.insert("id", modifier.id.to_string());
379                        packed.insert("amount", modifier.amount);
380                        packed.insert("operation", modifier.operation.name());
381                        packed
382                    })
383                    .collect::<Vec<_>>();
384                if !modifiers.is_empty() {
385                    attribute.insert("modifiers", NbtList::Compound(modifiers));
386                }
387
388                attribute
389            })
390            .collect();
391        NbtList::Compound(attributes)
392    }
393
394    /// Returns whether an attribute has a modifier with the given ID.
395    #[must_use]
396    pub fn has_modifier(&self, attribute: AttributeRef, modifier_id: &Identifier) -> bool {
397        self.get_instance(attribute)
398            .is_some_and(|instance| instance.has_modifier(modifier_id))
399    }
400
401    /// Sets the base value of an attribute
402    pub fn set_base_value(&mut self, attribute: AttributeRef, value: f64) {
403        let Some(id) = attribute.try_id() else { return };
404        let Some(Some(instance)) = self.instances.get_mut(id) else {
405            return;
406        };
407        if instance.set_base_value(value) {
408            self.mark_dirty(id, attribute);
409        }
410    }
411
412    /// Assigns base values from attributes present in both maps.
413    pub fn assign_base_values(&mut self, other: &Self) {
414        let Self {
415            instances,
416            to_update,
417            to_sync,
418        } = self;
419
420        for (id, other_instance) in other.instances.iter().enumerate() {
421            let Some(other_instance) = other_instance else {
422                continue;
423            };
424            let Some(Some(instance)) = instances.get_mut(id) else {
425                continue;
426            };
427            if instance.set_base_value(other_instance.base_value()) {
428                to_update.mark(id as u16);
429                if instance.attribute.syncable {
430                    to_sync.mark(id as u16);
431                }
432            }
433        }
434    }
435
436    /// Adds permanent modifiers from attributes present in both maps.
437    ///
438    /// # Panics
439    ///
440    /// Panics if the destination already has a modifier with the same ID,
441    /// matching vanilla's `AttributeInstance.addPermanentModifier` contract.
442    pub fn assign_permanent_modifiers(&mut self, other: &Self) {
443        let Self {
444            instances,
445            to_update,
446            to_sync,
447        } = self;
448
449        for (id, other_instance) in other.instances.iter().enumerate() {
450            let Some(other_instance) = other_instance else {
451                continue;
452            };
453            let Some(Some(instance)) = instances.get_mut(id) else {
454                continue;
455            };
456
457            for modifier in other_instance.permanent_modifiers() {
458                assert!(
459                    instance.add_modifier(modifier.clone(), true),
460                    "modifier {} is already applied to attribute {}",
461                    modifier.id,
462                    instance.attribute.key
463                );
464                to_update.mark(id as u16);
465                if instance.attribute.syncable {
466                    to_sync.mark(id as u16);
467                }
468            }
469        }
470    }
471
472    /// Adds a modifier to an attribute. Returns `false` if the modifier ID already exists
473    pub fn add_modifier(
474        &mut self,
475        attribute: AttributeRef,
476        modifier: AttributeModifier,
477        persistent: bool,
478    ) -> bool {
479        let Some(id) = attribute.try_id() else {
480            return false;
481        };
482        let Some(Some(instance)) = self.instances.get_mut(id) else {
483            return false;
484        };
485        if instance.add_modifier(modifier, persistent) {
486            self.mark_dirty(id, attribute);
487            true
488        } else {
489            false
490        }
491    }
492
493    /// Adds or replaces a modifier on an attribute
494    pub fn set_modifier(
495        &mut self,
496        attribute: AttributeRef,
497        modifier: AttributeModifier,
498        persistent: bool,
499    ) {
500        let Some(id) = attribute.try_id() else { return };
501        let Some(Some(instance)) = self.instances.get_mut(id) else {
502            return;
503        };
504        if instance.set_modifier(modifier, persistent) {
505            self.mark_dirty(id, attribute);
506        }
507    }
508
509    /// Removes a modifier from an attribute. Returns `true` if it existed
510    pub fn remove_modifier(&mut self, attribute: AttributeRef, modifier_id: &Identifier) -> bool {
511        let Some(id) = attribute.try_id() else {
512            return false;
513        };
514        let Some(Some(instance)) = self.instances.get_mut(id) else {
515            return false;
516        };
517        if instance.remove_modifier(modifier_id) {
518            self.mark_dirty(id, attribute);
519            true
520        } else {
521            false
522        }
523    }
524
525    fn mark_dirty(&mut self, id: usize, attribute: AttributeRef) {
526        let id = id as u16;
527        self.to_update.mark(id);
528        if attribute.syncable {
529            self.to_sync.mark(id);
530        }
531    }
532
533    /// Returns `true` if there are dirty attributes needing server-side effects
534    #[must_use]
535    pub fn has_dirty_updates(&self) -> bool {
536        !self.to_update.is_empty()
537    }
538
539    /// Returns `true` if there are syncable dirty attributes needing network send
540    #[must_use]
541    pub fn has_dirty_sync(&self) -> bool {
542        !self.to_sync.is_empty()
543    }
544
545    /// Drains `to_update` and returns the dirty attribute refs
546    pub fn drain_dirty_updates(&mut self) -> Vec<AttributeRef> {
547        self.to_update
548            .drain()
549            .filter_map(|id| {
550                self.instances
551                    .get(id as usize)?
552                    .as_ref()
553                    .map(|inst| inst.attribute)
554            })
555            .collect()
556    }
557
558    /// Drains `to_sync` and builds `AttributeSnapshot`s for `CUpdateAttributes`
559    pub fn drain_dirty_sync(&mut self) -> Vec<AttributeSnapshot> {
560        let ids: Vec<u16> = self.to_sync.drain().collect();
561        let mut snapshots = Vec::with_capacity(ids.len());
562        for id in ids {
563            if let Some(Some(instance)) = self.instances.get(id as usize) {
564                snapshots.push(instance.to_snapshot(i32::from(id)));
565            }
566        }
567        snapshots
568    }
569
570    /// Returns snapshots for ALL syncable attributes (initial tracking sync)
571    #[must_use]
572    pub fn syncable_snapshots(&self) -> Vec<AttributeSnapshot> {
573        let mut snapshots = Vec::new();
574        for (id, slot) in self.instances.iter().enumerate() {
575            if let Some(inst) = slot
576                && inst.attribute.syncable
577            {
578                snapshots.push(inst.to_snapshot(id as i32));
579            }
580        }
581        snapshots
582    }
583
584    /// Removes all transient modifiers (e.g. on respawn)
585    pub fn remove_all_transient(&mut self) {
586        let Self {
587            instances,
588            to_update,
589            to_sync,
590        } = self;
591        for (id, slot) in instances.iter_mut().enumerate() {
592            if let Some(inst) = slot
593                && inst.remove_transient_modifiers()
594            {
595                to_update.mark(id as u16);
596                if inst.attribute.syncable {
597                    to_sync.mark(id as u16);
598                }
599            }
600        }
601    }
602
603    /// Removes all permanent and transient modifiers while preserving base values
604    pub fn remove_all_modifiers(&mut self) {
605        let Self {
606            instances,
607            to_update,
608            to_sync,
609        } = self;
610        for (id, slot) in instances.iter_mut().enumerate() {
611            if let Some(inst) = slot
612                && inst.remove_modifiers()
613            {
614                to_update.mark(id as u16);
615                if inst.attribute.syncable {
616                    to_sync.mark(id as u16);
617                }
618            }
619        }
620    }
621}
622
623#[cfg(test)]
624mod tests {
625    use simdnbt::owned::NbtTag;
626    use steel_registry::{REGISTRY, init_vanilla_registry, vanilla_attributes, vanilla_entities};
627
628    use super::*;
629
630    #[test]
631    fn all_generated_entity_default_attributes_resolve() {
632        init_vanilla_registry();
633
634        for (_, entity_type) in REGISTRY.entity_types.iter() {
635            let _ = AttributeMap::new_for_entity(entity_type);
636        }
637    }
638
639    #[test]
640    fn player_gravity_is_initialized_from_default_attributes() {
641        init_vanilla_registry();
642
643        let attributes = AttributeMap::new_for_entity(&vanilla_entities::PLAYER);
644
645        assert_eq!(
646            attributes
647                .required_value(vanilla_attributes::GRAVITY)
648                .to_bits(),
649            vanilla_attributes::GRAVITY.default_value.to_bits()
650        );
651    }
652
653    #[test]
654    fn vanilla_nbt_only_contains_permanent_modifiers() {
655        init_vanilla_registry();
656        let mut attributes = AttributeMap::new_for_entity(&vanilla_entities::PLAYER);
657        attributes.add_modifier(
658            vanilla_attributes::MAX_HEALTH,
659            AttributeModifier {
660                id: Identifier::vanilla_static("transient_test"),
661                amount: 1.0,
662                operation: AttributeModifierOperation::AddValue,
663            },
664            false,
665        );
666        attributes.add_modifier(
667            vanilla_attributes::MAX_HEALTH,
668            AttributeModifier {
669                id: Identifier::vanilla_static("permanent_test"),
670                amount: 2.0,
671                operation: AttributeModifierOperation::AddMultipliedBase,
672            },
673            true,
674        );
675
676        let NbtList::Compound(packed_attributes) = attributes.to_vanilla_nbt() else {
677            panic!("attributes should serialize as a compound list");
678        };
679        let max_health = packed_attributes
680            .iter()
681            .find(|attribute| {
682                attribute.string("id").is_some_and(|id| {
683                    id.to_str().as_ref() == vanilla_attributes::MAX_HEALTH.key.to_string()
684                })
685            })
686            .unwrap_or_else(|| panic!("max health should be serialized"));
687        let Some(NbtTag::List(NbtList::Compound(modifiers))) = max_health.get("modifiers") else {
688            panic!("permanent modifier should be serialized");
689        };
690
691        assert_eq!(modifiers.len(), 1);
692        assert_eq!(
693            modifiers[0].string("id").map(ToString::to_string),
694            Some("minecraft:permanent_test".to_owned())
695        );
696        assert_eq!(modifiers[0].double("amount"), Some(2.0));
697        assert_eq!(
698            modifiers[0].string("operation").map(ToString::to_string),
699            Some("add_multiplied_base".to_owned())
700        );
701    }
702
703    #[test]
704    fn removing_all_modifiers_preserves_base_value() {
705        init_vanilla_registry();
706        let mut attributes = AttributeMap::new_for_entity(&vanilla_entities::PLAYER);
707        attributes.set_base_value(vanilla_attributes::MAX_HEALTH, 30.0);
708
709        let transient_id = Identifier::vanilla_static("transient_test");
710        let permanent_id = Identifier::vanilla_static("permanent_test");
711        assert!(attributes.add_modifier(
712            vanilla_attributes::MAX_HEALTH,
713            AttributeModifier {
714                id: Identifier::vanilla_static("transient_test"),
715                amount: 2.0,
716                operation: AttributeModifierOperation::AddValue,
717            },
718            false,
719        ));
720        assert!(attributes.add_modifier(
721            vanilla_attributes::MAX_HEALTH,
722            AttributeModifier {
723                id: Identifier::vanilla_static("permanent_test"),
724                amount: 3.0,
725                operation: AttributeModifierOperation::AddValue,
726            },
727            true,
728        ));
729
730        attributes.remove_all_modifiers();
731
732        assert_eq!(
733            attributes
734                .get_base_value(vanilla_attributes::MAX_HEALTH)
735                .map(f64::to_bits),
736            Some(30.0_f64.to_bits())
737        );
738        assert_eq!(
739            attributes
740                .get_value(vanilla_attributes::MAX_HEALTH)
741                .map(f64::to_bits),
742            Some(30.0_f64.to_bits())
743        );
744        assert!(!attributes.has_modifier(vanilla_attributes::MAX_HEALTH, &transient_id));
745        assert!(!attributes.has_modifier(vanilla_attributes::MAX_HEALTH, &permanent_id));
746    }
747
748    #[test]
749    fn assigning_base_values_only_changes_attributes_present_in_both_maps() {
750        init_vanilla_registry();
751        let mut source = AttributeMap::new_for_entity(&vanilla_entities::ZOMBIE);
752        let mut destination = AttributeMap::new_for_entity(&vanilla_entities::PLAYER);
753
754        source.set_base_value(vanilla_attributes::MAX_HEALTH, 37.0);
755        source.set_base_value(vanilla_attributes::ATTACK_DAMAGE, 8.0);
756        destination.set_base_value(vanilla_attributes::LUCK, 9.0);
757        destination.drain_dirty_updates();
758        destination.drain_dirty_sync();
759
760        destination.assign_base_values(&source);
761        let Some(max_health_id) = vanilla_attributes::MAX_HEALTH.try_id() else {
762            panic!("max health should be registered");
763        };
764        let Some(attack_damage_id) = vanilla_attributes::ATTACK_DAMAGE.try_id() else {
765            panic!("attack damage should be registered");
766        };
767
768        assert_eq!(
769            destination
770                .get_base_value(vanilla_attributes::MAX_HEALTH)
771                .map(f64::to_bits),
772            Some(37.0_f64.to_bits())
773        );
774        assert_eq!(
775            destination
776                .get_base_value(vanilla_attributes::LUCK)
777                .map(f64::to_bits),
778            Some(9.0_f64.to_bits())
779        );
780        assert_eq!(
781            destination
782                .get_base_value(vanilla_attributes::ATTACK_DAMAGE)
783                .map(f64::to_bits),
784            Some(8.0_f64.to_bits())
785        );
786        let dirty_updates = destination.drain_dirty_updates();
787        assert!(
788            dirty_updates
789                .iter()
790                .any(|attribute| attribute.key == vanilla_attributes::MAX_HEALTH.key)
791        );
792        assert!(
793            dirty_updates
794                .iter()
795                .any(|attribute| attribute.key == vanilla_attributes::ATTACK_DAMAGE.key)
796        );
797        let dirty_sync = destination.drain_dirty_sync();
798        assert!(
799            dirty_sync
800                .iter()
801                .any(|snapshot| snapshot.attribute_id == max_health_id as i32)
802        );
803        assert!(
804            dirty_sync
805                .iter()
806                .all(|snapshot| snapshot.attribute_id != attack_damage_id as i32)
807        );
808
809        destination.assign_base_values(&source);
810
811        assert!(!destination.has_dirty_updates());
812        assert!(!destination.has_dirty_sync());
813    }
814
815    #[test]
816    fn assigning_permanent_modifiers_adds_only_permanent_source_modifiers() {
817        init_vanilla_registry();
818        let mut source = AttributeMap::new_for_entity(&vanilla_entities::PLAYER);
819        let mut destination = AttributeMap::new_for_entity(&vanilla_entities::PLAYER);
820        let source_transient_id = Identifier::vanilla_static("source_transient_test");
821        let source_permanent_id = Identifier::vanilla_static("source_permanent_test");
822        let destination_transient_id = Identifier::vanilla_static("destination_transient_test");
823        let destination_permanent_id = Identifier::vanilla_static("destination_permanent_test");
824
825        source.set_base_value(vanilla_attributes::MAX_HEALTH, 40.0);
826        assert!(source.add_modifier(
827            vanilla_attributes::MAX_HEALTH,
828            AttributeModifier {
829                id: source_transient_id.clone(),
830                amount: 7.0,
831                operation: AttributeModifierOperation::AddValue,
832            },
833            false,
834        ));
835        assert!(source.add_modifier(
836            vanilla_attributes::MAX_HEALTH,
837            AttributeModifier {
838                id: source_permanent_id.clone(),
839                amount: 5.0,
840                operation: AttributeModifierOperation::AddValue,
841            },
842            true,
843        ));
844        destination.set_base_value(vanilla_attributes::MAX_HEALTH, 25.0);
845        assert!(destination.add_modifier(
846            vanilla_attributes::MAX_HEALTH,
847            AttributeModifier {
848                id: destination_transient_id.clone(),
849                amount: 2.0,
850                operation: AttributeModifierOperation::AddValue,
851            },
852            false,
853        ));
854        assert!(destination.add_modifier(
855            vanilla_attributes::MAX_HEALTH,
856            AttributeModifier {
857                id: destination_permanent_id.clone(),
858                amount: 3.0,
859                operation: AttributeModifierOperation::AddValue,
860            },
861            true,
862        ));
863        destination.drain_dirty_updates();
864        destination.drain_dirty_sync();
865
866        destination.assign_permanent_modifiers(&source);
867
868        assert_eq!(
869            destination
870                .get_base_value(vanilla_attributes::MAX_HEALTH)
871                .map(f64::to_bits),
872            Some(25.0_f64.to_bits())
873        );
874        assert_eq!(
875            destination
876                .get_value(vanilla_attributes::MAX_HEALTH)
877                .map(f64::to_bits),
878            Some(35.0_f64.to_bits())
879        );
880        assert!(!destination.has_modifier(vanilla_attributes::MAX_HEALTH, &source_transient_id));
881        assert!(destination.has_modifier(vanilla_attributes::MAX_HEALTH, &source_permanent_id));
882        assert!(
883            destination.has_modifier(vanilla_attributes::MAX_HEALTH, &destination_transient_id)
884        );
885        assert!(
886            destination.has_modifier(vanilla_attributes::MAX_HEALTH, &destination_permanent_id)
887        );
888        assert!(
889            destination
890                .get_instance(vanilla_attributes::MAX_HEALTH)
891                .is_some_and(|instance| {
892                    instance
893                        .permanent_modifiers()
894                        .any(|modifier| modifier.id == source_permanent_id)
895                })
896        );
897        assert!(destination.has_dirty_updates());
898        assert!(destination.has_dirty_sync());
899    }
900
901    #[test]
902    #[should_panic(expected = "is already applied")]
903    fn assigning_permanent_modifiers_rejects_duplicate_ids() {
904        init_vanilla_registry();
905        let mut source = AttributeMap::new_for_entity(&vanilla_entities::PLAYER);
906        let mut destination = AttributeMap::new_for_entity(&vanilla_entities::PLAYER);
907        let modifier_id = Identifier::vanilla_static("duplicate_test");
908
909        for attributes in [&mut source, &mut destination] {
910            assert!(attributes.add_modifier(
911                vanilla_attributes::MAX_HEALTH,
912                AttributeModifier {
913                    id: modifier_id.clone(),
914                    amount: 1.0,
915                    operation: AttributeModifierOperation::AddValue,
916                },
917                true,
918            ));
919        }
920
921        destination.assign_permanent_modifiers(&source);
922    }
923}