Compare commits

...
Author SHA1 Message Date
Horizon agentandClaude Opus 5.5 ec08a90d43 invulnerable_time compares a mob's attributes list unordered
AttributeMap.pack lists the instances in the iteration order of an
Object2ObjectOpenHashMap keyed by Holder.Reference, which keeps
Object.hashCode: an identity hash. The same script run under another
main class saves the pig's list in another order, so the oracle's
order is the JVM's, not the game's. All 11 differences on main were
that order. The driver gets unordered_attributes(), which sorts the
entries of every attributes list in a record and keeps their text and
styles; the harness compares through it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-09 13:22:31 +01:00
3 changed files with 88 additions and 2 deletions

No files matched your search

+25
View File
@@ -33,3 +33,28 @@ class CommandComparisonTests(unittest.TestCase):
def test_unknown_comparison_fails_loudly(self):
with self.assertRaises(ValueError):
compare(self.record(1), self.record(1), 'typo')
class UnorderedAttributesTests(unittest.TestCase):
KEY, PLAIN, ID = [5636095] + [False] * 5, [None] + [False] * 5, [5635925] + [False] * 5
def body(self, *entries):
"""`/data get entity` feedback holding an `attributes` list of (id, base) entries, as the printer styles it."""
spans = [['Pig has the following entity data: {', self.PLAIN], ['attributes', self.KEY], [': [', self.PLAIN]]
for n, (name, base) in enumerate(entries):
spans += [['{' if n == 0 else ', {', self.PLAIN], ['id', self.KEY], [': "', self.PLAIN],
[f'minecraft:{name}', self.ID], ['", ', self.PLAIN], ['base', self.KEY], [': ' + base, self.PLAIN],
['}', self.PLAIN]]
spans += [[']}', self.PLAIN]]
return {'cmd': '/data get entity x', 'messages': [spans], 'results': [[True, 1]]}
def test_the_lists_order_is_ignored_and_its_entries_are_not(self):
from tools.test_command_exec_parity import unordered_attributes
speed_first = self.body(('movement_speed', '0.25d'), ('max_health', '20.0d'))
health_first = self.body(('max_health', '20.0d'), ('movement_speed', '0.25d'))
other_base = self.body(('max_health', '10.0d'), ('movement_speed', '0.25d'))
self.assertNotEqual(speed_first, health_first)
self.assertEqual(unordered_attributes(speed_first), unordered_attributes(health_first))
self.assertNotEqual(unordered_attributes(other_base), unordered_attributes(health_first))
plain = {'cmd': '/say', 'messages': [[['attributes', self.PLAIN]]], 'results': [[True, 1]]}
self.assertEqual(unordered_attributes(plain), plain)
+59
View File
@@ -728,6 +728,65 @@ MASKS = [
GAMERULES = re.compile(r'/gamerule \(([^)]*)\)')
ATTRIBUTES_LIST = re.compile(r'attributes: \[')
def unordered_attributes(record):
"""The record with every SNBT `attributes: [...]` list in its messages sorted by entry.
`AttributeMap.pack` lists the instances in the iteration order of an `Object2ObjectOpenHashMap` keyed by
`Holder.Reference`, which keeps `Object.hashCode`: an identity hash, so the order changes with the JVM's
startup (the same script gives another order under another main class). Each entry keeps its text and
styles; only the entries' order is normalised. parity_fuzz compares these lists unordered too."""
out = json.loads(json.dumps(record))
for line in out.get('messages') or []:
if not isinstance(line, list) or not all(isinstance(span, list) and len(span) == 2 for span in line):
continue
chars = [(ch, span[1]) for span in line for ch in span[0]]
text = ''.join(ch for ch, _ in chars)
for match in reversed(list(ATTRIBUTES_LIST.finditer(text))):
start = i = match.end()
depth, quoted, cuts = 0, False, [start]
while i < len(text):
ch = text[i]
if quoted:
if ch == '\\':
i += 1
elif ch == '"':
quoted = False
elif ch == '"':
quoted = True
elif ch in '[{':
depth += 1
elif ch in ']}':
if depth == 0:
break
depth -= 1
elif ch == ',' and depth == 0 and text[i + 1:i + 2] == ' ':
cuts += [i, i + 2]
i += 1
i += 1
if len(cuts) == 1:
continue
cuts.append(i)
entries = [chars[a:b] for a, b in zip(cuts[0::2], cuts[1::2])]
separator = chars[cuts[1]:cuts[2]]
ordered = sorted(entries, key=lambda e: json.dumps(e))
joined = ordered[0]
for entry in ordered[1:]:
joined = joined + separator + entry
chars[start:i] = joined
text = ''.join(ch for ch, _ in chars)
spans = []
for ch, style in chars:
if spans and spans[-1][1] == style:
spans[-1][0] += ch
else:
spans.append([ch, style])
line[:] = spans
return out
def masked(record):
text = json.dumps(record['messages'], ensure_ascii=False)
# GameRuleCommand registers the rules in identity-hash order, different
+4 -2
View File
@@ -34,8 +34,10 @@ if __name__ == '__main__':
source = build / 'vanilla.jsonl' if full else (reference or build / 'vanilla.jsonl')
if reference and not full:
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()]
# A pig's saved `attributes` list is in identity-hash order (driver.unordered_attributes).
expected = [driver.unordered_attributes(json.loads(s)) for s in source.read_text().splitlines()]
native = [driver.unordered_attributes(json.loads(s))
for s in (build / 'rust.jsonl').read_text().splitlines()]
assert len(expected) == len(native) == len(recorded)
for op, a, b in zip(recorded, expected, native):
if op['admission'] and not full: