Compare commits

..

No commits in common. "master" and "20260727-492" have entirely different histories.

51 changed files with 271 additions and 63221 deletions

View file

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

View file

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

View file

@ -62,37 +62,6 @@ public sealed class AutoRelogRetryPolicyTests
Assert.Equal(0, retriesLeft);
}
[Fact]
public void CoalescedDuplicateRollsBackOnlyItsOwnReservation()
{
var policy = new AutoRelogRetryPolicy(new ManualTimeProvider());
Assert.True(policy.TryReserveAttempt(2, out int firstRetriesLeft));
Assert.True(policy.TryReserveAttempt(2, out int duplicateRetriesLeft));
policy.RollBackReservedAttempt();
Assert.Equal(1, firstRetriesLeft);
Assert.Equal(0, duplicateRetriesLeft);
Assert.Equal(1, policy.Attempts);
Assert.True(policy.TryReserveAttempt(2, out int secondFailureRetriesLeft));
Assert.Equal(0, secondFailureRetriesLeft);
Assert.False(policy.TryReserveAttempt(2, out _));
}
[Fact]
public void UnlimitedDuplicateRollbackKeepsUnlimitedBudget()
{
var policy = new AutoRelogRetryPolicy(new ManualTimeProvider());
Assert.True(policy.TryReserveAttempt(-1, out _));
Assert.True(policy.TryReserveAttempt(-1, out _));
policy.RollBackReservedAttempt();
Assert.Equal(1, policy.Attempts);
Assert.True(policy.TryReserveAttempt(-1, out int retriesLeft));
Assert.Equal(-1, retriesLeft);
}
[Fact]
public void StableConnectionResetsRetryBudget()
{

View file

@ -1,108 +0,0 @@
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

@ -1,14 +0,0 @@
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

@ -1,159 +0,0 @@
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

@ -1,122 +0,0 @@
using MinecraftClient.Scripting;
namespace MinecraftClient.Tests;
public sealed class McClientConnectionFailureTests
{
[Fact]
public void LoginRejectedClaimPreventsSyntheticConnectionLostFallback()
{
var lifecycle = new ConnectionAttemptLifecycle();
Assert.True(lifecycle.TryBeginDisconnect());
Assert.True(lifecycle.IsFailureClaimed);
Assert.False(lifecycle.TryBeginDisconnect());
lifecycle.CompleteDisconnect();
Assert.True(lifecycle.IsFailureClaimed);
Assert.False(lifecycle.TryBeginDisconnect());
}
[Fact]
public void UnclaimedGenericFailureCanBeClaimedExactlyOnce()
{
var lifecycle = new ConnectionAttemptLifecycle();
Assert.False(lifecycle.IsFailureClaimed);
Assert.True(lifecycle.TryBeginDisconnect());
Assert.False(lifecycle.TryBeginDisconnect());
}
[Fact]
public void HeldBotsAreRestoredBeforeFailureAndReceiveOriginalMessageOnce()
{
const string rejectionMessage = "You are not white-listed on this server!";
var bot = new RecordingBot();
List<ChatBot> heldBots = [bot];
var loadedBots = new List<ChatBot>();
ConnectionAttemptLifecycle.RestoreHeldBots(heldBots, loadedBots.Add);
foreach (ChatBot loadedBot in loadedBots)
loadedBot.OnDisconnect(ChatBot.DisconnectReason.LoginRejected, rejectionMessage);
Assert.Empty(heldBots);
Assert.Single(loadedBots);
Assert.Equal(1, bot.DisconnectCount);
Assert.Equal(ChatBot.DisconnectReason.LoginRejected, bot.LastReason);
Assert.Equal(rejectionMessage, bot.LastMessage);
}
[Fact]
public void OfflineRouteStaysOwnedAcrossReplacementAndSuccessfulHandoff()
{
var route = new AttemptOwnedRoute();
int activations = 0;
int deactivations = 0;
Assert.True(route.TryActivate(7, 7, () => activations++));
Assert.False(route.TryActivate(7, 7, () => activations++));
Assert.True(route.TryTransfer(7, 8));
Assert.False(route.TryDeactivate(7, () => deactivations++));
Assert.Equal(8, route.OwnerAttempt);
Assert.True(route.TryDeactivate(8, () => deactivations++));
Assert.Equal(1, activations);
Assert.Equal(1, deactivations);
Assert.Equal(-1, route.OwnerAttempt);
}
[Fact]
public void InitialConnectionAttemptCanOwnOfflineRoute()
{
var route = new AttemptOwnedRoute();
int activations = 0;
Assert.True(route.TryActivate(0, 0, () => activations++));
Assert.Equal(1, activations);
Assert.Equal(0, route.OwnerAttempt);
}
[Fact]
public void OlderAttemptCannotTakeAnEmptyOfflineRoute()
{
var route = new AttemptOwnedRoute();
int activations = 0;
Assert.False(route.TryActivate(4, 5, () => activations++));
Assert.Equal(0, activations);
Assert.Equal(-1, route.OwnerAttempt);
}
[Fact]
public void StaleCleanupCannotClearNewerOfflineRoute()
{
var route = new AttemptOwnedRoute();
int deactivations = 0;
Assert.True(route.TryActivate(10, 10, () => { }));
Assert.True(route.TryActivate(11, 11, () => { }));
Assert.False(route.TryDeactivate(10, () => deactivations++));
Assert.Equal(11, route.OwnerAttempt);
Assert.Equal(0, deactivations);
}
private sealed class RecordingBot : ChatBot
{
internal int DisconnectCount { get; private set; }
internal DisconnectReason? LastReason { get; private set; }
internal string? LastMessage { get; private set; }
public override bool OnDisconnect(DisconnectReason reason, string message)
{
DisconnectCount++;
LastReason = reason;
LastMessage = message;
return false;
}
}
}

View file

@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<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

@ -3,247 +3,55 @@ namespace MinecraftClient.Tests;
public sealed class RestartCoordinatorTests
{
[Fact]
public async Task PreparationCompletesBeforeRequestCanExecute()
public async Task ReplacesQueuedSameAttemptAndQueuesNewerAttempt()
{
var completed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
bool prepared = false;
RestartCoordinator coordinator = null!;
coordinator = new RestartCoordinator(
(request, cancellationToken) =>
{
Assert.True(Volatile.Read(ref prepared));
Assert.True(coordinator.TryBeginCommit(request, out _));
completed.SetResult();
return Task.CompletedTask;
},
exception => throw new Xunit.Sdk.XunitException(exception.ToString()));
using var cleanup = coordinator;
Assert.True(coordinator.TrySchedule(
new RestartRequest(1, TimeSpan.Zero, true),
() =>
{
Volatile.Write(ref prepared, true);
return true;
}));
await completed.Task.WaitAsync(TimeSpan.FromSeconds(5));
}
[Fact]
public async Task RejectedPreparationDoesNotPublishOrAdvanceAttempt()
{
var completed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
int executions = 0;
RestartCoordinator coordinator = null!;
coordinator = new RestartCoordinator(
(request, cancellationToken) =>
{
Interlocked.Increment(ref executions);
Assert.True(coordinator.TryBeginCommit(request, out _));
completed.SetResult();
return Task.CompletedTask;
},
exception => throw new Xunit.Sdk.XunitException(exception.ToString()));
using var cleanup = coordinator;
Assert.False(coordinator.TrySchedule(
new RestartRequest(2, TimeSpan.Zero, true),
() => false));
Assert.False(coordinator.HasScheduledRestart(2));
Assert.True(coordinator.TrySchedule(new RestartRequest(2, TimeSpan.Zero, true)));
await completed.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.Equal(1, executions);
}
[Fact]
public async Task FaultedSourceCleanupPreventsRestartExecution()
{
var failureReported = new TaskCompletionSource<Exception>(TaskCreationOptions.RunContinuationsAsynchronously);
int executions = 0;
var firstStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var releaseFirst = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var replacementCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var secondCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var executedAccounts = new List<string>();
using var coordinator = new RestartCoordinator(
(_, _) =>
{
Interlocked.Increment(ref executions);
return Task.CompletedTask;
},
exception => failureReported.SetResult(exception));
var cleanupFailure = new InvalidOperationException("cleanup failed");
Assert.True(coordinator.TrySchedule(new RestartRequest(
3,
TimeSpan.Zero,
true,
SourceCleanupCompletion: Task.FromException(cleanupFailure))));
Exception reportedException = await failureReported.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.Same(cleanupFailure, reportedException);
Assert.Equal(0, executions);
}
[Fact]
public async Task AutomaticSameAttemptIsCoalescedWhileQueued()
{
var blockerStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var releaseBlocker = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var queuedCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
int queuedExecutions = 0;
RestartCoordinator coordinator = null!;
coordinator = new RestartCoordinator(
async (request, cancellationToken) =>
{
if (request.ConnectionAttempt == 9)
{
blockerStarted.SetResult();
await releaseBlocker.Task.WaitAsync(cancellationToken);
return;
firstStarted.SetResult();
await releaseFirst.Task.WaitAsync(cancellationToken);
}
else if (request.ConnectionAttempt == 10)
{
executedAccounts.Add(request.SettingsSnapshot?.Account.Login ?? string.Empty);
replacementCompleted.SetResult();
}
else if (request.ConnectionAttempt == 11)
{
secondCompleted.SetResult();
}
Interlocked.Increment(ref queuedExecutions);
Assert.True(coordinator.TryBeginCommit(request, out _));
queuedCompleted.SetResult();
},
exception => throw new Xunit.Sdk.XunitException(exception.ToString()));
using var cleanup = coordinator;
Assert.True(coordinator.TrySchedule(new RestartRequest(9, TimeSpan.Zero, true)));
await blockerStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
await firstStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.True(coordinator.TrySchedule(new RestartRequest(10, TimeSpan.Zero, true)));
Assert.False(coordinator.TrySchedule(new RestartRequest(10, TimeSpan.Zero, true)));
Assert.True(coordinator.HasScheduledRestart(10));
Assert.True(coordinator.TrySchedule(new RestartRequest(10, TimeSpan.Zero, true, CreateSettingsSnapshot("first"))));
Assert.True(coordinator.TrySchedule(new RestartRequest(10, TimeSpan.Zero, true, CreateSettingsSnapshot("replacement"))));
Assert.True(coordinator.TrySchedule(new RestartRequest(11, TimeSpan.Zero, true)));
Assert.True(coordinator.HasScheduledRestart(11));
releaseBlocker.SetResult();
await queuedCompleted.Task.WaitAsync(TimeSpan.FromSeconds(5));
releaseFirst.SetResult();
await replacementCompleted.Task.WaitAsync(TimeSpan.FromSeconds(5));
await secondCompleted.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.Equal(1, queuedExecutions);
}
[Fact]
public async Task AutomaticSameAttemptIsCoalescedDuringCallback()
{
var callbackStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var releaseCallback = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var callbackCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
int executions = 0;
RestartCoordinator coordinator = null!;
coordinator = new RestartCoordinator(
async (request, cancellationToken) =>
{
Interlocked.Increment(ref executions);
callbackStarted.SetResult();
await releaseCallback.Task.WaitAsync(cancellationToken);
Assert.True(coordinator.TryBeginCommit(request, out _));
callbackCompleted.SetResult();
},
exception => throw new Xunit.Sdk.XunitException(exception.ToString()));
using var cleanup = coordinator;
Assert.True(coordinator.TrySchedule(new RestartRequest(42, TimeSpan.Zero, true)));
await callbackStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.False(coordinator.TrySchedule(new RestartRequest(42, TimeSpan.Zero, true)));
releaseCallback.SetResult();
await callbackCompleted.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.Equal(1, executions);
}
[Fact]
public async Task ExplicitReplacementDuringDelayUsesLatestSnapshotWithoutAnotherExecution()
{
var callbackStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var allowCommit = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var callbackCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
RestartRequest committedRequest = default;
int executions = 0;
RestartCoordinator coordinator = null!;
coordinator = new RestartCoordinator(
async (request, cancellationToken) =>
{
Interlocked.Increment(ref executions);
callbackStarted.SetResult();
await allowCommit.Task.WaitAsync(cancellationToken);
Assert.True(coordinator.TryBeginCommit(request, out committedRequest));
callbackCompleted.SetResult();
},
exception => throw new Xunit.Sdk.XunitException(exception.ToString()));
using var cleanup = coordinator;
Assert.True(coordinator.TrySchedule(new RestartRequest(
50,
TimeSpan.FromSeconds(10),
true,
CreateSettingsSnapshot("first"))));
await callbackStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.True(coordinator.TrySchedule(new RestartRequest(
50,
TimeSpan.Zero,
true,
CreateSettingsSnapshot("replacement"),
ReplaceUntilCommit: true)));
allowCommit.SetResult();
await callbackCompleted.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.Equal(1, executions);
Assert.Equal("replacement", committedRequest.SettingsSnapshot?.Account.Login);
}
[Fact]
public async Task RejectsSameAttemptReplacementAfterCommit()
{
var commitStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
var releaseCommit = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
RestartRequest committedRequest = default;
RestartCoordinator coordinator = null!;
coordinator = new RestartCoordinator(
async (request, cancellationToken) =>
{
Assert.True(coordinator.TryBeginCommit(request, out committedRequest));
commitStarted.SetResult();
await releaseCommit.Task.WaitAsync(cancellationToken);
},
exception => throw new Xunit.Sdk.XunitException(exception.ToString()));
using var cleanup = coordinator;
Assert.True(coordinator.TrySchedule(new RestartRequest(
60,
TimeSpan.Zero,
true,
CreateSettingsSnapshot("committed"))));
await commitStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.False(coordinator.TrySchedule(new RestartRequest(
60,
TimeSpan.Zero,
true,
CreateSettingsSnapshot("rejected"),
ReplaceUntilCommit: true)));
Assert.Equal("committed", committedRequest.SettingsSnapshot?.Account.Login);
releaseCommit.SetResult();
Assert.Equal(["replacement"], executedAccounts);
}
[Fact]
public void RejectsStaleAttempt()
{
RestartCoordinator coordinator = null!;
coordinator = new RestartCoordinator(
using var coordinator = new RestartCoordinator(
(_, _) => Task.CompletedTask,
exception => throw new Xunit.Sdk.XunitException(exception.ToString()));
using var cleanup = coordinator;
Assert.True(coordinator.TrySchedule(new RestartRequest(20, TimeSpan.Zero, true)));
Assert.False(coordinator.TrySchedule(new RestartRequest(19, TimeSpan.Zero, true)));
@ -253,16 +61,13 @@ public sealed class RestartCoordinatorTests
public async Task RejectsCompletedAttempt()
{
var completed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
RestartCoordinator coordinator = null!;
coordinator = new RestartCoordinator(
(request, cancellationToken) =>
using var coordinator = new RestartCoordinator(
(_, _) =>
{
Assert.True(coordinator.TryBeginCommit(request, out _));
completed.SetResult();
return Task.CompletedTask;
},
exception => throw new Xunit.Sdk.XunitException(exception.ToString()));
using var cleanup = coordinator;
Assert.True(coordinator.TrySchedule(new RestartRequest(20, TimeSpan.Zero, true)));
await completed.Task.WaitAsync(TimeSpan.FromSeconds(5));
@ -272,23 +77,15 @@ public sealed class RestartCoordinatorTests
}
[Fact]
public async Task TerminalStopCancelsInFlightWorkAndRejectsFurtherRestarts()
public void TerminalStopRejectsFurtherRestarts()
{
var callbackStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
using var coordinator = new RestartCoordinator(
async (_, cancellationToken) =>
{
callbackStarted.SetResult();
await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken);
},
(_, _) => Task.CompletedTask,
exception => throw new Xunit.Sdk.XunitException(exception.ToString()));
Assert.True(coordinator.TrySchedule(new RestartRequest(1, TimeSpan.Zero, true)));
await callbackStarted.Task.WaitAsync(TimeSpan.FromSeconds(5));
coordinator.Stop();
Assert.False(coordinator.TrySchedule(new RestartRequest(2, TimeSpan.Zero, true)));
Assert.False(coordinator.TrySchedule(new RestartRequest(1, TimeSpan.Zero, true)));
Assert.False(coordinator.HasScheduledRestart(1));
}

View file

@ -78,7 +78,6 @@ namespace MinecraftClient.ChatBots
}
private static readonly AutoRelogRetryPolicy s_retryPolicy = new(TimeProvider.System);
private readonly long? sourceConnectionAttempt;
/// <summary>
/// This bot automatically re-join the server if kick message contains predefined string
@ -86,13 +85,8 @@ namespace MinecraftClient.ChatBots
/// <param name="DelayBeforeRelogMin">Minimum delay before re-joining the server (in seconds)</param>
/// <param name="DelayBeforeRelogMax">Maximum delay before re-joining the server (in seconds)</param>
/// <param name="retries">Number of retries if connection fails (-1 = infinite)</param>
public AutoRelog() : this(null)
public AutoRelog()
{
}
private AutoRelog(long? sourceConnectionAttempt)
{
this.sourceConnectionAttempt = sourceConnectionAttempt;
LogDebugToConsole(string.Format(Translations.bot_autoRelog_launch, Config.Retries));
}
@ -169,31 +163,21 @@ namespace MinecraftClient.ChatBots
: retriesLeft.ToString();
McClient.ReconnectionAttemptsLeft = retriesLeft;
long connectionAttempt = sourceConnectionAttempt ?? Handler.ConnectionAttempt;
if (Program.TryRestart(
connectionAttempt,
TimeSpan.FromSeconds(delay),
keepAccountAndServerSettings: true,
sourceCleanupCompletion: sourceConnectionAttempt.HasValue ? null : Handler.DisconnectCompletion))
if (Program.TryRestart(TimeSpan.FromSeconds(delay), true))
{
LogToConsole(string.Format(Translations.bot_autoRelog_wait_with_retries, delay, retriesDisplay));
return true;
}
s_retryPolicy.RollBackReservedAttempt();
return Program.HasRestartPending(connectionAttempt);
return Program.HasRestartPending;
}
public static bool OnDisconnectStatic(DisconnectReason reason, string message)
{
return OnDisconnectStatic(reason, message, Program.CurrentConnectionAttempt);
}
internal static bool OnDisconnectStatic(DisconnectReason reason, string message, long sourceConnectionAttempt)
{
if (Config.Enabled)
{
AutoRelog bot = new(sourceConnectionAttempt);
AutoRelog bot = new();
bot.Initialize();
return bot.OnDisconnect(reason, message);
}

View file

@ -1,5 +1,4 @@
using System;
using Brigadier.NET;
using Brigadier.NET;
using Brigadier.NET.Builder;
using MinecraftClient.CommandHandler;
using static MinecraftClient.CommandHandler.CmdResult;
@ -43,55 +42,35 @@ namespace MinecraftClient.Commands
private int DoConnect(CmdResult r, string server, string account)
{
RestartSettingsSnapshot previousSettings = Program.CaptureRestartSettings();
if (!string.IsNullOrWhiteSpace(account) && !Settings.Config.Main.Advanced.SetAccount(account))
return r.SetAndReturn(Status.Fail, string.Format(Translations.cmd_connect_unknown, account));
if (Settings.Config.Main.SetServerIP(new Settings.MainConfigHelper.MainConfig.ServerInfoConfig(server), true))
{
if (Program.TryRestart(
Program.CurrentConnectionAttempt,
TimeSpan.Zero,
keepAccountAndServerSettings: true,
replaceUntilCommit: true))
{
return r.SetAndReturn(Status.Done);
}
Program.RestoreRestartSettings(previousSettings);
return r.SetAndReturn(Status.Fail);
return r.SetAndReturn(Program.TryRestart(keepAccountAndServerSettings: true)
? Status.Done
: Status.Fail);
}
else
{
Program.RestoreRestartSettings(previousSettings);
return r.SetAndReturn(Status.Fail, string.Format(Translations.cmd_connect_invalid_ip, server));
}
}
internal static string DoConnect(string command)
{
RestartSettingsSnapshot previousSettings = Program.CaptureRestartSettings();
string[] args = GetArgs(command);
if (args.Length > 1 && !Settings.Config.Main.Advanced.SetAccount(args[1]))
return string.Format(Translations.cmd_connect_unknown, args[1]);
if (Settings.Config.Main.SetServerIP(new Settings.MainConfigHelper.MainConfig.ServerInfoConfig(args[0]), true))
{
if (Program.TryRestart(
Program.CurrentConnectionAttempt,
TimeSpan.Zero,
keepAccountAndServerSettings: true,
replaceUntilCommit: true))
{
return string.Empty;
}
Program.RestoreRestartSettings(previousSettings);
return Translations.general_fail;
return Program.TryRestart(keepAccountAndServerSettings: true)
? string.Empty
: Translations.general_fail;
}
else
{
Program.RestoreRestartSettings(previousSettings);
return string.Format(Translations.cmd_connect_invalid_ip, args[0]);
}
}

View file

@ -41,30 +41,19 @@ namespace MinecraftClient.Commands
private int DoReconnect(CmdResult r, string account)
{
RestartSettingsSnapshot previousSettings = Program.CaptureRestartSettings();
if (!string.IsNullOrWhiteSpace(account))
{
account = account.Trim();
if (!Settings.Config.Main.Advanced.SetAccount(account))
return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_connect_unknown, account));
}
if (Program.TryRestart(
Program.CurrentConnectionAttempt,
TimeSpan.Zero,
keepAccountAndServerSettings: true,
replaceUntilCommit: true))
{
return r.SetAndReturn(CmdResult.Status.Done);
}
Program.RestoreRestartSettings(previousSettings);
return r.SetAndReturn(CmdResult.Status.Fail);
return r.SetAndReturn(Program.TryRestart(keepAccountAndServerSettings: true)
? CmdResult.Status.Done
: CmdResult.Status.Fail);
}
internal static string DoReconnect(string command)
{
RestartSettingsSnapshot previousSettings = Program.CaptureRestartSettings();
string[] args = GetArgs(command);
if (args.Length > 0)
{
@ -74,18 +63,9 @@ namespace MinecraftClient.Commands
return string.Format(Translations.cmd_connect_unknown, account);
}
}
if (Program.TryRestart(
Program.CurrentConnectionAttempt,
TimeSpan.Zero,
keepAccountAndServerSettings: true,
replaceUntilCommit: true))
{
return string.Empty;
}
Program.RestoreRestartSettings(previousSettings);
return Translations.general_fail;
return Program.TryRestart(keepAccountAndServerSettings: true)
? String.Empty
: Translations.general_fail;
}
}
}

