2026-03-20 01:49:14 +08:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""
|
|
|
|
|
Compare Minecraft registry data between two decompiled server versions.
|
|
|
|
|
|
|
|
|
|
Compares Items, EntityTypes, Blocks, DataComponents, and EntityDataSerializers
|
|
|
|
|
to determine which MCC palettes need updating for a new MC version.
|
|
|
|
|
|
|
|
|
|
Usage:
|
2026-03-21 15:04:20 +08:00
|
|
|
python3 tools/diff_registries.py <old_version> <new_version> [--registry <registries.json>]
|
2026-03-20 01:49:14 +08:00
|
|
|
|
2026-03-21 15:04:20 +08:00
|
|
|
Examples:
|
2026-03-20 01:49:14 +08:00
|
|
|
python3 tools/diff_registries.py 1.20.6 1.21.1
|
2026-03-21 15:04:20 +08:00
|
|
|
python3 tools/diff_registries.py 1.21.8 1.21.9 --registry /tmp/mc_reports/reports/registries.json
|
|
|
|
|
|
|
|
|
|
The optional --registry flag cross-validates decompiled source counts against
|
|
|
|
|
the server's authoritative registries.json (generated via --reports).
|
|
|
|
|
Since MC 1.21.9, some items/blocks are registered outside Items.java/Blocks.java,
|
|
|
|
|
making this cross-validation essential for detecting hidden entries.
|
2026-03-20 01:49:14 +08:00
|
|
|
"""
|
|
|
|
|
|
2026-03-21 15:04:20 +08:00
|
|
|
import json
|
2026-03-20 01:49:14 +08:00
|
|
|
import re
|
|
|
|
|
import sys
|
|
|
|
|
import os
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
DECOMPILED_ROOT = Path(__file__).resolve().parent.parent / "MinecraftOfficial"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def find_java_file(version_dir: Path, *possible_paths: str) -> Path | None:
|
|
|
|
|
for p in possible_paths:
|
|
|
|
|
full = version_dir / p
|
|
|
|
|
if full.exists():
|
|
|
|
|
return full
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def extract_field_names(filepath: Path, pattern: str) -> list[str]:
|
|
|
|
|
"""Extract field names from public static final declarations."""
|
|
|
|
|
results = []
|
|
|
|
|
with open(filepath) as f:
|
|
|
|
|
for line in f:
|
|
|
|
|
m = re.match(pattern, line)
|
|
|
|
|
if m:
|
|
|
|
|
results.append(m.group(1))
|
|
|
|
|
return results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def extract_register_multiline(filepath: Path) -> list[str]:
|
|
|
|
|
"""Extract register("name", ...) calls, handling multiline Java formatting."""
|
|
|
|
|
with open(filepath) as f:
|
|
|
|
|
content = f.read()
|
|
|
|
|
flat = re.sub(r'\s+', ' ', content)
|
|
|
|
|
return re.findall(r'(?:= |return )register\(\s*"([^"]+)"', flat)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def extract_static_register_order(filepath: Path) -> list[str]:
|
|
|
|
|
"""Extract registerSerializer(FIELD_NAME) calls from the static {} block."""
|
|
|
|
|
results = []
|
|
|
|
|
in_static = False
|
|
|
|
|
with open(filepath) as f:
|
|
|
|
|
for line in f:
|
|
|
|
|
if 'static {' in line:
|
|
|
|
|
in_static = True
|
|
|
|
|
continue
|
|
|
|
|
if in_static and 'registerSerializer(' in line:
|
|
|
|
|
m = re.search(r'registerSerializer\((\w+)\)', line)
|
|
|
|
|
if m:
|
|
|
|
|
results.append(m.group(1))
|
|
|
|
|
if in_static and '}' in line and 'registerSerializer' not in line:
|
|
|
|
|
break
|
|
|
|
|
return results
|
|
|
|
|
|
|
|
|
|
|
2026-03-21 15:04:20 +08:00
|
|
|
def compare_lists(old: list[str], new: list[str], label: str) -> bool:
|
2026-03-20 01:49:14 +08:00
|
|
|
"""Compare two ordered lists and report differences."""
|
|
|
|
|
set_old, set_new = set(old), set(new)
|
|
|
|
|
added = sorted(set_new - set_old)
|
|
|
|
|
removed = sorted(set_old - set_new)
|
|
|
|
|
|
|
|
|
|
print(f"\n{'='*60}")
|
|
|
|
|
print(f" {label}")
|
|
|
|
|
print(f"{'='*60}")
|
|
|
|
|
print(f" Old: {len(old)} entries, New: {len(new)} entries")
|
|
|
|
|
|
|
|
|
|
if not added and not removed:
|
|
|
|
|
if old == new:
|
|
|
|
|
print(f" Result: IDENTICAL — no palette update needed")
|
|
|
|
|
else:
|
|
|
|
|
print(f" Result: Same set but DIFFERENT ORDER — palette update needed!")
|
|
|
|
|
for i, (a, b) in enumerate(zip(old, new)):
|
|
|
|
|
if a != b:
|
|
|
|
|
print(f" First diff at index {i}: old={a}, new={b}")
|
|
|
|
|
break
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
if added:
|
|
|
|
|
print(f" Added ({len(added)}): {added}")
|
|
|
|
|
for item in added:
|
|
|
|
|
idx = new.index(item)
|
|
|
|
|
prev_name = new[idx - 1] if idx > 0 else "(start)"
|
|
|
|
|
next_name = new[idx + 1] if idx < len(new) - 1 else "(end)"
|
|
|
|
|
print(f" \"{item}\" at index {idx}, between \"{prev_name}\" and \"{next_name}\"")
|
|
|
|
|
if removed:
|
|
|
|
|
print(f" Removed ({len(removed)}): {removed}")
|
|
|
|
|
|
|
|
|
|
common_old = [x for x in old if x in set_new]
|
|
|
|
|
common_new = [x for x in new if x in set_old]
|
|
|
|
|
if common_old != common_new:
|
|
|
|
|
print(f" Common items REORDERED — palette update needed!")
|
|
|
|
|
else:
|
|
|
|
|
print(f" Common items have same relative order")
|
|
|
|
|
|
|
|
|
|
# ID shift analysis
|
|
|
|
|
id_old = {name: i for i, name in enumerate(old)}
|
|
|
|
|
id_new = {name: i for i, name in enumerate(new)}
|
|
|
|
|
shifted = [(n, id_old[n], id_new[n]) for n in sorted(set_old & set_new) if id_old[n] != id_new[n]]
|
|
|
|
|
if shifted:
|
|
|
|
|
from collections import Counter
|
|
|
|
|
deltas = Counter(new_id - old_id for _, old_id, new_id in shifted)
|
|
|
|
|
print(f" {len(shifted)} entries with changed IDs, delta distribution: {sorted(deltas.items())}")
|
|
|
|
|
|
|
|
|
|
print(f" Result: PALETTE UPDATE NEEDED")
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
2026-03-21 15:04:20 +08:00
|
|
|
def cross_validate(registry_data: dict, java_entries: list[str], registry_key: str,
|
|
|
|
|
label: str, convert_fn=None):
|
|
|
|
|
"""Cross-validate Java source entries against server registries.json."""
|
|
|
|
|
reg = registry_data.get(registry_key, {}).get("entries", {})
|
|
|
|
|
server_names = set()
|
|
|
|
|
for key in reg:
|
|
|
|
|
name = key.removeprefix("minecraft:")
|
|
|
|
|
server_names.add(name)
|
|
|
|
|
|
|
|
|
|
if convert_fn:
|
|
|
|
|
java_names = set(convert_fn(n) for n in java_entries)
|
|
|
|
|
else:
|
|
|
|
|
java_names = set(n.lower() for n in java_entries)
|
|
|
|
|
|
|
|
|
|
server_count = len(server_names)
|
|
|
|
|
java_count = len(java_names)
|
|
|
|
|
|
|
|
|
|
print(f"\n --- Cross-validation: {label} ---")
|
|
|
|
|
print(f" Java source: {java_count} entries, Server registry: {server_count} entries")
|
|
|
|
|
|
|
|
|
|
if server_count == java_count:
|
|
|
|
|
print(f" ✓ Counts match — Java source is complete")
|
|
|
|
|
else:
|
|
|
|
|
diff = server_count - java_count
|
|
|
|
|
print(f" ⚠ Count MISMATCH: server has {diff:+d} entries vs Java source")
|
|
|
|
|
extra_in_server = server_names - java_names
|
|
|
|
|
extra_in_java = java_names - server_names
|
|
|
|
|
if extra_in_server:
|
|
|
|
|
print(f" In server but NOT in Java source ({len(extra_in_server)}):")
|
|
|
|
|
for n in sorted(extra_in_server):
|
|
|
|
|
pid = reg[f"minecraft:{n}"]["protocol_id"]
|
|
|
|
|
print(f" [{pid}] {n}")
|
|
|
|
|
print(f" ⚠ MUST use --from-registry / server data to generate palette!")
|
|
|
|
|
if extra_in_java:
|
|
|
|
|
print(f" In Java source but NOT in server ({len(extra_in_java)}):")
|
|
|
|
|
for n in sorted(extra_in_java):
|
|
|
|
|
print(f" {n}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def diff_items(old_dir: Path, new_dir: Path, registry_data: dict | None = None):
|
2026-03-20 01:49:14 +08:00
|
|
|
old_f = find_java_file(old_dir, "net/minecraft/world/item/Items.java")
|
|
|
|
|
new_f = find_java_file(new_dir, "net/minecraft/world/item/Items.java")
|
|
|
|
|
if not old_f or not new_f:
|
|
|
|
|
print(" [SKIP] Items.java not found")
|
|
|
|
|
return
|
|
|
|
|
pattern = r'\s+public static final Item (\w+)\s*='
|
|
|
|
|
old = extract_field_names(old_f, pattern)
|
|
|
|
|
new = extract_field_names(new_f, pattern)
|
|
|
|
|
compare_lists(old, new, "Items.java (Item registry)")
|
|
|
|
|
|
2026-03-21 15:04:20 +08:00
|
|
|
if registry_data:
|
|
|
|
|
cross_validate(registry_data, new, "minecraft:item", "Items",
|
|
|
|
|
convert_fn=lambda n: n.lower())
|
2026-03-20 01:49:14 +08:00
|
|
|
|
2026-03-21 15:04:20 +08:00
|
|
|
|
|
|
|
|
def diff_entity_types(old_dir: Path, new_dir: Path, registry_data: dict | None = None):
|
2026-03-20 01:49:14 +08:00
|
|
|
old_f = find_java_file(old_dir, "net/minecraft/world/entity/EntityType.java")
|
|
|
|
|
new_f = find_java_file(new_dir, "net/minecraft/world/entity/EntityType.java")
|
|
|
|
|
if not old_f or not new_f:
|
|
|
|
|
print(" [SKIP] EntityType.java not found")
|
|
|
|
|
return
|
|
|
|
|
old = extract_register_multiline(old_f)
|
|
|
|
|
new = extract_register_multiline(new_f)
|
|
|
|
|
compare_lists(old, new, "EntityType.java (Entity registry)")
|
|
|
|
|
|
2026-03-21 15:04:20 +08:00
|
|
|
if registry_data:
|
|
|
|
|
cross_validate(registry_data, new, "minecraft:entity_type", "EntityType",
|
|
|
|
|
convert_fn=lambda n: n)
|
|
|
|
|
|
2026-03-20 01:49:14 +08:00
|
|
|
|
2026-03-21 15:04:20 +08:00
|
|
|
def diff_blocks(old_dir: Path, new_dir: Path, registry_data: dict | None = None):
|
2026-03-20 01:49:14 +08:00
|
|
|
old_f = find_java_file(old_dir, "net/minecraft/world/level/block/Blocks.java")
|
|
|
|
|
new_f = find_java_file(new_dir, "net/minecraft/world/level/block/Blocks.java")
|
|
|
|
|
if not old_f or not new_f:
|
|
|
|
|
print(" [SKIP] Blocks.java not found")
|
|
|
|
|
return
|
|
|
|
|
old = extract_register_multiline(old_f)
|
|
|
|
|
new = extract_register_multiline(new_f)
|
|
|
|
|
compare_lists(old, new, "Blocks.java (Block registry)")
|
|
|
|
|
|
2026-03-21 15:04:20 +08:00
|
|
|
if registry_data:
|
|
|
|
|
cross_validate(registry_data, new, "minecraft:block", "Blocks",
|
|
|
|
|
convert_fn=lambda n: n)
|
|
|
|
|
|
2026-03-20 01:49:14 +08:00
|
|
|
|
|
|
|
|
def diff_data_components(old_dir: Path, new_dir: Path):
|
|
|
|
|
old_f = find_java_file(old_dir, "net/minecraft/core/component/DataComponents.java")
|
|
|
|
|
new_f = find_java_file(new_dir, "net/minecraft/core/component/DataComponents.java")
|
|
|
|
|
if not old_f or not new_f:
|
|
|
|
|
print(" [SKIP] DataComponents.java not found")
|
|
|
|
|
return
|
|
|
|
|
old = extract_register_multiline(old_f)
|
|
|
|
|
new = extract_register_multiline(new_f)
|
|
|
|
|
needs_update = compare_lists(old, new, "DataComponents.java (StructuredComponents registry)")
|
|
|
|
|
if needs_update or True:
|
|
|
|
|
print("\n Registration order (new version):")
|
|
|
|
|
for i, name in enumerate(new):
|
|
|
|
|
marker = " <-- NEW" if name not in set(old) else ""
|
|
|
|
|
print(f" {i}: {name}{marker}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def diff_entity_data_serializers(old_dir: Path, new_dir: Path):
|
|
|
|
|
old_f = find_java_file(old_dir, "net/minecraft/network/syncher/EntityDataSerializers.java")
|
|
|
|
|
new_f = find_java_file(new_dir, "net/minecraft/network/syncher/EntityDataSerializers.java")
|
|
|
|
|
if not old_f or not new_f:
|
|
|
|
|
print(" [SKIP] EntityDataSerializers.java not found")
|
|
|
|
|
return
|
|
|
|
|
old = extract_static_register_order(old_f)
|
|
|
|
|
new = extract_static_register_order(new_f)
|
|
|
|
|
needs_update = compare_lists(old, new, "EntityDataSerializers.java (EntityMetadata palette)")
|
|
|
|
|
print("\n Registration order (new version):")
|
|
|
|
|
for i, name in enumerate(new):
|
|
|
|
|
marker = " <-- NEW" if name not in set(old) else ""
|
|
|
|
|
print(f" {i}: {name}{marker}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
2026-03-21 15:04:20 +08:00
|
|
|
# Parse arguments
|
|
|
|
|
args = sys.argv[1:]
|
|
|
|
|
registry_path = None
|
|
|
|
|
|
|
|
|
|
if "--registry" in args:
|
|
|
|
|
idx = args.index("--registry")
|
|
|
|
|
if idx + 1 >= len(args):
|
|
|
|
|
print("Error: --registry requires a path argument")
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
registry_path = Path(args[idx + 1])
|
|
|
|
|
args = args[:idx] + args[idx + 2:]
|
|
|
|
|
|
|
|
|
|
if len(args) != 2:
|
2026-03-20 01:49:14 +08:00
|
|
|
print(__doc__)
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
2026-03-21 15:04:20 +08:00
|
|
|
old_ver, new_ver = args[0], args[1]
|
2026-03-20 01:49:14 +08:00
|
|
|
old_dir = DECOMPILED_ROOT / f"{old_ver}-decompiled"
|
|
|
|
|
new_dir = DECOMPILED_ROOT / f"{new_ver}-decompiled"
|
|
|
|
|
|
|
|
|
|
for d, v in [(old_dir, old_ver), (new_dir, new_ver)]:
|
|
|
|
|
if not d.exists():
|
|
|
|
|
print(f"Error: {d} not found. Decompile {v} first:")
|
|
|
|
|
print(f" cd MinecraftOfficial && java -jar MinecraftDecompiler.jar "
|
|
|
|
|
f"--version {v} --side SERVER --decompile "
|
|
|
|
|
f"--output {v}-remapped.jar --decompiled-output {v}-decompiled")
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
2026-03-21 15:04:20 +08:00
|
|
|
registry_data = None
|
|
|
|
|
if registry_path:
|
|
|
|
|
if not registry_path.exists():
|
|
|
|
|
print(f"Error: {registry_path} not found")
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
with open(registry_path) as f:
|
|
|
|
|
registry_data = json.load(f)
|
|
|
|
|
print(f"Loaded server registries.json for cross-validation")
|
|
|
|
|
|
2026-03-20 01:49:14 +08:00
|
|
|
print(f"Comparing MC {old_ver} → {new_ver}")
|
|
|
|
|
print(f"Old: {old_dir}")
|
|
|
|
|
print(f"New: {new_dir}")
|
|
|
|
|
|
2026-03-21 15:04:20 +08:00
|
|
|
diff_items(old_dir, new_dir, registry_data)
|
|
|
|
|
diff_entity_types(old_dir, new_dir, registry_data)
|
|
|
|
|
diff_blocks(old_dir, new_dir, registry_data)
|
2026-03-20 01:49:14 +08:00
|
|
|
diff_data_components(old_dir, new_dir)
|
|
|
|
|
diff_entity_data_serializers(old_dir, new_dir)
|
|
|
|
|
|
|
|
|
|
print(f"\n{'='*60}")
|
|
|
|
|
print(" Summary")
|
|
|
|
|
print(f"{'='*60}")
|
|
|
|
|
print(" Review each section above. For any marked 'PALETTE UPDATE NEEDED',")
|
|
|
|
|
print(" create a new palette file in MCC and update the version routing.")
|
|
|
|
|
print(" For 'IDENTICAL' sections, the existing palette can be reused.")
|
2026-03-21 15:04:20 +08:00
|
|
|
if registry_data:
|
|
|
|
|
print("\n Cross-validation was performed against server registries.json.")
|
|
|
|
|
print(" If any count mismatches were found, use server data generator output")
|
|
|
|
|
print(" (--from-registry) instead of decompiled Java source for palette generation.")
|
2026-03-20 01:49:14 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|