Load animal age and love with NumericTag integer conversion

Co-Authored-By: Codex <[email protected]>
This commit is contained in:
Florian ERNSTandCodex committed 2026-11-06 02:04:52 +01:00
1 parent e5875b0b19
commit 97d414cc4d
6 files changed
+373 -6

No files matched your search

+1 -1
View File
@@ -81,7 +81,7 @@ fn elements(tag: &Tag) -> Option<Vec<Tag>> {
}
/// `DoubleTag.intValue` / `FloatTag.intValue` floor before the i32 conversion;
/// integral tags use their intValue narrowing. Byte arrays then keep the low byte.
fn int_value(t: &Tag) -> Option<i32> {
pub fn int_value(t: &Tag) -> Option<i32> {
Some(match t {
Tag::Byte(v) => i32::from(*v),
Tag::Short(v) => i32::from(*v),
+79 -3
View File
@@ -8,6 +8,7 @@ use crate::{
arrows::Pickup,
entities::{ArmorStand, Cushion, ExperienceOrb, ItemEntity, ItemFrame, Painting},
mobs::{Mob, MobType, Who},
nbt_path::int_value,
projectiles::{Projectile, ProjectileKind},
save::{
doubles, float, floats, item_from_nbt, item_to_nbt, number, put, read_doubles, text,
@@ -1689,12 +1690,12 @@ pub fn mob_from_nbt(
m.extra.drop_chances = Some(Box::new(chances));
}
if saves_age(m.species()) {
m.extra.age = number(tag.get("Age")).unwrap_or(0) as i32;
m.extra.forced_age = number(tag.get("ForcedAge")).unwrap_or(0) as i32;
m.extra.age = tag.get("Age").and_then(int_value).unwrap_or(0);
m.extra.forced_age = tag.get("ForcedAge").and_then(int_value).unwrap_or(0);
m.extra.age_locked = flag(tag, "AgeLocked");
}
if is_animal(&m.kind.id) {
m.extra.in_love = number(tag.get("InLove")).unwrap_or(0) as i32;
m.extra.in_love = tag.get("InLove").and_then(int_value).unwrap_or(0);
// Animal.readAdditionalSaveData: the player who fed it into love.
m.extra.love_cause = crate::save::uuid_from_tag(tag.get("LoveCause"));
}
@@ -4309,6 +4310,81 @@ mod tests {
let cleared = item_entity_to_nbt(&cleared, [1, 2, 3, 4], Some(&tag));
assert!(cleared.get("Thrower").is_none());
}
#[test]
fn animal_integer_loads_save_modeled_values_without_raw_source() {
let mut kind = crate::mobs::sensor_scan_tests::kind();
let def = Arc::make_mut(&mut kind);
def.id = "minecraft:pig".into();
def.species = crate::mobs::Species::Pig;
let types = [("minecraft:pig".into(), kind)].into();
for (input, expected) in [
(None, 0),
(Some(Tag::Int(0)), 0),
(Some(Tag::Byte(-7)), -7),
(Some(Tag::Short(13)), 13),
(Some(Tag::Int(i32::MIN)), i32::MIN),
(Some(Tag::Int(i32::MAX)), i32::MAX),
(Some(Tag::Long(2147483648)), i32::MIN),
(Some(Tag::Long(-2147483649)), i32::MAX),
(Some(Tag::Long(4294967296)), 0),
(Some(Tag::Long(i64::MAX)), -1),
(Some(Tag::Long(i64::MIN)), 0),
(Some(Tag::Float(2.5f32.to_bits())), 2),
(Some(Tag::Float((-2.5f32).to_bits())), -3),
(Some(Tag::Double(2.5f64.to_bits())), 2),
(Some(Tag::Double((-2.5f64).to_bits())), -3),
(Some(Tag::Float(2147483520f32.to_bits())), 2147483520),
(Some(Tag::Float(2147483648f32.to_bits())), i32::MAX),
(Some(Tag::Float((-2147483648f32).to_bits())), i32::MIN),
(Some(Tag::Float((-2147483904f32).to_bits())), i32::MIN),
(Some(Tag::Double(2147483647.75f64.to_bits())), i32::MAX),
(Some(Tag::Double(2147483648f64.to_bits())), i32::MAX),
(Some(Tag::Double((-2147483647.5f64).to_bits())), i32::MIN),
(Some(Tag::Double((-2147483648.5f64).to_bits())), i32::MIN),
(Some(Tag::Double(1e20f64.to_bits())), i32::MAX),
(Some(Tag::Double((-1e20f64).to_bits())), i32::MIN),
(Some(Tag::Float(1e20f32.to_bits())), i32::MAX),
(Some(Tag::Float((-1e20f32).to_bits())), i32::MIN),
] {
let mut raw = Tag::compound([
("id", string("minecraft:pig")),
("Pos", doubles(&[4.5, 200.0, 4.5])),
("NoAI", Tag::Byte(1)),
("NoGravity", Tag::Byte(1)),
("AgeLocked", Tag::Byte(1)),
("Unrelated", string("keep")),
]);
if let Some(value) = input.as_ref() {
for key in ["Age", "ForcedAge", "InLove"] {
put(&mut raw, key, value.clone());
}
}
let mob = mob_from_nbt(&raw, &types, 1, None).unwrap();
assert_eq!(
(mob.extra.age, mob.extra.forced_age, mob.extra.in_love),
(expected, expected, expected),
"{input:?}"
);
assert!(mob.no_ai && mob.no_gravity && mob.extra.age_locked);
for source in [None, Some(&raw)] {
let saved = mob_to_nbt(&mob, [0, 0, 0, 55], source, None);
for key in ["Age", "ForcedAge", "InLove"] {
assert_eq!(
saved.get(key),
Some(&Tag::Int(expected)),
"{key}: {input:?}"
);
}
assert_eq!(saved.get("NoAI"), Some(&Tag::Byte(1)));
assert_eq!(saved.get("AgeLocked"), Some(&Tag::Byte(1)));
assert_eq!(
saved.get("Unrelated"),
source.and_then(|tag| tag.get("Unrelated"))
);
}
}
}
#[test]
fn animal_nbt_preserves_signed_integer_in_love_and_defaults_after_removal() {
let mut kind = crate::mobs::sensor_scan_tests::kind();
+154
View File
@@ -0,0 +1,154 @@
# Animal integer NBT loads
Only `Age`, `ForcedAge` and `InLove` load conversion is changed. Pinned Java26.3
`AgeableMob.readAdditionalSaveData` and `Animal.readAdditionalSaveData` call
`ValueInput.getIntOr`. `TagValueInput` invokes `NumericTag.intValue`: Float/Double
use `Mth.floor` (Math.floor followed by JVM d2i), while Long narrows to32 bits.
Both finite overflow signs are measured; no guessed cast-then-decrement formula.
The existing `nbt_path::int_value` has precisely those semantics. Make that
unchanged helper available to the loader instead of going through an i64 view.
No global `number`/boolean helper, save clamp, NoAI, growth, breeding, ItemEntity
Health or other codec repair. Signed InLove saving is already independently fixed
by merged #7609; its saver remains untouched and negative controls stay strict.
Pending #10344 (ItemEntity Health) and #9991 (baby eligibility) are not imported.
## Frozen actual Game and fresh Java
Base: `e5875b0b192e64c2de121e30940d39bdd4c7728b`.
Existing authoritative command harness, no network observer or world steps.
27 fresh fixed-UUID NoAI/NoGravity pigs cover absent/default, Byte/Short/Int,
Long narrowing, Float/Double positive/negative fractions, adjacent int bounds and
large finite overflow. Actual UUID/type/tag/Health10/Pos/flags, owner identity,
loaded stone/air geometry and relative world-clock0 positively admit every case.
Age is allowed to be negative. Every field read resets result/success to-99 first.
Fresh Java independently verifies all expected callbacks and result/success stores.
All three fields save as Int, including defaults and negative values.
Predeclared strict **666** complete records; **695** executable full diagnostic.
Only the27 whole-body rows and clock-start/end are diagnostic; all commands remain
raw traced. Scalar messages/styles/field types/callbacks/stores and every setup
control remain strict. No masks/tolerances or after-capture selection changes.
```
python3 tools/test_ageable_integer_load_parity.py --binary /path/to/old-game --work-dir /scratch/old
python3 tools/test_ageable_integer_load_parity.py --binary /path/to/fixed-game --work-dir /scratch/fixed --reference /scratch/old/vanilla.jsonl
python3 tools/test_ageable_integer_load_parity.py --full-diagnostic --binary /path/to/game --work-dir /scratch/full
```
Emitted core script SHA256: `e5a6bcb1cae0853480fd72ce9675bdd0123d3d629c9d38af40f229d9c969d08d`.
Emitted full script SHA256: `f7fe07b37956beb8bf22143d50dd76e26961359df625c3bf8dc59ce130cbefd9`.
Old current: **606/666**. Exactly60 differences: scalar read and result-store for
these ten cases across EACH of Age/ForcedAge/InLove. Every success remains true/1;
these are numeric state failures, not parse/callback-admission failures. All other
controls pass, including signed integer InLove, Long narrowing and default types.
Full old diagnostic: **606/695**, retaining all27 bodies and raw absolute clocks.
| Exact case prefix | Java saved Int (each field) | Old native saved Int |
|---|---|---|
| `float_neg` | `-3` | `-2` |
| `double_neg` | `-3` | `-2` |
| `float_above` | `2147483647` | `-2147483648` |
| `float_below` | `-2147483648` | `2147483392` |
| `double_above` | `2147483647` | `-2147483648` |
| `double_neg_inside` | `-2147483648` | `-2147483647` |
| `double_huge` | `2147483647` | `-1` |
| `double_neg_huge` | `-2147483648` | `0` |
| `float_huge` | `2147483647` | `-1` |
| `float_neg_huge` | `-2147483648` | `0` |
## Reachable regression and retained evidence
The resource-free unit calls actual `mob_from_nbt`, checks all three modeled
fields, then `mob_to_nbt` with NO raw source and with the original compound. It
checks exact Int types/values, absent defaults, NoAI/AgeLocked and unrelated raw
field preservation. Expected constants are independently checked against Java;
no copied cast algorithm or direct model-field setter substitutes for decoding.
Owner-local `.local/h055` retains complete raw styled records, every program and
reference, source/bytecode/ownership evidence, logs and old/fixed executables.
Available timestamps/stage logs remain retained; unknown event times stay unknown.
No game files are published, and no broad whole-body/default-attribute/order or
startup-clock parity is claimed.
The predeclared input specifications (compact JSON hashes3769488f… /31f2a24d…)
include expected-result assertions for Java admission. The existing driver strips
only `expect_result` before execution and emits ordinary JSON spacing. The hashes
above identify those actual execution bytes; all commands, labels and comparison
selection are unchanged. Both specification and execution forms remain retained.
## Fixed paired result
**606/666 old →666/666 fixed**, using byte-identical emitted input scripts and
fresh Java reference bytes. All81 individual state reads and their independently
reset result/success stores pass. The resource-free decode-to-save regression also
fails old at the three wrong values and passes fixed across27 inputs. The shared
NBT-path helper body is byte-for-byte unchanged (visibility only), and ledger
mapping validation reports249 rows with no problems.
## Complete diagnostic residuals
Full diagnostic **606/695 old →666/695 fixed**. Same emitted full script bytes;
the independent fresh full Java references differ only at absolute clocks. All29
remaining complete raw mismatches are listed below; excerpts only describe the
difference and never replace whole-record comparison. Full styles/callbacks/body
values remain retained. No695-green or whole-body/raw-clock equality claim.
| Exact label | Java expected | Fixed native actual |
|---|---|---|
| `clock-start` | `old fresh: 309; fixed fresh: 293` | `0` |
| `absent-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `zero-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `byte-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `short-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `int_min-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `int_max-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `long_above-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `long_below-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `long_wrap-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `long_max-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `long_min-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `float_pos-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `float_neg-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `double_pos-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `double_neg-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `float_inside-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `float_above-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `float_min-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `float_below-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `double_inside-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `double_above-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `double_neg_inside-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `double_below-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `double_huge-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `double_neg_huge-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `float_huge-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `float_neg_huge-body` | `attributes: []` | `attributes: [{id: "minecraft:movement_speed", base: 0.25d}]` |
| `clock-end` | `old fresh: 309; fixed fresh: 293` | `0` |
Core fresh reference SHA256: `cb1532bb798fbfbc3f24794bacc0fb6e8192fbd9eb2120eb2ad3c1c72640da70`.
## Required checks and later source checkpoint
Final `pr_ready.py --quick --base e5875b0b192e64c2de121e30940d39bdd4c7728b`:
2024 Rust tests pass, one fails. The existing `drops_match_vanillas_motion_bit_for_bit`
Z last-bit difference at yaw37.5/pitch-21.25 is actual4597448471913462162 versus
expected4597448471913462163. This task's OWN archived old executable reproduces
exactly the same failure. No drop-motion repair. The resource-free H055 test passes
both explicitly and in the full run; the core suite has no failures.
Python:696 tests, four existing macOS packaging failures, one skip. The installer
signature fixture supplies x86_64 assets on this ARM64 host; three optional-release
archive combinations hit Bash3.2 unset arrays. A narrow installer rerun confirms
its prerequisite. The readiness command exits nonzero; no fully green claim.
Rustfmt, clippy with warnings denied, doc tests and public-tree checks pass. Broad
parity is skipped by `--quick`; the scoped/full actual Game pairs ran separately.
The new driver is discovered by the ordinary parity runner without extra flags.
Latest actual main pull: `2993ef0b205e7f6fb9478a1af9fd5084b31a5e5a`.
The integer loader/helpers, command harness/corpus and pinned lock have no delta
from frozen e5875b0b1. Later changes add opt-in legacy combat/rendering, unrelated
to these zero-tick reads. No combined-latest runtime claim or repeat of unaffected
proof. Evidence-only documentation completion is followed by whitespace/public
source checks; no production edits follow the required run.
+1 -1
View File
@@ -2,7 +2,7 @@ AbstractFish.addAdditionalSaveData inline crates/horizon-client/src/entity_nbt.r
AbstractFish.readAdditionalSaveData inline crates/horizon-client/src/entity_nbt.rs::mob_from_nbt FromBucket via read_variant defaults false; tadpole ignores loaded input
AgeableWaterCreature.handleAirSupply inline crates/horizon-client/src/mobs/ai.rs::base_tick the needs_water branch of base_tick; dolphins override it to nothing and refill air in aquatic.rs
AgeableWaterCreature.checkSurfaceAgeableWaterCreatureSpawnRules port crates/horizon-client/src/mobs/spawn_rules.rs::surface_water
Animal.readAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::mob_from_nbt InLove is read; LoveCause is not kept
Animal.readAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::mob_from_nbt InLove through NumericTag.intValue; existing LoveCause UUID loading retained
Animal.canMate port crates/horizon-client/src/mobs/animal_brain.rs::can_mate_with brain breeding; the Breed goal in goals.rs does the same for goal-driven animals
Animal.handleEntityEvent port crates/horizon-client-render/src/particles.rs::entity_event_burst event 18 love hearts
Animal.getDismountLocationForPassenger port crates/horizon-client/src/game/mounts.rs::Game::dismount_mount gap: tries both sides by the mob's yaw, then on top; not DismountHelper's offsets and pose boxes
1 AbstractFish.addAdditionalSaveData inline crates/horizon-client/src/entity_nbt.rs::mob_to_nbt FromBucket always written; tadpole getter is always true
2 AbstractFish.readAdditionalSaveData inline crates/horizon-client/src/entity_nbt.rs::mob_from_nbt FromBucket via read_variant defaults false; tadpole ignores loaded input
3 AgeableWaterCreature.handleAirSupply inline crates/horizon-client/src/mobs/ai.rs::base_tick the needs_water branch of base_tick; dolphins override it to nothing and refill air in aquatic.rs
4 AgeableWaterCreature.checkSurfaceAgeableWaterCreatureSpawnRules port crates/horizon-client/src/mobs/spawn_rules.rs::surface_water
5 Animal.readAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::mob_from_nbt InLove is read; LoveCause is not kept InLove through NumericTag.intValue; existing LoveCause UUID loading retained
6 Animal.canMate port crates/horizon-client/src/mobs/animal_brain.rs::can_mate_with brain breeding; the Breed goal in goals.rs does the same for goal-driven animals
7 Animal.handleEntityEvent port crates/horizon-client-render/src/particles.rs::entity_event_burst event 18 love hearts
8 Animal.getDismountLocationForPassenger port crates/horizon-client/src/game/mounts.rs::Game::dismount_mount gap: tries both sides by the mob's yaw, then on top; not DismountHelper's offsets and pose boxes
+1 -1
View File
@@ -44,7 +44,7 @@ AbstractInterpolationHandler.setLastPositionAndRotation skip:other only feeds t
AgeableMob.defineSynchedData port crates/horizon-client/src/game/lan.rs::mob_meta the baby flag (baby_index) and AGE_LOCKED after it; a kind outside baby_index (sulfur cube, nautilus, happy ghast) sends neither
AgeableMob.getAge inline crates/horizon-client/src/game/remote_mob_data.rs::apply m.extra.age on the host; a remote client only learns baby or not (-1 or 0)
AgeableMob.addAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::mob_to_nbt modeled Age, ForcedAge and AgeLocked for baby-capable creatures; zombie nautiluses omit ignored raw age keys
AgeableMob.readAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::mob_from_nbt Age, ForcedAge and AgeLocked
AgeableMob.readAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::mob_from_nbt Age/ForcedAge through NumericTag.intValue; AgeLocked unchanged
AnimationState.getTimeInMillis port crates/horizon-client/src/mobs/keyframes.rs::AnimationState::millis
AreaEffectCloud.defineSynchedData port crates/horizon-client/src/game/lan_entities.rs::Game::lan_projectile_states radius, waiting and particle for dragon breath and lingering clouds; read back in remote_projectiles.rs::remote_projectile_data
AreaEffectCloud.setRadius inline crates/horizon-client/src/game/potions.rs::Game::tick_potion_cloud loaded radius and nonzero per-tick/per-use changes clamp to 0..32
1 DamageSources.source port crates/horizon-client/src/mobs.rs::Damage::of a damage source is a DamageKind plus attacker and origin; type ids are resolved in creatures.rs::damage_type
44 AgeableMob.defineSynchedData port crates/horizon-client/src/game/lan.rs::mob_meta the baby flag (baby_index) and AGE_LOCKED after it; a kind outside baby_index (sulfur cube, nautilus, happy ghast) sends neither
45 AgeableMob.getAge inline crates/horizon-client/src/game/remote_mob_data.rs::apply m.extra.age on the host; a remote client only learns baby or not (-1 or 0)
46 AgeableMob.addAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::mob_to_nbt modeled Age, ForcedAge and AgeLocked for baby-capable creatures; zombie nautiluses omit ignored raw age keys
47 AgeableMob.readAdditionalSaveData port crates/horizon-client/src/entity_nbt.rs::mob_from_nbt Age, ForcedAge and AgeLocked Age/ForcedAge through NumericTag.intValue; AgeLocked unchanged
48 AnimationState.getTimeInMillis port crates/horizon-client/src/mobs/keyframes.rs::AnimationState::millis
49 AreaEffectCloud.defineSynchedData port crates/horizon-client/src/game/lan_entities.rs::Game::lan_projectile_states radius, waiting and particle for dragon breath and lingering clouds; read back in remote_projectiles.rs::remote_projectile_data
50 AreaEffectCloud.setRadius inline crates/horizon-client/src/game/potions.rs::Game::tick_potion_cloud loaded radius and nonzero per-tick/per-use changes clamp to 0..32
+137
View File
@@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""Ageable/Animal integer NBT loading through actual Game and fresh pinned Java.
Zero observation ticks. Full bodies and absolute startup clocks remain executable
strict diagnostics; all scalar values/types, feedback/callbacks/stores and actor
admissions are compared without masks or tolerances.
"""
import hashlib
import json
from pathlib import Path
import sys
import test_command_exec_parity as driver
# Independently source-derived expectations, required to pass fresh Java before
# the native capture. All floating inputs are finite; no NaN/boolean expansion.
CASES = [
('absent', None, 0), ('zero', '0', 0), ('byte', '-7b', -7), ('short', '13s', 13),
('int_min', '-2147483648', -2147483648), ('int_max', '2147483647', 2147483647),
('long_above', '2147483648L', -2147483648), ('long_below', '-2147483649L', 2147483647),
('long_wrap', '4294967296L', 0), ('long_max', '9223372036854775807L', -1),
('long_min', '-9223372036854775808L', 0),
('float_pos', '2.5f', 2), ('float_neg', '-2.5f', -3),
('double_pos', '2.5d', 2), ('double_neg', '-2.5d', -3),
('float_inside', '2147483520f', 2147483520), ('float_above', '2147483648f', 2147483647),
('float_min', '-2147483648f', -2147483648), ('float_below', '-2147483904f', -2147483648),
('double_inside', '2147483647.75d', 2147483647), ('double_above', '2147483648d', 2147483647),
('double_neg_inside', '-2147483647.5d', -2147483648),
('double_below', '-2147483648.5d', -2147483648),
('double_huge', '1e20d', 2147483647), ('double_neg_huge', '-1e20d', -2147483648),
('float_huge', '1e20f', 2147483647), ('float_neg_huge', '-1e20f', -2147483648),
]
FIELDS = ['Age', 'ForcedAge', 'InLove']
def program(full=False):
ops = []
def cmd(text, label, result=None, admission=True, diagnostic=False):
op = {'op': 'cmd', 'cmd': text, 'label': label, 'admission': admission}
if result is not None:
op['expect_result'] = result
if diagnostic and not full:
op.update(record=False, trace=True)
ops.append(op)
cmd('/tick freeze', 'freeze')
cmd('/gamerule spawn_mobs false', 'no-spawn')
cmd('/gamerule advance_time false', 'no-daylight')
cmd('/forceload add 0 0', 'force', 1)
ops.append({'op': 'settle', 'required_chunks': [[0, 0]]})
cmd('/fill 0 199 0 15 199 15 stone', 'floor', 256)
ops.append({'op': 'settle', 'required_chunks': [[0, 0]]})
cmd('/execute if loaded 4 200 4', 'loaded', 1)
cmd('/execute if block 4 199 4 stone', 'actor-support', 1)
cmd('/execute if block 4 200 4 air', 'actor-air', 1)
cmd('/tp @s 12.5 200 12.5 0 0', 'owner-position', 1)
cmd('/gamemode creative @s', 'owner-mode')
cmd('/execute if entity @s[type=player,name=Player]', 'owner-type', 1)
for key, expected in [('UUID', 4), ('Health', 20), ('playerGameType', 1), ('Pos', 3)]:
cmd('/data get entity @s ' + key, 'owner-' + key, expected)
cmd('/scoreboard objectives add h055 dummy', 'objective')
cmd('/execute store result score start h055 run time query gametime', 'clock-start', diagnostic=True)
for n, (case, source, expected) in enumerate(CASES, 1):
uuid = f'00000037-0000-0000-0000-{n:012x}'
values = '' if source is None else ',' + ','.join(k + ':' + source for k in FIELDS)
cmd(f'/summon pig 4.5 200 4.5 {{UUID:[I;55,0,0,{n}],Health:10f,NoAI:1b,NoGravity:1b,'
f'PersistenceRequired:1b,AgeLocked:1b,Tags:["h055"]{values}}}', case + '-summon', 1)
cmd(f'/execute as {uuid} if entity @s[type=pig,tag=h055]', case + '-type', 1)
for key, result in [('UUID', 4), ('Health', 10), ('Pos', 3), ('NoAI', 1),
('NoGravity', 1), ('PersistenceRequired', 1), ('AgeLocked', 1)]:
cmd(f'/data get entity {uuid} {key}', case + '-' + key, result)
for key in FIELDS:
label = case + '-' + key
for holder in ['result', 'success']:
cmd(f'/scoreboard players set {holder} h055 -99', label + '-reset-' + holder, -99)
cmd(f'/execute store result score result h055 store success score success h055 '
f'run data get entity {uuid} {key}', label + '-read', expected, admission=False)
cmd('/scoreboard players get result h055', label + '-result', expected, admission=False)
cmd('/scoreboard players get success h055', label + '-success', 1, admission=False)
cmd(f'/data get entity {uuid}', case + '-body', diagnostic=True)
cmd('/execute store result score end h055 run time query gametime', 'clock-end', diagnostic=True)
cmd('/scoreboard players operation end h055 -= start h055', 'clock-delta-operation', 0)
cmd('/scoreboard players get end h055', 'clock-delta', 0)
return ops
def main():
full = '--full-diagnostic' in sys.argv
if full:
sys.argv.remove('--full-diagnostic')
reference = None
if '--reference' in sys.argv:
i = sys.argv.index('--reference')
reference = Path(sys.argv[i + 1]).resolve()
del sys.argv[i:i + 2]
if '--work-dir' not in sys.argv:
sys.argv += ['--work-dir', str(driver.lab.LOCAL / 'ageable-integer-load-parity')]
build = Path(sys.argv[sys.argv.index('--work-dir') + 1]).resolve()
reference_bytes = reference.read_bytes() if reference else None
assert reference is None or reference != build / 'vanilla.jsonl', 'reference must remain separate and immutable'
ops = program(full)
driver.corpus_module.groups = lambda: [('ageable integer NBT load', ops)]
sys.argv.append('--allow-gaps')
driver.main()
recorded = [op for op in ops if op['op'] == 'cmd' and op.get('record', True)]
java = [json.loads(s) for s in (build / 'vanilla.jsonl').read_text().splitlines()]
assert len(java) == len(recorded)
by_label = {op['label']: row for op, row in zip(recorded, java)}
for case, _, expected in CASES:
for key in FIELDS:
for suffix, value in [('read', expected), ('result', expected), ('success', 1)]:
assert by_label[f'{case}-{key}-{suffix}']['results'] == [[True, value]], 'fresh Java admission'
if '--reference-only' in sys.argv:
return
if reference_bytes:
assert reference_bytes == (build / 'vanilla.jsonl').read_bytes(), 'fresh reference bytes changed'
expected = java
native = [json.loads(s) for s in (build / 'rust.jsonl').read_text().splitlines()]
assert len(expected) == len(native) == len(recorded)
differences = [{'label': op['label'], 'expected': a, 'actual': b}
for op, a, b in zip(recorded, expected, native) if a != b]
report = {'records': len(native), 'passed': len(native) - len(differences), 'differences': differences,
'comparison': 'complete raw records; no masks or tolerances',
'reference_sha256': hashlib.sha256((build / 'vanilla.jsonl').read_bytes()).hexdigest()}
(build / 'strict-report.json').write_text(json.dumps(report, indent=2) + '\n')
if not full:
for op, a, b in zip(recorded, expected, native):
if op['admission']:
assert a == b, ('fixture admission', op, a, b)
print(f'Strict raw records: {report["passed"]}/{report["records"]}')
if differences:
raise SystemExit(1)
if __name__ == '__main__':
main()