View file

@ -1,107 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using MinecraftClient.Scripting;
namespace MinecraftClient
{
internal sealed class ConnectionAttemptLifecycle
{
private int disconnectState;
internal bool IsFailureClaimed => Volatile.Read(ref disconnectState) != 0;
internal bool TryBeginDisconnect()
{
return Interlocked.CompareExchange(ref disconnectState, 1, 0) == 0;
}
internal void CompleteDisconnect()
{
Volatile.Write(ref disconnectState, 2);
}
internal static void RestoreHeldBots(ICollection<ChatBot> heldBots, Action<ChatBot> loadBot)
{
ArgumentNullException.ThrowIfNull(heldBots);
ArgumentNullException.ThrowIfNull(loadBot);
foreach (ChatBot bot in heldBots)
loadBot(bot);
heldBots.Clear();
}
}
internal sealed class AttemptOwnedRoute
{
private const long NoOwner = -1;
private readonly Lock stateLock = new();
private long ownerAttempt = NoOwner;
internal long OwnerAttempt
{
get
{
lock (stateLock)
return ownerAttempt;
}
}
internal bool TryActivate(long connectionAttempt, long currentConnectionAttempt, Action activate)
{
ArgumentNullException.ThrowIfNull(activate);
lock (stateLock)
{
if (connectionAttempt < currentConnectionAttempt || ownerAttempt >= connectionAttempt)
return false;
ownerAttempt = connectionAttempt;
activate();
return true;
}
}
internal bool TryDeactivate(long connectionAttempt, Action deactivate)
{
ArgumentNullException.ThrowIfNull(deactivate);
lock (stateLock)
{
if (ownerAttempt != connectionAttempt)
return false;
ownerAttempt = NoOwner;
deactivate();
return true;
}
}
internal bool TryTransfer(long sourceConnectionAttempt, long targetConnectionAttempt)
{
lock (stateLock)
{
if (ownerAttempt != sourceConnectionAttempt)
return false;
ownerAttempt = targetConnectionAttempt;
return true;
}
}
internal bool TryDeactivate(Action deactivate)
{
ArgumentNullException.ThrowIfNull(deactivate);
lock (stateLock)
{
if (ownerAttempt == NoOwner)
return false;
ownerAttempt = NoOwner;
deactivate();
return true;
}
}
}
}

