mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Omit the saved lodestone tracker true default
Co-Authored-By: Codex <[email protected]>
This commit is contained in:
1 parent
6d99e14ff0
commit
6df80522aa
3 files changed
+383
No files matched your search
@@ -714,6 +714,12 @@ fn canonical_other_component(id: &str, tag: &Tag) -> Tag {
|
||||
/// A filled map's empty decorations equal its prototype (`Items.mapProperties`).
|
||||
pub fn encode_components(stack: &ItemStack) -> Option<Tag> {
|
||||
let mut components = components_tag(stack.damage, stack.data())?;
|
||||
// LodestoneTracker.CODEC: optionalFieldOf("tracked", true).
|
||||
if let Some(Tag::Compound(tracker)) = components.get_mut("minecraft:lodestone_tracker")
|
||||
&& tracker.get(&JavaString::from("tracked")) == Some(&Tag::Byte(1))
|
||||
{
|
||||
tracker.remove(&JavaString::from("tracked"));
|
||||
}
|
||||
if &*stack.item == "minecraft:filled_map"
|
||||
&& matches!(components.get("minecraft:map_decorations"), Some(Tag::Compound(entries)) if entries.is_empty())
|
||||
&& let Tag::Compound(entries) = &mut components
|
||||
@@ -1141,6 +1147,102 @@ pub fn command_components(text: &str) -> Result<Tag, String> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn check_lodestone_saved_default(decoded: bool) {
|
||||
const TRACKER: &str = "minecraft:lodestone_tracker";
|
||||
const CUSTOM: &str = "minecraft:custom_data";
|
||||
let target = Tag::compound([
|
||||
("dimension", Tag::String("minecraft:overworld".into())),
|
||||
("pos", Tag::IntArray(vec![12, 200, 12])),
|
||||
]);
|
||||
let custom = Tag::compound([("keep", Tag::Int(17))]);
|
||||
assert_eq!(
|
||||
encode_components(&ItemStack::new("minecraft:compass", 1)),
|
||||
None
|
||||
);
|
||||
let mut only_custom = ItemStack::new("minecraft:compass", 1);
|
||||
only_custom
|
||||
.data_mut()
|
||||
.set_other(CUSTOM, Some(custom.clone()));
|
||||
assert_eq!(
|
||||
encode_components(&only_custom),
|
||||
Some(Tag::compound([(CUSTOM, custom.clone())]))
|
||||
);
|
||||
// The valid false and absent controls precede the old true-default failure.
|
||||
for (tracked, has_target) in [
|
||||
(Some(false), true),
|
||||
(None, true),
|
||||
(Some(true), true),
|
||||
(Some(true), false),
|
||||
] {
|
||||
let mut fields = Vec::new();
|
||||
let mut expected_fields = Vec::new();
|
||||
if has_target {
|
||||
fields.push(("target", target.clone()));
|
||||
expected_fields.push(("target", target.clone()));
|
||||
}
|
||||
if let Some(value) = tracked {
|
||||
fields.push(("tracked", Tag::Byte(i8::from(value))));
|
||||
if !value {
|
||||
expected_fields.push(("tracked", Tag::Byte(0)));
|
||||
}
|
||||
}
|
||||
let source = Tag::compound(fields);
|
||||
let input = Tag::compound([(TRACKER, source.clone()), (CUSTOM, custom.clone())]);
|
||||
let expected = Tag::compound([
|
||||
(TRACKER, Tag::compound(expected_fields)),
|
||||
(CUSTOM, custom.clone()),
|
||||
]);
|
||||
let stack = if decoded {
|
||||
let (damage, extra) = decode_components(Some(&input));
|
||||
ItemStack {
|
||||
damage,
|
||||
extra,
|
||||
..ItemStack::new("minecraft:compass", 1)
|
||||
}
|
||||
} else {
|
||||
let mut fresh = ItemStack::new("minecraft:compass", 1);
|
||||
assert!(fresh.extra.is_none());
|
||||
fresh.data_mut().set_other(TRACKER, Some(source.clone()));
|
||||
fresh.data_mut().set_other(CUSTOM, Some(custom.clone()));
|
||||
fresh
|
||||
};
|
||||
{
|
||||
assert_eq!(stack.data().unwrap().other(TRACKER), Some(&source));
|
||||
assert_eq!(encode_components(&stack), Some(expected.clone()));
|
||||
assert_eq!(components_tag(0, stack.data()), Some(input.clone()));
|
||||
let saved = crate::item_nbt::item_to_nbt(&stack);
|
||||
assert_eq!(saved.get("components"), Some(&expected));
|
||||
assert_eq!(
|
||||
saved.get("id"),
|
||||
Some(&Tag::String("minecraft:compass".into()))
|
||||
);
|
||||
assert_eq!(saved.get("count"), Some(&Tag::Int(1)));
|
||||
assert_eq!(stack.data().unwrap().other(TRACKER), Some(&source));
|
||||
}
|
||||
}
|
||||
let mut targetless = ItemStack::new("minecraft:compass", 1);
|
||||
targetless
|
||||
.data_mut()
|
||||
.set_other(TRACKER, Some(Tag::compound([("tracked", Tag::Byte(1))])));
|
||||
assert_eq!(
|
||||
encode_components(&targetless),
|
||||
Some(Tag::compound([(
|
||||
TRACKER,
|
||||
Tag::Compound(Default::default())
|
||||
)]))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lodestone_tracker_saved_default_decoded() {
|
||||
check_lodestone_saved_default(true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lodestone_tracker_saved_default_fresh_model() {
|
||||
check_lodestone_saved_default(false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_decoration_types_round_trip_with_default_namespaces() {
|
||||
let parse = |text| horizon_world::nbt::parse_snbt(text).unwrap().0;
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
# Lodestone tracker saved true default
|
||||
|
||||
Pinned Java 26.3 `LodestoneTracker.CODEC` uses
|
||||
`Codec.BOOL.optionalFieldOf("tracked", true)`: valid true is omitted when saved,
|
||||
false remains explicit, and an absent target is independent of that default.
|
||||
The narrow repair is in `encode_components`, the actual saved ItemStack boundary.
|
||||
It removes only tracked Byte1 from that component's cloned saved compound.
|
||||
|
||||
The decoder, live StackData.other value and `components_tag` remain intact; the
|
||||
latter also supplies client stack views. No renderer, packet or ticking parity
|
||||
claim. Existing map-decoration namespace/prototype behavior is unchanged. No
|
||||
Item ID hard-code, generic boolean conversion, registry/validation expansion,
|
||||
useOn, inventory ticking, POI/unloaded logic or other component-default cleanup.
|
||||
|
||||
## Declared actual Game/Java fixture
|
||||
|
||||
```
|
||||
python3 tools/test_lodestone_default_parity.py --binary /path/to/game --work-dir /scratch/run
|
||||
python3 tools/test_lodestone_default_parity.py --full-diagnostic --binary /path/to/game --work-dir /scratch/full
|
||||
```
|
||||
|
||||
`--reference /scratch/old/vanilla.jsonl` requires byte-identical fresh Java bytes
|
||||
before comparing another binary. The existing command harness drives real Game
|
||||
and independently runs pinned Java, with fresh isolated data and a real owner.
|
||||
No new observer. Five actual stationary ItemEntities hold compass count 1, with unique
|
||||
UUIDs/fixed entity names, NoGravity and zero Motion. They are away from the player
|
||||
and have PickupDelay 32767. No world steps or living-holder inventory updates.
|
||||
|
||||
Cases: true+target, false+target, target without tracked, targetless explicit
|
||||
true, and absent whole tracker component. Every case retains custom_data.keep 17.
|
||||
A loaded real lodestone guards the codec-valid GlobalPos target. UUID/type,
|
||||
Health/Age/Pos/Motion/NoGravity/Item ID/count, loaded floor/air, owner identity,
|
||||
setup acknowledgements and actual relative clock 0 are strict positive controls.
|
||||
Position/Motion/UUID/count are read again afterward. The targetless default must
|
||||
stay as a present empty component, distinct from the missing-component control.
|
||||
|
||||
Whole saved component and tracked-field reads use fresh result/success sentinels
|
||||
at -99. Actual data-presence branches have fresh independent sentinels and exact
|
||||
callbacks/no-action behavior. Successful zero (false or empty compound) remains
|
||||
distinct from a missing path. Target dimension/typed IntArray position and the
|
||||
unrelated custom_data value stay strict.
|
||||
|
||||
Predeclared before native capture: **199** complete raw records. Only the five
|
||||
whole entity bodies and absolute clock-start/end labels are diagnostic; they
|
||||
remain traced, and `--full-diagnostic` compares all **206** records. No masks,
|
||||
tolerances, feedback/style normalization or dropped commands.
|
||||
|
||||
Frozen source: `6d99e14ff0c70ae60bc56b67829b526409fa068e`.
|
||||
|
||||
| Program | Exact script SHA256 |
|
||||
|---|---|
|
||||
| Scoped199 | `761185d59bbc90249a393a19abbdd33342dee9af0cca2d7956837ff805e91094` |
|
||||
| Full206 | `f803a10ec5e8fa78f663358d309ecf2a07bd41f764cb37979513735f1a2353a4` |
|
||||
|
||||
Old actual Game: **185/199**. The seven suffixes below differ in EACH of `true`
|
||||
and `targetless`, yielding 14 target observations from this one saved-codec gap.
|
||||
All false/absent/missing-component, actual actor, type/target/custom-data, clock
|
||||
and other controls pass. Full old diagnostic: **188/206** (also both affected
|
||||
whole bodies and two absolute clocks).
|
||||
|
||||
| Exact suffix | Java expected | Old actual |
|
||||
|---|---|---|
|
||||
| -component-read | tracker with default omitted | tracker retaining tracked:1b |
|
||||
| -component-result | 1 with target / 0 targetless | 2 with target / 1 targetless |
|
||||
| -tracked-read | missing-field error, callback false/0 | tracked:1b, callback true/1 |
|
||||
| -tracked-result | 0 | 1 |
|
||||
| -tracked-success | 0 | 1 |
|
||||
| -tracked-presence | no action/no callback | action/callback true/1 |
|
||||
| -tracked-presence-result | -99 | 1 |
|
||||
|
||||
## Reachable regressions and evidence
|
||||
|
||||
`lodestone_tracker_saved_default_decoded` and
|
||||
`lodestone_tracker_saved_default_fresh_model` independently fail old at saved
|
||||
tracked Byte1 versus omission. Both pass fixed. Each uses the real ItemStack
|
||||
encoder, with false/absent and missing-component controls preceding the failure.
|
||||
The fresh model has no original item NBT. Tests cover target/custom_data retention,
|
||||
full saved ItemStack ID/count, unchanged decoded/live value and unsaved component
|
||||
view, and a targetless empty component even without unrelated custom_data.
|
||||
|
||||
The final source applies the default only at encode_components, after reviewing
|
||||
its shared helper's client consumers. Intermediate unit drafts and final old/fixed
|
||||
logs are retained; no intermediate helper implementation is part of the runtime
|
||||
pair or published production patch.
|
||||
|
||||
Complete programs, fresh references, raw styled feedback/callbacks, full bodies,
|
||||
transcripts, pinned bytecode, source hashes and immutable binaries are retained
|
||||
under the owner's `.local/h329`, without publishing game files. Available file
|
||||
capture timestamps and logged stage durations are retained. Unobserved event
|
||||
stage timestamps remain unknown.
|
||||
|
||||
## Fixed results and all full-diagnostic residuals
|
||||
|
||||
Final actual Game: **199/199**, versus **185/199** old, using byte-identical
|
||||
scoped scripts and fresh Java reference bytes. Both final decoded and fresh-model
|
||||
unit tests fail independently old and pass fixed. Full diagnostics improve
|
||||
**188/206 → 204/206**. All five complete saved entity body records match in this
|
||||
fixture. No full 206/raw-clock equality, general entity/packet or tick claim.
|
||||
|
||||
The full script is byte-identical across builds; independent full Java references
|
||||
differ only at clock-start/end. Their non-clock records are identical. The two
|
||||
remaining raw record differences are listed below. Actual relative delta 0 is an
|
||||
independently strict admission; no captured absolute value is rewritten.
|
||||
|
||||
| Exact label | Java expected (old / fixed) | Native actual (both) |
|
||||
|---|---|---|
|
||||
| `clock-start` | `317 / 309` | `0` |
|
||||
| `clock-end` | `317 / 309` | `0` |
|
||||
|
||||
Scoped Java reference SHA256 (both): `fbe623194668b9bf0639ff632198aa9045c61558c3744901ad804dde906eb28d`.
|
||||
Full Java reference SHA256 old: `2f20e1c75aa35dba8a7c3e02a50ee31286af77650a5c4be67e2dfcc93c43144e`; fixed: `5d3af57ac2dbf287daed95cc9775ef7b8bce9fcdcba3113897eef93aa2829ea9`.
|
||||
|
||||
## Required checks and source checkpoints
|
||||
|
||||
`tools/pr_ready.py --quick --base 6d99e14ff0c70ae60bc56b67829b526409fa068e`
|
||||
ran once on the final production/test patch: **70 item tests** and **25 content-root
|
||||
tests pass**, with formatting, workspace/headless clippy, doc tests and public
|
||||
checks passing. Jobs 2, incremental 0, Rust test threads 2 and isolated test homes
|
||||
were used; prepared resources were actually available for the content-root suite.
|
||||
Broad parity is skipped by --quick; the explicit actual Game/Java pair above ran.
|
||||
|
||||
Python: **690 run, 4 failures, 1 skipped** in unchanged ARM64/Bash 3.2 packaging
|
||||
checks. The narrow install-signature diagnostic confirms the missing ARM64 mock
|
||||
archive; optional archive subcases fail on unset Bash arrays. No packaging fix.
|
||||
The ledger check flags a long regression helper (not a vanilla implementation)
|
||||
and an inference-only DispenserBlock classification change; dispenser source is
|
||||
unchanged and no dispenser behavior is claimed. No full LodestoneTracker codec,
|
||||
registry or stream-codec implementation is claimed by this one-field repair.
|
||||
|
||||
Actual main was pulled through `2266c4de14561146044c422e6c525b3ce52b0f79` after
|
||||
batches. Later mod-index, wire-stream/client input/tick-script and HUD changes
|
||||
were reviewed. The item codec, command harness/driver and Java command oracle are
|
||||
unchanged. The new optional tick-script route is separate from this fixture's
|
||||
command-script route. These are source reviews, not combined latest-main runtime
|
||||
claims: the paired proof remains on frozen 6d99. Only evidence text was appended
|
||||
after required checks; final public-tree and whitespace checks were repeated.
|
||||
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Saved LodestoneTracker default with stationary real item entities, no world steps.
|
||||
|
||||
--full-diagnostic records whole entity bodies and absolute clocks too. Those
|
||||
commands remain traced in the scoped program. All saved-component, typed identity,
|
||||
presence, callback/store and relative-clock records are strict.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import test_command_exec_parity as driver
|
||||
|
||||
|
||||
def program(full=False):
|
||||
ops = []
|
||||
|
||||
def cmd(text, label, result=None, callback=None, admission=True, diagnostic=False):
|
||||
op = {'op': 'cmd', 'cmd': text, 'label': label, 'admission': admission}
|
||||
if result is not None:
|
||||
op['expect_result'] = result
|
||||
if callback is not None:
|
||||
op['expect_callback'] = callback
|
||||
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)
|
||||
cmd('/setblock 12 200 12 lodestone', 'lodestone', 1)
|
||||
cmd('/execute if block 12 200 12 lodestone', 'target-block', 1)
|
||||
cmd('/execute if loaded 12 200 12', 'target-loaded', 1)
|
||||
cmd('/tp @s 8.5 200 12.5 0 0', 'owner-position', 1)
|
||||
cmd('/gamemode creative @s', 'owner-mode')
|
||||
cmd('/scoreboard objectives add h329 dummy', 'objective')
|
||||
cmd('/execute store result score start h329 run time query gametime', 'clock-start', diagnostic=True)
|
||||
cmd('/execute if entity @s[type=player,name=Player]', 'owner-type', 1)
|
||||
for field, result in [('UUID', 4), ('Health', 20), ('playerGameType', 1), ('Pos', 3)]:
|
||||
cmd(f'/data get entity @s {field}', 'owner-' + field, result)
|
||||
|
||||
target = 'target:{dimension:"minecraft:overworld",pos:[I;12,200,12]}'
|
||||
cases = [('true', target + ',tracked:true'), ('false', target + ',tracked:false'),
|
||||
('absent', target), ('targetless', 'tracked:true'), ('none', None)]
|
||||
for n, (case, value) in enumerate(cases, 1):
|
||||
x = n * 2
|
||||
uuid = f'00000149-0000-0000-0000-{n:012x}'
|
||||
lodestone = '' if value is None else f',"minecraft:lodestone_tracker":{{{value}}}'
|
||||
cmd(f'/execute if loaded {x} 201 4', case + '-loaded', 1)
|
||||
cmd(f'/execute if block {x} 199 4 stone', case + '-floor', 1)
|
||||
cmd(f'/execute if block {x} 201 4 air', case + '-gap', 1)
|
||||
cmd(f'/summon item {x + .5} 201 4.5 {{UUID:[I;329,0,0,{n}],NoGravity:1b,'
|
||||
f'Motion:[0d,0d,0d],PickupDelay:32767s,CustomName:"Tracker_{case}",'
|
||||
f'Item:{{id:"minecraft:compass",count:1,components:{{"minecraft:custom_data":{{keep:17}}{lodestone}}}}}}}',
|
||||
case + '-summon', 1)
|
||||
cmd(f'/execute as {uuid} if entity @s[type=minecraft:item]', case + '-actual-type', 1)
|
||||
for field, result in [('UUID', 4), ('Pos', 3), ('Motion', 3), ('NoGravity', 1),
|
||||
('Health', 5), ('Age', 0), ('PickupDelay', 32767),
|
||||
('Item.id', 17), ('Item.count', 1)]:
|
||||
cmd(f'/data get entity {uuid} {field}', case + '-' + field, result)
|
||||
cmd(f'/data get entity {uuid} Item.components."minecraft:custom_data".keep',
|
||||
case + '-custom-data', 17)
|
||||
cmd(f'/data get entity {uuid}', case + '-full-body', diagnostic=True)
|
||||
component = 'Item.components."minecraft:lodestone_tracker"'
|
||||
if case in ('true', 'false', 'absent'):
|
||||
cmd(f'/data get entity {uuid} {component}.target.dimension', case + '-dimension', 19)
|
||||
cmd(f'/data get entity {uuid} {component}.target.pos', case + '-target-pos', 3)
|
||||
for suffix, path, exists in [('component', component, case != 'none'),
|
||||
('tracked', component + '.tracked', case == 'false')]:
|
||||
affected = case in ('true', 'targetless')
|
||||
for holder in ['result', 'success']:
|
||||
cmd(f'/scoreboard players set {holder} h329 -99', case + '-' + suffix + '-reset-' + holder, -99)
|
||||
cmd(f'/execute store result score result h329 store success score success h329 '
|
||||
f'run data get entity {uuid} {path}', case + '-' + suffix + '-read',
|
||||
callback=([True, 0] if case == 'false' else [False, 0]) if suffix == 'tracked' else None,
|
||||
admission=not affected)
|
||||
for holder in ['result', 'success']:
|
||||
cmd(f'/scoreboard players get {holder} h329', case + '-' + suffix + '-' + holder,
|
||||
admission=not affected)
|
||||
cmd('/scoreboard players set presence h329 -99', case + '-' + suffix + '-reset-presence', -99)
|
||||
cmd(f'/execute if data entity {uuid} {path} run scoreboard players set presence h329 1',
|
||||
case + '-' + suffix + '-presence', admission=not affected or suffix == 'component')
|
||||
ops[-1]['expect_results'] = [[True, 1]] if exists else []
|
||||
cmd('/scoreboard players get presence h329', case + '-' + suffix + '-presence-result',
|
||||
1 if exists else -99, admission=not affected or suffix == 'component')
|
||||
for n, (case, _) in enumerate(cases, 1):
|
||||
uuid = f'00000149-0000-0000-0000-{n:012x}'
|
||||
for field, result in [('UUID', 4), ('Pos', 3), ('Motion', 3), ('Item.count', 1)]:
|
||||
cmd(f'/data get entity {uuid} {field}', case + '-after-' + field, result)
|
||||
cmd('/execute store result score now h329 run time query gametime', 'clock-end', diagnostic=True)
|
||||
cmd('/scoreboard players operation now h329 -= start h329', 'clock-delta-operation', 0)
|
||||
cmd('/scoreboard players get now h329', '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 / 'lodestone-default-parity')]
|
||||
build = Path(sys.argv[sys.argv.index('--work-dir') + 1]).resolve()
|
||||
ops = program(full)
|
||||
driver.corpus_module.groups = lambda: [('saved lodestone tracker default', 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()]
|
||||
for op, row in zip(recorded, java):
|
||||
if 'expect_results' in op:
|
||||
assert row['results'] == op['expect_results'], ('Java action admission', op, row)
|
||||
if '--reference-only' in sys.argv:
|
||||
return
|
||||
source = reference or build / 'vanilla.jsonl'
|
||||
if reference:
|
||||
assert source.read_bytes() == (build / 'vanilla.jsonl').read_bytes(), 'fresh Java bytes changed'
|
||||
expected = [json.loads(s) for s in source.read_text().splitlines()]
|
||||
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),
|
||||
'reference_sha256': hashlib.sha256(source.read_bytes()).hexdigest(),
|
||||
'comparison': 'complete raw records, no masks/tolerances', 'differences': differences}
|
||||
(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, ('actual fixture admission', op, a, b)
|
||||
print(f'Strict raw records: {report["passed"]}/{report["records"]}')
|
||||
if differences:
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user