Merge pull request #11451 from MSedgeMC/fix/armadillo-state-save

🤖 Agent: An armadillo's state is saved always and restored through switchToState on load, as Armadillo.add/readAdditionalSaveData do.
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-11-07 12:59:39 +01:00
commit 18840db914
3 files changed
+102 -6

No files matched your search

+21 -6
View File
@@ -1122,9 +1122,17 @@ pub fn mob_to_nbt(
map.insert("anger".into(), Tag::compound(Vec::<(&str, Tag)>::new()));
}
}
// Armadillo.addAdditionalSaveData: the time left to the next scute.
// Armadillo.addAdditionalSaveData: the time left to the next scute, and
// the state it is in, which is always written (`idle` when it is not
// rolled up).
"minecraft:armadillo" => {
map.insert("scute_time".into(), Tag::Int(m.extra.egg_time));
map.insert(
"state".into(),
string(crate::mobs::armadillo::state_name(
crate::mobs::armadillo::state(m),
)),
);
}
"minecraft:ravager" => {
map.insert("AttackTick".into(), Tag::Int(m.extra.counter));
@@ -2217,11 +2225,18 @@ pub fn mob_from_nbt(
st.vibration = crate::sculk::Data::from_nbt(listener);
}
}
// Armadillo.readAdditionalSaveData: keeps the constructor's time when absent.
if m.species() == crate::mobs::Species::Armadillo
&& let Some(t) = number(tag.get("scute_time"))
{
m.extra.egg_time = t as i32;
// Armadillo.readAdditionalSaveData: the saved state through `switchToState`
// (`onSyncedDataUpdated` then restarts the time in that state), idle for a
// save that names none; then the time to the next scute, the constructor's
// when absent.
if m.species() == crate::mobs::Species::Armadillo {
crate::mobs::armadillo::set_state(
&mut m,
crate::mobs::armadillo::state_by_name(text(tag.get("state")).as_deref()),
);
if let Some(t) = number(tag.get("scute_time")) {
m.extra.egg_time = t as i32;
}
}
if m.species() == crate::mobs::Species::Ravager {
m.extra.counter = number(tag.get("AttackTick")).unwrap_or(0) as i32;
@@ -188,6 +188,26 @@ pub fn set_state(m: &mut Mob, state: u8) {
a.in_state_ticks = 0;
}
}
/// `ArmadilloState`'s name (`StringRepresentable`, which its `CODEC` writes):
/// what `Armadillo.addAdditionalSaveData` stores under `state`.
pub fn state_name(state: u8) -> &'static str {
match state {
ROLLING => "rolling",
SCARED => "scared",
UNROLLING => "unrolling",
_ => "idle",
}
}
/// `ArmadilloState.CODEC` read back: a name that is no state, and no name at
/// all, are `IDLE` (`read("state", CODEC).orElse(IDLE)`).
pub fn state_by_name(name: Option<&str>) -> u8 {
match name {
Some("rolling") => ROLLING,
Some("scared") => SCARED,
Some("unrolling") => UNROLLING,
_ => IDLE,
}
}
/// `Armadillo.canStayRolledUp`.
fn can_stay_rolled_up(m: &Mob) -> bool {
!m.ai.panicking
@@ -807,6 +827,65 @@ mod tests {
assert!(!source.causes_panic());
assert!(!environmental(DamageKind::Campfire));
}
/// `Armadillo.addAdditionalSaveData` writes `state` (the `ArmadilloState`
/// name, always) and `readAdditionalSaveData` switches to it, `idle` when
/// the save names none. Horizon models the state and wrote only
/// `scute_time`, so a rolled-up armadillo unloaded with its chunk came back
/// idle: hits it took for half the damage past the first point cost it the
/// whole amount, and its shell went away.
#[test]
fn a_rolled_up_armadillo_keeps_its_state_through_the_chunk_save() {
use crate::entity_nbt::{mob_from_nbt, mob_to_nbt};
use horizon_world::nbt::Tag;
fn armadillo_tag(state: Option<&str>) -> Tag {
let mut fields = vec![
("id", Tag::String("minecraft:armadillo".into())),
(
"Pos",
Tag::List(vec![
Tag::Double(0.5_f64.to_bits()),
Tag::Double(64.0_f64.to_bits()),
Tag::Double(0.5_f64.to_bits()),
]),
),
];
if let Some(state) = state {
fields.push(("state", Tag::String(state.into())));
}
Tag::compound(fields)
}
let types = [("minecraft:armadillo".into(), armadillo().kind.clone())].into();
// A ball takes half of the damage past the first point, and hides in
// its shell once the roll is five ticks in.
let mut ball = mob_from_nbt(&armadillo_tag(Some("rolling")), &types, 1, None).unwrap();
assert_eq!(state(&ball), ROLLING, "the loaded state");
assert_eq!(reduce_damage(&ball, 10.0), 4.5, "a rolling armadillo");
ball.extra.armadillo.in_state_ticks = 6;
assert!(hiding_in_shell(&ball), "six ticks into the roll");
// The save names the state the armadillo is in now, not the one its
// compound came in with (`store`, not `storeNullable`).
let saved = mob_to_nbt(
&ball,
[0; 4],
Some(&armadillo_tag(Some("idle"))),
None,
|_| None,
);
assert_eq!(saved.get("state"), Some(&Tag::String("rolling".into())));
let back = mob_from_nbt(&saved, &types, 2, None).unwrap();
assert_eq!(state(&back), ROLLING, "the reloaded state");
assert_eq!(reduce_damage(&back, 10.0), 4.5);
// `switchToState` restarts the time in the state
// (`onSyncedDataUpdated`), so the shell goes in again six ticks later.
assert_eq!(back.extra.armadillo.in_state_ticks, 0);
assert!(!hiding_in_shell(&back), "a reload restarts the roll");
// No state, or a name that is no state: `orElse(IDLE)`, full damage.
for absent in [None, Some("sneaking")] {
let idle = mob_from_nbt(&armadillo_tag(absent), &types, 3, None).unwrap();
assert_eq!(state(&idle), IDLE, "{absent:?}");
assert_eq!(reduce_damage(&idle, 10.0), 10.0, "{absent:?}");
}
}
}
#[cfg(test)]
+2
View File
@@ -35,6 +35,8 @@ AllayAi.shouldDepositItemsAtLikedNoteblock inline crates/horizon-client/src/mobs
AllayAi.getLikedPlayerPositionTracker port crates/horizon-client/src/game/allays.rs::Game::allay_liked_player
Armadillo.shouldSwitchToScaredState inline crates/horizon-client/src/mobs/armadillo.rs::ball_up_tick rolling past its animation length
Armadillo.onSyncedDataUpdated port crates/horizon-client/src/mobs/armadillo.rs::set_state resets in_state_ticks on a state change
Armadillo.addAdditionalSaveData inline crates/horizon-client/src/entity_nbt.rs::mob_to_nbt state is written as its ArmadilloState name, always, beside scute_time
Armadillo.readAdditionalSaveData inline crates/horizon-client/src/entity_nbt.rs::mob_from_nbt state goes through switchToState, idle when the save names none; scute_time keeps the constructor's value when absent
Armadillo.checkArmadilloSpawnRules port crates/horizon-client/src/mobs/spawn_rules.rs::check_spawn_rules
Armadillo.brushOffScute port crates/horizon-client/src/game/creatures.rs::Game::interact_armadillo brush use; dispenser.rs dispense_brush does the same for a dispenser
ArmadilloAi.initCoreActivity inline crates/horizon-client/src/mobs/armadillo.rs::register float and panic goals; rolling out is in server_ai_step
1 AbstractFish.addAdditionalSaveData inline crates/horizon-client/src/entity_nbt.rs::mob_to_nbt FromBucket always written; tadpole getter is always true
35 AllayAi.getLikedPlayerPositionTracker port crates/horizon-client/src/game/allays.rs::Game::allay_liked_player
36 Armadillo.shouldSwitchToScaredState inline crates/horizon-client/src/mobs/armadillo.rs::ball_up_tick rolling past its animation length
37 Armadillo.onSyncedDataUpdated port crates/horizon-client/src/mobs/armadillo.rs::set_state resets in_state_ticks on a state change
38 Armadillo.addAdditionalSaveData inline crates/horizon-client/src/entity_nbt.rs::mob_to_nbt state is written as its ArmadilloState name, always, beside scute_time
39 Armadillo.readAdditionalSaveData inline crates/horizon-client/src/entity_nbt.rs::mob_from_nbt state goes through switchToState, idle when the save names none; scute_time keeps the constructor's value when absent
40 Armadillo.checkArmadilloSpawnRules port crates/horizon-client/src/mobs/spawn_rules.rs::check_spawn_rules
41 Armadillo.brushOffScute port crates/horizon-client/src/game/creatures.rs::Game::interact_armadillo brush use; dispenser.rs dispense_brush does the same for a dispenser
42 ArmadilloAi.initCoreActivity inline crates/horizon-client/src/mobs/armadillo.rs::register float and panic goals; rolling out is in server_ai_step