Compare commits

...

25 commits

Author SHA1 Message Date
Anon
d50e90d860
fix: send dedicated attack packet on 26.1+
Some checks failed
Build MCC and Documents / determine-build (push) Has been cancelled
Build MCC and Documents / fetch-translations (push) Has been cancelled
Build MCC and Documents / create-tag (push) Has been cancelled
Build MCC and Documents / build (linux-arm) (push) Has been cancelled
Build MCC and Documents / build (linux-arm64) (push) Has been cancelled
Build MCC and Documents / build (linux-x64) (push) Has been cancelled
Build MCC and Documents / build (osx-arm64) (push) Has been cancelled
Build MCC and Documents / build (osx-x64) (push) Has been cancelled
Build MCC and Documents / build (win-arm64) (push) Has been cancelled
Build MCC and Documents / build (win-x64) (push) Has been cancelled
Build MCC and Documents / build (win-x86) (push) Has been cancelled
Build MCC and Documents / create-release (push) Has been cancelled
fix: send dedicated attack packet on 26.1+
2026-08-11 19:02:13 +02:00
Anon
196dc54222 fix: send dedicated attack packet on 26.1+ 2026-08-11 18:43:06 +02:00
Anon
a0710fd82f
feat: expose block state properties through MCP
feat: expose block state properties through MCP
2026-08-11 17:57:33 +02:00
Anon
cfb77088b6 feat: add block state data to all palettes 2026-08-11 16:44:16 +02:00
Anon
f1d844afbe feat: expose block state properties through MCP 2026-08-11 14:51:13 +02:00
Anon
1fd4b0244d
[skipci]Merge pull request #3202 from MCCTeam/dependabot/npm_and_yarn/docs/mermaid-11.16.1
chore(deps-dev): bump mermaid from 11.15.0 to 11.16.1 in /docs
2026-08-09 19:55:10 +02:00
Anon
844fa7af2e
[skipci]Merge pull request #3201 from MCCTeam/dependabot/npm_and_yarn/docs/fast-uri-3.1.5
chore(deps): bump fast-uri from 3.1.4 to 3.1.5 in /docs
2026-08-09 19:55:01 +02:00
Anon
9523275eb6
[skipci]Merge pull request #3200 from MCCTeam/dependabot/npm_and_yarn/docs/undici-7.29.0
chore(deps): bump undici from 7.28.0 to 7.29.0 in /docs
2026-08-09 19:54:51 +02:00
Anon
8e11304bf9
fix: Colored Timestamps
It solves the problem of timestamps being colored according to the last message's color.
2026-08-09 19:54:22 +02:00
dependabot[bot]
34e9fd46fd
chore(deps-dev): bump mermaid from 11.15.0 to 11.16.1 in /docs
Bumps [mermaid](https://github.com/mermaid-js/mermaid) from 11.15.0 to 11.16.1.
- [Release notes](https://github.com/mermaid-js/mermaid/releases)
- [Commits](https://github.com/mermaid-js/mermaid/compare/mermaid@11.15.0...mermaid@11.16.1)

---
updated-dependencies:
- dependency-name: mermaid
  dependency-version: 11.16.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-08 04:51:56 +00:00
dependabot[bot]
15dd55d10a
chore(deps): bump fast-uri from 3.1.4 to 3.1.5 in /docs
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.4 to 3.1.5.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.4...v3.1.5)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-05 15:33:07 +00:00
dependabot[bot]
8f8e03b1c9
chore(deps): bump undici from 7.28.0 to 7.29.0 in /docs
Bumps [undici](https://github.com/nodejs/undici) from 7.28.0 to 7.29.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.28.0...v7.29.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 7.29.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-04 09:58:03 +00:00
Somersault
c9225aac58 Zaman damgalarının son mesaj rengine göre renklenme sorununu çözer. 2026-08-01 19:26:22 +03:00
Anon
7b100a3149
fix: accept semicolonless script using directives - #3196
fix: accept semicolonless script using directives
2026-07-31 19:17:40 +02:00
Anon
dbce402842 fix: accept semicolonless script using directives
Fixes #3195
2026-07-31 19:12:51 +02:00
Anon
503652a760
fix: decode modern chat type holders correctly 1.21.1+
fix: decode modern chat type holders correctly
2026-07-27 22:25:07 +02:00
Anon
3ae1d746fa fix: decode modern chat type holders correctly 2026-07-27 21:58:01 +02:00
Anon
4a68205f02
[skipci]Merge pull request #3190 from MCCTeam/dependabot/npm_and_yarn/docs/lodash-es-4.18.1
chore(deps): bump lodash-es from 4.17.23 to 4.18.1 in /docs
2026-07-27 19:14:14 +02:00
Anon
c377e52306
[skipci]Merge pull request #3191 from MCCTeam/dependabot/npm_and_yarn/docs/ws-8.21.1
chore(deps): bump ws from 8.20.0 to 8.21.1 in /docs
2026-07-27 19:14:04 +02:00
Anon
7fee87c98f
[skipci]Merge pull request #3192 from MCCTeam/dependabot/npm_and_yarn/docs/postcss-8.5.23
chore(deps): bump postcss from 8.5.14 to 8.5.23 in /docs
2026-07-27 19:13:57 +02:00
dependabot[bot]
07aa032214
chore(deps): bump postcss from 8.5.14 to 8.5.23 in /docs
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.14 to 8.5.23.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.14...8.5.23)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.23
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 16:57:56 +00:00
dependabot[bot]
6b0f1a8f77
chore(deps): bump ws from 8.20.0 to 8.21.1 in /docs
Bumps [ws](https://github.com/websockets/ws) from 8.20.0 to 8.21.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.20.0...8.21.1)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 8.21.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 16:57:44 +00:00
dependabot[bot]
a780312c19
chore(deps): bump lodash-es from 4.17.23 to 4.18.1 in /docs
Bumps [lodash-es](https://github.com/lodash/lodash) from 4.17.23 to 4.18.1.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.23...4.18.1)

---
updated-dependencies:
- dependency-name: lodash-es
  dependency-version: 4.18.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 16:57:40 +00:00
Anon
19f02dfc25
[skipci]chore: update build dependencies
chore: update build dependencies
2026-07-27 18:56:19 +02:00
Anon
dc472c3df6 chore: update build dependencies 2026-07-27 17:58:08 +02:00
41 changed files with 62436 additions and 156 deletions

View file

@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ModelContextProtocol" Version="1.2.0" />
<PackageReference Include="ModelContextProtocol" Version="1.4.1" />
</ItemGroup>
<ItemGroup>

View file

@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ModelContextProtocol" Version="1.2.0" />
<PackageReference Include="ModelContextProtocol" Version="1.4.1" />
</ItemGroup>
</Project>

View 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;
}
}
}

View file

@ -0,0 +1,14 @@
using MinecraftClient.Scripting;
namespace MinecraftClient.Tests;
public sealed class CSharpRunnerTests
{
[Theory]
[InlineData("//using MinecraftClient.CommandHandler", "using MinecraftClient.CommandHandler;")]
[InlineData("//using MinecraftClient.CommandHandler;", "using MinecraftClient.CommandHandler;")]
public void NormalizeUsingDirectiveAddsMissingSemicolon(string directive, string expected)
{
Assert.Equal(expected, CSharpRunner.NormalizeUsingDirective(directive));
}
}

View file

@ -0,0 +1,159 @@
using MinecraftClient.Protocol.Handlers;
using MinecraftClient.Protocol.Message;
namespace MinecraftClient.Tests;
public sealed class ChatTypeHolderTests
{
[Fact]
public void ReferenceHolderUsesOneBasedWireIdIn121AndNewer()
{
var dataTypes = new DataTypes(Protocol18Handler.MC_1_21_Version);
var packetData = new Queue<byte>(DataTypes.GetVarInt(2));
int chatTypeId = ChatParser.ReadChatTypeHolder(
dataTypes,
packetData,
Protocol18Handler.MC_1_21_Version,
out var directDecoration);
Assert.Equal(1, chatTypeId);
Assert.Null(directDecoration);
Assert.Empty(packetData);
}
[Fact]
public void RegistryIdRemainsUnchangedBefore121()
{
var dataTypes = new DataTypes(Protocol18Handler.MC_1_20_6_Version);
var packetData = new Queue<byte>(DataTypes.GetVarInt(2));
int chatTypeId = ChatParser.ReadChatTypeHolder(
dataTypes,
packetData,
Protocol18Handler.MC_1_20_6_Version,
out var directDecoration);
Assert.Equal(2, chatTypeId);
Assert.Null(directDecoration);
Assert.Empty(packetData);
}
[Fact]
public void DirectHolderConsumesChatAndNarrationDecorations()
{
var dataTypes = new DataTypes(Protocol18Handler.MC_1_21_Version);
var packetBytes = new List<byte>();
packetBytes.AddRange(DataTypes.GetVarInt(0));
AddDecoration(packetBytes, dataTypes, "chat.type.text", 0, 2);
AddDecoration(packetBytes, dataTypes, "chat.type.text.narrate", 0, 2);
var packetData = new Queue<byte>(packetBytes);
int chatTypeId = ChatParser.ReadChatTypeHolder(
dataTypes,
packetData,
Protocol18Handler.MC_1_21_Version,
out var directDecoration);
Assert.Equal(-1, chatTypeId);
Assert.NotNull(directDecoration);
Assert.Equal("chat.type.text", directDecoration.TranslationKey);
Assert.Equal(
[ChatParser.ChatTypeParameter.Sender, ChatParser.ChatTypeParameter.Content],
directDecoration.Parameters);
Assert.Empty(packetData);
}
[Fact]
public void RegistryDecorationControlsParameterSelectionAndOrdering()
{
Dictionary<int, ChatParser.MessageType>? originalChatTypes = ChatParser.ChatId2Type;
try
{
ChatParser.ClearChatTypeDecorations();
ChatParser.ChatId2Type = [];
var chatTypeData = new Dictionary<string, object>
{
["chat"] = new Dictionary<string, object>
{
["translation_key"] = "commands.message.display.outgoing",
["parameters"] = new object[] { "target", "content" }
}
};
ChatParser.ReadChatType(42, "example:custom_chat", chatTypeData);
var message = new ChatMessage(
"hello",
false,
42,
Guid.Empty,
null,
"Alice",
"Bob",
DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
null,
false);
string rendered = ChatParser.ParseSignedChat(message);
Assert.Equal("You whisper to Bob: hello", rendered);
}
finally
{
ChatParser.ClearChatTypeDecorations();
ChatParser.ChatId2Type = originalChatTypes;
}
}
[Fact]
public void UnknownTranslationKeyIsUsedAsVanillaFormatPattern()
{
Dictionary<int, ChatParser.MessageType>? originalChatTypes = ChatParser.ChatId2Type;
try
{
ChatParser.ClearChatTypeDecorations();
ChatParser.ChatId2Type = [];
var chatTypeData = new Dictionary<string, object>
{
["chat"] = new Dictionary<string, object>
{
["translation_key"] = "%s",
["parameters"] = new object[] { "sender", "content" }
}
};
ChatParser.ReadChatType(42, "ordinary:custom_chat", chatTypeData);
var message = new ChatMessage(
"hello",
false,
42,
Guid.Empty,
null,
"Alice » hello",
null,
DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
null,
false);
string rendered = ChatParser.ParseSignedChat(message);
Assert.Equal("Alice » hello", rendered);
}
finally
{
ChatParser.ClearChatTypeDecorations();
ChatParser.ChatId2Type = originalChatTypes;
}
}
private static void AddDecoration(
List<byte> packetBytes,
DataTypes dataTypes,
string translationKey,
params int[] parameters)
{
packetBytes.AddRange(dataTypes.GetString(translationKey));
packetBytes.AddRange(DataTypes.GetVarInt(parameters.Length));
foreach (int parameter in parameters)
packetBytes.AddRange(DataTypes.GetVarInt(parameter));
packetBytes.AddRange(dataTypes.GetNbtTag(new Dictionary<string, object>()));
}
}

View file

@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View file

@ -176,6 +176,7 @@ namespace MinecraftClient
return;
}
output.Append(str);
output.Append("§r");
Backend.WriteLineFormatted(output.ToString());
}
}

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using MinecraftClient.Mapping.BlockPalettes;
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>
/// Material of the block
/// </summary>

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
namespace MinecraftClient.Mapping.BlockPalettes
{
@ -23,6 +24,46 @@ namespace MinecraftClient.Mapping.BlockPalettes
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>
/// Returns TRUE if block ID uses old metadata encoding with ID and Meta inside one ushort
/// Only Palette112 should override this.

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -3796,6 +3796,8 @@ namespace MinecraftClient
{
UpdateKeepAlive();
Log.Debug(string.Format(Translations.protocol_chat_raw_message, message.content));
List<string> links = new();
string messageText;

View file

@ -1090,6 +1090,8 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
typeLabel,
blockId = block.BlockId,
blockMeta = block.BlockMeta,
stateId = block.StateId,
properties = block.GetStateProperties(),
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 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();
for (int y = cy - radius; y <= cy + radius && found.Count < limit; y++)
@ -1167,6 +1169,8 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
block.GetTypeString(),
block.BlockId,
block.BlockMeta,
block.StateId,
block.GetStateProperties(),
Math.Sqrt(dx * dx + dy * dy + dz * dz)));
}
}
@ -1190,6 +1194,8 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
entry.typeLabel,
entry.blockId,
entry.blockMeta,
entry.stateId,
entry.properties,
entry.distance
})
.ToArray()
@ -1853,7 +1859,9 @@ public sealed class MccMcpCapabilities : IMccMcpCapabilities
z,
material = block.Type.ToString(),
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(),
typeLabel = block.GetTypeString(),
blockId = block.BlockId,
blockMeta = block.BlockMeta
blockMeta = block.BlockMeta,
stateId = block.StateId,
properties = block.GetStateProperties()
};
}

