feat: add block state data to all palettes

This commit is contained in:
Anon 2026-08-11 16:44:16 +02:00
parent f1d844afbe
commit cfb77088b6
21 changed files with 57289 additions and 26 deletions

View file

@ -1,3 +1,4 @@
using MinecraftClient.Mapping;
using MinecraftClient.Mapping.BlockPalettes;
namespace MinecraftClient.Tests;
@ -40,4 +41,68 @@ public sealed class BlockStatePropertiesTests
Assert.Empty(properties);
}
public static TheoryData<string, BlockPalette> ModernPalettes => new()
{
{ "1.13.2", new Palette113() },
{ "1.14.4", new Palette114() },
{ "1.15.2", new Palette115() },
{ "1.16.5", new Palette116() },
{ "1.17.1", new Palette117() },
{ "1.19.2", new Palette119() },
{ "1.19.3", new Palette1193() },
{ "1.19.4", new Palette1194() },
{ "1.20", new Palette120() },
{ "1.20.4", new Palette1204() },
{ "1.20.6", new Palette1206() },
{ "1.21.2", new Palette1212() },
{ "1.21.4", new Palette1214() },
{ "1.21.5", new Palette1215() },
{ "1.21.6", new Palette1216() },
{ "1.21.9", new Palette1219() },
{ "26.1", new Palette261() },
{ "26.2", new Palette262() }
};
[Theory]
[MemberData(nameof(ModernPalettes))]
public void EveryModernPaletteExposesOakLogAxis(string version, BlockPalette palette)
{
bool foundExpectedState = false;
for (int stateId = 0; stateId <= ushort.MaxValue; stateId++)
{
if (palette.FromId(stateId) != Material.OakLog)
continue;
IReadOnlyDictionary<string, string> properties = palette.GetStateProperties(stateId);
if (properties.TryGetValue("axis", out string? axis) && axis == "x")
{
foundExpectedState = true;
break;
}
}
Assert.True(foundExpectedState, $"Minecraft {version} did not expose oak_log[axis=x]");
}
[Fact]
public void LegacyPaletteExposesPackedMetadata()
{
BlockPalette previousPalette = Block.Palette;
try
{
Block.Palette = new Palette112();
Block block = new(17, 4);
IReadOnlyDictionary<string, string> properties = block.GetStateProperties();
Assert.Equal(276, block.StateId);
Assert.Equal("4", properties["metadata"]);
}
finally
{
Block.Palette = previousPalette;
}
}
}

View file

@ -89,6 +89,14 @@ namespace MinecraftClient.Mapping
/// </summary>
public IReadOnlyDictionary<string, string> GetStateProperties()
{
if (Palette.IdHasMetadata)
{
return new Dictionary<string, string>
{
["metadata"] = BlockMeta.ToString(System.Globalization.CultureInfo.InvariantCulture)
};
}
return Palette.GetStateProperties(StateId);
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -2402,6 +2402,7 @@ namespace MinecraftClient.Mapping.BlockPalettes
materials[i] = Material.FireflyBush;
}
// <auto-generated block-state-properties>
private static readonly BlockStateDefinition[] stateDefinitions =
[
new(8, 2,
@ -6856,6 +6857,7 @@ namespace MinecraftClient.Mapping.BlockPalettes
new("tip", ["true", "false"], 1)
]),
];
// </auto-generated block-state-properties>
protected override Dictionary<int, Material> GetDict()
{

View file

@ -17,23 +17,39 @@ 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:")
return "".join(word.capitalize() for word in name.split("_"))
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 open(MATERIAL_CS) as f:
with MATERIAL_CS.open(encoding="utf-8-sig") as f:
for line in f:
m = re.match(r'\s+(\w+),?\s*$', line)
if m:
@ -89,6 +105,74 @@ def csharp_string(value: str) -> str:
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 = [
" // <auto-generated block-state-properties>",
" 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 += [
" ];",
" // </auto-generated block-state-properties>",
]
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<int, Material> 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(" // <auto-generated block-state-properties>")
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__)
@ -101,7 +185,7 @@ def main():
print(f"Error: {blocks_json} not found")
sys.exit(1)
with open(blocks_json) as f:
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.
@ -130,7 +214,15 @@ def main():
print("Insert them in alphabetical order within the enum.")
class_name = f"Palette{class_suffix}"
output_path = OUTPUT_DIR / f"{class_name}.cs"
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;",
@ -152,28 +244,10 @@ def main():
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 += [
" ];",
*state_lines,
"",
]
lines += [
" protected override Dictionary<int, Material> GetDict()",
" {",
" return materials;",
@ -188,7 +262,7 @@ def main():
"",
]
output_path.write_text("\n".join(lines))
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"