mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #10208 from ilguerro/fix/entity-die-non-mobs
🤖 Agent: Requeued after merging main twice (lan_e2e.py keep-both: this PR's entity-die check beside main's new mob-thorns check; then #11735's clippy/lock fix); verify green. Breaking a painting, cushion, boat, minecart, frame or armor stand emits ENTITY_DIE like vanilla's kill path, and its sculk/vibration harnesses passed on the earlier head.
This commit is contained in:
10 files changed
+173
-4
No files matched your search
@@ -856,6 +856,8 @@ impl Game<'_> {
|
||||
self.audio
|
||||
.play("minecraft:entity.armor_stand.break", Some(at), 1.0, 1.0);
|
||||
self.show_breaking_particles(at, f64::from(width), f64::from(height));
|
||||
// ArmorStand.kill(level, player).
|
||||
self.entity_killed_event(at, true);
|
||||
return true;
|
||||
}
|
||||
if time - self.armor_stands[i].last_hit > 5 {
|
||||
@@ -888,6 +890,8 @@ impl Game<'_> {
|
||||
];
|
||||
self.spawn_item(stack, center, v, 10);
|
||||
}
|
||||
// ArmorStand.kill(level, player), after the drops.
|
||||
self.entity_killed_event(at, true);
|
||||
self.world
|
||||
.modified
|
||||
.insert(((at[0].floor() as i32) >> 4, (at[2].floor() as i32) >> 4));
|
||||
@@ -1087,6 +1091,7 @@ impl Game<'_> {
|
||||
}
|
||||
self.audio
|
||||
.play("minecraft:entity.armor_stand.break", Some(at), 1.0, 1.0);
|
||||
self.entity_killed_event(at, false);
|
||||
self.world.modified.insert((pos[0] >> 4, pos[2] >> 4));
|
||||
}
|
||||
/// ArmorStandRenderer: the stand with its default pose, then armor layers.
|
||||
|
||||
@@ -1519,8 +1519,12 @@ impl Game<'_> {
|
||||
for stack in b.items.iter().flatten() {
|
||||
self.spawn_item(stack.clone(), at, super::dispenser::new_item_motion(), 10);
|
||||
}
|
||||
// `VehicleEntity.destroy(level, dropItem)` drops the boat item on
|
||||
// the death path only, and only with `entity_drops` on.
|
||||
// `VehicleEntity.destroy(level, dropItem)`: `kill` (the boat's own
|
||||
// ENTITY_DIE), then the boat item on the death path only, and only
|
||||
// with `entity_drops` on.
|
||||
if !creative {
|
||||
self.entity_killed_event(at, false);
|
||||
}
|
||||
if !creative && self.ui.rules.bool("entity_drops") {
|
||||
let item = b.entity_type();
|
||||
self.spawn_item(
|
||||
|
||||
@@ -2281,6 +2281,30 @@ impl Game<'_> {
|
||||
let _ = self.in_target_level(t, |g| g.kill_target_in_level(t));
|
||||
}
|
||||
fn kill_target_in_level(&mut self, t: Target) {
|
||||
// Entity.kill (BlockAttachedEntity.kill; ArmorStand's generic_kill hit,
|
||||
// which bypasses its invulnerability; a vehicle is no LivingEntity):
|
||||
// removed, then ENTITY_DIE by the entity itself. Mobs post theirs as
|
||||
// they die, the crystal and the dragon on their own paths, and an item
|
||||
// entity can dampen it (ItemEntity.dampensVibrations), so it is not here.
|
||||
let died = matches!(
|
||||
t,
|
||||
Target::Stand(_)
|
||||
| Target::Frame(_)
|
||||
| Target::Painting(_)
|
||||
| Target::Boat(_)
|
||||
| Target::Cart(_)
|
||||
| Target::Orb(_)
|
||||
| Target::Projectile(_)
|
||||
| Target::Fangs(_)
|
||||
| Target::Tnt(_)
|
||||
| Target::Falling(_)
|
||||
| Target::Rocket(_)
|
||||
| Target::Trident(_)
|
||||
| Target::Spawner(_)
|
||||
| Target::Cloud(_)
|
||||
)
|
||||
.then(|| self.target_position(t))
|
||||
.flatten();
|
||||
match t {
|
||||
Target::Guest(_) => {
|
||||
self.for_player(t, |g| g.kill_target(Target::Player));
|
||||
@@ -2356,6 +2380,9 @@ impl Game<'_> {
|
||||
}
|
||||
// The orbs marked for removal are gone (Entity.kill removes them).
|
||||
self.orbs.retain(|o| o.age < u32::MAX / 2);
|
||||
if let Some(at) = died {
|
||||
self.entity_killed_event(at, false);
|
||||
}
|
||||
self.native_leaves_flush();
|
||||
}
|
||||
fn cmd_kill(&mut self, ctx: &Context) -> R<i32> {
|
||||
|
||||
@@ -387,6 +387,8 @@ impl Game<'_> {
|
||||
if self.riding == Some(c.id) {
|
||||
self.dismount_removed_vehicle(p.y);
|
||||
}
|
||||
// BlockAttachedEntity.kill, before dropItem.
|
||||
self.entity_killed_event([p.x, p.y, p.z], by_player);
|
||||
self.audio.play(
|
||||
"minecraft:entity.cushion.break",
|
||||
Some([p.x, p.y, p.z]),
|
||||
|
||||
@@ -363,6 +363,10 @@ impl Game<'_> {
|
||||
);
|
||||
if !with_frame {
|
||||
self.item_frames[i].item = None;
|
||||
} else {
|
||||
// BlockAttachedEntity.kill, before dropItem. A frame holding an
|
||||
// item only gives the item up (ItemFrame.hurtServer).
|
||||
self.entity_killed_event(at, true);
|
||||
}
|
||||
self.drop_frame_items(&f, with_frame, true);
|
||||
self.world.modified.insert((f.pos[0] >> 4, f.pos[2] >> 4));
|
||||
@@ -470,6 +474,7 @@ impl Game<'_> {
|
||||
continue;
|
||||
}
|
||||
let f = self.item_frames.remove(i);
|
||||
self.entity_killed_event(center(&f), false);
|
||||
self.audio
|
||||
.play(&sound(&f, "break"), Some(center(&f)), 1.0, 1.0);
|
||||
self.drop_frame_items(&f, true, false);
|
||||
@@ -497,6 +502,7 @@ impl Game<'_> {
|
||||
if !self.frame_survives(&f) && !self.hanging_break_cancelled(f.kind(), f.pos, false)
|
||||
{
|
||||
self.item_frames.remove(i);
|
||||
self.entity_killed_event(center(&f), false);
|
||||
self.audio
|
||||
.play(&sound(&f, "break"), Some(center(&f)), 1.0, 1.0);
|
||||
self.drop_frame_items(&f, true, false);
|
||||
|
||||
@@ -2394,6 +2394,11 @@ impl Game<'_> {
|
||||
self.carts.insert(i, c);
|
||||
} else {
|
||||
let at = [c.position.x, c.position.y, c.position.z];
|
||||
// VehicleEntity.destroy: `kill` (the cart's own ENTITY_DIE),
|
||||
// then the drops; a creative hit only discards it.
|
||||
if !creative {
|
||||
self.entity_killed_event(at, false);
|
||||
}
|
||||
if self.ui.rules.bool("entity_drops") {
|
||||
for s in c.items.iter().flatten() {
|
||||
self.spawn_item(s.clone(), at, [0.0, 0.1, 0.0], 10);
|
||||
|
||||
@@ -350,6 +350,8 @@ impl Game<'_> {
|
||||
(lo[2] + hi[2]) / 2.0,
|
||||
];
|
||||
self.world.modified.insert((p.pos[0] >> 4, p.pos[2] >> 4));
|
||||
// BlockAttachedEntity.kill, before dropItem.
|
||||
self.entity_killed_event(at, by_player);
|
||||
if !self.ui.rules.bool("entity_drops") {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2282,6 +2282,17 @@ impl Game<'_> {
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
/// `Entity.kill` for an entity that is no mob (`BlockAttachedEntity.kill`,
|
||||
/// `ArmorStand.kill`, `VehicleEntity.destroy`): removed, then
|
||||
/// `gameEvent(ENTITY_DIE, attributedTo != null ? attributedTo : this)` at
|
||||
/// the entity's position. `by_player`: the player's hit killed it, so the
|
||||
/// player is the source. Otherwise the source is the entity itself (or a
|
||||
/// bolt), which no listener tells apart from none: it is not living,
|
||||
/// sneaking, a spectator or a warden.
|
||||
pub(super) fn entity_killed_event(&mut self, at: [f64; 3], by_player: bool) {
|
||||
let ctx = self.who_context(by_player.then_some(Who::Player), None);
|
||||
self.post_game_event("minecraft:entity_die", at, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/// `MobEffects.DARKNESS`'s `setBlendDuration(22)`: its fade in, its fade out,
|
||||
|
||||
@@ -13389,6 +13389,113 @@ CHECKS += ['levelup-sounds']
|
||||
CHECK_FNS['levelup-sounds'] = check_levelup_sounds
|
||||
|
||||
|
||||
def check_entity_die(host, guest, out):
|
||||
"""#10046: killing an entity that is no mob is heard (`gameEvent(ENTITY_DIE)`: frequency 15) by a
|
||||
sculk sensor about 3 blocks away, from the player's hit while sneaking (the attacker is the source
|
||||
of `BlockAttachedEntity.kill` and `ArmorStand.kill`, and ENTITY_DIE is no event sneaking hides) and
|
||||
from `/kill`: a cushion, a painting, an empty item frame, an armor stand, a boat and a minecart.
|
||||
An item frame holding an item only gives the item up and does not die (`ItemFrame.hurtServer`)."""
|
||||
hx, hy, hz = (int(v // 1) for v in pos_of(host))
|
||||
old_modes = {g.name: g('state')['gamemode'] for g in (host, guest)}
|
||||
seen = {}
|
||||
|
||||
def quiet(game, sensor):
|
||||
state, still = None, 0
|
||||
for _ in range(200):
|
||||
game.wait(1)
|
||||
state = game.raw('getblock', *sensor)['block']
|
||||
still = still + 1 if 'sculk_sensor_phase=inactive' in state else 0
|
||||
if still == 10:
|
||||
return
|
||||
raise Failed(f'sensor did not settle: {state}')
|
||||
|
||||
try:
|
||||
for game, dz in ((host, -6), (guest, 6)):
|
||||
x, z = hx + 8, hz + dz
|
||||
sensor = (x + 3, hy, z + 2)
|
||||
box = f'x={x - 2},y={hy - 1},z={z - 2},dx=7,dy=5,dz=7'
|
||||
target = (x + 0.5, hy, z + 2.5)
|
||||
hanging = (x + 0.5, hy + 1.5, z + 2.97)
|
||||
cases = {
|
||||
'cushion': ('creative', None, (target[0], hy + .125, target[2]), 'click'),
|
||||
'painting': ('creative', f'summon minecraft:painting {x + .5} {hy + 1} {z + 2.97} '
|
||||
f'{{block_pos:[I;{x},{hy + 1},{z + 2}],facing:2b,variant:"minecraft:kebab"}}',
|
||||
hanging, 'click'),
|
||||
'item_frame': ('creative', f'summon minecraft:item_frame {x + .5} {hy + 1} {z + 2.97} '
|
||||
f'{{block_pos:[I;{x},{hy + 1},{z + 2}],Facing:2b}}', hanging, 'click'),
|
||||
'framed_item': ('creative', f'summon minecraft:item_frame {x + .5} {hy + 1} {z + 2.97} '
|
||||
f'{{block_pos:[I;{x},{hy + 1},{z + 2}],Facing:2b,'
|
||||
'Item:{id:"minecraft:diamond",count:1}}', hanging, 'click'),
|
||||
'armor_stand': ('creative', f'summon minecraft:armor_stand {target[0]} {hy} {target[2]}',
|
||||
(target[0], hy + 1, target[2]), 'click'),
|
||||
'boat': ('survival', f'summon minecraft:oak_boat {target[0]} {hy} {target[2]}',
|
||||
(target[0], hy + .3, target[2]), 'click'),
|
||||
'minecart': ('survival', f'summon minecraft:minecart {target[0]} {hy} {target[2]}',
|
||||
(target[0], hy + .3, target[2]), 'click'),
|
||||
'kill_painting': ('creative', f'summon minecraft:painting {x + .5} {hy + 1} {z + 2.97} '
|
||||
f'{{block_pos:[I;{x},{hy + 1},{z + 2}],facing:2b,variant:"minecraft:kebab"}}',
|
||||
None, f'kill @e[type=minecraft:painting,{box}]'),
|
||||
'kill_boat': ('creative', f'summon minecraft:oak_boat {target[0]} {hy} {target[2]}',
|
||||
None, f'kill @e[type=minecraft:oak_boat,{box}]'),
|
||||
}
|
||||
heard = {}
|
||||
for kind, (mode, summon, aim, act) in cases.items():
|
||||
host('command', command=f'fill {x - 2} {hy - 1} {z - 2} {x + 4} {hy - 1} {z + 5} minecraft:stone')
|
||||
host('command', command=f'fill {x - 2} {hy} {z - 2} {x + 4} {hy + 3} {z + 5} minecraft:air')
|
||||
# The wall the hanging entities hang on, north of it.
|
||||
host('command', command=f'setblock {x} {hy + 1} {z + 3} minecraft:stone')
|
||||
host('command', command=f'setblock {sensor[0]} {sensor[1]} {sensor[2]} minecraft:sculk_sensor')
|
||||
game('command', command=f'gamemode {mode}')
|
||||
game('command', command=f'tp @s {x + 0.5} {hy} {z + 0.5} 0 0')
|
||||
game('command', command='item replace entity @s weapon.mainhand with minecraft:iron_sword')
|
||||
game.raw('key', 'shift', 'down')
|
||||
game.wait(10)
|
||||
if kind == 'cushion':
|
||||
game('command', command='item replace entity @s weapon.mainhand with minecraft:red_cushion')
|
||||
game.wait(5)
|
||||
game.raw('look-at', target[0], hy, target[2])
|
||||
game.raw('mouse', 'right', 'click')
|
||||
else:
|
||||
host('command', command=summon)
|
||||
game.wait(20)
|
||||
quiet(game, sensor)
|
||||
# A fresh sensor: whatever it heard while the entity was set up is gone.
|
||||
host('command', command=f'setblock {sensor[0]} {sensor[1]} {sensor[2]} minecraft:air')
|
||||
host('command', command=f'setblock {sensor[0]} {sensor[1]} {sensor[2]} minecraft:sculk_sensor')
|
||||
game.wait(5)
|
||||
if aim is None:
|
||||
game('command', command=act)
|
||||
else:
|
||||
game.raw('look-at', *aim)
|
||||
game.wait(2)
|
||||
game.raw('mouse', 'left', 'click')
|
||||
game.wait(10)
|
||||
said = command_says(game, f'data get block {sensor[0]} {sensor[1]} {sensor[2]} last_vibration_frequency')
|
||||
heard[kind] = said.rsplit(' ', 1)[-1]
|
||||
game.raw('key', 'shift', 'up')
|
||||
host('command', command=f'kill @e[type=!minecraft:player,{box}]')
|
||||
seen[game.name] = heard
|
||||
host('command', command=f'fill {x - 2} {hy - 1} {z - 2} {x + 4} {hy + 3} {z + 5} minecraft:air')
|
||||
finally:
|
||||
for game in (host, guest):
|
||||
game.raw('key', 'shift', 'up')
|
||||
back_to_game(game)
|
||||
game('command', command='clear @s')
|
||||
game('command', command='gamemode ' + old_modes[game.name])
|
||||
face_each_other(host, guest)
|
||||
for name, heard in seen.items():
|
||||
for kind, frequency in heard.items():
|
||||
if kind == 'framed_item':
|
||||
assert frequency != '15', f'{name}: the frame holding an item died: {seen}'
|
||||
else:
|
||||
assert frequency == '15', f'{name}: {kind} death not heard: {seen}'
|
||||
return seen
|
||||
|
||||
|
||||
CHECKS += ['entity-die']
|
||||
CHECK_FNS['entity-die'] = check_entity_die
|
||||
|
||||
|
||||
def check_mob_thorns(host, guest, out):
|
||||
"""A mob's Thorns answers the player who hits it (#9954): `doPostAttackEffectsWithItemSource` runs the victim's
|
||||
equipment on every hit, so a husk in a Thorns 255 chestplate (the 15% per level always rolls) hurts the host's
|
||||
|
||||
@@ -100,7 +100,7 @@ ArmorStand.brokenByAnything inline crates/horizon-client/src/game/armor_stands.r
|
||||
ArmorStand.playBrokenSound inline crates/horizon-client/src/game/armor_stands.rs::Game::attack_armor_stand break sound on both break paths
|
||||
ArmorStand.setYBodyRot skip:other client rotation setter from packets; a stand turns only with its yaw field
|
||||
ArmorStand.setYHeadRot skip:other client rotation setter from packets; a stand turns only with its yaw field
|
||||
ArmorStand.kill inline crates/horizon-client/src/game/armor_stands.rs::Game::attack_armor_stand removes the stand; no game event
|
||||
ArmorStand.kill port crates/horizon-client/src/game/sculk.rs::Game::entity_killed_event the ENTITY_DIE event, by the attacker or the stand; armor_stands.rs removes the stand
|
||||
ArmorStand.setSmall port crates/horizon-client/src/entity_nbt.rs::armor_stand_from_nbt Small tag
|
||||
ArmorStand.isSmall skip:other accessor; reads the small field
|
||||
ArmorStand.setShowArms port crates/horizon-client/src/entity_nbt.rs::armor_stand_from_nbt ShowArms tag
|
||||
@@ -117,7 +117,7 @@ ArmorStand.getLightProbePosition port crates/horizon-client/src/entities.rs::Arm
|
||||
ArmorStand.setArmorStandPose port crates/horizon-client/src/entity_nbt.rs::armor_stand_from_nbt Pose tag read into the six pose parts
|
||||
ArmorStand.getArmorStandPose port crates/horizon-client/src/entity_nbt.rs::armor_stand_to_nbt Pose tag written from the six pose parts
|
||||
BlockAttachedEntity.skipAttackInteraction inline crates/horizon-client/src/game/cushions.rs::Game::attack_cushion the mayInteract gate and the break; frames and paintings keep their own copies
|
||||
BlockAttachedEntity.kill inline crates/horizon-client/src/game/cushions.rs::Game::break_cushion removal and the break; no game event
|
||||
BlockAttachedEntity.kill port crates/horizon-client/src/game/sculk.rs::Game::entity_killed_event the ENTITY_DIE event, by the attacker or the entity; break_cushion, break_painting and the frame paths remove it
|
||||
BlockAttachedEntity.ignoreExplosion port crates/horizon-client/src/game/explosion.rs::blast_spares_decorations a source in water spares paintings, item frames, cushions and armour stands, and so does a blast that keeps blocks without mob griefing; read when the blast picks its entities
|
||||
BlockAttachedEntity.move missing minor: a piston push does not break a cushion, painting or frame here
|
||||
BlockAttachedEntity.push missing minor: a push from a blast or piston does not break a cushion, painting or frame
|
||||
|
||||
|
Reference in new issue
Block a user