mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #11515 from horizon-rust/fix/mob-entity-collisions-10445
🤖 Agent: mobs path-stop at shulkers and a held-still happy ghast; earlier parity failures were env + main's /say regression (#11687 fixed it); CI parity reruns with run-parity before landing.
This commit is contained in:
6 files changed
+205
-2
No files matched your search
@@ -1200,6 +1200,7 @@ pub fn main() -> horizon_client::Result<()> {
|
||||
others_grid: $grid,
|
||||
others_index: None,
|
||||
others_villagers: None,
|
||||
others_solid: None,
|
||||
daylight: (world.day_time.rem_euclid(24000)) < 12000,
|
||||
monsters_burn: !(12542..23460).contains(&world.day_time.rem_euclid(24000)),
|
||||
game_time: world.game_time,
|
||||
|
||||
@@ -927,6 +927,7 @@ impl Game<'_> {
|
||||
others_grid: None,
|
||||
others_index: None,
|
||||
others_villagers: None,
|
||||
others_solid: None,
|
||||
daylight: self.dim != Dim::Nether && self.sky_darken() < 4,
|
||||
// day.json: false from tick 12542, true from 23460 (a step track).
|
||||
monsters_burn: self.dim == Dim::Overworld
|
||||
@@ -1148,11 +1149,15 @@ impl Game<'_> {
|
||||
// id answers, as a scan would.
|
||||
let mut index = horizon_client::fast_hash::FxHashMap::<u32, u32>::default();
|
||||
let mut villager_slots = Vec::new();
|
||||
let mut solid_slots = Vec::new();
|
||||
for (i, s) in snapshot.iter().enumerate() {
|
||||
index.entry(s.id).or_insert(i as u32);
|
||||
if s.species == horizon_client::mobs::Species::Villager {
|
||||
villager_slots.push(i as u32);
|
||||
}
|
||||
if s.may_block_mobs() {
|
||||
solid_slots.push(i as u32);
|
||||
}
|
||||
}
|
||||
laps.lap("creature_prepare");
|
||||
let mut cached_difficulty: Option<((i32, i32), spawning::LocalDifficulty)> = None;
|
||||
@@ -1191,6 +1196,7 @@ impl Game<'_> {
|
||||
others_grid: Some(&grid),
|
||||
others_index: Some(&index),
|
||||
others_villagers: Some(&villager_slots),
|
||||
others_solid: Some(&solid_slots),
|
||||
nearest_player,
|
||||
player: None,
|
||||
teams: std::sync::Arc::clone(&base.teams),
|
||||
@@ -1254,6 +1260,7 @@ impl Game<'_> {
|
||||
others_grid: Some(&grid),
|
||||
others_index: Some(&index),
|
||||
others_villagers: Some(&villager_slots),
|
||||
others_solid: Some(&solid_slots),
|
||||
difficulty,
|
||||
nearest_player,
|
||||
player: active_player,
|
||||
@@ -8685,6 +8692,7 @@ mod pearl_tests {
|
||||
others_grid: None,
|
||||
others_index: None,
|
||||
others_villagers: None,
|
||||
others_solid: None,
|
||||
daylight: true,
|
||||
monsters_burn: false,
|
||||
game_time: 1000,
|
||||
|
||||
@@ -2487,8 +2487,33 @@ pub struct Seen {
|
||||
pub hurt_recently: bool,
|
||||
/// `Hoglin.canBeHunted`: an adult hoglin without `CannotBeHunted`.
|
||||
pub huntable: bool,
|
||||
/// A happy ghast's `isOnStillTimeout` (a player on its back holds it).
|
||||
pub still: bool,
|
||||
/// `Entity.isVehicle`: something rides it.
|
||||
pub carrying: bool,
|
||||
}
|
||||
impl Seen {
|
||||
/// Whether `canBeCollidedWith` can ever hold for this kind: the slots
|
||||
/// [`MobWorld::others_solid`] lists.
|
||||
pub fn may_block_mobs(&self) -> bool {
|
||||
matches!(self.species, Species::Shulker | Species::HappyGhast)
|
||||
}
|
||||
/// `canBeCollidedWith(mover)` for a mob of species `mover` moving into
|
||||
/// it (`EntityGetter.getEntityCollisions`): a living shulker
|
||||
/// (`Shulker`), or an adult happy ghast held still, or carrying someone
|
||||
/// when the mover is a happy ghast too (`HappyGhast`). Every other mob
|
||||
/// keeps `Entity`'s false.
|
||||
pub fn blocks_mob(&self, mover: Species) -> bool {
|
||||
match self.species {
|
||||
Species::Shulker => self.alive,
|
||||
Species::HappyGhast => {
|
||||
self.alive
|
||||
&& !self.baby
|
||||
&& (self.still || (self.carrying && mover == Species::HappyGhast))
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
/// `LivingEntity.hasEffect`, by id without namespace.
|
||||
pub fn has_effect(&self, effect: &str) -> bool {
|
||||
self.effects.contains(&effect)
|
||||
@@ -2678,6 +2703,8 @@ impl Mob {
|
||||
lying: m.species() == Species::Cat && (m.extra.flag || m.extra.relax_one),
|
||||
ridden: m.rider.is_some() || m.extra.rider.is_some() || m.extra.mount.is_some(),
|
||||
is_vehicle: equine::is_vehicle(m),
|
||||
still: m.species() == Species::HappyGhast && happy_ghast::on_still_timeout(m),
|
||||
carrying: m.rider.is_some() || m.extra.rider.is_some(),
|
||||
knockback_resistance: m.attrs.knockback_resistance,
|
||||
effects: m
|
||||
.extra
|
||||
@@ -3055,6 +3082,9 @@ pub struct MobWorld<'a> {
|
||||
/// keeps them (the mob ticks: a slot keeps its mob, and its species, all
|
||||
/// tick). Each zombie looking for a village read every slot.
|
||||
pub others_villagers: Option<&'a [u32]>,
|
||||
/// The slots of `others` that may stop a moving mob
|
||||
/// ([`Seen::may_block_mobs`]), when the caller keeps them; None: any.
|
||||
pub others_solid: Option<&'a [u32]>,
|
||||
pub daylight: bool,
|
||||
/// The `gameplay/monsters_burn` attribute: the overworld day timeline's
|
||||
/// step track, false in every other dimension.
|
||||
|
||||
@@ -653,6 +653,40 @@ impl Mob {
|
||||
}
|
||||
/// Entity.move with block collisions and step-up; updates collision flags
|
||||
/// and fall distance (Entity.checkFallDamage).
|
||||
/// `EntityGetter.getEntityCollisions(this, box.expandTowards(movement))`:
|
||||
/// the boxes of the mobs whose `canBeCollidedWith(this)` holds (a living
|
||||
/// shulker, a happy ghast held still), but not this mob's vehicle or a
|
||||
/// fellow passenger (`Entity.canCollideWith`).
|
||||
fn entity_collisions(&self, w: &MobWorld, movement: Vec3) -> Vec<Aabb> {
|
||||
let area = self.bounds().expand_towards(movement);
|
||||
if area.average_size() < 1.0e-7 {
|
||||
return Vec::new();
|
||||
}
|
||||
let area = area.inflate(Vec3::new(1.0e-7, 1.0e-7, 1.0e-7));
|
||||
let mover = self.species();
|
||||
let blocks = |s: &&super::Seen| {
|
||||
s.id != self.id
|
||||
&& s.blocks_mob(mover)
|
||||
&& s.bounds.intersects(area)
|
||||
&& !self.passenger_of_same_vehicle(s.id, s.vehicle)
|
||||
};
|
||||
match w.others_solid {
|
||||
Some(slots) => slots
|
||||
.iter()
|
||||
.map(|&i| &w.others[i as usize])
|
||||
.filter(blocks)
|
||||
.map(|s| s.bounds)
|
||||
.collect(),
|
||||
None => w.others.iter().filter(blocks).map(|s| s.bounds).collect(),
|
||||
}
|
||||
}
|
||||
/// `Entity.isPassengerOfSameVehicle` toward the entity `id` riding
|
||||
/// `vehicle`: one rides the other, or both ride the same one.
|
||||
fn passenger_of_same_vehicle(&self, id: u32, vehicle: Option<u32>) -> bool {
|
||||
self.extra.mount == Some(id)
|
||||
|| vehicle == Some(self.id)
|
||||
|| (self.extra.mount.is_some() && vehicle == self.extra.mount)
|
||||
}
|
||||
pub fn do_move(&mut self, w: &MobWorld, events: &mut Vec<MobEvent>) {
|
||||
let mut wanted = self.velocity;
|
||||
// Entity.move: a block the mob is stuck in (sweet berry bush) scales
|
||||
@@ -672,10 +706,19 @@ impl Mob {
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
// Entity.collide: the world border's walls stop the mob too.
|
||||
// Entity.collide: the world border's walls stop the mob too, and so
|
||||
// do the entities it may not pass through.
|
||||
let border = w.border.map(|b| (b, [self.position.x, self.position.z]));
|
||||
let entities = self.entity_collisions(w, wanted);
|
||||
let moved = scene
|
||||
.resolve_in_border(self.bounds(), wanted, step, self.on_ground, border)
|
||||
.resolve_in_border_with(
|
||||
self.bounds(),
|
||||
wanted,
|
||||
step,
|
||||
self.on_ground,
|
||||
border,
|
||||
&entities,
|
||||
)
|
||||
.unwrap_or(Vec3::ZERO);
|
||||
let length = moved.x * moved.x + moved.y * moved.y + moved.z * moved.z;
|
||||
let wanted_length = wanted.x * wanted.x + wanted.y * wanted.y + wanted.z * wanted.z;
|
||||
@@ -2132,6 +2175,7 @@ mod tests {
|
||||
others_grid: None,
|
||||
others_index: None,
|
||||
others_villagers: None,
|
||||
others_solid: None,
|
||||
daylight: true,
|
||||
monsters_burn: false,
|
||||
game_time: 1000,
|
||||
@@ -2249,6 +2293,7 @@ mod tests {
|
||||
others_grid: None,
|
||||
others_index: None,
|
||||
others_villagers: None,
|
||||
others_solid: None,
|
||||
daylight: true,
|
||||
monsters_burn: false,
|
||||
game_time: 1000,
|
||||
@@ -2307,6 +2352,123 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// `Entity.collide` takes `getEntityCollisions` too: a zombie dropping
|
||||
/// onto a living shulker or a happy ghast held still (a player on its
|
||||
/// back) lands on it; a dead shulker, a happy ghast that is not held, or
|
||||
/// a ghastling lets it through (`canBeCollidedWith`).
|
||||
#[test]
|
||||
fn a_falling_mob_lands_on_a_shulker_or_a_held_happy_ghast() {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../.local/horizon-client")
|
||||
.join(horizon_core::VANILLA_VERSION);
|
||||
if !root.join("entity-types.json").exists()
|
||||
|| !root.join("movement-properties.nbt").exists()
|
||||
{
|
||||
return;
|
||||
}
|
||||
let assets = crate::assets::Assets::load(&root).unwrap();
|
||||
let registry = assets.registry;
|
||||
let facts = crate::blocks::BlockFacts::load(&root, registry).unwrap();
|
||||
let motion = crate::motion::MotionResources::load(&root, &assets).unwrap();
|
||||
let data = super::super::MobData::load(&root, &facts);
|
||||
let types = super::super::MobTypes::load(&root).unwrap();
|
||||
let biomes = crate::biome::Biomes::load(&root).unwrap();
|
||||
let items = crate::items::Items::load(
|
||||
&root,
|
||||
registry,
|
||||
&facts,
|
||||
&horizon_world::loot::PackOverlay::NONE,
|
||||
)
|
||||
.unwrap();
|
||||
let collision = std::sync::Arc::new(crate::collisions::load(&root, registry).unwrap());
|
||||
let suffocation =
|
||||
crate::mobs::suffocation::bind(collision.clone(), &motion.causes_suffocation).unwrap();
|
||||
let lid_openers = std::collections::HashMap::new();
|
||||
let air = registry.default_state("minecraft:air").unwrap();
|
||||
let mut world = crate::world::World::new(air, 0);
|
||||
world.chunks.insert(
|
||||
(0, 0),
|
||||
std::sync::Arc::new(crate::chunk::Chunk::empty(0, 0)),
|
||||
);
|
||||
let mob = |id: u32, kind: &str, at: Vec3| {
|
||||
Mob::new(id, std::sync::Arc::new(types.types[kind].clone()), at, 0.0)
|
||||
};
|
||||
// Where a zombie starting 3 blocks above `below`'s top ends after 20
|
||||
// falling moves, with `below` in the mobs it sees.
|
||||
let landing = |below: &Mob| -> f64 {
|
||||
let others = [below.seen(None)];
|
||||
let w = MobWorld {
|
||||
teams: Default::default(),
|
||||
world: &world,
|
||||
registry,
|
||||
facts: &facts,
|
||||
fluid_tables: &motion.fluids,
|
||||
movement: &motion.blocks,
|
||||
collision: &collision,
|
||||
suffocation: &suffocation,
|
||||
data: &data,
|
||||
player: None,
|
||||
controlling_player_movement: None,
|
||||
nearest_player: None,
|
||||
others: &others,
|
||||
dragon: None,
|
||||
leash_boats: &[],
|
||||
others_grid: None,
|
||||
others_index: None,
|
||||
others_villagers: None,
|
||||
others_solid: None,
|
||||
daylight: true,
|
||||
monsters_burn: false,
|
||||
game_time: 1000,
|
||||
day_time: 6000,
|
||||
raining: false,
|
||||
thundering: false,
|
||||
nether: false,
|
||||
block_tags: &items.block_tags,
|
||||
landing: &motion.landing,
|
||||
biomes: &biomes,
|
||||
universal_anger: false,
|
||||
mob_griefing: true,
|
||||
mob_drops: true,
|
||||
max_entity_cramming: 24,
|
||||
difficulty: super::super::spawning::LocalDifficulty::new(2, 6000, 0, 0.0),
|
||||
end: false,
|
||||
world_seed: 0,
|
||||
sea_level: 63,
|
||||
items: Some(&items),
|
||||
lid_openers: &lid_openers,
|
||||
mod_effects: None,
|
||||
border: None,
|
||||
};
|
||||
let top = below.bounds().max_position().y;
|
||||
let mut zombie = mob(1, "minecraft:zombie", Vec3::new(8.5, top + 3.0, 8.5));
|
||||
for _ in 0..20 {
|
||||
zombie.velocity = Vec3::new(0.0, -0.5, 0.0);
|
||||
zombie.do_move(&w, &mut Vec::new());
|
||||
}
|
||||
zombie.position.y - top
|
||||
};
|
||||
let mut shulker = mob(2, "minecraft:shulker", Vec3::new(8.5, 64.0, 8.5));
|
||||
assert_eq!(landing(&shulker), 0.0, "a living shulker holds it up");
|
||||
shulker.health = 0.0;
|
||||
assert!(landing(&shulker) < -5.0, "a dead shulker does not");
|
||||
let mut ghast = mob(3, "minecraft:happy_ghast", Vec3::new(8.5, 64.0, 8.5));
|
||||
assert!(
|
||||
landing(&ghast) < -5.0,
|
||||
"a happy ghast left alone is not solid"
|
||||
);
|
||||
ghast.extra.counter = 10;
|
||||
assert_eq!(
|
||||
landing(&ghast),
|
||||
0.0,
|
||||
"held still, its back holds the zombie"
|
||||
);
|
||||
let mut ghastling = mob(4, "minecraft:happy_ghast", Vec3::new(8.5, 64.0, 8.5));
|
||||
super::super::finalize::set_baby(&mut ghastling, true);
|
||||
ghastling.extra.counter = 10;
|
||||
assert!(landing(&ghastling) < -5.0, "a ghastling never is");
|
||||
}
|
||||
|
||||
/// `LivingEntity.aiStep` calls `travel` and only then
|
||||
/// `applyEffectsFromBlocks`, so the lift of a bubble column is added after
|
||||
/// the water's drag and is not scaled by it. A pig 1.0 high in a column of
|
||||
|
||||
@@ -512,6 +512,7 @@ pub(crate) fn with_resource_test_world(
|
||||
others_grid: None,
|
||||
others_index: None,
|
||||
others_villagers: None,
|
||||
others_solid: None,
|
||||
daylight: true,
|
||||
monsters_burn: false,
|
||||
game_time: 1000,
|
||||
|
||||
@@ -236,6 +236,7 @@ mod tests {
|
||||
others_grid: None,
|
||||
others_index: None,
|
||||
others_villagers: None,
|
||||
others_solid: None,
|
||||
daylight: true,
|
||||
monsters_burn: false,
|
||||
game_time: 1000,
|
||||
|
||||
Reference in new issue
Block a user