View file

@ -38,25 +38,25 @@
<ItemGroup>
<PackageReference Include="Brigadier.NET" Version="1.2.13" />
<PackageReference Include="DiscordRichPresence" Version="1.143.0" />
<PackageReference Include="Consolonia" Version="11.3.12.3" />
<PackageReference Include="Consolonia" Version="11.3.12.6" />
<PackageReference Include="DnsClient" Version="1.8.0" />
<PackageReference Include="DSharpPlus" Version="4.5.1" />
<PackageReference Include="DSharpPlus" Version="4.5.2" />
<PackageReference Include="DynamicExpresso.Core" Version="2.19.3" />
<PackageReference Include="FuzzySharp" Version="2.0.2" />
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="14.11.1" />
<PackageReference Include="MessagePack" Version="3.1.4" />
<PackageReference Include="ModelContextProtocol" Version="1.2.0" />
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="1.2.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="14.15.0" />
<PackageReference Include="MessagePack" Version="3.1.8" />
<PackageReference Include="ModelContextProtocol" Version="1.4.1" />
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="1.4.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
<PackageReference Include="Microsoft.Windows.Compatibility" Version="10.0.5" />
<PackageReference Include="Microsoft.Windows.Compatibility" Version="10.0.10" />
<PackageReference Include="Samboy063.Tomlet" Version="6.2.0" />
<PackageReference Include="Sentry" Version="6.3.1" />
<PackageReference Include="Sentry" Version="6.8.0" />
<PackageReference Include="SingleFileExtractor.Core" Version="2.3.0" />
<PackageReference Include="starksoft.aspen" Version="1.1.8">
<NoWarn>NU1701</NoWarn>
</PackageReference>
<PackageReference Include="Telegram.Bot" Version="22.9.5.3" />
<PackageReference Include="Telegram.Bot" Version="22.10.2.1" />
</ItemGroup>
<ItemGroup>
<Compile Remove="config\**\*.cs" />

