#!/usr/bin/env python3 """ Generate an MCC BlockPalette C# file from server-generated blocks.json. The blocks.json file is generated by running: java -DbundlerMainClass=net.minecraft.data.Main -jar server.jar --reports Usage: python3 tools/gen_block_palette.py Example: python3 tools/gen_block_palette.py /tmp/mc_reports/reports/blocks.json 1219 # Generates Palette1219.cs """ import json import math import re import sys from collections.abc import Sequence from pathlib import Path OUTPUT_DIR = (Path(__file__).resolve().parent.parent / "MinecraftClient" / "Mapping" / "BlockPalettes") MATERIAL_CS = OUTPUT_DIR.parent / "Material.cs" # Minecraft renamed these registry keys after the corresponding MCC Material # names had already stabilized. Keep historical reports mapped to the current # enum names instead of generating references to members that do not exist. MATERIAL_NAME_ALIASES = { "Grass": "ShortGrass", "GrassPath": "DirtPath", "Sign": "OakSign", "WallSign": "OakWallSign", } OUTPUT_FILE_ALIASES = { "120": "BlockPalette120.cs", } def mc_name_to_csharp(mc_name: str) -> str: """Convert minecraft:snake_case to PascalCase C# enum name.""" name = mc_name.removeprefix("minecraft:") csharp_name = "".join(word.capitalize() for word in name.split("_")) return MATERIAL_NAME_ALIASES.get(csharp_name, csharp_name) def load_known_materials() -> set[str]: known = set() if MATERIAL_CS.exists(): with MATERIAL_CS.open(encoding="utf-8-sig") as f: for line in f: m = re.match(r'\s+(\w+),?\s*$', line) if m: known.add(m.group(1)) return known def get_state_property_definitions( block_key: str, states: list[dict], properties: dict[str, list[str]], ) -> list[tuple[str, list[str], int]]: """Build and verify the compact stride schema against every reported state.""" if not properties: return [] ordered_states = sorted(states, key=lambda state: state["id"]) first_state_id = ordered_states[0]["id"] expected_ids = list(range(first_state_id, first_state_id + len(ordered_states))) actual_ids = [state["id"] for state in ordered_states] if actual_ids != expected_ids: raise ValueError(f"{block_key} has non-contiguous state IDs") expected_count = math.prod(len(values) for values in properties.values()) if expected_count != len(ordered_states): raise ValueError( f"{block_key} has {len(ordered_states)} states but its properties describe {expected_count} combinations" ) definitions = [] for name, values in properties.items(): stride = next( ( candidate for candidate in range(1, len(ordered_states) + 1) if all( state.get("properties", {}).get(name) == values[(offset // candidate) % len(values)] for offset, state in enumerate(ordered_states) ) ), None, ) if stride is None: raise ValueError(f"{block_key} property {name} has no regular state stride") definitions.append((name, values, stride)) return definitions def csharp_string(value: str) -> str: """Encode a Python string as a compatible C# string literal.""" return json.dumps(value, ensure_ascii=False) def render_state_definitions( block_ranges: Sequence[tuple[int, int, str, list[tuple[str, list[str], int]]]], ) -> tuple[list[str], int]: """Render the generated state-property section and return its definition count.""" lines = [ " // ", " private static readonly BlockStateDefinition[] stateDefinitions =", " [", ] property_definition_count = 0 for min_s, max_s, _, properties in block_ranges: if not properties: continue property_definition_count += 1 lines.append(f" new({min_s}, {max_s - min_s + 1},") lines.append(" [") for index, (name, values, stride) in enumerate(properties): encoded_values = ", ".join(csharp_string(value) for value in values) suffix = "," if index < len(properties) - 1 else "" lines.append(f" new({csharp_string(name)}, [{encoded_values}], {stride}){suffix}") lines.append(" ]),") lines += [ " ];", " // ", ] return lines, property_definition_count def update_existing_palette(output_path: Path, state_lines: list[str]) -> bool: """Replace only generated state metadata while preserving established material mappings.""" if not output_path.exists(): return False source = output_path.read_text(encoding="utf-8") dictionary_method = " protected override Dictionary GetDict()" dictionary_index = source.find(dictionary_method) if dictionary_index < 0: raise ValueError(f"{output_path} does not contain the expected GetDict method") generated_start = source.find(" // ") legacy_start = source.find(" private static readonly BlockStateDefinition[] stateDefinitions =") section_start = generated_start if generated_start >= 0 else legacy_start if section_start < 0: section_start = dictionary_index prefix = source[:section_start].rstrip() suffix = source[dictionary_index:] state_override = """ protected override BlockStateDefinition[] GetStateDefinitions() { return stateDefinitions; } """ suffix = suffix.replace("\n" + state_override, "", 1) class_end = suffix.rfind("\n }\n}") if class_end < 0: raise ValueError(f"{output_path} does not contain the expected class terminator") suffix = suffix[:class_end].rstrip() + "\n\n" + state_override.rstrip() + suffix[class_end:] output_path.write_text( prefix + "\n\n" + "\n".join(state_lines) + "\n\n" + suffix, encoding="utf-8", ) return True def main(): if len(sys.argv) != 3: print(__doc__) sys.exit(1) blocks_json = Path(sys.argv[1]) class_suffix = sys.argv[2] if not blocks_json.exists(): print(f"Error: {blocks_json} not found") sys.exit(1) with blocks_json.open(encoding="utf-8") as f: data = json.load(f) # Build block ranges and compact state-property definitions, sorted by min state. block_ranges = [] for block_key, block_info in data.items(): cs_name = mc_name_to_csharp(block_key) states = block_info.get("states", []) state_ids = [s["id"] for s in states] if state_ids: properties = get_state_property_definitions(block_key, states, block_info.get("properties", {})) block_ranges.append((min(state_ids), max(state_ids), cs_name, properties)) block_ranges.sort(key=lambda x: x[0]) print(f"Loaded {len(block_ranges)} blocks from {blocks_json}") max_state = max(r[1] for r in block_ranges) print(f"State ID range: 0 - {max_state}") known_materials = load_known_materials() missing = [cs for _, _, cs, _ in block_ranges if known_materials and cs not in known_materials] if missing: print(f"\nWARNING: {len(missing)} blocks not found in Material.cs enum:") for cs_name in missing: print(f" {cs_name}") print("\nYou need to add these to Material.cs before the palette will compile.") print("Insert them in alphabetical order within the enum.") class_name = f"Palette{class_suffix}" output_path = OUTPUT_DIR / OUTPUT_FILE_ALIASES.get(class_suffix, f"{class_name}.cs") state_lines, property_definition_count = render_state_definitions(block_ranges) if update_existing_palette(output_path, state_lines): print( f"Updated {output_path} with {property_definition_count} property definitions " "while preserving material mappings" ) return lines = [ "using System.Collections.Generic;", "", "namespace MinecraftClient.Mapping.BlockPalettes", "{", f" public class {class_name} : BlockPalette", " {", " private static readonly Dictionary materials = new();", "", f" static {class_name}()", " {", ] for min_s, max_s, cs_name, _ in block_ranges: lines.append(f" for (int i = {min_s}; i <= {max_s}; i++)") lines.append(f" materials[i] = Material.{cs_name};") lines += [ " }", "", *state_lines, "", ] lines += [ " protected override Dictionary GetDict()", " {", " return materials;", " }", "", " protected override BlockStateDefinition[] GetStateDefinitions()", " {", " return stateDefinitions;", " }", " }", "}", "", ] output_path.write_text("\n".join(lines), encoding="utf-8") print( f"Generated {output_path} with {len(block_ranges)} blocks, " f"{max_state + 1} total states, and {property_definition_count} property definitions" ) if __name__ == "__main__": main()