mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
Adopt live TNT command data without replacing its identity
Co-Authored-By: Codex <[email protected]>
This commit is contained in:
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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user