View file

@ -287,6 +287,7 @@ namespace MinecraftClient.Protocol.Handlers
},
_ => ChatParser.ChatId2Type
};
ChatParser.ClearChatTypeDecorations();
}
/// <summary>
@ -576,7 +577,6 @@ namespace MinecraftClient.Protocol.Handlers
var isEnchantment = registryId == "minecraft:enchantment";
var isDialog = registryId == "minecraft:dialog";
var availableChats = isChat ? new Dictionary<int, string>() : null;
var dimensionIdMap = isDimension ? new Dictionary<int, string>() : null;
var attributeIdMap = isAttribute ? new Dictionary<int, string>() : null;
var enchantmentIdMap = isEnchantment ? new Dictionary<int, string>() : null;
@ -591,7 +591,7 @@ namespace MinecraftClient.Protocol.Handlers
nbtData = dataTypes.ReadNextNbt(packetData);
if (isChat)
availableChats!.Add(i, entryId);
ChatParser.ReadChatType(i, entryId, nbtData);
else if (isDimension)
{
dimensionIdMap!.Add(i, entryId);
@ -611,9 +611,7 @@ namespace MinecraftClient.Protocol.Handlers
handler.OnDialogRegistryData(i, entryId, dialogNbtParser.Parse(nbtData));
}
if (isChat)
ChatParser.ReadChatType(availableChats!);
else if (isDimension)
if (isDimension)
{
World.SetDimensionIdMap(dimensionIdMap!);
if (!handler.GetTerrainEnabled() || !World.HasAnyDimension())
@ -1232,7 +1230,8 @@ namespace MinecraftClient.Protocol.Handlers
// Network Target
// net.minecraft.network.message.MessageType.Serialized#write
var chatTypeId = dataTypes.ReadNextVarInt(packetData);
var chatTypeId = ChatParser.ReadChatTypeHolder(
dataTypes, packetData, protocolVersion, out var directChatTypeDecoration);
var chatName = dataTypes.ReadNextChat(packetData);
var targetName = dataTypes.ReadNextBool(packetData)
? dataTypes.ReadNextChat(packetData)
@ -1281,7 +1280,10 @@ namespace MinecraftClient.Protocol.Handlers
}
ChatMessage chat = new(message, false, chatTypeId, senderUuid, unsignedChatContent,
senderDisplayName, senderTeamName, timestamp, messageSignature, verifyResult);
senderDisplayName, senderTeamName, timestamp, messageSignature, verifyResult)
{
chatTypeDecoration = directChatTypeDecoration
};
lock (MessageSigningLock)
Acknowledge(chat);
handler.OnTextReceived(chat);
@ -1345,14 +1347,17 @@ namespace MinecraftClient.Protocol.Handlers
break;
case PacketTypesIn.ProfilelessChatMessage:
var message_ = dataTypes.ReadNextChat(packetData);
var messageType_ = dataTypes.ReadNextVarInt(packetData);
var messageType_ = ChatParser.ReadChatTypeHolder(
dataTypes, packetData, protocolVersion, out var directProfilelessChatTypeDecoration);
var messageName = dataTypes.ReadNextChat(packetData);
var targetName_ = dataTypes.ReadNextBool(packetData)
? dataTypes.ReadNextChat(packetData)
: null;
ChatMessage profilelessChat = new(message_, targetName_ ?? messageName, false, messageType_,
ChatMessage profilelessChat = new(message_, messageName, false, messageType_,
Guid.Empty, true);
profilelessChat.isSenderJson = false;
profilelessChat.teamName = targetName_;
profilelessChat.chatTypeDecoration = directProfilelessChatTypeDecoration;
handler.OnTextReceived(profilelessChat);
break;
case PacketTypesIn.CombatEvent:
@ -5716,6 +5721,13 @@ namespace MinecraftClient.Protocol.Handlers
{
List<byte> fields = new();
fields.AddRange(DataTypes.GetVarInt(EntityID));
if (protocolVersion >= MC_26_1_Version && type == (int)InteractType.Attack)
{
SendPacket(PacketTypesOut.Attack, fields);
return true;
}
fields.AddRange(DataTypes.GetVarInt(type));
// Is player Sneaking (Only 1.16 and above)

View file

@ -35,6 +35,8 @@ namespace MinecraftClient.Protocol.Message
public bool? isSignatureLegal;
internal ChatParser.ChatTypeDecoration? chatTypeDecoration;
public ChatMessage(string content, bool isJson, int chatType, Guid senderUUID, string? unsignedContent, string displayName, string? teamName, long timestamp, byte[]? signature, bool isSignatureLegal)
{
isSignedChat = true;

View file

@ -11,6 +11,7 @@ using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using MinecraftClient.Protocol.Handlers;
using Tomlet;
using Tomlet.Models;
using static MinecraftClient.Settings;
@ -36,33 +37,49 @@ namespace MinecraftClient.Protocol.Message
public static Dictionary<int, MessageType>? ChatId2Type;
internal enum ChatTypeParameter
{
Sender,
Target,
Content
}
internal sealed record ChatTypeDecoration(string TranslationKey, ChatTypeParameter[] Parameters);
private static readonly Dictionary<int, ChatTypeDecoration> ChatId2Decoration = new();
internal static void ClearChatTypeDecorations()
{
ChatId2Decoration.Clear();
}
// Used to store Chat Types in 1.20.6+
public static void ReadChatType(Dictionary<int, string> data)
public static void ReadChatType(int chatId, string chatName, Dictionary<string, object>? chatTypeData)
{
var chatTypeDictionary = ChatId2Type ?? new Dictionary<int, MessageType>();
foreach (var (chatId, chatName) in data)
chatTypeDictionary[chatId] = chatName switch
{
chatTypeDictionary[chatId] = chatName switch
{
"minecraft:chat" => MessageType.CHAT,
"minecraft:emote_command" => MessageType.EMOTE_COMMAND,
"minecraft:msg_command_incoming" => MessageType.MSG_COMMAND_INCOMING,
"minecraft:msg_command_outgoing" => MessageType.MSG_COMMAND_OUTGOING,
"minecraft:say_command" => MessageType.SAY_COMMAND,
"minecraft:team_msg_command_incoming" => MessageType.TEAM_MSG_COMMAND_INCOMING,
"minecraft:team_msg_command_outgoing" => MessageType.TEAM_MSG_COMMAND_OUTGOING,
_ => MessageType.CHAT,
};
}
"minecraft:chat" => MessageType.CHAT,
"minecraft:emote_command" => MessageType.EMOTE_COMMAND,
"minecraft:msg_command_incoming" => MessageType.MSG_COMMAND_INCOMING,
"minecraft:msg_command_outgoing" => MessageType.MSG_COMMAND_OUTGOING,
"minecraft:say_command" => MessageType.SAY_COMMAND,
"minecraft:team_msg_command_incoming" => MessageType.TEAM_MSG_COMMAND_INCOMING,
"minecraft:team_msg_command_outgoing" => MessageType.TEAM_MSG_COMMAND_OUTGOING,
_ => MessageType.CHAT,
};
ChatId2Type = chatTypeDictionary;
if (TryReadChatTypeDecoration(chatTypeData, out var decoration))
ChatId2Decoration[chatId] = decoration;
else
ChatId2Decoration.Remove(chatId);
}
public static void ReadChatType(Dictionary<string, object> registryCodec)
{
Dictionary<int, MessageType> chatTypeDictionary = ChatId2Type ?? new();
// Check if the chat type registry is in the correct format
if (!registryCodec.ContainsKey("minecraft:chat_type"))
{
@ -84,25 +101,80 @@ namespace MinecraftClient.Protocol.Message
}
var chatTypeListNbt = (object[])(((Dictionary<string, object>)registryCodec["minecraft:chat_type"])["value"]);
foreach (var (chatName, chatId) in from Dictionary<string, object> chatTypeNbt in chatTypeListNbt
let chatName = (string)chatTypeNbt["name"]
let chatId = (int)chatTypeNbt["id"]
select (chatName, chatId))
foreach (Dictionary<string, object> chatTypeNbt in chatTypeListNbt)
{
chatTypeDictionary[chatId] = chatName switch
string chatName = (string)chatTypeNbt["name"];
int chatId = (int)chatTypeNbt["id"];
var chatTypeData = chatTypeNbt.TryGetValue("element", out var element)
? element as Dictionary<string, object>
: null;
ReadChatType(chatId, chatName, chatTypeData);
}
}
internal static int ReadChatTypeHolder(
DataTypes dataTypes,
Queue<byte> packetData,
int protocolVersion,
out ChatTypeDecoration? directDecoration)
{
int encodedId = dataTypes.ReadNextVarInt(packetData);
directDecoration = null;
if (protocolVersion < Protocol18Handler.MC_1_21_Version)
return encodedId;
if (encodedId > 0)
return encodedId - 1;
directDecoration = ReadNetworkChatTypeDecoration(dataTypes, packetData);
_ = ReadNetworkChatTypeDecoration(dataTypes, packetData); // Narration decoration
return -1;
}
private static ChatTypeDecoration ReadNetworkChatTypeDecoration(
DataTypes dataTypes,
Queue<byte> packetData)
{
string translationKey = dataTypes.ReadNextString(packetData);
int parameterCount = dataTypes.ReadNextVarInt(packetData);
var parameters = new ChatTypeParameter[parameterCount];
for (int i = 0; i < parameterCount; i++)
parameters[i] = (ChatTypeParameter)dataTypes.ReadNextVarInt(packetData);
_ = dataTypes.ReadNextNbtTag(packetData); // Style
return new ChatTypeDecoration(translationKey, parameters);
}
private static bool TryReadChatTypeDecoration(
Dictionary<string, object>? chatTypeData,
[NotNullWhen(true)] out ChatTypeDecoration? decoration)
{
decoration = null;
if (chatTypeData is null
|| !chatTypeData.TryGetValue("chat", out var chat)
|| chat is not Dictionary<string, object> chatDecoration
|| !chatDecoration.TryGetValue("translation_key", out var translationKey)
|| translationKey is not string translationKeyText
|| !chatDecoration.TryGetValue("parameters", out var parameters)
|| parameters is not object[] parameterList)
return false;
var parsedParameters = new ChatTypeParameter[parameterList.Length];
for (int i = 0; i < parameterList.Length; i++)
{
parsedParameters[i] = parameterList[i] switch
{
"minecraft:chat" => MessageType.CHAT,
"minecraft:emote_command" => MessageType.EMOTE_COMMAND,
"minecraft:msg_command_incoming" => MessageType.MSG_COMMAND_INCOMING,
"minecraft:msg_command_outgoing" => MessageType.MSG_COMMAND_OUTGOING,
"minecraft:say_command" => MessageType.SAY_COMMAND,
"minecraft:team_msg_command_incoming" => MessageType.TEAM_MSG_COMMAND_INCOMING,
"minecraft:team_msg_command_outgoing" => MessageType.TEAM_MSG_COMMAND_OUTGOING,
_ => MessageType.CHAT,
"sender" => ChatTypeParameter.Sender,
"target" => ChatTypeParameter.Target,
"content" => ChatTypeParameter.Content,
_ => ChatTypeParameter.Sender
};
}
ChatId2Type = chatTypeDictionary;
decoration = new ChatTypeDecoration(translationKeyText, parsedParameters);
return true;
}
/// <summary>
@ -147,6 +219,26 @@ namespace MinecraftClient.Protocol.Message
string text;
List<string> usingData = new();
ChatTypeDecoration? decoration = message.chatTypeDecoration;
if (decoration is null)
ChatId2Decoration.TryGetValue(message.chatTypeId, out decoration);
if (decoration is not null)
{
foreach (var parameter in decoration.Parameters)
{
usingData.Add(parameter switch
{
ChatTypeParameter.Sender => sender,
ChatTypeParameter.Target => message.teamName ?? string.Empty,
ChatTypeParameter.Content => content,
_ => string.Empty
});
}
return TranslateString(decoration.TranslationKey, usingData);
}
MessageType chatType;
if (message.chatTypeId == -1)
chatType = MessageType.RAW_MSG;
@ -557,48 +649,47 @@ namespace MinecraftClient.Protocol.Message
RulesInitialized = true;
}
if (TryGetTranslationRule(rulename, out string? rule))
{
int using_idx = 0;
StringBuilder result = new();
for (int i = 0; i < rule.Length; i++)
{
if (rule[i] == '%' && i + 1 < rule.Length)
{
//Using string or int with %s or %d
if (rule[i + 1] == 's' || rule[i + 1] == 'd')
{
if (using_data.Count > using_idx)
{
result.Append(using_data[using_idx]);
using_idx++;
i += 1;
continue;
}
}
if (!TryGetTranslationRule(rulename, out string? rule))
rule = rulename;
//Using specified string or int with %1$s, %2$s...
else if (char.IsDigit(rule[i + 1])
&& i + 3 < rule.Length && rule[i + 2] == '$'
&& (rule[i + 3] == 's' || rule[i + 3] == 'd'))
int using_idx = 0;
StringBuilder result = new();
for (int i = 0; i < rule.Length; i++)
{
if (rule[i] == '%' && i + 1 < rule.Length)
{
//Using string or int with %s or %d
if (rule[i + 1] == 's' || rule[i + 1] == 'd')
{
if (using_data.Count > using_idx)
{
int specified_idx = rule[i + 1] - '1';
if (using_data.Count > specified_idx)
{
result.Append(using_data[specified_idx]);
using_idx++;
i += 3;
continue;
}
result.Append(using_data[using_idx]);
using_idx++;
i += 1;
continue;
}
}
result.Append(rule[i]);
//Using specified string or int with %1$s, %2$s...
else if (char.IsDigit(rule[i + 1])
&& i + 3 < rule.Length && rule[i + 2] == '$'
&& (rule[i + 3] == 's' || rule[i + 3] == 'd'))
{
int specified_idx = rule[i + 1] - '1';
if (using_data.Count > specified_idx)
{
result.Append(using_data[specified_idx]);
using_idx++;
i += 3;
continue;
}
}
}
return result.ToString();
result.Append(rule[i]);
}
else return "[" + rulename + "] " + string.Join(" ", using_data);
return result.ToString();
}
private static bool TryGetTranslationRule(string rulename, [NotNullWhen(true)] out string? result)

View file

@ -8296,5 +8296,9 @@ namespace MinecraftClient {
get { return ResourceManager.GetString("dialog.render.help_hint", resourceCulture); }
}
internal static string protocol_chat_raw_message {
get { return ResourceManager.GetString("protocol.chat.raw_message", resourceCulture); }
}
}
}

View file

@ -3127,4 +3127,7 @@ see item details.</value>
<data name="dialog.render.help_hint" xml:space="preserve">
<value>Use /dialog help for a list of commands.</value>
</data>
<data name="protocol.chat.raw_message" xml:space="preserve">
<value>Raw server chat message: {0}</value>
</data>
</root>

View file

@ -60,7 +60,7 @@ namespace MinecraftClient.Scripting
string line = lines[i];
if (line.StartsWith("//using"))
{
libs.Add(line.Replace("//", "").Trim());
libs.Add(NormalizeUsingDirective(line));
}
else if (line.StartsWith("//dll"))
{
@ -125,6 +125,12 @@ namespace MinecraftClient.Scripting
else return null;
}
internal static string NormalizeUsingDirective(string line)
{
string directive = line[2..].Trim();
return directive.EndsWith(';') ? directive : $"{directive};";
}
private static string BuildScriptCode(string scriptName, IEnumerable<ScriptSourceLine> script, IEnumerable<ScriptSourceLine> extensions, IEnumerable<string> libs, bool hasImplicitReturn)
{
StringBuilder codeBuilder = new();

View file

@ -26,6 +26,8 @@ public sealed class MccBlockStateSnapshot
public required string TypeLabel { get; init; }
public required int BlockId { get; init; }
public required int BlockMeta { get; init; }
public required int StateId { get; init; }
public required IReadOnlyDictionary<string, string> Properties { get; init; }
}
/// <summary>
@ -143,7 +145,9 @@ public static class MccGameCommon
Material = block.Type.ToString(),
TypeLabel = block.GetTypeString(),
BlockId = block.BlockId,
BlockMeta = block.BlockMeta
BlockMeta = block.BlockMeta,
StateId = block.StateId,
Properties = block.GetStateProperties()
};
}

