2026-03-20 01:49:14 +08:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""
|
2026-03-21 15:04:20 +08:00
|
|
|
Generate an MCC ItemPalette C# file.
|
2026-03-20 01:49:14 +08:00
|
|
|
|
2026-03-21 15:04:20 +08:00
|
|
|
Supports two input modes:
|
|
|
|
|
1. Server registry (preferred since 1.21.9):
|
|
|
|
|
python3 tools/gen_item_palette.py --from-registry /tmp/mc_reports/reports/registries.json <suffix>
|
2026-03-20 01:49:14 +08:00
|
|
|
|
2026-03-21 15:04:20 +08:00
|
|
|
2. Decompiled Items.java (legacy):
|
|
|
|
|
python3 tools/gen_item_palette.py <mc_version> <suffix>
|
2026-03-20 01:49:14 +08:00
|
|
|
|
2026-03-21 15:04:20 +08:00
|
|
|
The --from-registry mode uses the server's authoritative protocol_id assignments,
|
|
|
|
|
which is required since MC 1.21.9 where some items are registered outside Items.java.
|
2026-03-20 01:49:14 +08:00
|
|
|
|
2026-03-21 15:04:20 +08:00
|
|
|
The <suffix> determines the class name (ItemPalette<suffix>) and should match MCC's
|
|
|
|
|
naming convention (e.g., 121 for 1.21, 1219 for 1.21.9).
|
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
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
DECOMPILED_ROOT = Path(__file__).resolve().parent.parent / "MinecraftOfficial"
|
|
|
|
|
OUTPUT_DIR = Path(__file__).resolve().parent.parent / "MinecraftClient" / "Inventory" / "ItemPalettes"
|
2026-03-21 15:04:20 +08:00
|
|
|
ITEM_TYPE_CS = OUTPUT_DIR.parent / "ItemType.cs"
|
2026-03-20 01:49:14 +08:00
|
|
|
|
|
|
|
|
OVERRIDES = {
|
|
|
|
|
"CUT_STANDSTONE_SLAB": "CutSandstoneSlab", # Mojang typo in source
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-03-21 15:04:20 +08:00
|
|
|
def mc_name_to_csharp(mc_name: str) -> str:
|
|
|
|
|
"""Convert minecraft:snake_case to PascalCase C# enum name."""
|
|
|
|
|
name = mc_name.removeprefix("minecraft:")
|
|
|
|
|
if name.upper() in OVERRIDES:
|
|
|
|
|
return OVERRIDES[name.upper()]
|
|
|
|
|
return "".join(word.capitalize() for word in name.split("_"))
|
|
|
|
|
|
|
|
|
|
|
2026-03-20 01:49:14 +08:00
|
|
|
def java_to_csharp_name(java_name: str) -> str:
|
|
|
|
|
"""Convert SCREAMING_SNAKE_CASE Java field name to PascalCase C# enum name."""
|
|
|
|
|
if java_name in OVERRIDES:
|
|
|
|
|
return OVERRIDES[java_name]
|
|
|
|
|
return "".join(word.capitalize() for word in java_name.lower().split("_"))
|
|
|
|
|
|
|
|
|
|
|
2026-03-21 15:04:20 +08:00
|
|
|
def load_known_enums() -> set[str]:
|
|
|
|
|
known = set()
|
|
|
|
|
if ITEM_TYPE_CS.exists():
|
|
|
|
|
with open(ITEM_TYPE_CS) as f:
|
|
|
|
|
for line in f:
|
|
|
|
|
m = re.match(r'\s+(\w+),?\s*$', line)
|
|
|
|
|
if m and m.group(1) not in ("Null", "Unknown"):
|
|
|
|
|
known.add(m.group(1))
|
|
|
|
|
return known
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def items_from_registry(registry_path: Path) -> list[tuple[int, str]]:
|
|
|
|
|
"""Load items from server registries.json, returns sorted (protocol_id, cs_name) pairs."""
|
|
|
|
|
with open(registry_path) as f:
|
|
|
|
|
data = json.load(f)
|
|
|
|
|
items_reg = data.get("minecraft:item", {}).get("entries", {})
|
|
|
|
|
result = []
|
|
|
|
|
for item_key, info in items_reg.items():
|
|
|
|
|
pid = info["protocol_id"]
|
|
|
|
|
cs_name = mc_name_to_csharp(item_key)
|
|
|
|
|
result.append((pid, cs_name))
|
|
|
|
|
result.sort(key=lambda x: x[0])
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def items_from_java(mc_version: str) -> list[tuple[int, str]]:
|
|
|
|
|
"""Load items from decompiled Items.java field declaration order."""
|
2026-03-20 01:49:14 +08:00
|
|
|
version_dir = DECOMPILED_ROOT / f"{mc_version}-decompiled"
|
|
|
|
|
items_java = version_dir / "net" / "minecraft" / "world" / "item" / "Items.java"
|
|
|
|
|
if not items_java.exists():
|
|
|
|
|
print(f"Error: {items_java} not found")
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
pattern = re.compile(r'\s+public static final Item (\w+)\s*=')
|
2026-03-21 15:04:20 +08:00
|
|
|
result = []
|
2026-03-20 01:49:14 +08:00
|
|
|
with open(items_java) as f:
|
|
|
|
|
for line in f:
|
|
|
|
|
m = pattern.match(line)
|
|
|
|
|
if m:
|
2026-03-21 15:04:20 +08:00
|
|
|
idx = len(result)
|
|
|
|
|
cs_name = java_to_csharp_name(m.group(1))
|
|
|
|
|
result.append((idx, cs_name))
|
|
|
|
|
return result
|
2026-03-20 01:49:14 +08:00
|
|
|
|
|
|
|
|
|
2026-03-21 15:04:20 +08:00
|
|
|
def main():
|
|
|
|
|
if len(sys.argv) < 3:
|
|
|
|
|
print(__doc__)
|
|
|
|
|
sys.exit(1)
|
2026-03-20 01:49:14 +08:00
|
|
|
|
2026-03-21 15:04:20 +08:00
|
|
|
from_registry = sys.argv[1] == "--from-registry"
|
|
|
|
|
|
|
|
|
|
if from_registry:
|
|
|
|
|
if len(sys.argv) != 4:
|
|
|
|
|
print("Usage: gen_item_palette.py --from-registry <registries.json> <suffix>")
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
registry_path = Path(sys.argv[2])
|
|
|
|
|
class_suffix = sys.argv[3]
|
|
|
|
|
if not registry_path.exists():
|
|
|
|
|
print(f"Error: {registry_path} not found")
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
mappings = items_from_registry(registry_path)
|
|
|
|
|
print(f"Loaded {len(mappings)} items from {registry_path}")
|
|
|
|
|
else:
|
|
|
|
|
mc_version = sys.argv[1]
|
|
|
|
|
class_suffix = sys.argv[2]
|
|
|
|
|
mappings = items_from_java(mc_version)
|
|
|
|
|
print(f"Found {len(mappings)} items in MC {mc_version} Items.java")
|
|
|
|
|
|
|
|
|
|
known_enums = load_known_enums()
|
|
|
|
|
missing = [(pid, cs) for pid, cs in mappings if known_enums and cs not in known_enums]
|
2026-03-20 01:49:14 +08:00
|
|
|
if missing:
|
|
|
|
|
print(f"\nWARNING: {len(missing)} items not found in ItemType.cs enum:")
|
2026-03-21 15:04:20 +08:00
|
|
|
for pid, cs_name in missing:
|
|
|
|
|
print(f" [{pid}] {cs_name}")
|
2026-03-20 01:49:14 +08:00
|
|
|
print("\nYou need to add these to ItemType.cs before the palette will compile.")
|
|
|
|
|
print("Insert them in alphabetical order within the enum.")
|
|
|
|
|
|
|
|
|
|
class_name = f"ItemPalette{class_suffix}"
|
|
|
|
|
output_path = OUTPUT_DIR / f"{class_name}.cs"
|
|
|
|
|
|
|
|
|
|
lines = [
|
|
|
|
|
"using System.Collections.Generic;",
|
|
|
|
|
"",
|
|
|
|
|
"namespace MinecraftClient.Inventory.ItemPalettes",
|
|
|
|
|
"{",
|
|
|
|
|
f" public class {class_name} : ItemPalette",
|
|
|
|
|
" {",
|
|
|
|
|
" private static readonly Dictionary<int, ItemType> mappings = new();",
|
|
|
|
|
"",
|
|
|
|
|
f" static {class_name}()",
|
|
|
|
|
" {",
|
|
|
|
|
]
|
2026-03-21 15:04:20 +08:00
|
|
|
for pid, cs_name in mappings:
|
|
|
|
|
lines.append(f" mappings[{pid}] = ItemType.{cs_name};")
|
2026-03-20 01:49:14 +08:00
|
|
|
lines += [
|
|
|
|
|
" }",
|
|
|
|
|
"",
|
|
|
|
|
" protected override Dictionary<int, ItemType> GetDict()",
|
|
|
|
|
" {",
|
|
|
|
|
" return mappings;",
|
|
|
|
|
" }",
|
|
|
|
|
" }",
|
|
|
|
|
"}",
|
|
|
|
|
"",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
output_path.write_text("\n".join(lines))
|
|
|
|
|
print(f"Generated {output_path} with {len(mappings)} mappings")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|