View file

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

View file

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using MinecraftClient.Mapping.BlockPalettes;
using MinecraftClient.Protocol.Message;
@ -79,27 +78,6 @@ 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,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Collections.Generic;
namespace MinecraftClient.Mapping.BlockPalettes
{
@ -24,46 +23,6 @@ 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

@ -1,71 +0,0 @@
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

@ -231,14 +231,12 @@ namespace MinecraftClient
SessionToken _sessionToken;
Tuple<Thread, CancellationTokenSource>? timeoutdetector = null;
private int transferInProgress = 0;
private readonly ConnectionAttemptLifecycle connectionLifecycle = new();
private int disconnectState;
private int disconnectOwnerThreadId;
private readonly TaskCompletionSource<bool> disconnectCompletion = new(TaskCreationOptions.RunContinuationsAsynchronously);
public ILogger Log;
public DialogManager Dialogs { get; }
internal long ConnectionAttempt { get; }
internal Task DisconnectCompletion => disconnectCompletion.Task;
private static IMinecraftComHandler? instance;
public static IMinecraftComHandler? Instance => instance;
@ -253,22 +251,9 @@ namespace MinecraftClient
/// <param name="protocolversion">Minecraft protocol version to use</param>
/// <param name="forgeInfo">ForgeInfo item stating that Forge is enabled</param>
public McClient(SessionToken session, PlayerKeyPair? playerKeyPair, string server_ip, ushort port, int protocolversion, ForgeInfo? forgeInfo)
: this(session, playerKeyPair, server_ip, port, protocolversion, forgeInfo, Program.CurrentConnectionAttempt)
{
}
internal McClient(
SessionToken session,
PlayerKeyPair? playerKeyPair,
string server_ip,
ushort port,
int protocolversion,
ForgeInfo? forgeInfo,
long connectionAttempt)
{
CmdResult.currentHandler = this;
instance = this;
ConnectionAttempt = connectionAttempt;
terrainAndMovementsEnabled = Config.Main.Advanced.TerrainAndMovements;
inventoryHandlingEnabled = Config.Main.Advanced.InventoryHandling;
@ -326,13 +311,7 @@ namespace MinecraftClient
LoadCommands();
if (botsOnHold.Count == 0)
{
RegisterBots();
}
else
{
ConnectionAttemptLifecycle.RestoreHeldBots(botsOnHold, bot => BotLoad(bot, false));
}
try
{
@ -352,6 +331,10 @@ namespace MinecraftClient
{
if (handler.Login(this.playerKeyPair, session))
{
foreach (ChatBot bot in botsOnHold)
BotLoad(bot, false);
botsOnHold.Clear();
Log.Info(string.Format(Translations.mcc_joined, Config.Main.Advanced.InternalCmdChar.ToLogString()));
StartConsoleSession();
@ -385,9 +368,6 @@ namespace MinecraftClient
timeoutdetector = null;
}
if (connectionLifecycle.IsFailureClaimed)
return;
if (!InternalConfig.InteractiveMode)
{
StopConsoleSession();
@ -411,8 +391,14 @@ namespace MinecraftClient
return;
}
OnConnectionLost(ChatBot.DisconnectReason.ConnectionLost, Translations.mcc_disconnect_lost);
return;
// AutoRelog is enabled - invoke its static handler to trigger reconnection.
// Use the same "Connection has been lost" message that OnConnectionLost uses
// for ConnectionLost, so it matches the default Kick_Messages.
if (AutoRelog.OnDisconnectStatic(ChatBot.DisconnectReason.ConnectionLost, Translations.mcc_disconnect_lost))
return;
StopConsoleSession();
Program.HandleFailure();
}
public void Transfer(string newHost, int newPort)
@ -544,7 +530,6 @@ namespace MinecraftClient
private void StartConsoleSession()
{
Program.EndOfflinePrompt(ConnectionAttempt);
ConsoleInputRouter.RouteToClient(this);
}
@ -890,7 +875,7 @@ namespace MinecraftClient
}
}
restartScheduled = !exitOnFailure && Program.HasRestartPending(ConnectionAttempt);
restartScheduled = !exitOnFailure && Program.HasRestartPending;
}
finally
{
@ -903,7 +888,7 @@ namespace MinecraftClient
private bool TryBeginDisconnect()
{
if (!connectionLifecycle.TryBeginDisconnect())
if (Interlocked.CompareExchange(ref disconnectState, 1, 0) != 0)
return false;
Volatile.Write(ref disconnectOwnerThreadId, Environment.CurrentManagedThreadId);
@ -952,7 +937,7 @@ namespace MinecraftClient
}
finally
{
connectionLifecycle.CompleteDisconnect();
Volatile.Write(ref disconnectState, 2);
Volatile.Write(ref disconnectOwnerThreadId, 0);
disconnectCompletion.TrySetResult(true);
}
@ -3796,8 +3781,6 @@ namespace MinecraftClient
{
UpdateKeepAlive();
Log.Debug(string.Format(Translations.protocol_chat_raw_message, message.content));
List<string> links = new();
string messageText;

View file

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

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.6" />
<PackageReference Include="Consolonia" Version="11.3.12.3" />
<PackageReference Include="DnsClient" Version="1.8.0" />
<PackageReference Include="DSharpPlus" Version="4.5.2" />
<PackageReference Include="DSharpPlus" Version="4.5.1" />
<PackageReference Include="DynamicExpresso.Core" Version="2.19.3" />
<PackageReference Include="FuzzySharp" Version="2.0.2" />
<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="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="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
<PackageReference Include="Microsoft.Windows.Compatibility" Version="10.0.10" />
<PackageReference Include="Microsoft.Windows.Compatibility" Version="10.0.5" />
<PackageReference Include="Samboy063.Tomlet" Version="6.2.0" />
<PackageReference Include="Sentry" Version="6.8.0" />
<PackageReference Include="Sentry" Version="6.3.1" />
<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.10.2.1" />
<PackageReference Include="Telegram.Bot" Version="22.9.5.3" />
</ItemGroup>
<ItemGroup>
<Compile Remove="config\**\*.cs" />

View file

@ -55,7 +55,7 @@ namespace MinecraftClient
private static bool useMcVersionOnce = false;
private static readonly RestartCoordinator restartCoordinator = new(ExecuteRestartAsync, ReportRestartFailure);
private static long connectionAttempt;
private static readonly AttemptOwnedRoute offlinePromptRoute = new();
private static int offlinePromptActive;
private static int exitOnFailurePending;
private static string settingsIniPath = "MinecraftClient.ini";
private static AuthenticationSelection? pendingAuthenticationSelection;
@ -747,14 +747,12 @@ namespace MinecraftClient
private sealed record AuthenticationSelection(LoginType AccountType, LoginMethod Method, string AuthServerUrl);
internal static long CurrentConnectionAttempt => Volatile.Read(ref connectionAttempt);
/// <summary>
/// Start a new Client
/// </summary>
private static void InitializeClient()
{
long attempt = Interlocked.Increment(ref connectionAttempt);
Interlocked.Increment(ref connectionAttempt);
// Ensure that we use the provided Minecraft version if we can't connect automatically.
//
@ -986,7 +984,7 @@ namespace MinecraftClient
try
{
//Start the main TCP client
client = new McClient(session, playerKeyPair, InternalConfig.ServerIP, InternalConfig.ServerPort, protocolversion, forgeInfo, attempt);
client = new McClient(session, playerKeyPair, InternalConfig.ServerIP, InternalConfig.ServerPort, protocolversion, forgeInfo);
//Update console title
if (OperatingSystem.IsWindows() && !string.IsNullOrWhiteSpace(Config.Main.Advanced.ConsoleTitle))
@ -1064,22 +1062,7 @@ namespace MinecraftClient
TryRestart(TimeSpan.FromSeconds(Math.Max(0, delaySeconds)), keepAccountAndServerSettings);
}
internal static bool HasRestartPending(long sourceConnectionAttempt)
{
return restartCoordinator.HasScheduledRestart(sourceConnectionAttempt);
}
internal static RestartSettingsSnapshot CaptureRestartSettings()
{
return new RestartSettingsSnapshot(InternalConfig.Account, InternalConfig.ServerIP, InternalConfig.ServerPort);
}
internal static void RestoreRestartSettings(RestartSettingsSnapshot settingsSnapshot)
{
InternalConfig.Account = settingsSnapshot.Account;
InternalConfig.ServerIP = settingsSnapshot.ServerIP;
InternalConfig.ServerPort = settingsSnapshot.ServerPort;
}
internal static bool HasRestartPending => restartCoordinator.HasScheduledRestart(Volatile.Read(ref connectionAttempt));
internal static bool TryRestart(int delaySeconds = 0, bool keepAccountAndServerSettings = false)
{
@ -1087,47 +1070,26 @@ namespace MinecraftClient
}
internal static bool TryRestart(TimeSpan delay, bool keepAccountAndServerSettings = false)
{
return TryRestart(CurrentConnectionAttempt, delay, keepAccountAndServerSettings);
}
internal static bool TryRestart(
long sourceConnectionAttempt,
TimeSpan delay,
bool keepAccountAndServerSettings = false,
bool replaceUntilCommit = false,
Task? sourceCleanupCompletion = null)
{
if (Volatile.Read(ref exitOnFailurePending) != 0)
return false;
if (sourceConnectionAttempt != CurrentConnectionAttempt)
return false;
if (delay < TimeSpan.Zero)
delay = TimeSpan.Zero;
RestartSettingsSnapshot? settingsSnapshot = keepAccountAndServerSettings
? CaptureRestartSettings()
? new RestartSettingsSnapshot(InternalConfig.Account, InternalConfig.ServerIP, InternalConfig.ServerPort)
: null;
bool scheduled = restartCoordinator.TrySchedule(
new RestartRequest(
sourceConnectionAttempt,
delay,
keepAccountAndServerSettings,
settingsSnapshot,
replaceUntilCommit,
sourceCleanupCompletion),
() => BeginOfflinePrompt(sourceConnectionAttempt));
return scheduled;
return restartCoordinator.TrySchedule(new RestartRequest(
Volatile.Read(ref connectionAttempt),
delay,
keepAccountAndServerSettings,
settingsSnapshot));
}
private static async Task ExecuteRestartAsync(RestartRequest request, CancellationToken cancellationToken)
{
if (request.ConnectionAttempt != CurrentConnectionAttempt)
return;
McClient? disconnectedClient = client;
if (disconnectedClient is not null)
{
@ -1136,6 +1098,7 @@ namespace MinecraftClient
client = null;
}
EndOfflinePrompt();
ConsoleIO.Reset();
if (request.Delay > TimeSpan.Zero)
@ -1145,22 +1108,14 @@ namespace MinecraftClient
}
cancellationToken.ThrowIfCancellationRequested();
if (request.ConnectionAttempt != CurrentConnectionAttempt
|| !restartCoordinator.TryBeginCommit(request, out RestartRequest latestRequest)
|| latestRequest.ConnectionAttempt != CurrentConnectionAttempt)
{
return;
}
ConsoleIO.WriteLine(Translations.mcc_restart);
ReloadSettings(latestRequest.KeepAccountAndServerSettings);
if (latestRequest.SettingsSnapshot is RestartSettingsSnapshot settingsSnapshot)
ReloadSettings(request.KeepAccountAndServerSettings);
if (request.SettingsSnapshot is RestartSettingsSnapshot settingsSnapshot)
{
InternalConfig.Account = settingsSnapshot.Account;
InternalConfig.ServerIP = settingsSnapshot.ServerIP;
InternalConfig.ServerPort = settingsSnapshot.ServerPort;
}
TransferOfflinePrompt(latestRequest.ConnectionAttempt, latestRequest.ConnectionAttempt + 1);
InitializeClient();
}
@ -1242,7 +1197,7 @@ namespace MinecraftClient
if (!string.IsNullOrEmpty(errorMessage) && disconnectReason.HasValue)
{
autoRelogHandled = true;
if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage, CurrentConnectionAttempt))
if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage))
return;
}
@ -1262,58 +1217,35 @@ namespace MinecraftClient
if (!autoRelogHandled && disconnectReason.HasValue)
{
if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage!, CurrentConnectionAttempt))
if (ChatBots.AutoRelog.OnDisconnectStatic(disconnectReason.Value, errorMessage!))
return;
}
BeginOfflinePrompt(CurrentConnectionAttempt);
BeginOfflinePrompt();
}
}
private static bool BeginOfflinePrompt(long connectionAttempt)
private static void BeginOfflinePrompt()
{
long currentConnectionAttempt = CurrentConnectionAttempt;
if (connectionAttempt != currentConnectionAttempt)
return false;
if (Interlocked.CompareExchange(ref offlinePromptActive, 1, 0) != 0)
return;
if (offlinePromptRoute.TryActivate(connectionAttempt, currentConnectionAttempt, () =>
{
ConsoleInputRouter.RouteOffline(HandleOfflineCommand);
ConsoleIO.WriteLine(string.Empty);
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_disconnected, Config.Main.Advanced.InternalCmdChar.ToLogString()));
if (ConsoleIO.Backend is Tui.TuiConsoleBackend)
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_use_quit_to_exit, Config.Main.Advanced.InternalCmdChar.ToLogString()));
else
ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true);
}))
{
return true;
}
return offlinePromptRoute.OwnerAttempt == connectionAttempt;
ConsoleInputRouter.RouteOffline(HandleOfflineCommand);
ConsoleIO.WriteLine(string.Empty);
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_disconnected, Config.Main.Advanced.InternalCmdChar.ToLogString()));
if (ConsoleIO.Backend is Tui.TuiConsoleBackend)
ConsoleIO.WriteLineFormatted(string.Format(Translations.mcc_use_quit_to_exit, Config.Main.Advanced.InternalCmdChar.ToLogString()));
else
ConsoleIO.WriteLineFormatted(Translations.mcc_press_exit, acceptnewlines: true);
}
private static void EndOfflinePrompt()
{
offlinePromptRoute.TryDeactivate(() =>
{
ConsoleInputRouter.ClearOfflineRoute(HandleOfflineCommand);
ConsoleIO.Reset();
});
}
if (Interlocked.Exchange(ref offlinePromptActive, 0) == 0)
return;
internal static void EndOfflinePrompt(long connectionAttempt)
{
offlinePromptRoute.TryDeactivate(connectionAttempt, () =>
{
ConsoleInputRouter.ClearOfflineRoute(HandleOfflineCommand);
ConsoleIO.Reset();
});
}
private static void TransferOfflinePrompt(long sourceConnectionAttempt, long targetConnectionAttempt)
{
offlinePromptRoute.TryTransfer(sourceConnectionAttempt, targetConnectionAttempt);
ConsoleInputRouter.ClearOfflineRoute(HandleOfflineCommand);
ConsoleIO.Reset();
}
private static void HandleOfflineCommand(string input)
@ -1337,13 +1269,19 @@ namespace MinecraftClient
{
message = Commands.Reco.DoReconnect(Config.AppVar.ExpandVars(command));
if (message.Length == 0)
{
EndOfflinePrompt();
return;
}
}
else if (command.StartsWith("connect", StringComparison.Ordinal))
{
message = Commands.Connect.DoConnect(Config.AppVar.ExpandVars(command));
if (message.Length == 0)
{
EndOfflinePrompt();
return;
}
}
else if (command.StartsWith("exit", StringComparison.Ordinal)
|| command.StartsWith("quit", StringComparison.Ordinal))