View file

@ -20,7 +20,7 @@
"@vuepress/plugin-search": "2.0.0-rc.125",
"@vuepress/plugin-shiki": "2.0.0-rc.125",
"@vuepress/theme-default": "2.0.0-rc.125",
"mermaid": "11.15.0",
"mermaid": "11.16.1",
"sass-embedded": "1.98.0",
"sass-loader": "16.0.7",
"vuepress": "2.0.0-rc.26"

View file

@ -56,7 +56,7 @@
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.28.5"
"@braintree/sanitize-url@^7.1.1":
"@braintree/sanitize-url@^7.1.2":
version "7.1.2"
resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz#ca2035b0fefe956a8676ff0c69af73e605fcd81f"
integrity sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==
@ -66,7 +66,7 @@
resolved "https://registry.yarnpkg.com/@bufbuild/protobuf/-/protobuf-2.11.0.tgz#3ec3985c9074b23aea337957225fe15a0e845f8e"
integrity sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==
"@chevrotain/types@~11.1.1":
"@chevrotain/types@~11.1.2":
version "11.1.2"
resolved "https://registry.yarnpkg.com/@chevrotain/types/-/types-11.1.2.tgz#e83a1a2704f0c5e49e7592b214031a0f4a34d7e5"
integrity sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==
@ -739,12 +739,12 @@
"@mdit/helper" "0.23.1"
"@types/markdown-it" "^14.1.2"
"@mermaid-js/parser@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@mermaid-js/parser/-/parser-1.1.1.tgz#30f3ab68d816912e43f245a72a0d4081bf69d966"
integrity sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==
"@mermaid-js/parser@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@mermaid-js/parser/-/parser-1.2.0.tgz#266d728c54d2d4034d270f8b31d790e26296a5fa"
integrity sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==
dependencies:
"@chevrotain/types" "~11.1.1"
"@chevrotain/types" "~11.1.2"
"@noble/hashes@1.4.0":
version "1.4.0"
@ -3246,10 +3246,10 @@ cytoscape-fcose@^2.2.0:
dependencies:
cose-base "^2.2.0"
cytoscape@^3.33.1:
version "3.33.1"
resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.33.1.tgz#449e05d104b760af2912ab76482d24c01cdd4c97"
integrity sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==
cytoscape@^3.33.3:
version "3.34.0"
resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.34.0.tgz#5fbe2eb1cf76b070a8ecd5647c35f65aa097c9c6"
integrity sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==
"d3-array@1 - 2":
version "2.12.1"
@ -3530,10 +3530,10 @@ dagre-d3-es@7.0.14:
d3 "^7.9.0"
lodash-es "^4.17.21"
dayjs@^1.11.19:
version "1.11.20"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.20.tgz#88d919fd639dc991415da5f4cb6f1b6650811938"
integrity sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==
dayjs@^1.11.20:
version "1.11.21"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.21.tgz#57f87562e62de76f3c704bd2b8d522fc33068eb2"
integrity sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==
debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
version "2.6.9"
@ -3701,10 +3701,10 @@ domhandler@^5.0.2, domhandler@^5.0.3:
dependencies:
domelementtype "^2.3.0"
dompurify@^3.3.1:
version "3.4.12"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.12.tgz#6fa2265e9bbdce882c4ace4107626051b448ffa8"
integrity sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==
dompurify@^3.3.3:
version "3.4.13"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.13.tgz#fc28949d59f92d62e28a3a764bcbeee35897a1be"
integrity sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==
optionalDependencies:
"@types/trusted-types" "^2.0.7"
@ -4077,9 +4077,9 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-uri@^3.0.1:
version "3.1.4"
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.4.tgz#3b3daf9ce68f41f956df0b505132c0cfce9ec7af"
integrity sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==
version "3.1.5"
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.5.tgz#610f37419a030270430cecd68d74e3d4d96725d0"
integrity sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==
faye-websocket@^0.11.3:
version "0.11.4"
@ -4908,10 +4908,10 @@ jsonfile@^6.0.1:
optionalDependencies:
graceful-fs "^4.1.6"
katex@^0.16.25:
version "0.16.40"
resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.40.tgz#87c94e4149f8fa7c22ff95bae1dc687355a38d63"
integrity sha512-1DJcK/L05k1Y9Gf7wMcyuqFOL6BiY3vY0CFcAM/LPRN04NALxcl6u7lOWNsp3f/bCHWxigzQl6FbR95XJ4R84Q==
katex@^0.16.45:
version "0.16.47"
resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.47.tgz#0a13a42c2deb4f74e61f162d440b9165a548030f"
integrity sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==
dependencies:
commander "^8.3.0"
@ -5068,9 +5068,9 @@ loader-utils@^2.0.4:
json5 "^2.1.2"
lodash-es@^4.17.21:
version "4.17.23"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.23.tgz#58c4360fd1b5d33afc6c0bbd3d1149349b1138e0"
integrity sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==
version "4.18.1"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.18.1.tgz#b962eeb80d9d983a900bf342961fb7418ca10b1d"
integrity sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==
lodash.memoize@^4.1.2:
version "4.1.2"
@ -5223,26 +5223,26 @@ merge-stream@^2.0.0:
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
mermaid@11.15.0:
version "11.15.0"
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-11.15.0.tgz#b485c13ea5e1e74f3328c4bb00427bda87fa1c1e"
integrity sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==
mermaid@11.16.1:
version "11.16.1"
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-11.16.1.tgz#57ae2342f6c45b967113b04c9258430bdd057ee8"
integrity sha512-TQsq6u22fAn3rek5VOubrhKPo1g5hwC3FXUN9hiyupTckcYiGuuKGkNQrKYwGJkXUxZdojwRG46gsSCFZMDp4g==
dependencies:
"@braintree/sanitize-url" "^7.1.1"
"@braintree/sanitize-url" "^7.1.2"
"@iconify/utils" "^3.0.2"
"@mermaid-js/parser" "^1.1.1"
"@mermaid-js/parser" "^1.2.0"
"@types/d3" "^7.4.3"
"@upsetjs/venn.js" "^2.0.0"
cytoscape "^3.33.1"
cytoscape "^3.33.3"
cytoscape-cose-bilkent "^4.1.0"
cytoscape-fcose "^2.2.0"
d3 "^7.9.0"
d3-sankey "^0.12.3"
dagre-d3-es "7.0.14"
dayjs "^1.11.19"
dompurify "^3.3.1"
dayjs "^1.11.20"
dompurify "^3.3.3"
es-toolkit "^1.45.1"
katex "^0.16.25"
katex "^0.16.45"
khroma "^2.1.0"
marked "^16.3.0"
roughjs "^4.6.6"
@ -5407,10 +5407,10 @@ multicast-dns@^7.2.5:
dns-packet "^5.2.2"
thunky "^1.0.2"
nanoid@^3.3.11:
version "3.3.11"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
nanoid@^3.3.16:
version "3.3.16"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.16.tgz#a04d8ec4b1f10009d2d533947aefe4293737816c"
integrity sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==
nanoid@^5.1.6:
version "5.1.7"
@ -5986,11 +5986,11 @@ postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0:
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@^8.4.40, postcss@^8.5.6, postcss@^8.5.8:
version "8.5.14"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.14.tgz#a66c2d7808fadf69ebb5b84a03f8bafd76c4919c"
integrity sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==
version "8.5.23"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.23.tgz#3493550116f478487298301d2c2e8dc5a56e6594"
integrity sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==
dependencies:
nanoid "^3.3.11"
nanoid "^3.3.16"
picocolors "^1.1.1"
source-map-js "^1.2.1"
@ -7146,9 +7146,9 @@ undici-types@~7.16.0:
integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==
undici@^7.19.0:
version "7.28.0"
resolved "https://registry.yarnpkg.com/undici/-/undici-7.28.0.tgz#97d64564198b285bc281f0e8e29597e3d11fe7ec"
integrity sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==
version "7.29.0"
resolved "https://registry.yarnpkg.com/undici/-/undici-7.29.0.tgz#ae0f6f62e06e057a9cbb7b2b5fde2bb74f791b8f"
integrity sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==
unified@^11.0.0, unified@^11.0.5:
version "11.0.5"
@ -7531,9 +7531,9 @@ wildcard@^2.0.1:
integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==
ws@^8.18.0:
version "8.20.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.20.0.tgz#4cd9532358eba60bc863aad1623dfb045a4d4af8"
integrity sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==
version "8.21.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.21.1.tgz#045650cd4b1207809e7547146223c3814a9af586"
integrity sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==
wsl-utils@^0.1.0:
version "0.1.0"

View file

@ -14,25 +14,42 @@ Example:
"""
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:
@ -40,6 +57,122 @@ def load_known_materials() -> set[str]:
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():
if len(sys.argv) != 3:
print(__doc__)
@ -52,17 +185,18 @@ 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 (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 = []
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:
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])
print(f"Loaded {len(block_ranges)} blocks from {blocks_json}")
@ -71,7 +205,7 @@ def main():
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]
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:
@ -80,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;",
@ -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" materials[i] = Material.{cs_name};")
lines += [
" }",
"",
*state_lines,
"",
]
lines += [
" protected override Dictionary<int, Material> 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 ({max_state + 1} total states)")
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__":