mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
feat: expose block state properties through MCP
feat: expose block state properties through MCP
This commit is contained in:
commit
a0710fd82f
25 changed files with 62001 additions and 15 deletions
108
MinecraftClient.Tests/BlockStatePropertiesTests.cs
Normal file
108
MinecraftClient.Tests/BlockStatePropertiesTests.cs
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
using MinecraftClient.Mapping;
|
||||||
|
using MinecraftClient.Mapping.BlockPalettes;
|
||||||
|
|
||||||
|
namespace MinecraftClient.Tests;
|
||||||
|
|
||||||
|
public sealed class BlockStatePropertiesTests
|
||||||
|
{
|
||||||
|
private readonly Palette262 _palette = new();
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(32162, "north", "true", "inactive")]
|
||||||
|
[InlineData(32168, "north", "false", "unlocking")]
|
||||||
|
[InlineData(32193, "east", "false", "ejecting")]
|
||||||
|
public void VaultStatesExposeAllProperties(
|
||||||
|
int stateId,
|
||||||
|
string facing,
|
||||||
|
string ominous,
|
||||||
|
string vaultState)
|
||||||
|
{
|
||||||
|
IReadOnlyDictionary<string, string> properties = _palette.GetStateProperties(stateId);
|
||||||
|
|
||||||
|
Assert.Equal(facing, properties["facing"]);
|
||||||
|
Assert.Equal(ominous, properties["ominous"]);
|
||||||
|
Assert.Equal(vaultState, properties["vault_state"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PropertiesUseServerReportedStateStride()
|
||||||
|
{
|
||||||
|
IReadOnlyDictionary<string, string> properties = _palette.GetStateProperties(3989);
|
||||||
|
|
||||||
|
Assert.Equal("left", properties["type"]);
|
||||||
|
Assert.Equal("north", properties["facing"]);
|
||||||
|
Assert.Equal("true", properties["waterlogged"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StateWithoutPropertiesReturnsEmptyMap()
|
||||||
|
{
|
||||||
|
IReadOnlyDictionary<string, string> properties = _palette.GetStateProperties(1);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using MinecraftClient.Mapping.BlockPalettes;
|
using MinecraftClient.Mapping.BlockPalettes;
|
||||||
using MinecraftClient.Protocol.Message;
|
using MinecraftClient.Protocol.Message;
|
||||||
|
|
@ -78,6 +79,27 @@ namespace MinecraftClient.Mapping
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exact raw block state ID. For Minecraft 1.12 and older this contains the packed block ID and metadata.
|
||||||
|
/// </summary>
|
||||||
|
public int StateId => blockIdAndMeta;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the properties associated with this block's exact state ID.
|
||||||
|
/// </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);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Material of the block
|
/// Material of the block
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace MinecraftClient.Mapping.BlockPalettes
|
namespace MinecraftClient.Mapping.BlockPalettes
|
||||||
{
|
{
|
||||||
|
|
@ -23,6 +24,46 @@ namespace MinecraftClient.Mapping.BlockPalettes
|
||||||
return Material.Air;
|
return Material.Air;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get block-state properties for a modern block state ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stateId">Raw block state ID.</param>
|
||||||
|
/// <returns>Block-state property names and values, or an empty map when unavailable.</returns>
|
||||||
|
public IReadOnlyDictionary<string, string> GetStateProperties(int stateId)
|
||||||
|
{
|
||||||
|
BlockStateDefinition[] definitions = GetStateDefinitions();
|
||||||
|
int low = 0;
|
||||||
|
int high = definitions.Length - 1;
|
||||||
|
|
||||||
|
while (low <= high)
|
||||||
|
{
|
||||||
|
int middle = low + ((high - low) / 2);
|
||||||
|
BlockStateDefinition definition = definitions[middle];
|
||||||
|
if (stateId < definition.FirstStateId)
|
||||||
|
{
|
||||||
|
high = middle - 1;
|
||||||
|
}
|
||||||
|
else if (stateId > definition.LastStateId)
|
||||||
|
{
|
||||||
|
low = middle + 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return definition.GetProperties(stateId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return BlockStateDefinition.EmptyProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get compact block-state definitions sorted by their first state ID.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual BlockStateDefinition[] GetStateDefinitions()
|
||||||
|
{
|
||||||
|
return Array.Empty<BlockStateDefinition>();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns TRUE if block ID uses old metadata encoding with ID and Meta inside one ushort
|
/// Returns TRUE if block ID uses old metadata encoding with ID and Meta inside one ushort
|
||||||
/// Only Palette112 should override this.
|
/// Only Palette112 should override this.
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,71 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
|
namespace MinecraftClient.Mapping.BlockPalettes
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Compact description of the property combinations in a contiguous block-state range.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BlockStateDefinition
|
||||||
|
{
|
||||||
|
private static readonly IReadOnlyDictionary<string, string> s_emptyProperties =
|
||||||
|
new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
|
||||||
|
|
||||||
|
private readonly BlockStatePropertyDefinition[] _properties;
|
||||||
|
public int FirstStateId { get; }
|
||||||
|
public int LastStateId { get; }
|
||||||
|
public static IReadOnlyDictionary<string, string> EmptyProperties => s_emptyProperties;
|
||||||
|
|
||||||
|
public BlockStateDefinition(int firstStateId, int stateCount, BlockStatePropertyDefinition[] properties)
|
||||||
|
{
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegative(firstStateId);
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(stateCount);
|
||||||
|
ArgumentNullException.ThrowIfNull(properties);
|
||||||
|
|
||||||
|
FirstStateId = firstStateId;
|
||||||
|
LastStateId = checked(firstStateId + stateCount - 1);
|
||||||
|
_properties = properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, string> GetProperties(int stateId)
|
||||||
|
{
|
||||||
|
if (stateId < FirstStateId || stateId > LastStateId)
|
||||||
|
return EmptyProperties;
|
||||||
|
|
||||||
|
int offset = stateId - FirstStateId;
|
||||||
|
Dictionary<string, string> result = new(_properties.Length, StringComparer.Ordinal);
|
||||||
|
for (int i = 0; i < _properties.Length; i++)
|
||||||
|
{
|
||||||
|
BlockStatePropertyDefinition property = _properties[i];
|
||||||
|
int valueIndex = (offset / property.Stride) % property.Values.Length;
|
||||||
|
result[property.Name] = property.Values[valueIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Property name and its values in Minecraft's block-state iteration order.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BlockStatePropertyDefinition
|
||||||
|
{
|
||||||
|
public string Name { get; }
|
||||||
|
public string[] Values { get; }
|
||||||
|
public int Stride { get; }
|
||||||
|
|
||||||
|
public BlockStatePropertyDefinition(string name, string[] values, int stride)
|
||||||
|
{
|
||||||
|
ArgumentException.ThrowIfNullOrEmpty(name);
|
||||||
|
ArgumentNullException.ThrowIfNull(values);
|
||||||
|
if (values.Length == 0)
|
||||||
|
throw new ArgumentException("A block-state property must define at least one value.", nameof(values));
|
||||||
|
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(stride);
|
||||||
|
|
||||||
|
Name = name;
|
||||||
|
Values = values;
|
||||||
|
Stride = stride;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
|
@ -1090,6 +1090,8 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
||||||
typeLabel,
|
typeLabel,
|
||||||
blockId = block.BlockId,
|
blockId = block.BlockId,
|
||||||
blockMeta = block.BlockMeta,
|
blockMeta = block.BlockMeta,
|
||||||
|
stateId = block.StateId,
|
||||||
|
properties = block.GetStateProperties(),
|
||||||
distance = Math.Sqrt(dx * dx + dy * dy + dz * dz)
|
distance = Math.Sqrt(dx * dx + dy * dy + dz * dz)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -1139,7 +1141,7 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
||||||
int cy = (int)Math.Floor(playerLocation.Y) - 1;
|
int cy = (int)Math.Floor(playerLocation.Y) - 1;
|
||||||
int cz = (int)Math.Floor(playerLocation.Z);
|
int cz = (int)Math.Floor(playerLocation.Z);
|
||||||
|
|
||||||
List<(int x, int y, int z, string material, string typeLabel, int blockId, byte blockMeta, double distance)> found = new();
|
List<(int x, int y, int z, string material, string typeLabel, int blockId, byte blockMeta, int stateId, IReadOnlyDictionary<string, string> properties, double distance)> found = new();
|
||||||
World world = client.GetWorld();
|
World world = client.GetWorld();
|
||||||
|
|
||||||
for (int y = cy - radius; y <= cy + radius && found.Count < limit; y++)
|
for (int y = cy - radius; y <= cy + radius && found.Count < limit; y++)
|
||||||
|
|
@ -1167,6 +1169,8 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
||||||
block.GetTypeString(),
|
block.GetTypeString(),
|
||||||
block.BlockId,
|
block.BlockId,
|
||||||
block.BlockMeta,
|
block.BlockMeta,
|
||||||
|
block.StateId,
|
||||||
|
block.GetStateProperties(),
|
||||||
Math.Sqrt(dx * dx + dy * dy + dz * dz)));
|
Math.Sqrt(dx * dx + dy * dy + dz * dz)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1190,6 +1194,8 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
||||||
entry.typeLabel,
|
entry.typeLabel,
|
||||||
entry.blockId,
|
entry.blockId,
|
||||||
entry.blockMeta,
|
entry.blockMeta,
|
||||||
|
entry.stateId,
|
||||||
|
entry.properties,
|
||||||
entry.distance
|
entry.distance
|
||||||
})
|
})
|
||||||
.ToArray()
|
.ToArray()
|
||||||
|
|
@ -1853,7 +1859,9 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
||||||
z,
|
z,
|
||||||
material = block.Type.ToString(),
|
material = block.Type.ToString(),
|
||||||
blockId = block.BlockId,
|
blockId = block.BlockId,
|
||||||
blockMeta = block.BlockMeta
|
blockMeta = block.BlockMeta,
|
||||||
|
stateId = block.StateId,
|
||||||
|
properties = block.GetStateProperties()
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -3121,7 +3129,9 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
|
||||||
material = block.Type.ToString(),
|
material = block.Type.ToString(),
|
||||||
typeLabel = block.GetTypeString(),
|
typeLabel = block.GetTypeString(),
|
||||||
blockId = block.BlockId,
|
blockId = block.BlockId,
|
||||||
blockMeta = block.BlockMeta
|
blockMeta = block.BlockMeta,
|
||||||
|
stateId = block.StateId,
|
||||||
|
properties = block.GetStateProperties()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ public sealed class MccBlockStateSnapshot
|
||||||
public required string TypeLabel { get; init; }
|
public required string TypeLabel { get; init; }
|
||||||
public required int BlockId { get; init; }
|
public required int BlockId { get; init; }
|
||||||
public required int BlockMeta { get; init; }
|
public required int BlockMeta { get; init; }
|
||||||
|
public required int StateId { get; init; }
|
||||||
|
public required IReadOnlyDictionary<string, string> Properties { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -143,7 +145,9 @@ public static class MccGameCommon
|
||||||
Material = block.Type.ToString(),
|
Material = block.Type.ToString(),
|
||||||
TypeLabel = block.GetTypeString(),
|
TypeLabel = block.GetTypeString(),
|
||||||
BlockId = block.BlockId,
|
BlockId = block.BlockId,
|
||||||
BlockMeta = block.BlockMeta
|
BlockMeta = block.BlockMeta,
|
||||||
|
StateId = block.StateId,
|
||||||
|
Properties = block.GetStateProperties()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,25 +14,42 @@ Example:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import math
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
from collections.abc import Sequence
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
OUTPUT_DIR = (Path(__file__).resolve().parent.parent /
|
OUTPUT_DIR = (Path(__file__).resolve().parent.parent /
|
||||||
"MinecraftClient" / "Mapping" / "BlockPalettes")
|
"MinecraftClient" / "Mapping" / "BlockPalettes")
|
||||||
MATERIAL_CS = OUTPUT_DIR.parent / "Material.cs"
|
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:
|
def mc_name_to_csharp(mc_name: str) -> str:
|
||||||
"""Convert minecraft:snake_case to PascalCase C# enum name."""
|
"""Convert minecraft:snake_case to PascalCase C# enum name."""
|
||||||
name = mc_name.removeprefix("minecraft:")
|
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]:
|
def load_known_materials() -> set[str]:
|
||||||
known = set()
|
known = set()
|
||||||
if MATERIAL_CS.exists():
|
if MATERIAL_CS.exists():
|
||||||
with open(MATERIAL_CS) as f:
|
with MATERIAL_CS.open(encoding="utf-8-sig") as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
m = re.match(r'\s+(\w+),?\s*$', line)
|
m = re.match(r'\s+(\w+),?\s*$', line)
|
||||||
if m:
|
if m:
|
||||||
|
|
@ -40,6 +57,122 @@ def load_known_materials() -> set[str]:
|
||||||
return known
|
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 = [
|
||||||
|
" // <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():
|
def main():
|
||||||
if len(sys.argv) != 3:
|
if len(sys.argv) != 3:
|
||||||
print(__doc__)
|
print(__doc__)
|
||||||
|
|
@ -52,17 +185,18 @@ def main():
|
||||||
print(f"Error: {blocks_json} not found")
|
print(f"Error: {blocks_json} not found")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
with open(blocks_json) as f:
|
with blocks_json.open(encoding="utf-8") as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
# Build (min_state, max_state, cs_name) for each block, sorted by min_state
|
# Build block ranges and compact state-property definitions, sorted by min state.
|
||||||
block_ranges = []
|
block_ranges = []
|
||||||
for block_key, block_info in data.items():
|
for block_key, block_info in data.items():
|
||||||
cs_name = mc_name_to_csharp(block_key)
|
cs_name = mc_name_to_csharp(block_key)
|
||||||
states = block_info.get("states", [])
|
states = block_info.get("states", [])
|
||||||
state_ids = [s["id"] for s in states]
|
state_ids = [s["id"] for s in states]
|
||||||
if state_ids:
|
if state_ids:
|
||||||
block_ranges.append((min(state_ids), max(state_ids), cs_name))
|
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])
|
block_ranges.sort(key=lambda x: x[0])
|
||||||
print(f"Loaded {len(block_ranges)} blocks from {blocks_json}")
|
print(f"Loaded {len(block_ranges)} blocks from {blocks_json}")
|
||||||
|
|
@ -71,7 +205,7 @@ def main():
|
||||||
print(f"State ID range: 0 - {max_state}")
|
print(f"State ID range: 0 - {max_state}")
|
||||||
|
|
||||||
known_materials = load_known_materials()
|
known_materials = load_known_materials()
|
||||||
missing = [cs for _, _, cs in block_ranges if known_materials and cs not in known_materials]
|
missing = [cs for _, _, cs, _ in block_ranges if known_materials and cs not in known_materials]
|
||||||
if missing:
|
if missing:
|
||||||
print(f"\nWARNING: {len(missing)} blocks not found in Material.cs enum:")
|
print(f"\nWARNING: {len(missing)} blocks not found in Material.cs enum:")
|
||||||
for cs_name in missing:
|
for cs_name in missing:
|
||||||
|
|
@ -80,7 +214,15 @@ def main():
|
||||||
print("Insert them in alphabetical order within the enum.")
|
print("Insert them in alphabetical order within the enum.")
|
||||||
|
|
||||||
class_name = f"Palette{class_suffix}"
|
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 = [
|
lines = [
|
||||||
"using System.Collections.Generic;",
|
"using System.Collections.Generic;",
|
||||||
|
|
@ -95,24 +237,36 @@ def main():
|
||||||
" {",
|
" {",
|
||||||
]
|
]
|
||||||
|
|
||||||
for min_s, max_s, cs_name in block_ranges:
|
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" for (int i = {min_s}; i <= {max_s}; i++)")
|
||||||
lines.append(f" materials[i] = Material.{cs_name};")
|
lines.append(f" materials[i] = Material.{cs_name};")
|
||||||
|
|
||||||
lines += [
|
lines += [
|
||||||
" }",
|
" }",
|
||||||
"",
|
"",
|
||||||
|
*state_lines,
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
lines += [
|
||||||
" protected override Dictionary<int, Material> GetDict()",
|
" protected override Dictionary<int, Material> GetDict()",
|
||||||
" {",
|
" {",
|
||||||
" return materials;",
|
" return materials;",
|
||||||
" }",
|
" }",
|
||||||
|
"",
|
||||||
|
" protected override BlockStateDefinition[] GetStateDefinitions()",
|
||||||
|
" {",
|
||||||
|
" return stateDefinitions;",
|
||||||
|
" }",
|
||||||
" }",
|
" }",
|
||||||
"}",
|
"}",
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
|
|
||||||
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 ({max_state + 1} total states)")
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue