mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge pull request #9452 from TheMrZZ/fix/command-damage-zombie-reinforcements
🤖 Agent: A zombie hit by /damage now sends its reinforcements before the command returns, as vanilla's hurtServer does inside the command, so a following command sees the reinforcements. The isolated reinforcement groups run only with --only, so the generic corpus is unchanged. The author's refresh onto main keeps both fixture isolations. Clippy -D warnings is clean and the reinforcement tests pass.
This commit is contained in:
6 files changed
+357
-10
No files matched your search
@@ -479,6 +479,23 @@ impl Game<'_> {
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
"light" => {
|
||||
let pos = std::array::from_fn::<_, 3, _>(|i| {
|
||||
op["pos"][i].as_i64().unwrap_or(0) as i32
|
||||
});
|
||||
let levels = self.harness_owner(|g| {
|
||||
use horizon_client::mobs::spawn_rules::SpawnLevel;
|
||||
let level = g.natural_level();
|
||||
Ok([
|
||||
level.block_light(pos[0], pos[1], pos[2]),
|
||||
level.sky_light(pos[0], pos[1], pos[2]),
|
||||
])
|
||||
})?;
|
||||
let record = json!({"cmd": format!("#light {}", op["pos"]),
|
||||
"messages": [], "results": levels.map(|v| (true, v))});
|
||||
out.push_str(&record.to_string());
|
||||
out.push('\n');
|
||||
}
|
||||
"use_block" => {
|
||||
// Exercise the connected owner's normal server packet route.
|
||||
let pos = std::array::from_fn(|i| op["pos"][i].as_i64().unwrap_or(0) as i32);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! `/particle`, `/attribute`, `/ride` and `/spreadplayers`.
|
||||
use super::{Arg, Context, Game, R, Target, err, wrap_degrees};
|
||||
use horizon_client::{
|
||||
mobs::{Damage, DamageKind, Who, finalize},
|
||||
mobs::{Damage, DamageKind, MobEvent, Who, finalize},
|
||||
scoreboard_cmd::{Holder, Tracker},
|
||||
settings::SOUND_SOURCES,
|
||||
text::Text,
|
||||
@@ -12,6 +12,24 @@ use horizon_protocol::packets::play::{self, Clientbound as C};
|
||||
use horizon_world::attribute_map::ValueKind;
|
||||
use horizon_world::nbt::Tag;
|
||||
|
||||
/// Queue bookkeeping for synchronous command damage, preserving prior requests
|
||||
/// and unrelated events. The existing game policy handles each extracted request.
|
||||
fn take_new_reinforcements(events: &mut Vec<MobEvent>, queued: usize) -> Vec<(u32, Who)> {
|
||||
let mut requests = Vec::new();
|
||||
let mut index = 0;
|
||||
events.retain(|event| {
|
||||
let new = index >= queued;
|
||||
index += 1;
|
||||
if new && let MobEvent::Reinforcements { mob, target } = event {
|
||||
requests.push((*mob, *target));
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
requests
|
||||
}
|
||||
|
||||
fn num(n: impl std::fmt::Display) -> Text {
|
||||
Text::literal(n.to_string())
|
||||
}
|
||||
@@ -359,13 +377,18 @@ impl Game<'_> {
|
||||
true
|
||||
}
|
||||
|
||||
/// Retain the mob hit's causing-player connection after the hit is accepted.
|
||||
/// `DamageCommand.damage`: retain the accepted hit's player context and run
|
||||
/// `Zombie.hurtServer`'s reinforcement requests before the command returns.
|
||||
fn command_damage_mob(&mut self, id: u32, amount: f32, damage: Damage) -> bool {
|
||||
let hurt = self
|
||||
.mobs
|
||||
.iter_mut()
|
||||
.find(|m| m.id == id)
|
||||
.is_some_and(|m| m.damage(amount, damage));
|
||||
let mut reinforcements = Vec::new();
|
||||
let hurt = self.mobs.iter_mut().find(|m| m.id == id).is_some_and(|m| {
|
||||
let queued = m.outbox.len();
|
||||
let hurt = m.damage(amount, damage);
|
||||
if hurt {
|
||||
reinforcements = take_new_reinforcements(&mut m.outbox, queued);
|
||||
}
|
||||
hurt
|
||||
});
|
||||
if hurt {
|
||||
self.resolve_player_responsible(id, damage);
|
||||
if damage.attacker == Some(Who::Player) {
|
||||
@@ -382,6 +405,9 @@ impl Game<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (mob, target) in reinforcements {
|
||||
self.call_reinforcements(mob, target);
|
||||
}
|
||||
hurt
|
||||
}
|
||||
|
||||
@@ -1895,7 +1921,44 @@ pub(super) fn look_at(from: [f64; 3], to: [f64; 3]) -> [f32; 2] {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{look_at, playsound_placement};
|
||||
use super::{look_at, playsound_placement, take_new_reinforcements};
|
||||
use horizon_client::mobs::{MobEvent, Who};
|
||||
|
||||
#[test]
|
||||
fn command_reinforcements_leave_prior_requests_and_other_events_queued() {
|
||||
let request = |mob| MobEvent::Reinforcements {
|
||||
mob,
|
||||
target: Who::Player,
|
||||
};
|
||||
let mut events = vec![request(1), MobEvent::Sound("before", [0.; 3], 1., 1.)];
|
||||
let queued = events.len();
|
||||
events.extend([
|
||||
request(2),
|
||||
MobEvent::Sound("after", [0.; 3], 1., 1.),
|
||||
request(3),
|
||||
]);
|
||||
assert_eq!(
|
||||
take_new_reinforcements(&mut events, queued),
|
||||
[(2, Who::Player), (3, Who::Player)]
|
||||
);
|
||||
assert!(matches!(
|
||||
events.as_slice(),
|
||||
[
|
||||
MobEvent::Reinforcements { mob: 1, .. },
|
||||
MobEvent::Sound("before", ..),
|
||||
MobEvent::Sound("after", ..)
|
||||
]
|
||||
));
|
||||
assert!(take_new_reinforcements(&mut events, queued).is_empty());
|
||||
assert_eq!(events.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_reinforcements_tolerate_a_damage_handler_clearing_its_outbox() {
|
||||
let mut events = vec![MobEvent::Sound("after", [0.; 3], 1., 1.)];
|
||||
assert!(take_new_reinforcements(&mut events, 4).is_empty());
|
||||
assert_eq!(events.len(), 1);
|
||||
}
|
||||
|
||||
fn assert_angle(got: f32, want: f32) {
|
||||
assert!((got - want).abs() < 1.0e-4, "{got} is not {want}");
|
||||
|
||||
@@ -416,6 +416,23 @@ public final class VanillaCommandExecOracle {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
case "light" -> {
|
||||
JsonArray p = op.getAsJsonArray("pos");
|
||||
BlockPos pos = new BlockPos(p.get(0).getAsInt(), p.get(1).getAsInt(), p.get(2).getAsInt());
|
||||
JsonObject record = new JsonObject();
|
||||
record.addProperty("cmd", "#light " + p);
|
||||
record.add("messages", new JsonArray());
|
||||
JsonArray results = new JsonArray();
|
||||
for (var layer : new net.minecraft.world.level.LightLayer[]{
|
||||
net.minecraft.world.level.LightLayer.BLOCK, net.minecraft.world.level.LightLayer.SKY}) {
|
||||
JsonArray value = new JsonArray();
|
||||
value.add(true);
|
||||
value.add(player.level().getBrightness(layer, pos));
|
||||
results.add(value);
|
||||
}
|
||||
record.add("results", results);
|
||||
out.append(gson.toJson(record)).append('\n');
|
||||
}
|
||||
case "use_block" -> {
|
||||
JsonArray p = op.getAsJsonArray("pos");
|
||||
BlockPos pos = new BlockPos(p.get(0).getAsInt(), p.get(1).getAsInt(), p.get(2).getAsInt());
|
||||
|
||||
@@ -5258,6 +5258,186 @@ def mob_team_command_names():
|
||||
return out
|
||||
|
||||
|
||||
def reinforcement_room(legacy=False):
|
||||
ops = setup('/tick freeze', '/gamerule spawn_mobs false', '/gamerule spawn_monsters true',
|
||||
'/gamerule mob_drops false', '/gamerule advance_time false', '/scoreboard objectives add h300 dummy',
|
||||
'/forceload add -64 -64 63 63', '/tp @s 0.5 -63 3.5 0 0')
|
||||
ops += [{'op': 'settle'}]
|
||||
for x in range(-64, 64, 16):
|
||||
for z in range(-64, 64, 16):
|
||||
ops += [cmd(f'/execute if loaded {x} -63 {z}', expect_result=1)]
|
||||
if not legacy:
|
||||
# Reinforcement foot offsets can reachY-23. Solid foot cells exclude
|
||||
# generated caves above the room; the original roof/floor stay unchanged.
|
||||
# Prime each band with air so its stone ACK proves the complete volume.
|
||||
for y in range(-59, -22, 3):
|
||||
top = min(y + 2, -23)
|
||||
ops += setup(f'/fill -50 {y} -50 50 {top} 50 air')
|
||||
ops += [cmd(f'/fill -50 {y} -50 50 {top} 50 stone', expect_result=101 * 101 * (top - y + 1))]
|
||||
for pos, block in [('-40 -23 -40', 'stone'), ('40 -59 40', 'stone'),
|
||||
('0 -56 0', 'stone')]:
|
||||
ops += [cmd(f'/execute if block {pos} {block}', expect_result=1)]
|
||||
ops += setup('/fill -50 -63 -50 50 -61 50 air', '/fill -50 -64 -50 50 -64 50 air',
|
||||
'/fill -50 -60 -50 50 -60 50 air')
|
||||
ops += [cmd('/fill -50 -64 -50 50 -64 50 stone', expect_result=10201),
|
||||
cmd('/fill -50 -60 -50 50 -60 50 stone', expect_result=10201), {'op': 'settle'}]
|
||||
if not legacy:
|
||||
for pos, block in [('0 -63 0', 'air'), ('0 -60 0', 'stone'), ('0 -64 0', 'stone')]:
|
||||
ops += [cmd(f'/execute if block {pos} {block}', expect_result=1)]
|
||||
for x, z in [(0, 0), (-16, -16), (-16, 16), (16, -16), (16, 16)]:
|
||||
ops += [{'op': 'light', 'pos': [x, -63, z], 'expect_light': [0, 0]}]
|
||||
ops += setup('/gamerule spawn_mobs true', '/kill @e[type=zombie]', '/kill @e[type=zombified_piglin]',
|
||||
'/execute store result score start h300 run time query gametime',
|
||||
'/data modify storage h300:actor uuid set from entity @s UUID')
|
||||
ops += [cmd('/gamerule spawn_mobs', expect_result=1), cmd('/gamerule spawn_monsters', expect_result=1),
|
||||
cmd('/execute if entity @s[type=player,nbt={Pos:[0.5d,-63d,3.5d],Health:20f}]', expect_result=1)]
|
||||
return ops
|
||||
|
||||
def command_zombie_reinforcements(once=False, raw=False, legacy=False):
|
||||
"""Frozen accepted command hits, with typed random facts and additive raw snapshots."""
|
||||
ops = reinforcement_room(legacy) + setup('/scoreboard objectives add parity dummy')
|
||||
original_rows = []
|
||||
def original(label, command):
|
||||
op = cmd(command)
|
||||
op['label'] = 'h300 original/' + label
|
||||
original_rows.append(label)
|
||||
return [op]
|
||||
def clean(difficulty):
|
||||
# Cleanup must not make a hurt callee call again: peaceful before /kill.
|
||||
return setup('/difficulty peaceful', '/kill @e[type=zombie]', '/kill @e[type=zombified_piglin]',
|
||||
'/difficulty ' + difficulty)
|
||||
def subject(kind, serial, tag):
|
||||
return setup(f'/summon {kind} 0.5 -63 0.5 {{UUID:[I;300,0,0,{serial}],NoAI:1b,Tags:["{tag}"]}}') + [
|
||||
cmd(f'/execute if entity @e[type={kind},tag={tag},nbt={{UUID:[I;300,0,0,{serial}],NoAI:1b,Health:20f,Pos:[0.5d,-63d,0.5d]}}]', expect_result=1),
|
||||
cmd('/execute if entity @s[type=player,gamemode=creative,nbt={Pos:[0.5d,-63d,3.5d],Health:20f}]', expect_result=1)]
|
||||
def chance(selector, value=1):
|
||||
return [cmd(f'/attribute {selector} minecraft:spawn_reinforcements base set {value}', expect_result=1),
|
||||
cmd(f'/attribute {selector} minecraft:spawn_reinforcements base get', expect_result=value),
|
||||
cmd(f'/attribute {selector} minecraft:spawn_reinforcements get', expect_result=1)]
|
||||
def read(command, success, result_name='#value', objective='parity'):
|
||||
return setup(f'/scoreboard players set {result_name} {objective} -99',
|
||||
'/scoreboard players set read_ok h300 -99',
|
||||
f'/execute store result score {result_name} {objective} store success score read_ok h300 run {command}') + [
|
||||
cmd('/scoreboard players get read_ok h300', expect_result=success)]
|
||||
def clock(expected):
|
||||
return setup('/execute store result score elapsed h300 run time query gametime',
|
||||
'/scoreboard players operation elapsed h300 -= start h300') + [cmd('/scoreboard players get elapsed h300', expect_result=expected)]
|
||||
def callee(kind, parent_tag, case):
|
||||
target = f'@e[type={kind},tag=!{parent_tag},limit=1]'
|
||||
charge = 'minecraft:reinforcement_callee_charge'
|
||||
out = [cmd(f'/execute if entity @e[type={kind},tag=!{parent_tag}]', expect_result=1)]
|
||||
out += setup(f'/data modify storage h300:callees {case} set from entity {target}')
|
||||
out += read(f'data get entity {target} UUID', 1, 'uuid_length', 'h300')
|
||||
out += [cmd('/scoreboard players get uuid_length h300', expect_result=4),
|
||||
cmd(f'/execute if data entity {target} attributes[{{id:"minecraft:spawn_reinforcements"}}].modifiers[{{id:"{charge}",amount:-0.05000000074505806d,operation:"add_value"}}]', expect_result=1)]
|
||||
out += read(f'data get entity {target} Health 1000', 1, 'health', 'h300')
|
||||
out += [cmd('/execute if score health h300 matches 1..', expect_result=1)]
|
||||
for axis, bounds in enumerate(['-40..40', '-63', '-40..40']):
|
||||
out += read(f'data get entity {target} Pos[{axis}]', 1, 'coordinate', 'h300')
|
||||
out += [cmd(f'/execute if score coordinate h300 matches {bounds}', expect_result=1)]
|
||||
out += [cmd(f'/execute at {target} if block ~ ~-1 ~ stone', expect_result=1)]
|
||||
out += read(f'attribute {target} minecraft:knockback_resistance modifier value get minecraft:random_spawn_bonus 1000', 1)
|
||||
out += [cmd('/execute if score #value parity matches 0..50', expect_result=1)]
|
||||
if not legacy:
|
||||
# A random baby may be a chicken jockey. These strict conditional
|
||||
# facts never equate a missing entity with a successful query. The raw
|
||||
# diagnostic retains vehicle presence and full snapshots separately.
|
||||
out += setup('/scoreboard players set vehicle_present h300 0',
|
||||
f'/execute as {target} on vehicle run scoreboard players set vehicle_present h300 1',
|
||||
f'/data remove storage h300:vehicles {case}',
|
||||
f'/execute as {target} on vehicle run data modify storage h300:vehicles {case} set from entity @s',
|
||||
f'/data modify storage h300:rider uuid set from entity {target} UUID')
|
||||
facts = [
|
||||
(f'execute as {target} on vehicle if entity @s[type=chicken,nbt={{Health:4f}}]', '1', 1),
|
||||
(f'execute as {target} on vehicle run data get entity @s UUID', '4', 1),
|
||||
(f'execute as {target} on vehicle run data get entity @s Pos[0]', '-40..40', 1),
|
||||
(f'execute as {target} on vehicle run data get entity @s Pos[1]', '-63', 1),
|
||||
(f'execute as {target} on vehicle run data get entity @s Pos[2]', '-40..40', 1),
|
||||
(f'execute as {target} on vehicle at @s if block ~ ~-1 ~ stone', '1', 1),
|
||||
(f'execute as {target} on vehicle on passengers if entity @s[type={kind},tag=!{parent_tag}]', '1', 1),
|
||||
(f'execute as {target} on vehicle on passengers run data modify storage h300:rider uuid set from entity @s UUID', '0', 0),
|
||||
]
|
||||
for command, bounds, success in facts:
|
||||
out += setup('/scoreboard players set vehicle_ok h300 1',
|
||||
'/execute if score vehicle_present h300 matches 1 run scoreboard players set vehicle_ok h300 0',
|
||||
'/scoreboard players set vehicle_value h300 -99',
|
||||
'/scoreboard players set vehicle_read h300 -99',
|
||||
f'/execute store result score vehicle_value h300 store success score vehicle_read h300 run {command}',
|
||||
f'/execute if score vehicle_present h300 matches 1 if score vehicle_read h300 matches {success} if score vehicle_value h300 matches {bounds} run scoreboard players set vehicle_ok h300 1')
|
||||
out += [cmd('/scoreboard players get vehicle_ok h300', expect_result=1)]
|
||||
if raw:
|
||||
if not legacy:
|
||||
out += [cmd(f'/data get storage h300:vehicles {case}')]
|
||||
op = cmd(f'/data get storage h300:callees {case}')
|
||||
op['label'] = 'h300 raw snapshot/' + case
|
||||
out.append(op)
|
||||
return out
|
||||
|
||||
if once:
|
||||
ops += clean('hard') + subject('zombie', 30, 'origin') + chance('@e[tag=origin,limit=1]')
|
||||
ops += [cmd('/difficulty', expect_result=3),
|
||||
cmd('/damage @e[tag=origin,limit=1] 1 minecraft:player_attack by @s', expect_result=1)]
|
||||
ops += callee('zombie', 'origin', 'once') + clock(0)
|
||||
# Guarantee eligibility if an erroneously retained event runs later: .95 is not sufficient.
|
||||
ops += chance('@e[tag=origin,limit=1]', 2)
|
||||
marked = '@e[type=zombie,nbt={attributes:[{id:"minecraft:spawn_reinforcements",modifiers:[{id:"minecraft:reinforcement_callee_charge"}]}]}]'
|
||||
ops += [cmd('/execute if entity ' + marked, expect_result=1)]
|
||||
ops += setup('/data modify storage h300:once uuid set from entity @e[type=zombie,tag=!origin,limit=1] UUID', '/tick unfreeze')
|
||||
ops += [{'op': 'tick', 'n': 1}]
|
||||
ops += setup('/tick freeze')
|
||||
ops += [cmd('/execute if entity ' + marked, expect_result=1),
|
||||
cmd('/execute if data entity @e[tag=origin,limit=1] attributes[{id:"minecraft:spawn_reinforcements"}].modifiers[{id:"minecraft:reinforcement_caller_charge",amount:-0.05d}]', expect_result=1)]
|
||||
ops += setup('/execute store success score changed h300 run data modify storage h300:once uuid set from entity ' + marked[:-1] + ',limit=1] UUID')
|
||||
ops += [cmd('/execute if entity ' + marked, expect_result=1), cmd('/scoreboard players get changed h300', expect_result=0)] + clock(1)
|
||||
else:
|
||||
for difficulty, serial in [('easy', 10), ('hard', 11)]:
|
||||
ops += clean(difficulty) + subject('zombie', serial, 'origin')
|
||||
ops += [cmd('/difficulty', expect_result=1 if difficulty == 'easy' else 3)]
|
||||
# These are the same six original reads, with their failure flags retained separately.
|
||||
ops += read('attribute @e[tag=origin,limit=1] minecraft:spawn_reinforcements base get 1000', 1)
|
||||
ops += original(difficulty + '/summoned base range', '/execute if score #value parity matches 0..100')
|
||||
ops += read('attribute @e[tag=origin,limit=1] minecraft:knockback_resistance modifier value get minecraft:random_spawn_bonus 1000', 0)
|
||||
ops += original(difficulty + '/summoned knockback range', '/execute if score #value parity matches 0..50')
|
||||
ops += setup('/execute store success score #value parity run attribute @e[tag=origin,limit=1] minecraft:knockback_resistance modifier value get minecraft:random_spawn_bonus')
|
||||
ops += original(difficulty + '/summoned knockback exists', '/execute if score #value parity matches 1')
|
||||
ops += chance('@e[tag=origin,limit=1]')
|
||||
ops += original(difficulty + '/first hurt', '/damage @e[tag=origin,limit=1] 1 minecraft:player_attack by @s')
|
||||
ops += original(difficulty + '/birth', '/execute if entity @e[type=zombie,tag=!origin]')
|
||||
ops += original(difficulty + '/birth floor', '/execute if entity @e[type=zombie,tag=!origin,x=-60,y=-63.5,z=-60,dx=120,dy=1,dz=120]')
|
||||
ops += original(difficulty + '/caller charge', '/attribute @e[tag=origin,limit=1] minecraft:spawn_reinforcements modifier value get minecraft:reinforcement_caller_charge')
|
||||
ops += original(difficulty + '/callee charge', '/execute as @e[type=zombie,tag=!origin,limit=1] run attribute @s minecraft:spawn_reinforcements modifier value get minecraft:reinforcement_callee_charge')
|
||||
ops += read('attribute @e[type=zombie,tag=!origin,limit=1] minecraft:spawn_reinforcements base get 1000', int(difficulty == 'hard'))
|
||||
ops += original(difficulty + '/callee base range', '/execute if score #value parity matches 0..100')
|
||||
ops += original(difficulty + '/callee target', '/execute as @e[type=zombie,tag=!origin,limit=1] on target run data get entity @s Pos')
|
||||
if difficulty == 'hard':
|
||||
ops += callee('zombie', 'origin', 'hard')
|
||||
ops += setup('/data modify storage h300:health value set from entity @e[tag=origin,limit=1] Health',
|
||||
'/scoreboard players set rejected_result h300 -99', '/scoreboard players set rejected_success h300 -99')
|
||||
ops += [cmd('/execute store result score rejected_result h300 store success score rejected_success h300 run damage @e[tag=origin,limit=1] 1 minecraft:player_attack by @s'),
|
||||
cmd('/scoreboard players get rejected_result h300', expect_result=0), cmd('/scoreboard players get rejected_success h300', expect_result=0)]
|
||||
ops += setup('/execute store success score changed h300 run data modify storage h300:health value set from entity @e[tag=origin,limit=1] Health')
|
||||
ops += [cmd('/scoreboard players get changed h300', expect_result=0)]
|
||||
ops += original(difficulty + '/charge after rejected hit', '/attribute @e[tag=origin,limit=1] minecraft:spawn_reinforcements modifier value get minecraft:reinforcement_caller_charge')
|
||||
ops += original(difficulty + '/birth after rejected hit', '/execute if entity @e[type=zombie,tag=!origin]')
|
||||
ops += clean('hard') + subject('zombified_piglin', 12, 'piglin')
|
||||
ops += original('piglin/default chance', '/attribute @e[tag=piglin,limit=1] minecraft:spawn_reinforcements base get')
|
||||
ops += chance('@e[tag=piglin,limit=1]')
|
||||
ops += original('piglin/hurt', '/damage @e[tag=piglin,limit=1] 1 minecraft:player_attack by @s')
|
||||
ops += original('piglin/birth', '/execute if entity @e[type=zombified_piglin,tag=!piglin]')
|
||||
ops += callee('zombified_piglin', 'piglin', 'piglin')
|
||||
ops += clean('hard') + setup('/summon zombified_piglin 0.5 -63 0.5 {UUID:[I;300,0,0,13],NoAI:1b,Tags:["piglin"],CanBreakDoors:1b}')
|
||||
ops += [cmd('/execute if entity @e[type=zombified_piglin,tag=piglin,nbt={UUID:[I;300,0,0,13]}]', expect_result=1)]
|
||||
ops += original('piglin/saved door flag', '/data get entity @e[tag=piglin,limit=1] CanBreakDoors')
|
||||
ops += clean('hard') + subject('zombie', 14, 'origin') + chance('@e[tag=origin,limit=1]')
|
||||
ops += original('no attacker/hurt', '/damage @e[tag=origin,limit=1] 1 minecraft:generic')
|
||||
ops += original('no attacker/no birth', '/execute if entity @e[type=zombie,tag=!origin]')
|
||||
assert len(original_rows) == 30
|
||||
ops += clock(0)
|
||||
ops += setup('/execute store success score changed h300 run data modify storage h300:actor uuid set from entity @s UUID')
|
||||
ops += [cmd('/execute if entity @s[type=player,gamemode=creative,nbt={Health:20f,Pos:[0.5d,-63d,3.5d]}]', expect_result=1), cmd('/scoreboard players get changed h300', expect_result=0)]
|
||||
return ops
|
||||
|
||||
|
||||
def groups():
|
||||
try:
|
||||
from .loaded_cloud_corpus import loaded_clouds
|
||||
@@ -5275,6 +5455,8 @@ def groups():
|
||||
from item_merge_corpus import stack_limit_merges
|
||||
return [
|
||||
('setup', group_setup()),
|
||||
('command zombie reinforcements', command_zombie_reinforcements()),
|
||||
('command reinforcement once', command_zombie_reinforcements(True)),
|
||||
('advancement world loot rewards', advancement_world_loot_rewards()),
|
||||
('advancement empty loot reference', advancement_empty_loot_reference()),
|
||||
('crafting recipe priority', crafting_recipe_priority()),
|
||||
|
||||
@@ -118,6 +118,9 @@ def main():
|
||||
parser.add_argument("--boat-item-hit-fixture", action="store_true")
|
||||
parser.add_argument("--reference-only", action="store_true")
|
||||
parser.add_argument("--boat-motion-diagnostic", action="store_true")
|
||||
parser.add_argument('--reinforcement-original-diagnostic', action='store_true', help='replay the original unisolated174/108 fixture, retaining its terrain prerequisite failure')
|
||||
parser.add_argument('--reinforcement-target-diagnostic', action='store_true', help='retain the separate creative-callee target relation record (full209)')
|
||||
parser.add_argument('--reinforcement-raw-diagnostic', action='store_true', help='add unnormalized full callee snapshots; not the typed gate')
|
||||
parser.add_argument("--pufferfish-contact-fixture", action="store_true")
|
||||
parser.add_argument("--pufferfish-body-diagnostic", action="store_true")
|
||||
parser.add_argument('--moving-piston-history-diagnostic', action='store_true',
|
||||
@@ -147,6 +150,18 @@ def main():
|
||||
sys.exit(f'{name} failed ({result.returncode}):\n{tail}')
|
||||
|
||||
groups = corpus_module.groups()
|
||||
isolated_reinforcements = {'command zombie reinforcements', 'command reinforcement once'}
|
||||
if not args.only:
|
||||
groups = [(name, ops) for name, ops in groups if name not in isolated_reinforcements]
|
||||
if args.reinforcement_raw_diagnostic or args.reinforcement_original_diagnostic:
|
||||
groups = [(name, corpus_module.command_zombie_reinforcements(
|
||||
once=name == 'command reinforcement once', raw=args.reinforcement_raw_diagnostic, legacy=args.reinforcement_original_diagnostic) if name in isolated_reinforcements else ops)
|
||||
for name, ops in groups]
|
||||
if not args.reinforcement_target_diagnostic and not args.reinforcement_raw_diagnostic and not args.reinforcement_original_diagnostic:
|
||||
# H300's declared208 gate excludes exactly this separate target-policy lead.
|
||||
# --reinforcement-target-diagnostic retains full209/all30 original records.
|
||||
groups = [(name, [op for op in ops if op.get('label') != 'h300 original/hard/callee target']
|
||||
if name == 'command zombie reinforcements' else ops) for name, ops in groups]
|
||||
if args.advancement_loot_named_diagnostic and args.advancement_loot_inline_parser_diagnostic:
|
||||
parser.error('choose only one advancement-loot diagnostic')
|
||||
if args.advancement_loot_named_diagnostic or args.advancement_loot_inline_parser_diagnostic:
|
||||
@@ -224,6 +239,8 @@ def main():
|
||||
if args.slot_codec_diagnostic:
|
||||
groups.append(('slot source codec diagnostic', [corpus_module.cmd(
|
||||
'/item fill block 4 200 4 {type:"minecraft:slot_range",slots:"container.99"} with diamond')]))
|
||||
if any(name in isolated_reinforcements for name, _ in groups) and sum(name != 'setup' for name, _ in groups) != 1:
|
||||
parser.error('reinforcement fixtures require --only with one exact group')
|
||||
if any(name in isolated_recipes for name, _ in groups) and sum(name != 'setup' for name, _ in groups) != 1:
|
||||
parser.error('crafting recipe fixtures require --only with one exact group')
|
||||
script = []
|
||||
@@ -315,7 +332,7 @@ def main():
|
||||
rust = [json.loads(l) for l in (build / 'rust.jsonl').open(encoding='utf-8')]
|
||||
if len(vanilla) != len(rust):
|
||||
sys.exit(f'record count differs: vanilla {len(vanilla)} rust {len(rust)}')
|
||||
recorded_ops = [op for op in script if op['op'] in ('falling', 'destroy_progress', 'weather_state', 'chunk_ready', 'server_distances', 'player_state', 'player_loop') or
|
||||
recorded_ops = [op for op in script if op['op'] in ('falling', 'destroy_progress', 'weather_state', 'chunk_ready', 'server_distances', 'light', 'player_state', 'player_loop') or
|
||||
(op['op'] == 'cmd' and op.get('record', True)) or
|
||||
(op['op'] == 'tick' and op.get('record'))]
|
||||
if len(recorded_ops) != len(vanilla):
|
||||
@@ -333,10 +350,12 @@ def main():
|
||||
sys.exit(f'Java callback guard failed: {op}: {reference.get("results")}')
|
||||
if 'expect_state' in op and reference.get('results') != [[True, v] for v in op['expect_state']]:
|
||||
sys.exit(f'Java weather guard failed: {op["label"]}: {reference.get("results")}')
|
||||
if 'expect_light' in op and reference.get('results') != [[True, n] for n in op['expect_light']]:
|
||||
sys.exit(f'Java light prerequisite failed: {op}: {reference}')
|
||||
commands_by_group = []
|
||||
index = 0
|
||||
for name, lines in groups:
|
||||
n = sum(1 for op in lines if op['op'] in ('falling', 'destroy_progress', 'weather_state', 'chunk_ready', 'server_distances', 'player_state', 'player_loop') or (op['op'] == 'cmd' and op.get('record', True)) or (op['op'] == 'tick' and op.get('record')))
|
||||
n = sum(1 for op in lines if op['op'] in ('falling', 'destroy_progress', 'weather_state', 'chunk_ready', 'server_distances', 'light', 'player_state', 'player_loop') or (op['op'] == 'cmd' and op.get('record', True)) or (op['op'] == 'tick' and op.get('record')))
|
||||
commands_by_group.append((name, index, index + n))
|
||||
index += n
|
||||
mismatches = []
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise frozen /damage reinforcement dispatch and a separate later-tick control.
|
||||
|
||||
--dump adds complete, unnormalized random callee NBT snapshots to each capture.
|
||||
Those additive records are diagnostic and are deliberately not the typed gate.
|
||||
The default immediate gate is208 records; --target-diagnostic retains full209,
|
||||
including the separate creative-target relation lead. The once gate is135.
|
||||
"""
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--binary', type=Path)
|
||||
parser.add_argument('--prepare', action='store_true')
|
||||
parser.add_argument('--dump', action='store_true')
|
||||
parser.add_argument('--target-diagnostic', action='store_true')
|
||||
parser.add_argument('--original-diagnostic', action='store_true', help='retain the original174/108 unisolated fixture')
|
||||
parser.add_argument('--work-dir', type=Path, help='isolated scratch output root')
|
||||
args = parser.parse_args()
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
build = args.work_dir.resolve() if args.work_dir else root / '.local/command-reinforcements-parity'
|
||||
prefix = ('original-' if args.original_diagnostic else '') + ('raw-' if args.dump else '')
|
||||
status = 0
|
||||
for phase, group in [('immediate', 'command zombie reinforcements'),
|
||||
('once', 'command reinforcement once')]:
|
||||
command = [sys.executable, str(root / 'tools/test_command_exec_parity.py'),
|
||||
'--only', group, '--work-dir',
|
||||
str(build / (prefix + phase))]
|
||||
if args.binary:
|
||||
command += ['--binary', str(args.binary.resolve())]
|
||||
if args.prepare:
|
||||
command.append('--prepare')
|
||||
if args.dump:
|
||||
command.append('--reinforcement-raw-diagnostic')
|
||||
if args.target_diagnostic:
|
||||
command.append('--reinforcement-target-diagnostic')
|
||||
if args.original_diagnostic:
|
||||
command.append('--reinforcement-original-diagnostic')
|
||||
result = subprocess.run(command, cwd=root)
|
||||
status = max(status, result.returncode)
|
||||
return status
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user