Merge pull request #10119 from DUzzL/fix/10049-frost-walker-equip

🤖 Agent: Frost Walker's speed modifier now follows a boots change made by a guest too (the guest's armor is swapped in before the comparison); armor_changes copies only changed slots. armor_stand_frost passes on a union with main.
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-11-07 06:12:42 +01:00
commit 318e263959
4 files changed
+61 -8

No files matched your search

+3
View File
@@ -3010,6 +3010,8 @@ struct Game<'a> {
map_frame_model: frames::FrameModel,
glow_map_frame_model: frames::FrameModel,
armor_stands: horizon_client::join_seq::JoinList<horizon_client::entities::ArmorStand>,
/// Worn armor as of the last `collectEquipmentChanges` look (frost walker).
location_armor: [Option<ItemStack>; 4],
/// `Cushion` decoration entities (game/cushions.rs).
cushions: Vec<horizon_client::entities::Cushion>,
/// Entities of types Horizon does not simulate.
@@ -20389,6 +20391,7 @@ impl<'a> Game<'a> {
map_frame_model: prepared.map_frame_model,
glow_map_frame_model: prepared.glow_map_frame_model,
armor_stands: Default::default(),
location_armor: Default::default(),
cushions: Vec::new(),
others: Vec::new(),
armor_stand_assets: prepared.armor_stand_assets,
@@ -840,16 +840,26 @@ impl Game<'_> {
let at = [p.position.x, p.position.y, p.position.z];
let on_ground = p.ground.on_ground;
let cell = |v: [f64; 3]| v.map(|c| c.floor() as i32);
if cell(from) == cell(at) && !(on_ground && !was_on_ground) {
// LivingEntity.collectEquipmentChanges: newly worn armor runs its
// location_changed effects at once, even when standing still.
let changed = armor_changes(&mut self.location_armor, &self.armor);
let moved = cell(from) != cell(at) || (on_ground && !was_on_ground);
if !moved && !changed.iter().any(|c| *c) {
return;
}
let riding = self.riding.is_some();
let disks = location_disks(
&self.stations().enchantments,
self.armor.iter().zip(["head", "chest", "legs", "feet"]),
on_ground,
riding,
);
let slots = ["head", "chest", "legs", "feet"];
let mut disks = Vec::new();
for i in 0..4 {
if moved || changed[i] {
disks.extend(location_disks(
&self.stations().enchantments,
std::iter::once((&self.armor[i], slots[i])),
on_ground,
riding,
));
}
}
for (effect, level) in disks {
self.replace_disk(&effect, level, at, Who::Player);
}
@@ -1182,9 +1192,43 @@ fn location_requirements(req: &Value, on_ground: bool, riding: bool) -> bool {
}
}
/// `LivingEntity.collectEquipmentChanges`: which armor slots differ from the
/// `snapshot` taken at the last look, which is then brought up to date.
fn armor_changes(
snapshot: &mut [Option<ItemStack>; 4],
armor: &[Option<ItemStack>; 4],
) -> [bool; 4] {
let changed: [bool; 4] = std::array::from_fn(|i| armor[i] != snapshot[i]);
// Only a changed slot is copied: the check runs every tick for every player.
for ((kept, worn), changed) in snapshot.iter_mut().zip(armor).zip(changed) {
if changed {
kept.clone_from(worn);
}
}
changed
}
#[cfg(test)]
mod tests {
use super::block_break_speed_factor;
use super::{armor_changes, block_break_speed_factor};
use horizon_client::items::ItemStack;
/// The snapshot reports a newly worn piece once, then settles.
#[test]
fn armor_changes_settle_after_one_look() {
let worn = [
None,
None,
None,
Some(ItemStack::new("minecraft:iron_boots", 1)),
];
let mut snapshot: [Option<ItemStack>; 4] = Default::default();
assert_eq!(
armor_changes(&mut snapshot, &worn),
[false, false, false, true]
);
assert_eq!(armor_changes(&mut snapshot, &worn), [false; 4]);
}
/// #4094: `BLOCK_BREAK_SPEED` is a multiplier on the destroy speed.
#[test]
@@ -542,6 +542,7 @@ impl Game<'_> {
self.pitch = g.pitch;
std::mem::swap(&mut self.inventory, &mut g.inventory);
std::mem::swap(&mut self.armor, &mut g.armor);
std::mem::swap(&mut self.location_armor, &mut g.location_armor);
std::mem::swap(&mut self.offhand, &mut g.offhand);
std::mem::swap(&mut self.carried, &mut g.carried);
let selected = std::mem::replace(&mut self.selected, g.selected);
@@ -1046,6 +1047,7 @@ impl Game<'_> {
g.selected = std::mem::replace(&mut self.selected, selected);
std::mem::swap(&mut self.inventory, &mut g.inventory);
std::mem::swap(&mut self.armor, &mut g.armor);
std::mem::swap(&mut self.location_armor, &mut g.location_armor);
std::mem::swap(&mut self.offhand, &mut g.offhand);
std::mem::swap(&mut self.carried, &mut g.carried);
// A teleport with a rotation (commands) turns the guest. `/rotate`
+4
View File
@@ -281,6 +281,9 @@ pub struct Guest {
/// The 36 inventory slots (hotbar first), armor (head..feet) and off hand.
pub inventory: Vec<Option<ItemStack>>,
pub armor: [Option<ItemStack>; 4],
/// Armor as of this player's last `collectEquipmentChanges` look; swapped
/// with `armor` so each player compares against their own snapshot.
pub location_armor: [Option<ItemStack>; 4],
pub offhand: Option<ItemStack>,
pub selected: usize,
pub carried: Option<ItemStack>,
@@ -530,6 +533,7 @@ impl Guest {
logs_kept: None,
inventory: vec![None; 36],
armor: [None, None, None, None],
location_armor: [None, None, None, None],
offhand: None,
selected: 0,
carried: None,