Minecraft-Console-Client/tools/diff_registries.py
BruceChen 34671fdab2 feat: enhance palette generation and validation for MC 1.21.9
Updated the SKILL.md documentation to include critical steps for generating server reports and validating decompiled source against server data, emphasizing the importance of using server data since MC 1.21.9. Enhanced the diff_registries.py script to support cross-validation with server registries.json, allowing for accurate palette generation. Added new scripts for generating block and entity palettes from server data, ensuring completeness and correctness of entries.

This update improves the workflow for adapting to new Minecraft versions and ensures that palette generation reflects the latest changes in item and block registration.

Made-with: Cursor
2026-03-21 15:04:20 +08:00

304 lines
12 KiB
Python

#!/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:
python3 tools/diff_registries.py <old_version> <new_version> [--registry <registries.json>]
Examples:
python3 tools/diff_registries.py 1.20.6 1.21.1
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.
"""
import json
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
def compare_lists(old: list[str], new: list[str], label: str) -> bool:
"""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
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):
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)")
if registry_data:
cross_validate(registry_data, new, "minecraft:item", "Items",
convert_fn=lambda n: n.lower())
def diff_entity_types(old_dir: Path, new_dir: Path, registry_data: dict | None = None):
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)")
if registry_data:
cross_validate(registry_data, new, "minecraft:entity_type", "EntityType",
convert_fn=lambda n: n)
def diff_blocks(old_dir: Path, new_dir: Path, registry_data: dict | None = None):
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)")
if registry_data:
cross_validate(registry_data, new, "minecraft:block", "Blocks",
convert_fn=lambda n: n)
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():
# 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:
print(__doc__)
sys.exit(1)
old_ver, new_ver = args[0], args[1]
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)
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")
print(f"Comparing MC {old_ver}{new_ver}")
print(f"Old: {old_dir}")
print(f"New: {new_dir}")
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)
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.")
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.")
if __name__ == "__main__":
main()