View file

@ -287,7 +287,6 @@ namespace MinecraftClient.Protocol.Handlers
},
_ => ChatParser.ChatId2Type
};
ChatParser.ClearChatTypeDecorations();
}
/// <summary>
@ -577,6 +576,7 @@ 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)
ChatParser.ReadChatType(i, entryId, nbtData);
availableChats!.Add(i, entryId);
else if (isDimension)
{
dimensionIdMap!.Add(i, entryId);
@ -611,7 +611,9 @@ namespace MinecraftClient.Protocol.Handlers
handler.OnDialogRegistryData(i, entryId, dialogNbtParser.Parse(nbtData));
}
if (isDimension)
if (isChat)
ChatParser.ReadChatType(availableChats!);
else if (isDimension)
{
World.SetDimensionIdMap(dimensionIdMap!);
if (!handler.GetTerrainEnabled() || !World.HasAnyDimension())
@ -1230,8 +1232,7 @@ namespace MinecraftClient.Protocol.Handlers
// Network Target
// net.minecraft.network.message.MessageType.Serialized#write
var chatTypeId = ChatParser.ReadChatTypeHolder(
dataTypes, packetData, protocolVersion, out var directChatTypeDecoration);
var chatTypeId = dataTypes.ReadNextVarInt(packetData);
var chatName = dataTypes.ReadNextChat(packetData);
var targetName = dataTypes.ReadNextBool(packetData)
? dataTypes.ReadNextChat(packetData)
@ -1280,10 +1281,7 @@ namespace MinecraftClient.Protocol.Handlers
}
ChatMessage chat = new(message, false, chatTypeId, senderUuid, unsignedChatContent,
senderDisplayName, senderTeamName, timestamp, messageSignature, verifyResult)
{
chatTypeDecoration = directChatTypeDecoration
};
senderDisplayName, senderTeamName, timestamp, messageSignature, verifyResult);
lock (MessageSigningLock)
Acknowledge(chat);
handler.OnTextReceived(chat);
@ -1347,17 +1345,14 @@ namespace MinecraftClient.Protocol.Handlers
break;
case PacketTypesIn.ProfilelessChatMessage:
var message_ = dataTypes.ReadNextChat(packetData);
var messageType_ = ChatParser.ReadChatTypeHolder(
dataTypes, packetData, protocolVersion, out var directProfilelessChatTypeDecoration);
var messageType_ = dataTypes.ReadNextVarInt(packetData);
var messageName = dataTypes.ReadNextChat(packetData);
var targetName_ = dataTypes.ReadNextBool(packetData)
? dataTypes.ReadNextChat(packetData)
: null;
ChatMessage profilelessChat = new(message_, messageName, false, messageType_,
ChatMessage profilelessChat = new(message_, targetName_ ?? messageName, false, messageType_,
Guid.Empty, true);
profilelessChat.isSenderJson = false;
profilelessChat.teamName = targetName_;
profilelessChat.chatTypeDecoration = directProfilelessChatTypeDecoration;
handler.OnTextReceived(profilelessChat);
break;
case PacketTypesIn.CombatEvent:
@ -5721,13 +5716,6 @@ 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,8 +35,6 @@ 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,7 +11,6 @@ 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;
@ -37,49 +36,33 @@ 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(int chatId, string chatName, Dictionary<string, object>? chatTypeData)
public static void ReadChatType(Dictionary<int, string> data)
{
var chatTypeDictionary = ChatId2Type ?? new Dictionary<int, MessageType>();
chatTypeDictionary[chatId] = chatName switch
foreach (var (chatId, chatName) in data)
{
"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,
};
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,
};
}
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"))
{
@ -101,80 +84,25 @@ namespace MinecraftClient.Protocol.Message
}
var chatTypeListNbt = (object[])(((Dictionary<string, object>)registryCodec["minecraft:chat_type"])["value"]);
foreach (Dictionary<string, object> chatTypeNbt in chatTypeListNbt)
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))
{
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
chatTypeDictionary[chatId] = chatName switch
{
"sender" => ChatTypeParameter.Sender,
"target" => ChatTypeParameter.Target,
"content" => ChatTypeParameter.Content,
_ => ChatTypeParameter.Sender
"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,
};
}
decoration = new ChatTypeDecoration(translationKeyText, parsedParameters);
return true;
ChatId2Type = chatTypeDictionary;
}
/// <summary>
@ -219,26 +147,6 @@ 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;
@ -649,47 +557,48 @@ namespace MinecraftClient.Protocol.Message
RulesInitialized = true;
}
if (!TryGetTranslationRule(rulename, out string? rule))
rule = rulename;
int using_idx = 0;
StringBuilder result = new();
for (int i = 0; i < rule.Length; i++)
if (TryGetTranslationRule(rulename, out string? rule))
{
if (rule[i] == '%' && i + 1 < rule.Length)
int using_idx = 0;
StringBuilder result = new();
for (int i = 0; i < rule.Length; i++)
{
//Using string or int with %s or %d
if (rule[i + 1] == 's' || rule[i + 1] == 'd')
if (rule[i] == '%' && i + 1 < rule.Length)
{
if (using_data.Count > using_idx)
//Using string or int with %s or %d
if (rule[i + 1] == 's' || rule[i + 1] == 'd')
{
result.Append(using_data[using_idx]);
using_idx++;
i += 1;
continue;
if (using_data.Count > using_idx)
{
result.Append(using_data[using_idx]);
using_idx++;
i += 1;
continue;
}
}
//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;
}
}
}
//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;
}
}
result.Append(rule[i]);
}
result.Append(rule[i]);
return result.ToString();
}
return result.ToString();
else return "[" + rulename + "] " + string.Join(" ", using_data);
}
private static bool TryGetTranslationRule(string rulename, [NotNullWhen(true)] out string? result)

