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:
Ludovic Marie authored and GitHub committed 2026-11-05 05:29:09 +01:00
commit 61afe4fa8e
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());
+182
View File
@@ -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()),
+21 -2
View File
@@ -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())