mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-12 18:35:16 -10:00
Sheep grazing: the eat goal only changes the world with mobGriefing on
`EatBlockGoal.tick` gates both the `destroyBlock` of an edible block and the grass block's `levelEvent(2001)` + `setBlock(DIRT)` behind `MOB_GRIEFING`, and keeps `Mob.ate` outside that gate. Horizon pushed the two `MobEvent`s unconditionally, so a sheep on a grass block with the rule off turned it to dirt: measured in a running client, 0 -> 1 dirt blocks at 41.6 s of a 150 s window. `MobWorld.mob_griefing` already carries the rule (it gates an allay's pickup, the dragon's block breaking, blasts and a mob's projectiles); this goal was the outlier. The test world gains `with_built_test_world_rules` so the flag is reachable from a test, and the new cases tick a sheep for 6000 ticks through `mobs::ai::tick` with the rule on and off, one per half of the gate: dirt events 4 on and 0 off with `EntityEvent` 10 in both, and the short grass under its feet destroyed on, untouched off. Inverting the gate fails both. Co-Authored-By: Hermes Agent (deepseek-v4.1-flash) <[email protected]>
This commit is contained in:
1 parent
dea6dea98e
commit
7d7a73e84c
2 files changed
+137
-11
No files matched your search
@@ -2590,19 +2590,27 @@ impl Goal for EatBlock {
|
||||
m.position.z.floor() as i32,
|
||||
);
|
||||
let here = cx.w.block_name(x, y, z).to_owned();
|
||||
// EatBlockGoal.tick: both halves of the world change sit behind
|
||||
// MOB_GRIEFING (`ifeq 89`, `ifeq 182` in the bytecode); `Mob.ate`
|
||||
// runs either way, so a sheep still sheds its wool and ages up.
|
||||
if cx.w.has_tag("minecraft:edible_for_sheep", &here) {
|
||||
cx.events.push(MobEvent::BreakBlock {
|
||||
pos: [x, y, z],
|
||||
drop: false,
|
||||
});
|
||||
if cx.w.mob_griefing {
|
||||
cx.events.push(MobEvent::BreakBlock {
|
||||
pos: [x, y, z],
|
||||
drop: false,
|
||||
});
|
||||
}
|
||||
super::ai::ate(m);
|
||||
} else if cx.w.block_name(x, y - 1, z) == "minecraft:grass_block" {
|
||||
cx.events.push(MobEvent::SetBlockName {
|
||||
pos: [x, y - 1, z],
|
||||
name: "minecraft:dirt",
|
||||
effect: true,
|
||||
flags: horizon_world::level::flags::UPDATE_CLIENTS,
|
||||
});
|
||||
// The particles and the dirt `setBlock` are the same gate.
|
||||
if cx.w.mob_griefing {
|
||||
cx.events.push(MobEvent::SetBlockName {
|
||||
pos: [x, y - 1, z],
|
||||
name: "minecraft:dirt",
|
||||
effect: true,
|
||||
flags: horizon_world::level::flags::UPDATE_CLIENTS,
|
||||
});
|
||||
}
|
||||
super::ai::ate(m);
|
||||
}
|
||||
}
|
||||
@@ -2821,6 +2829,114 @@ impl MoveToBlock {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod eat_block_tests {
|
||||
use super::*;
|
||||
use crate::mobs::world::with_built_test_world_rules;
|
||||
|
||||
/// A sheep on a wide grass shelf, ticked long enough for its `EatBlockGoal`
|
||||
/// (1 in 1000 a tick) to fire several times: every event it pushed, with
|
||||
/// the `mob_griefing` game rule at `griefing`. `tufts` puts short grass at
|
||||
/// the sheep's own feet, so the goal takes the `destroyBlock` half; without
|
||||
/// it the sheep stands on the grass and takes the grass-to-dirt half.
|
||||
fn sheep_events(griefing: bool, tufts: bool) -> Option<Vec<MobEvent>> {
|
||||
let mut out = None;
|
||||
let build = |world: &mut crate::world::World,
|
||||
registry: &horizon_world::registry::BlockRegistry| {
|
||||
let grass = registry.default_state("minecraft:grass_block").unwrap();
|
||||
let tuft = registry.default_state("minecraft:short_grass").unwrap();
|
||||
for cx in -1..1 {
|
||||
for cz in -1..1 {
|
||||
world
|
||||
.chunks
|
||||
.insert((cx, cz), Arc::new(crate::chunk::Chunk::empty(cx, cz)));
|
||||
}
|
||||
}
|
||||
for x in -8..=8 {
|
||||
for z in -8..=8 {
|
||||
world.set(x, 63, z, grass).unwrap();
|
||||
if tufts {
|
||||
world.set(x, 64, z, tuft).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
with_built_test_world_rules(&[], griefing, build, |w, types| {
|
||||
let kind = Arc::new(types.types["minecraft:sheep"].clone());
|
||||
let mut m = Mob::new(1, kind, Vec3::new(0.5, 64.0, 0.5), 0.0);
|
||||
let mut random = LegacyRandom::new(0);
|
||||
let mut events = Vec::new();
|
||||
for _ in 0..6000 {
|
||||
crate::mobs::ai::tick(&mut m, w, &mut random, &mut events);
|
||||
}
|
||||
out = Some(events);
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
/// `EatBlockGoal.tick` puts the `levelEvent` + `setBlock` behind
|
||||
/// `MOB_GRIEFING` and leaves `Mob.ate` outside it, so with the rule off the
|
||||
/// goal still runs (`EntityEvent` 10) and still shears the coat, and the
|
||||
/// grass stays.
|
||||
#[test]
|
||||
fn the_eat_goal_turns_grass_to_dirt_only_with_mob_griefing() {
|
||||
let Some(on) = sheep_events(true, false) else {
|
||||
return;
|
||||
};
|
||||
let Some(off) = sheep_events(false, false) else {
|
||||
return;
|
||||
};
|
||||
let dirt = |events: &[MobEvent]| {
|
||||
events
|
||||
.iter()
|
||||
.filter(|e| matches!(e, MobEvent::SetBlockName { name, .. } if *name == "minecraft:dirt"))
|
||||
.count()
|
||||
};
|
||||
let ate = |events: &[MobEvent]| {
|
||||
events
|
||||
.iter()
|
||||
.filter(|e| matches!(e, MobEvent::EntityEvent { event: 10, .. }))
|
||||
.count()
|
||||
};
|
||||
assert!(dirt(&on) > 0, "mob_griefing on: {}", dirt(&on));
|
||||
assert_eq!(dirt(&off), 0, "mob_griefing off must leave the grass alone");
|
||||
assert!(ate(&off) > 0, "the goal still runs with the rule off");
|
||||
}
|
||||
|
||||
/// The edible half of the same gate (`ifeq 89` skips
|
||||
/// `Level.destroyBlock(pos, false)`): short grass at the sheep's feet
|
||||
/// disappears with the rule on and stays with it off, while the goal eats
|
||||
/// either way.
|
||||
#[test]
|
||||
fn the_eat_goal_breaks_short_grass_only_with_mob_griefing() {
|
||||
let Some(on) = sheep_events(true, true) else {
|
||||
return;
|
||||
};
|
||||
let Some(off) = sheep_events(false, true) else {
|
||||
return;
|
||||
};
|
||||
let bitten = |events: &[MobEvent]| {
|
||||
events
|
||||
.iter()
|
||||
.filter(|e| matches!(e, MobEvent::BreakBlock { drop: false, .. }))
|
||||
.count()
|
||||
};
|
||||
let ate = |events: &[MobEvent]| {
|
||||
events
|
||||
.iter()
|
||||
.filter(|e| matches!(e, MobEvent::EntityEvent { event: 10, .. }))
|
||||
.count()
|
||||
};
|
||||
assert!(bitten(&on) > 0, "mob_griefing on: {}", bitten(&on));
|
||||
assert_eq!(
|
||||
bitten(&off),
|
||||
0,
|
||||
"mob_griefing off must leave the tufts alone"
|
||||
);
|
||||
assert!(ate(&off) > 0, "the goal still runs with the rule off");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod built_by_player_tests {
|
||||
use super::*;
|
||||
|
||||
@@ -403,6 +403,16 @@ pub(crate) fn with_built_test_world(
|
||||
others: &[Seen],
|
||||
build: impl FnOnce(&mut crate::world::World, &horizon_world::registry::BlockRegistry),
|
||||
f: impl FnOnce(&MobWorld<'_>, &super::MobTypes),
|
||||
) -> bool {
|
||||
with_built_test_world_rules(others, true, build, f)
|
||||
}
|
||||
/// `with_built_test_world` with the `mob_griefing` game rule the world reports.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn with_built_test_world_rules(
|
||||
others: &[Seen],
|
||||
mob_griefing: bool,
|
||||
build: impl FnOnce(&mut crate::world::World, &horizon_world::registry::BlockRegistry),
|
||||
f: impl FnOnce(&MobWorld<'_>, &super::MobTypes),
|
||||
) -> bool {
|
||||
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../.local/horizon-client")
|
||||
@@ -454,7 +464,7 @@ pub(crate) fn with_built_test_world(
|
||||
block_tags: &items.block_tags,
|
||||
landing: &motion.landing,
|
||||
biomes: &biomes,
|
||||
mob_griefing: true,
|
||||
mob_griefing,
|
||||
universal_anger: false,
|
||||
mob_drops: true,
|
||||
max_entity_cramming: 24,
|
||||
|
||||
Reference in new issue
Block a user