View file

@ -8296,9 +8296,5 @@ 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,7 +3127,4 @@ 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

@ -11,8 +11,6 @@ namespace MinecraftClient
TimeSpan Delay,
bool KeepAccountAndServerSettings,
RestartSettingsSnapshot? SettingsSnapshot = null,
bool ReplaceUntilCommit = false,
Task? SourceCleanupCompletion = null,
long RequestId = 0);
internal readonly record struct RestartSettingsSnapshot(
@ -20,17 +18,6 @@ namespace MinecraftClient
string ServerIP,
ushort ServerPort);
internal enum RestartRequestState
{
Replaceable,
Committing,
}
internal readonly record struct PendingRestart(
long RequestId,
RestartRequest Request,
RestartRequestState State);
internal sealed class RestartCoordinator : IDisposable
{
private readonly Lock stateLock = new();
@ -39,7 +26,7 @@ namespace MinecraftClient
private readonly Func<RestartRequest, CancellationToken, Task> restart;
private readonly Action<Exception> reportFailure;
private readonly Task worker;
private readonly Dictionary<long, PendingRestart> pendingAttempts = [];
private readonly Dictionary<long, long> pendingAttempts = [];
private long highestScheduledAttempt = -1;
private long nextRequestId;
private bool stopped;
@ -68,82 +55,29 @@ namespace MinecraftClient
return !stopped && pendingAttempts.ContainsKey(connectionAttempt);
}
internal bool TrySchedule(RestartRequest request, Func<bool>? beforePublish = null)
internal bool TrySchedule(RestartRequest request)
{
lock (stateLock)
{
if (stopped)
return false;
if (pendingAttempts.TryGetValue(request.ConnectionAttempt, out PendingRestart pendingRequest))
{
if (pendingRequest.State != RestartRequestState.Replaceable || !request.ReplaceUntilCommit)
return false;
request = request with
{
RequestId = pendingRequest.RequestId,
SourceCleanupCompletion = pendingRequest.Request.SourceCleanupCompletion,
};
pendingAttempts[request.ConnectionAttempt] = pendingRequest with { Request = request };
return true;
}
if (request.ConnectionAttempt <= highestScheduledAttempt)
bool hasPendingRequest = pendingAttempts.TryGetValue(request.ConnectionAttempt, out long previousRequestId);
if (stopped || request.ConnectionAttempt < highestScheduledAttempt
|| (request.ConnectionAttempt == highestScheduledAttempt && !hasPendingRequest))
return false;
highestScheduledAttempt = Math.Max(highestScheduledAttempt, request.ConnectionAttempt);
request = request with { RequestId = ++nextRequestId };
pendingAttempts[request.ConnectionAttempt] = new PendingRestart(
request.RequestId,
request,
RestartRequestState.Replaceable);
try
{
if (beforePublish is not null && !beforePublish())
{
pendingAttempts.Remove(request.ConnectionAttempt);
return false;
}
pendingAttempts[request.ConnectionAttempt] = request.RequestId;
if (requests.Writer.TryWrite(request))
return true;
if (requests.Writer.TryWrite(request))
{
highestScheduledAttempt = Math.Max(highestScheduledAttempt, request.ConnectionAttempt);
return true;
}
}
catch
{
if (hasPendingRequest)
pendingAttempts[request.ConnectionAttempt] = previousRequestId;
else
pendingAttempts.Remove(request.ConnectionAttempt);
throw;
}
pendingAttempts.Remove(request.ConnectionAttempt);
return false;
}
}
internal bool TryBeginCommit(RestartRequest scheduledRequest, out RestartRequest latestRequest)
{
lock (stateLock)
{
if (stopped
|| !pendingAttempts.TryGetValue(scheduledRequest.ConnectionAttempt, out PendingRestart pendingRequest)
|| pendingRequest.RequestId != scheduledRequest.RequestId
|| pendingRequest.State != RestartRequestState.Replaceable)
{
latestRequest = default;
return false;
}
latestRequest = pendingRequest.Request;
pendingAttempts[scheduledRequest.ConnectionAttempt] = pendingRequest with
{
State = RestartRequestState.Committing,
};
return true;
}
}
internal void Stop()
{
lock (stateLock)
@ -166,16 +100,13 @@ namespace MinecraftClient
{
lock (stateLock)
{
if (!pendingAttempts.TryGetValue(request.ConnectionAttempt, out PendingRestart pendingRequest)
|| pendingRequest.RequestId != request.RequestId)
if (!pendingAttempts.TryGetValue(request.ConnectionAttempt, out long requestId)
|| requestId != request.RequestId)
continue;
}
try
{
if (request.SourceCleanupCompletion is Task sourceCleanupCompletion)
await sourceCleanupCompletion.WaitAsync(shutdown.Token).ConfigureAwait(false);
await restart(request, shutdown.Token).ConfigureAwait(false);
}
catch (OperationCanceledException) when (shutdown.IsCancellationRequested)
@ -190,8 +121,8 @@ namespace MinecraftClient
{
lock (stateLock)
{
if (pendingAttempts.TryGetValue(request.ConnectionAttempt, out PendingRestart pendingRequest)
&& pendingRequest.RequestId == request.RequestId)
if (pendingAttempts.TryGetValue(request.ConnectionAttempt, out long requestId)
&& requestId == request.RequestId)
pendingAttempts.Remove(request.ConnectionAttempt);
}
}

View file

@ -60,7 +60,7 @@ namespace MinecraftClient.Scripting
string line = lines[i];
if (line.StartsWith("//using"))
{
libs.Add(NormalizeUsingDirective(line));
libs.Add(line.Replace("//", "").Trim());
}
else if (line.StartsWith("//dll"))
{
@ -125,12 +125,6 @@ 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,8 +26,6 @@ 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>
@ -145,9 +143,7 @@ public static class MccGameCommon
Material = block.Type.ToString(),
TypeLabel = block.GetTypeString(),
BlockId = block.BlockId,
BlockMeta = block.BlockMeta,
StateId = block.StateId,
Properties = block.GetStateProperties()
BlockMeta = block.BlockMeta
};
}

View file

@ -1322,8 +1322,6 @@ redirectFrom:
A lost TCP connection always triggers Auto Relog when the bot is enabled. `Kick_Messages` only filters server kick and login rejection messages. Logging out with an MCC command never triggers Auto Relog.
One logical disconnect or login rejection consumes one retry. `Ignore_Kick_Message` controls filtering, but it does not create additional restart decisions for the same failure.
- **Settings:**
**Section:** **`ChatBot.AutoRelog`**
@ -1351,8 +1349,6 @@ redirectFrom:
If `min` and `max` are equal, every attempt uses that delay. Otherwise, MCC picks a random value in the range. Values are seconds and may include a fractional part, such as `0.5` or `37.0`.
For multi-process deployments, use a nonzero range such as `{ min = 3.0, max = 10.0 }` so clients do not reconnect in lockstep during maintenance. Equal values remain supported when a fixed interval is required.
- **Format:** `{ min = <seconds (double)>, max = <seconds (double)> }`
- **Type:** `inline table`

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.16.1",
"mermaid": "11.15.0",
"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.2":
"@braintree/sanitize-url@^7.1.1":
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.2":
"@chevrotain/types@~11.1.1":
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.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==
"@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==
dependencies:
"@chevrotain/types" "~11.1.2"
"@chevrotain/types" "~11.1.1"
"@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.3:
version "3.34.0"
resolved "https://registry.yarnpkg.com/cytoscape/-/cytoscape-3.34.0.tgz#5fbe2eb1cf76b070a8ecd5647c35f65aa097c9c6"
integrity sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==
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==
"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.20:
version "1.11.21"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.21.tgz#57f87562e62de76f3c704bd2b8d522fc33068eb2"
integrity sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==
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==
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.3:
version "3.4.13"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.4.13.tgz#fc28949d59f92d62e28a3a764bcbeee35897a1be"
integrity sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==
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==
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.5"
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.5.tgz#610f37419a030270430cecd68d74e3d4d96725d0"
integrity sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==
version "3.1.4"
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.4.tgz#3b3daf9ce68f41f956df0b505132c0cfce9ec7af"
integrity sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==
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.45:
version "0.16.47"
resolved "https://registry.yarnpkg.com/katex/-/katex-0.16.47.tgz#0a13a42c2deb4f74e61f162d440b9165a548030f"
integrity sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==
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==
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.18.1"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.18.1.tgz#b962eeb80d9d983a900bf342961fb7418ca10b1d"
integrity sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==
version "4.17.23"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.23.tgz#58c4360fd1b5d33afc6c0bbd3d1149349b1138e0"
integrity sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==
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.16.1:
version "11.16.1"
resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-11.16.1.tgz#57ae2342f6c45b967113b04c9258430bdd057ee8"
integrity sha512-TQsq6u22fAn3rek5VOubrhKPo1g5hwC3FXUN9hiyupTckcYiGuuKGkNQrKYwGJkXUxZdojwRG46gsSCFZMDp4g==
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==
dependencies:
"@braintree/sanitize-url" "^7.1.2"
"@braintree/sanitize-url" "^7.1.1"
"@iconify/utils" "^3.0.2"
"@mermaid-js/parser" "^1.2.0"
"@mermaid-js/parser" "^1.1.1"
"@types/d3" "^7.4.3"
"@upsetjs/venn.js" "^2.0.0"
cytoscape "^3.33.3"
cytoscape "^3.33.1"
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.20"
dompurify "^3.3.3"
dayjs "^1.11.19"
dompurify "^3.3.1"
es-toolkit "^1.45.1"
katex "^0.16.45"
katex "^0.16.25"
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.16:
version "3.3.16"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.16.tgz#a04d8ec4b1f10009d2d533947aefe4293737816c"
integrity sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==
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@^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.23"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.23.tgz#3493550116f478487298301d2c2e8dc5a56e6594"
integrity sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==
version "8.5.14"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.14.tgz#a66c2d7808fadf69ebb5b84a03f8bafd76c4919c"
integrity sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==
dependencies:
nanoid "^3.3.16"
nanoid "^3.3.11"
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.29.0"
resolved "https://registry.yarnpkg.com/undici/-/undici-7.29.0.tgz#ae0f6f62e06e057a9cbb7b2b5fde2bb74f791b8f"
integrity sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==
version "7.28.0"
resolved "https://registry.yarnpkg.com/undici/-/undici-7.28.0.tgz#97d64564198b285bc281f0e8e29597e3d11fe7ec"
integrity sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==
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.21.1"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.21.1.tgz#045650cd4b1207809e7547146223c3814a9af586"
integrity sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==
version "8.20.0"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.20.0.tgz#4cd9532358eba60bc863aad1623dfb045a4d4af8"
integrity sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==
wsl-utils@^0.1.0:
version "0.1.0"

View file

@ -14,42 +14,25 @@ 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:")
csharp_name = "".join(word.capitalize() for word in name.split("_"))
return MATERIAL_NAME_ALIASES.get(csharp_name, csharp_name)
return "".join(word.capitalize() for word in name.split("_"))
def load_known_materials() -> set[str]:
known = set()
if MATERIAL_CS.exists():
with MATERIAL_CS.open(encoding="utf-8-sig") as f:
with open(MATERIAL_CS) as f:
for line in f:
m = re.match(r'\s+(\w+),?\s*$', line)
if m:
@ -57,122 +40,6 @@ 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__)
@ -185,18 +52,17 @@ def main():
print(f"Error: {blocks_json} not found")
sys.exit(1)
with blocks_json.open(encoding="utf-8") as f:
with open(blocks_json) as f:
data = json.load(f)
# Build block ranges and compact state-property definitions, sorted by min state.
# Build (min_state, max_state, cs_name) for each block, sorted by min_state
block_ranges = []
for block_key, block_info in data.items():
cs_name = mc_name_to_csharp(block_key)
states = block_info.get("states", [])
state_ids = [s["id"] for s in states]
if state_ids:
properties = get_state_property_definitions(block_key, states, block_info.get("properties", {}))
block_ranges.append((min(state_ids), max(state_ids), cs_name, properties))
block_ranges.append((min(state_ids), max(state_ids), cs_name))
block_ranges.sort(key=lambda x: x[0])
print(f"Loaded {len(block_ranges)} blocks from {blocks_json}")
@ -205,7 +71,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:
@ -214,15 +80,7 @@ def main():
print("Insert them in alphabetical order within the enum.")
class_name = f"Palette{class_suffix}"
output_path = OUTPUT_DIR / OUTPUT_FILE_ALIASES.get(class_suffix, f"{class_name}.cs")
state_lines, property_definition_count = render_state_definitions(block_ranges)
if update_existing_palette(output_path, state_lines):
print(
f"Updated {output_path} with {property_definition_count} property definitions "
"while preserving material mappings"
)
return
output_path = OUTPUT_DIR / f"{class_name}.cs"
lines = [
"using System.Collections.Generic;",
@ -237,36 +95,24 @@ 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), 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"
)
output_path.write_text("\n".join(lines))
print(f"Generated {output_path} with {len(block_ranges)} blocks ({max_state + 1} total states)")
if __name__ == "__main__":