#!/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 pathlib import Path OUTPUT_DIR = (Path(__file__).resolve().parent.parent / "MinecraftClient" / "Mapping" / "BlockPalettes") MATERIAL_CS = OUTPUT_DIR.parent / "Material.cs" def mc_name_to_csharp(mc_name: str) -> str: """Convert minecraft:snake_case to PascalCase C# enum name.""" name = mc_name.removeprefix("minecraft:") return "".join(word.capitalize() for word in name.split("_")) def load_known_materials() -> set[str]: known = set() if MATERIAL_CS.exists(): with open(MATERIAL_CS) 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 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 open(blocks_json) 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 / f"{class_name}.cs" 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 += [ " }", "", " 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(" [") property_items = properties for index, (name, values, stride) in enumerate(property_items): encoded_values = ", ".join(csharp_string(value) for value in values) suffix = "," if index < len(property_items) - 1 else "" lines.append(f" new({csharp_string(name)}, [{encoded_values}], {stride}){suffix}") lines.append(" ]),") lines += [ " ];", "", " protected override Dictionary GetDict()", " {", " return materials;", " }", "", " protected override BlockStateDefinition[] GetStateDefinitions()", " {", " return stateDefinitions;", " }", " }", "}", "", ] output_path.write_text("\n".join(lines)) 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()