Adopt live TNT command data without replacing its identity

Co-Authored-By: Codex <[email protected]>
This commit is contained in:
Florian ERNSTandCodex committed 2026-11-06 20:53:04 +01:00
1 parent 41f822b659
commit 5efccc59f0
4 files changed
+264

No files matched your search

@@ -433,6 +433,35 @@ impl Game<'_> {
}
set_key(&mut tag, "UUID", Tag::IntArray(uuid.to_vec()));
}
Target::Tnt(id) => {
// Entity.load and PrimedTnt.readAdditionalSaveData mutate the
// resident instance; load_tnt would allocate another id/join.
let position = entity_nbt::position(&tag).ok_or_else(stale)?;
let uuid = self.uuid_for(id);
let block_state = tag
.get("block_state")
.and_then(|v| entity_nbt::state_from_nbt(self.pack.assets.registry, v));
let tnt = self
.tnts
.iter_mut()
.find(|tnt| tnt.id == id)
.ok_or_else(stale)?;
tnt.position = position;
tnt.previous = position;
tnt.velocity = entity_nbt::motion(&tag);
tnt.rotation = entity_nbt::rotation(&tag);
// Keep the existing loaded-fuse conversion policy (H090 is separate).
tnt.fuse = match tag.get("fuse") {
Some(Tag::Short(fuse)) => i32::from(*fuse),
_ => 80,
};
tnt.on_ground = matches!(tag.get("OnGround"), Some(Tag::Byte(1)));
tnt.no_gravity = entity_nbt::flag(&tag, "NoGravity");
tnt.power = entity_nbt::tnt_power(&tag);
tnt.block_state = block_state;
// Runtime id/join, owner binding and guest attribution stay live.
set_key(&mut tag, "UUID", Tag::IntArray(uuid.to_vec()));
}
Target::Cart(id) => {
let uuid = self.uuid_for(id);
let i = self
@@ -475,6 +504,7 @@ impl Game<'_> {
| Target::Frame(id)
| Target::Boat(id)
| Target::Cart(id)
| Target::Tnt(id)
| Target::Other(id)
| Target::Fangs(id) = t
{
+98
View File
@@ -1131,6 +1131,104 @@ mod tests {
Some(game)
}
#[test]
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES"]
fn live_data_commands_adopt_into_the_existing_tnt() {
use crate::game::commands::Target;
use horizon_world::nbt::Tag;
let mut game = server_game().expect("prepared Game resources are required");
game.build_registries();
game.set_allow_commands(true);
game.block_ticks
.add_empty_chunk(horizon_core::position::ChunkPos::new(0, 0))
.unwrap();
assert!(game.streaming.forced.contains(&(0, 0)));
// The existing unit helper supplies a resident column. The paired
// runtime harness separately admits actual ticket/streaming readiness.
assert!(game.world.chunks.contains_key(&(0, 0)));
assert!(game.run_command(
"summon pig 8.5 200 5.5 {UUID:[I;89,0,0,3],Health:10f,NoAI:1b,NoGravity:1b}"
));
assert!(game.run_command("summon tnt 5.5 200 5.5 {UUID:[I;89,0,0,1],fuse:40s,Motion:[0d,0d,0d],NoGravity:1b,explosion_power:2f,owner:[I;89,0,0,3],Tags:[keep],Silent:1b}"));
let pig = game
.mobs
.iter()
.find(|m| game.persist.uuid_of(m.id) == Some([89, 0, 0, 3]))
.unwrap()
.id;
assert_eq!(
game.mobs.iter().find(|m| m.id == pig).unwrap().kind.id,
"minecraft:pig"
);
assert_eq!(game.tnts.len(), 1);
let id = game.tnts[0].id;
assert_eq!(game.persist.uuid_of(id), Some([89, 0, 0, 1]));
assert_eq!(game.tnts[0].fuse, 40);
assert_eq!(game.tnts[0].velocity, Vec3::ZERO);
assert!(game.tnts[0].no_gravity);
// Preserve a live owner referencing an actually registered mob.
game.tnts[0].owner = Some(Who::Mob(pig));
let before = game.tnts[0].clone();
let next_id = game.next_entity;
let clock = game.world.game_time;
assert!(before.join.is_joined());
let reference = Target::Tnt(id);
let saved_before = game.entity_nbt(reference).unwrap();
assert_eq!(saved_before.get("NoGravity"), Some(&Tag::Byte(1)));
assert_eq!(
saved_before.get("owner"),
Some(&Tag::IntArray(vec![89, 0, 0, 3]))
);
assert!(
!game.run_command("data merge entity 00000059-0000-0000-0000-000000000001 {fuse:40s}")
);
assert!(game.run_command("data merge entity 00000059-0000-0000-0000-000000000001 {fuse:60s,Motion:[0.125d,0d,-0.25d],Tags:[adopted]}"), "valid TNT data must reach the existing live instance");
assert_eq!(
game.next_entity, next_id,
"adoption must not spawn another entity"
);
assert_eq!(game.tnts.len(), 1);
let live = game.tnts.iter().find(|t| t.id == id).unwrap();
assert_eq!(live.fuse, 60, "the actual modeled counter changes");
assert_eq!(live.velocity, Vec3::new(0.125, 0.0, -0.25));
assert_eq!(live.position, before.position);
assert_eq!(live.previous, live.position);
assert_eq!(live.rotation, before.rotation);
assert_eq!(live.join, before.join);
assert_eq!(live.owner, before.owner);
assert_eq!(live.guest, before.guest);
assert_eq!(live.power, before.power);
assert_eq!(live.no_gravity, before.no_gravity);
assert_eq!(game.world.game_time, clock);
assert_eq!(game.persist.uuid_of(id), Some([89, 0, 0, 1]));
let saved = game.entity_nbt(reference).unwrap();
assert_eq!(saved.get("fuse"), Some(&Tag::Short(60)));
assert_eq!(
saved.get("Tags"),
Some(&Tag::List(vec![Tag::String("adopted".into())]))
);
for field in [
"UUID",
"Pos",
"NoGravity",
"Rotation",
"owner",
"block_state",
"explosion_power",
"Silent",
] {
assert!(
saved_before.get(field).is_some(),
"positive saved {field} control"
);
assert_eq!(
saved.get(field),
saved_before.get(field),
"retained {field}"
);
}
}
/// #10063: TNT lit between level ticks (flint and steel, a command) has its
/// first tick in the next level tick, so its fuse reads 79 after it, and it
/// explodes on the 80th (`PrimedTnt.tick`). Horizon held every TNT that
+38
View File
@@ -0,0 +1,38 @@
# Live TNT command data adoption
Own frozen main `41f822b659789780f58d32c63ddfb6887ea1b8bf`, actual Game/fresh pinned Java26.3: **92/100 → 100/100** complete strict records. The same protected script/reference bytes are used across the pair. No masks, tolerance, callback normalization or row selection.
## Actual command proof
Two independent fresh TNTs, UUID-bound, stationary with NoGravity, Short fuse40, Float power2 and a reference to an actually registered Pig. Actual TNT type/UUID/position/motion/fuse, floor/load, rotation, owner, block-state, tags and Silent are read before/after. The actual host's UUID/type/Health/mode/position and strict relative-clock0 are positive controls. A plain Pig Silent update proves the ordinary data route works.
An unchanged update is executed against each independently admitted initial TNT state BEFORE its target mutation. No failed reset supplies setup. Target changes are Short fuse40→60 and Double motion `[0,0,0]→[0.125,0,-0.25]`, with no observation ticks. Every action has fresh result/success sentinels and complete feedback/styles/callbacks. Full saved bodies and absolute clocks are separate predeclared raw traces.
Eight old records differ; all other records and every setup/control match:
| Labels | Java expected | Untouched Game actual |
|---|---|---|
| fuse-update, motion-update | modified TNT feedback; callback true/1 | unchanged/failure feedback; callback false/0 |
| fuse-update-result, fuse-update-success, motion-update-result, motion-update-success | stored1 | stored0 |
| fuse-saved-fuse | Short60 | Short40 |
| motion-saved-Motion | exact Double vector `[0.125d,0.0d,-0.25d]` | `[0.0d,0.0d,0.0d]` (both length-result3) |
## In-place boundary
`apply_entity_nbt` lacked a TNT arm. The patch mutates the existing registered instance with current position/motion/rotation/flag/power/block-state converters and the existing Short fuse reader. It includes TNT in the boundary's normal raw-tag bookkeeping and keeps the actual UUID. `Entity.load` resets previous position to the loaded position; no world tick occurs.
`load_tnt` is deliberately not called: it allocates an id/join, handles passengers and initializes runtime ownership. Existing runtime id/join, owner/guest binding and world clock stay intact. No field/schema, spawn route, global decoder, shared numeric helper, formatter, physics or explosion-policy change.
H090 PR11121 and H091 PR11071 actual diffs were inspected, with no pending production imported. Their fuse numeric-load policy and default-power saved-key cleanup are separate. This proof uses a valid Short and nondefault Float2, and does not claim all numeric forms or H090/H091 closure. Preserve their loader/writer changes and tests when combining; keep this independent live adoption arm and test. Owner editing, attribution-policy, malformed input, generic Entity-load limits and cross-dimension/lifecycle behavior are not claimed.
## Actual Game regression and prerequisites
The registered Game command test admits the TNT model/UUID, unchanged-update result and a real owner Mob reference before asserting a valid update. Untouched fails at that actual command. Fixed passes verification of real modeled fuse/motion, unchanged runtime identity/join/owner/guest/next-id/clock, saved types/fields and supported raw-tag adoption. It does not supply a synthetic observer answer.
The existing prepared-resource unit helper has a resident origin chunk, not a full ticket/streaming-readiness fixture. Runtime readiness remains strict in the independent100-record proof. Retained rejected unit prerequisites: a private-module import, a duplicate force-load assertion and an inappropriate streaming-ready assertion on that resident-only helper. They are not target failures. The first misplaced production draft was caught in the getter match, preserved, and its own obsolete build stopped before any runtime capture; the valid edit is solely in the adoption match/bookkeeping.
Public replay `tools/test_tnt_data_adoption_parity.py` is byte-identical to the protected original. Complete raw/style/callback captures, invalid attempts, source/artifact hashes and known phase times remain in `.local/h089/`; unknown times are null. Required local run passed1,078 Game tests, rustfmt, workspace/server Clippy, public-tree and lock checks. The explicit prepared-resource Game regression passed (one actual test). Overall required-run exit remains nonzero for the three existing optional-release Python Bash-array failures (`musl[@]`/`arm64[@]: unbound variable`,708run,one skipped); no overall green claim. The382 broader harnesses were listed for CI, not run locally. One persistent checkout/git switch, protected perf checkpoint, no worktrees.
## Raw diagnostics
Eight predeclared raw traces remain complete: old2/8, fixed4/8 match. All four full TNT before/after bodies match fixed. The two Pig bodies retain unrelated differences, and absolute clocks remain old Java301/Game0 and fixed Java321/Game0 at both start/end. Relative delta0 stays strict; no raw-clock or all-entity-body equality claim. Original Java/native/reference/style/callback outputs remain protected.
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Live TNT data adoption with independent stationary inputs and unchanged controls."""
import hashlib,json,sys,tempfile,uuid
from pathlib import Path
import test_command_exec_parity as driver
CASES=[('fuse',1,'{fuse:40s}','{fuse:60s}'),('motion',2,'{Motion:[0d,0d,0d]}','{Motion:[0.125d,0d,-0.25d]}')]
def program():
ops=[]
def cmd(text,label,result=None,admission=True,diagnostic=False):
op=dict(op='cmd',cmd=text,label=label,admission=admission)
if result is not None:op['expect_result']=result
if diagnostic:op.update(record=False,trace=True)
ops.append(op)
def action(text,label,admission=False):
for key in ['result','success']:cmd(f'/scoreboard players set {key} h089 -99',label+'-reset-'+key,-99)
cmd('/execute store result score result h089 store success score success h089 run '+text,label,admission=admission)
for key in ['result','success']:cmd(f'/scoreboard players get {key} h089',label+'-'+key,admission=admission)
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(dict(op='settle',required_chunks=[[0,0]]))
cmd('/fill 0 199 0 15 199 15 stone','floor',256)
cmd('/tp @s 14.5 200 14.5 0 0','owner-position',1);cmd('/gamemode creative @s','owner-mode')
cmd('/execute if entity @s[type=player,name=Player,nbt={Health:20f,playerGameType:1,Pos:[14.5d,200d,14.5d]}]','owner',1)
cmd('/data get entity @s UUID','owner-uuid',4)
cmd('/execute if loaded 4 200 4','loaded',1);cmd('/execute if block 4 199 4 stone','support',1)
cmd('/scoreboard objectives add h089 dummy','objective')
cmd('/execute store result score start h089 run time query gametime','clock-start',diagnostic=True)
pig=str(uuid.UUID(int=(890<<96)|3))
cmd('/summon pig 11.5 200 5.5 {UUID:[I;890,0,0,3],Health:10f,NoAI:1b,NoGravity:1b}','pig-summon',1)
pig_guard='{UUID:[I;890,0,0,3],Health:10f,Pos:[11.5d,200d,5.5d],NoAI:1b,NoGravity:1b}'
cmd(f'/execute as {pig} if entity @s[type=pig,nbt='+pig_guard+']','pig-actor',1)
cmd(f'/data get entity {pig} UUID','pig-uuid',4)
cmd(f'/data get entity {pig}','pig-before-full',diagnostic=True)
for label,i,unchanged,update in CASES:
u=str(uuid.UUID(int=(890<<96)|i));x=3+2*i
nbt=f'{{UUID:[I;890,0,0,{i}],fuse:40s,NoGravity:1b,Motion:[0d,0d,0d],explosion_power:2f,owner:[I;890,0,0,3],Tags:[keep],Silent:1b}}'
cmd(f'/summon tnt {x}.5 200 5.5 '+nbt,label+'-summon',1)
guard=f'{{UUID:[I;890,0,0,{i}],Pos:[{x}.5d,200d,5.5d],NoGravity:1b}}'
cmd(f'/execute as {u} if entity @s[type=tnt,nbt='+guard+']',label+'-actor',1)
cmd(f'/execute if block {x} 199 5 stone',label+'-floor',1)
for field in ['UUID','Pos','Motion','fuse','NoGravity','Rotation','owner','block_state','explosion_power','Tags','Silent']:
cmd(f'/data get entity {u} {field}',label+'-initial-'+field)
cmd(f'/data get entity {u}',label+'-initial-full',diagnostic=True)
action(f'data merge entity {u} '+unchanged,label+'-unchanged',admission=True)
cmd(f'/data get entity {u} fuse',label+'-unchanged-fuse',40)
cmd(f'/data get entity {u} Motion',label+'-unchanged-motion',3)
action(f'data merge entity {u} '+update,label+'-update')
for field in ['UUID','Pos','Motion','fuse','NoGravity','Rotation','owner','block_state','explosion_power','Tags','Silent']:
cmd(f'/data get entity {u} {field}',label+'-saved-'+field,admission=field not in ['fuse','Motion'])
cmd(f'/execute as {u} if entity @s[type=tnt,nbt='+guard+']',label+'-actor-retained',1)
cmd(f'/data get entity {u}',label+'-saved-full',diagnostic=True)
action(f'data merge entity {pig} {{Silent:1b}}','pig-update',admission=True)
cmd(f'/execute as {pig} if entity @s[type=pig,nbt='+pig_guard+']','pig-actor-retained',1)
cmd(f'/data get entity {pig} Silent','pig-silent',1)
cmd(f'/data get entity {pig}','pig-after-full',diagnostic=True)
cmd('/execute store result score end h089 run time query gametime','clock-end',diagnostic=True)
cmd('/scoreboard players operation end h089 -= start h089','clock-delta-operation',0)
cmd('/scoreboard players get end h089','clock-delta',0)
return ops
def main():
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:
parent = Path(tempfile.mkdtemp(prefix='tnt-adoption-', dir=driver.lab.LOCAL))
sys.argv += ['--work-dir', str(parent / 'run')]
build = Path(sys.argv[sys.argv.index('--work-dir')+1]).resolve()
assert not build.exists(), 'preserve attempts; choose a fresh directory'
build.mkdir(parents=True)
ops = program(); recorded = [op for op in ops if op['op']=='cmd' and op.get('record', True)]
(build/'declaration.json').write_text(json.dumps(dict(
script_sha256=hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), records=len(recorded),
cases=CASES, labels=[op['label'] for op in recorded],
raw_diagnostics=[op['label'] for op in ops if op.get('trace')],
comparison='complete records, no masks/tolerances; bodies and absolute clocks are separate raw traces'), indent=2)+'\n')
expected_bytes = reference.read_bytes() if reference else None
driver.corpus_module.groups = lambda: [('Live TNT data adoption', ops)]
sys.argv.append('--allow-gaps'); driver.main()
java_bytes = (build/'vanilla.jsonl').read_bytes()
if expected_bytes is not None:
assert java_bytes==expected_bytes and reference.read_bytes()==expected_bytes, 'fresh Java reference changed'
java = [json.loads(s) for s in java_bytes.splitlines()]
if '--reference-only' in sys.argv: return
native = [json.loads(s) for s in (build/'rust.jsonl').read_bytes().splitlines()]
assert len(recorded)==len(java)==len(native)
residuals = [dict(label=op['label'], expected=a, actual=b) for op,a,b in zip(recorded,java,native) if a!=b]
report = dict(records=len(recorded), passed=len(recorded)-len(residuals), residuals=residuals,
reference_sha256=hashlib.sha256(java_bytes).hexdigest())
(build/'strict-report.json').write_text(json.dumps(report, indent=2)+'\n')
for op,a,b in zip(recorded,java,native):
if op['admission']: assert a==b, ('admission',op,a,b)
print(f'Strict records: {report["passed"]}/{report["records"]}')
if residuals: raise SystemExit(1)
if __name__ == '__main__':
main()