This commit is contained in:
Anon 2026-04-04 00:32:07 +02:00
parent 1ca023be36
commit 3a4d8951d5
20 changed files with 1511 additions and 389 deletions

View file

@ -233,79 +233,13 @@ namespace MinecraftClient
DoClearSuggestions();
return;
}
_cancellationTokenSource?.Cancel();
using var cts = new CancellationTokenSource();
var cts = new CancellationTokenSource();
_cancellationTokenSource = cts;
var previousTask = _latestTask;
var newTask = new Task(async () =>
{
string command = fullCommand[offset..];
if (command.Length == 0)
{
List<ConsoleInteractive.ConsoleSuggestion.Suggestion> sugList = new();
sugList.Add(new("/"));
var childs = McClient.dispatcher.GetRoot().Children;
if (childs is not null)
foreach (var child in childs)
sugList.Add(new(child.Name));
foreach (var cmd in Commands)
sugList.Add(new(cmd));
SendSuggestions(sugList.ToArray(), new(offset, offset));
}
else if (command.Length > 0 && command[0] == '/' && !command.Contains(' '))
{
var sorted = Process.ExtractSorted(command[1..], Commands);
var sugList = new ConsoleInteractive.ConsoleSuggestion.Suggestion[sorted.Count()];
int index = 0;
foreach (var sug in sorted)
sugList[index++] = new(sug.Value);
SendSuggestions(sugList, new(offset, offset + command.Length));
}
else
{
CommandDispatcher<CmdResult>? dispatcher = McClient.dispatcher;
if (dispatcher is null)
return;
ParseResults<CmdResult> parse = dispatcher.Parse(command, CmdResult.Empty);
Brigadier.NET.Suggestion.Suggestions suggestions = await dispatcher.GetCompletionSuggestions(parse, buffer.CursorPosition - offset);
int sugLen = suggestions.List.Count;
if (sugLen == 0)
{
DoClearSuggestions();
return;
}
Dictionary<string, string?> dictionary = new();
foreach (var sug in suggestions.List)
dictionary.Add(sug.Text, sug.Tooltip?.String);
var sugList = new ConsoleInteractive.ConsoleSuggestion.Suggestion[sugLen];
if (cts.IsCancellationRequested)
return;
Tuple<int, int> range = new(suggestions.Range.Start + offset, suggestions.Range.End + offset);
var sorted = Process.ExtractSorted(fullCommand[range.Item1..range.Item2], dictionary.Keys);
if (cts.IsCancellationRequested)
return;
int index = 0;
foreach (var sug in sorted)
sugList[index++] = new(sug.Value, dictionary[sug.Value] ?? string.Empty);
SendSuggestions(sugList, range);
}
}, cts.Token);
Task newTask = UpdateSuggestionsAsync(fullCommand, offset, buffer.CursorPosition, cts.Token);
_latestTask = newTask;
try { newTask.Start(); } catch { }
if (_cancellationTokenSource == cts) _cancellationTokenSource = null;
_ = ObserveAutocompleteTaskAsync(newTask, cts);
}
else
{
@ -314,6 +248,108 @@ namespace MinecraftClient
}
}
private static async Task UpdateSuggestionsAsync(string fullCommand, int offset, int cursorPosition, CancellationToken cancellationToken)
{
string command = fullCommand[offset..];
if (command.Length == 0)
{
List<ConsoleInteractive.ConsoleSuggestion.Suggestion> suggestionList = new()
{
new("/")
};
var childs = McClient.dispatcher.GetRoot().Children;
if (childs is not null)
{
foreach (var child in childs)
suggestionList.Add(new(child.Name));
}
foreach (var cmd in Commands)
suggestionList.Add(new(cmd));
if (cancellationToken.IsCancellationRequested)
return;
SendSuggestions(suggestionList.ToArray(), new(offset, offset));
return;
}
if (command[0] == '/' && !command.Contains(' '))
{
var sorted = Process.ExtractSorted(command[1..], Commands);
var suggestionList = new ConsoleInteractive.ConsoleSuggestion.Suggestion[sorted.Count()];
int index = 0;
foreach (var suggestion in sorted)
suggestionList[index++] = new(suggestion.Value);
if (cancellationToken.IsCancellationRequested)
return;
SendSuggestions(suggestionList, new(offset, offset + command.Length));
return;
}
CommandDispatcher<CmdResult>? dispatcher = McClient.dispatcher;
if (dispatcher is null)
return;
ParseResults<CmdResult> parse = dispatcher.Parse(command, CmdResult.Empty);
Brigadier.NET.Suggestion.Suggestions suggestions =
await dispatcher.GetCompletionSuggestions(parse, cursorPosition - offset);
if (cancellationToken.IsCancellationRequested)
return;
int suggestionCount = suggestions.List.Count;
if (suggestionCount == 0)
{
DoClearSuggestions();
return;
}
Dictionary<string, string?> tooltips = new();
foreach (var suggestion in suggestions.List)
tooltips.Add(suggestion.Text, suggestion.Tooltip?.String);
Tuple<int, int> range = new(suggestions.Range.Start + offset, suggestions.Range.End + offset);
var sortedSuggestions = Process.ExtractSorted(fullCommand[range.Item1..range.Item2], tooltips.Keys);
if (cancellationToken.IsCancellationRequested)
return;
var suggestionListWithTooltips = new ConsoleInteractive.ConsoleSuggestion.Suggestion[suggestionCount];
int suggestionIndex = 0;
foreach (var suggestion in sortedSuggestions)
suggestionListWithTooltips[suggestionIndex++] = new(suggestion.Value, tooltips[suggestion.Value] ?? string.Empty);
SendSuggestions(suggestionListWithTooltips, range);
}
private static async Task ObserveAutocompleteTaskAsync(Task task, CancellationTokenSource cancellationTokenSource)
{
try
{
await task;
}
catch (OperationCanceledException) when (cancellationTokenSource.IsCancellationRequested)
{
}
catch (Exception e)
{
if (Settings.Config.Logging.DebugMessages)
WriteLogLine(e.ToString(), acceptnewlines: true);
DoClearSuggestions();
}
finally
{
if (ReferenceEquals(_cancellationTokenSource, cancellationTokenSource))
_cancellationTokenSource = null;
cancellationTokenSource.Dispose();
}
}
public static void AutocompleteHandler(object? sender, ConsoleInputBuffer buffer)
{
if (Settings.Config.Console.CommandSuggestion.Enable)

View file

@ -2,6 +2,8 @@
using System.IO;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
namespace MinecraftClient.Crypto
{
@ -59,6 +61,11 @@ namespace MinecraftClient.Crypto
BaseStream.Flush();
}
public override Task FlushAsync(CancellationToken cancellationToken)
{
return BaseStream.FlushAsync(cancellationToken);
}
public override long Length
{
get { throw new NotSupportedException(); }
@ -101,6 +108,15 @@ namespace MinecraftClient.Crypto
return (byte)(blockOutput[0] ^ inputBuf);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void EncryptBlock(ReadOnlySpan<byte> blockInput, Span<byte> blockOutput)
{
if (FastAes is not null)
FastAes.EncryptEcb(blockInput, blockOutput);
else
Aes!.EncryptEcb(blockInput, blockOutput, PaddingMode.None);
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public override int Read(byte[] buffer, int outOffset, int required)
{
@ -122,23 +138,11 @@ namespace MinecraftClient.Crypto
}
int processEnd = readed + curRead;
if (FastAes is not null)
for (int idx = readed; idx < processEnd; idx++)
{
for (int idx = readed; idx < processEnd; idx++)
{
ReadOnlySpan<byte> blockInput = new(inputBuf, idx, blockSize);
FastAes.EncryptEcb(blockInput, blockOutput);
buffer[outOffset + idx] = (byte)(blockOutput[0] ^ inputBuf[idx + blockSize]);
}
}
else
{
for (int idx = readed; idx < processEnd; idx++)
{
ReadOnlySpan<byte> blockInput = new(inputBuf, idx, blockSize);
Aes!.EncryptEcb(blockInput, blockOutput, PaddingMode.None);
buffer[outOffset + idx] = (byte)(blockOutput[0] ^ inputBuf[idx + blockSize]);
}
ReadOnlySpan<byte> blockInput = new(inputBuf, idx, blockSize);
EncryptBlock(blockInput, blockOutput);
buffer[outOffset + idx] = (byte)(blockOutput[0] ^ inputBuf[idx + blockSize]);
}
}
@ -161,10 +165,7 @@ namespace MinecraftClient.Crypto
{
Span<byte> blockOutput = stackalloc byte[blockSize];
if (FastAes is not null)
FastAes.EncryptEcb(WriteStreamIV, blockOutput);
else
Aes!.EncryptEcb(WriteStreamIV, blockOutput, PaddingMode.None);
EncryptBlock(WriteStreamIV, blockOutput);
byte outputBuf = (byte)(blockOutput[0] ^ b);
@ -185,15 +186,88 @@ namespace MinecraftClient.Crypto
for (int wirtten = 0; wirtten < required; ++wirtten)
{
ReadOnlySpan<byte> blockInput = new(outputBuf, wirtten, blockSize);
if (FastAes is not null)
FastAes.EncryptEcb(blockInput, blockOutput);
else
Aes!.EncryptEcb(blockInput, blockOutput, PaddingMode.None);
EncryptBlock(blockInput, blockOutput);
outputBuf[blockSize + wirtten] = (byte)(blockOutput[0] ^ input[offset + wirtten]);
}
BaseStream.WriteAsync(outputBuf, blockSize, required);
BaseStream.Write(outputBuf, blockSize, required);
Array.Copy(outputBuf, required, WriteStreamIV, 0, blockSize);
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
if (inStreamEnded || buffer.Length == 0)
return 0;
byte[] inputBuf = new byte[blockSize + buffer.Length];
Array.Copy(ReadStreamIV, inputBuf, blockSize);
for (int readed = 0; readed < buffer.Length;)
{
int curRead = await BaseStream.ReadAsync(inputBuf.AsMemory(blockSize + readed, buffer.Length - readed), cancellationToken);
if (curRead == 0)
{
inStreamEnded = true;
Array.Copy(inputBuf, readed, ReadStreamIV, 0, blockSize);
return readed;
}
int processEnd = readed + curRead;
DecryptToOutputBuffer(inputBuf, buffer, readed, processEnd);
readed = processEnd;
}
Array.Copy(inputBuf, buffer.Length, ReadStreamIV, 0, blockSize);
return buffer.Length;
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
{
if (buffer.Length == 0)
return;
byte[] outputBuf = new byte[blockSize + buffer.Length];
Array.Copy(WriteStreamIV, outputBuf, blockSize);
EncryptToOutputBuffer(buffer, outputBuf);
await BaseStream.WriteAsync(outputBuf.AsMemory(blockSize, buffer.Length), cancellationToken);
Array.Copy(outputBuf, buffer.Length, WriteStreamIV, 0, blockSize);
}
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
return ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
}
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
return WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask();
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private void DecryptToOutputBuffer(byte[] inputBuf, Memory<byte> output, int start, int end)
{
Span<byte> blockOutput = stackalloc byte[blockSize];
for (int idx = start; idx < end; idx++)
{
ReadOnlySpan<byte> blockInput = new(inputBuf, idx, blockSize);
EncryptBlock(blockInput, blockOutput);
output.Span[idx] = (byte)(blockOutput[0] ^ inputBuf[idx + blockSize]);
}
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private void EncryptToOutputBuffer(ReadOnlyMemory<byte> input, byte[] outputBuf)
{
Span<byte> blockOutput = stackalloc byte[blockSize];
for (int written = 0; written < input.Length; ++written)
{
ReadOnlySpan<byte> blockInput = new(outputBuf, written, blockSize);
EncryptBlock(blockInput, blockOutput);
outputBuf[blockSize + written] = (byte)(blockOutput[0] ^ input.Span[written]);
}
}
}
}

View file

@ -0,0 +1,45 @@
using System;
using System.Threading;
namespace MinecraftClient
{
internal static class MainThreadExecutionScope
{
private sealed class ScopeNode(object owner, ScopeNode? parent) : IDisposable
{
public object Owner { get; } = owner;
public ScopeNode? Parent { get; } = parent;
public void Dispose()
{
if (!ReferenceEquals(s_currentScope.Value, this))
throw new InvalidOperationException("Main-thread execution scope disposed out of order.");
s_currentScope.Value = Parent;
}
}
private static readonly AsyncLocal<ScopeNode?> s_currentScope = new();
public static IDisposable Enter(object owner)
{
ScopeNode scopeNode = new(owner, s_currentScope.Value);
s_currentScope.Value = scopeNode;
return scopeNode;
}
public static bool IsActive(object owner)
{
ScopeNode? scopeNode = s_currentScope.Value;
while (scopeNode is not null)
{
if (ReferenceEquals(scopeNode.Owner, owner))
return true;
scopeNode = scopeNode.Parent;
}
return false;
}
}
}

View file

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace MinecraftClient.Mapping
{
@ -150,17 +149,8 @@ namespace MinecraftClient.Mapping
public static Queue<Location>? CalculatePath(World world, Location start, Location goal, bool allowUnsafe,
int maxOffset, int minOffset, TimeSpan timeout)
{
CancellationTokenSource cts = new();
Task<Queue<Location>?> pathfindingTask = Task.Factory.StartNew(() =>
CalculatePath(world, start, goal, allowUnsafe, maxOffset, minOffset, cts.Token));
pathfindingTask.Wait(timeout);
if (!pathfindingTask.IsCompleted)
{
cts.Cancel();
pathfindingTask.Wait();
}
return pathfindingTask.Result;
using CancellationTokenSource cts = new(timeout);
return CalculatePath(world, start, goal, allowUnsafe, maxOffset, minOffset, cts.Token);
}
/// <summary>
@ -713,4 +703,4 @@ namespace MinecraftClient.Mapping
return true;
}
}
}
}

View file

@ -4,6 +4,8 @@ using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using Brigadier.NET;
using Brigadier.NET.Exceptions;
using MinecraftClient.ChatBots;
@ -42,10 +44,12 @@ namespace MinecraftClient
private readonly Queue<string> chatQueue = new();
private static DateTime nextMessageSendTime = DateTime.MinValue;
private readonly Queue<Action> threadTasks = new();
private Queue<IMainThreadTask> threadTasks = new();
private readonly Lock threadTasksLock = new();
private readonly Lock recipeBookLock = new();
private readonly Lock achievementsLock = new();
private readonly Lock consoleCommandProcessingLock = new();
private readonly Lock networkAutoCompleteLock = new();
private readonly List<ChatBot> bots = new();
private static readonly List<ChatBot> botsOnHold = new();
@ -223,7 +227,11 @@ namespace MinecraftClient
IMinecraftCom handler = null!;
SessionToken _sessionToken;
CancellationTokenSource? cmdprompt = null;
Tuple<Thread, CancellationTokenSource>? timeoutdetector = null;
private Channel<string>? consoleCommandChannel;
private Task? consoleCommandProcessingTask;
private TaskCompletionSource<string[]>? pendingNetworkAutoCompleteRequest;
private TaskCompletionSource<bool>? pendingCommandListInitialization;
Tuple<Task, CancellationTokenSource>? timeoutdetector = null;
private int transferInProgress = 0;
public ILogger Log;
@ -310,9 +318,10 @@ namespace MinecraftClient
handler = Protocol.ProtocolHandler.GetProtocolHandler(client, protocolversion, forgeInfo, this);
Log.Info(Translations.mcc_version_supported);
timeoutdetector = new(new Thread(new ParameterizedThreadStart(TimeoutDetector)), new CancellationTokenSource());
timeoutdetector.Item1.Name = "MCC Connection timeout detector";
timeoutdetector.Item1.Start(timeoutdetector.Item2.Token);
CancellationTokenSource timeoutDetectorCancellationTokenSource = new();
Task timeoutDetectorTask = TimeoutDetectorAsync(timeoutDetectorCancellationTokenSource.Token);
timeoutdetector = new(timeoutDetectorTask, timeoutDetectorCancellationTokenSource);
_ = ObserveTimeoutDetectorAsync(timeoutDetectorTask, timeoutDetectorCancellationTokenSource.Token);
try
{
@ -324,10 +333,7 @@ namespace MinecraftClient
Log.Info(string.Format(Translations.mcc_joined, Config.Main.Advanced.InternalCmdChar.ToLogString()));
cmdprompt = new CancellationTokenSource();
ConsoleIO.Backend.BeginReadThread();
ConsoleIO.Backend.MessageReceived += ConsoleReaderOnMessageReceived;
ConsoleIO.Backend.OnInputChange += ConsoleIO.AutocompleteHandler;
StartConsoleHandlers();
}
else
{
@ -369,9 +375,7 @@ namespace MinecraftClient
}
else if (InternalConfig.InteractiveMode)
{
ConsoleIO.Backend.StopReadThread();
ConsoleIO.Backend.MessageReceived -= ConsoleReaderOnMessageReceived;
ConsoleIO.Backend.OnInputChange -= ConsoleIO.AutocompleteHandler;
StopConsoleHandlers();
Program.HandleFailure();
}
@ -389,9 +393,7 @@ namespace MinecraftClient
// kick messages and Ignore_Kick_Message is false, or retry limit reached)
if (InternalConfig.InteractiveMode)
{
ConsoleIO.Backend.StopReadThread();
ConsoleIO.Backend.MessageReceived -= ConsoleReaderOnMessageReceived;
ConsoleIO.Backend.OnInputChange -= ConsoleIO.AutocompleteHandler;
StopConsoleHandlers();
Program.HandleFailure();
}
@ -415,6 +417,7 @@ namespace MinecraftClient
try
{
Log.Info($"Initiating a transfer to: {newHost}:{newPort}");
StopConsoleHandlers();
// Unload bots
UnloadAllBots();
@ -449,10 +452,7 @@ namespace MinecraftClient
UpdateKeepAlive();
Log.Info($"Successfully transferred connection and logged in to {newHost}:{newPort}.");
cmdprompt = new CancellationTokenSource();
ConsoleIO.Backend.BeginReadThread();
ConsoleIO.Backend.MessageReceived += ConsoleReaderOnMessageReceived;
ConsoleIO.Backend.OnInputChange += ConsoleIO.AutocompleteHandler;
StartConsoleHandlers();
}
else
{
@ -496,9 +496,7 @@ namespace MinecraftClient
}
else if (InternalConfig.InteractiveMode)
{
ConsoleIO.Backend.StopReadThread();
ConsoleIO.Backend.MessageReceived -= ConsoleReaderOnMessageReceived;
ConsoleIO.Backend.OnInputChange -= ConsoleIO.AutocompleteHandler;
StopConsoleHandlers();
Program.HandleFailure();
}
@ -703,15 +701,22 @@ namespace MinecraftClient
}
}
Queue<IMainThreadTask>? pendingThreadTasks = null;
lock (threadTasksLock)
{
while (threadTasks.Count > 0)
if (threadTasks.Count > 0)
{
Action taskToRun = threadTasks.Dequeue();
taskToRun();
pendingThreadTasks = threadTasks;
threadTasks = new();
}
}
if (pendingThreadTasks is not null)
{
while (pendingThreadTasks.Count > 0)
pendingThreadTasks.Dequeue().ExecuteSynchronously();
}
lock (DigLock)
{
if (RemainingDiggingTime > 0)
@ -734,29 +739,44 @@ namespace MinecraftClient
/// <summary>
/// Periodically checks for server keepalives and consider that connection has been lost if the last received keepalive is too old.
/// </summary>
private void TimeoutDetector(object? o)
private async Task TimeoutDetectorAsync(CancellationToken cancellationToken)
{
UpdateKeepAlive();
do
using PeriodicTimer periodicTimer = new(TimeSpan.FromSeconds(15));
try
{
Thread.Sleep(TimeSpan.FromSeconds(15));
if (((CancellationToken)o!).IsCancellationRequested)
return;
lock (lastKeepAliveLock)
while (await periodicTimer.WaitForNextTickAsync(cancellationToken))
{
if (lastKeepAlive.AddSeconds(Config.Main.Advanced.TcpTimeout) < DateTime.Now)
lock (lastKeepAliveLock)
{
if (((CancellationToken)o!).IsCancellationRequested)
return;
if (lastKeepAlive.AddSeconds(Config.Main.Advanced.TcpTimeout) < DateTime.Now)
{
cancellationToken.ThrowIfCancellationRequested();
OnConnectionLost(ChatBot.DisconnectReason.ConnectionLost, Translations.error_timeout);
return;
OnConnectionLost(ChatBot.DisconnectReason.ConnectionLost, Translations.error_timeout);
return;
}
}
}
}
while (!((CancellationToken)o!).IsCancellationRequested);
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
}
}
private async Task ObserveTimeoutDetectorAsync(Task timeoutDetectorTask, CancellationToken cancellationToken)
{
try
{
await timeoutDetectorTask;
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
}
catch (Exception e)
{
Log.Warn(e.ToString());
}
}
/// <summary>
@ -770,6 +790,259 @@ namespace MinecraftClient
}
}
private void StartConsoleHandlers()
{
if (ConsoleIO.Backend is null)
return;
cmdprompt = new CancellationTokenSource();
StartConsoleCommandProcessing(cmdprompt.Token);
ConsoleIO.Backend.BeginReadThread();
ConsoleIO.Backend.MessageReceived += ConsoleReaderOnMessageReceived;
ConsoleIO.Backend.OnInputChange += ConsoleIO.AutocompleteHandler;
}
private void StopConsoleHandlers()
{
if (ConsoleIO.Backend is not null)
{
ConsoleIO.Backend.StopReadThread();
ConsoleIO.Backend.MessageReceived -= ConsoleReaderOnMessageReceived;
ConsoleIO.Backend.OnInputChange -= ConsoleIO.AutocompleteHandler;
}
StopConsoleCommandProcessing();
}
private void StartConsoleCommandProcessing(CancellationToken cancellationToken)
{
lock (consoleCommandProcessingLock)
{
consoleCommandChannel = Channel.CreateUnbounded<string>(new UnboundedChannelOptions()
{
SingleReader = true,
SingleWriter = false,
AllowSynchronousContinuations = false
});
consoleCommandProcessingTask = ProcessConsoleMessagesAsync(consoleCommandChannel.Reader, cancellationToken);
_ = ObserveConsoleCommandProcessingAsync(consoleCommandProcessingTask, cancellationToken);
}
}
private void StopConsoleCommandProcessing()
{
Channel<string>? activeChannel;
lock (consoleCommandProcessingLock)
{
activeChannel = consoleCommandChannel;
consoleCommandChannel = null;
}
activeChannel?.Writer.TryComplete();
if (cmdprompt is not null)
{
cmdprompt.Cancel();
cmdprompt = null;
}
CancelPendingNetworkAutoComplete();
CancelPendingCommandListInitialization();
}
private async Task ObserveConsoleCommandProcessingAsync(Task processingTask, CancellationToken cancellationToken)
{
try
{
await processingTask;
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
}
catch (Exception e)
{
Log.Warn(e.ToString());
}
finally
{
lock (consoleCommandProcessingLock)
{
if (ReferenceEquals(consoleCommandProcessingTask, processingTask))
consoleCommandProcessingTask = null;
}
}
}
private async Task ProcessConsoleMessagesAsync(ChannelReader<string> channelReader, CancellationToken cancellationToken)
{
await foreach (string message in channelReader.ReadAllAsync(cancellationToken))
{
if (cancellationToken.IsCancellationRequested)
return;
if (TryParseBasicIoAutocompleteRequest(message, out _))
await HandleBasicIoAutocompleteRequestAsync(message, cancellationToken);
else
await InvokeOnMainThreadAsync(() => HandleCommandPromptText(message));
}
}
private async Task HandleBasicIoAutocompleteRequestAsync(string text, CancellationToken cancellationToken)
{
try
{
string[] command = text[1..].Split((char)0x00);
if (command.Length < 2 || !command[0].Equals("autocomplete", StringComparison.OrdinalIgnoreCase))
return;
await WaitForCommandListInitializationAsync(cancellationToken);
Task<string[]> requestTask = InvokeRequired
? await InvokeOnMainThreadAsync(() => BeginNetworkAutoCompleteRequest(command[1]))
: BeginNetworkAutoCompleteRequest(command[1]);
await requestTask.WaitAsync(cancellationToken);
if (command.Length > 1)
ConsoleIO.WriteLine((char)0x00 + "autocomplete" + (char)0x00 + ConsoleIO.AutoCompleteResult);
else ConsoleIO.WriteLine((char)0x00 + "autocomplete" + (char)0x00);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
}
catch (OperationCanceledException)
{
}
}
private static bool TryParseBasicIoAutocompleteRequest(string text, out string behindCursor)
{
behindCursor = string.Empty;
if (!ConsoleIO.BasicIO || string.IsNullOrEmpty(text) || text[0] != (char)0x00)
return false;
string[] command = text[1..].Split((char)0x00);
if (command.Length < 2 || !command[0].Equals("autocomplete", StringComparison.OrdinalIgnoreCase))
return false;
behindCursor = command[1];
return true;
}
private Task<string[]> BeginNetworkAutoCompleteRequest(string behindCursor)
{
if (string.IsNullOrEmpty(behindCursor))
return Task.FromResult(Array.Empty<string>());
TaskCompletionSource<string[]> request = new(TaskCreationOptions.RunContinuationsAsynchronously);
lock (networkAutoCompleteLock)
{
pendingNetworkAutoCompleteRequest?.TrySetException(new OperationCanceledException());
pendingNetworkAutoCompleteRequest = request;
}
try
{
if (handler.AutoComplete(behindCursor) < 0)
{
CompletePendingNetworkAutoComplete(Array.Empty<string>());
}
}
catch (Exception e)
{
lock (networkAutoCompleteLock)
{
if (ReferenceEquals(pendingNetworkAutoCompleteRequest, request))
pendingNetworkAutoCompleteRequest = null;
}
request.TrySetException(e);
}
return request.Task;
}
private void BeginCommandListInitialization()
{
lock (networkAutoCompleteLock)
{
pendingCommandListInitialization?.TrySetCanceled();
pendingCommandListInitialization = new(TaskCreationOptions.RunContinuationsAsynchronously);
}
}
private void CompletePendingNetworkAutoComplete(string[] result)
{
TaskCompletionSource<string[]>? pendingRequest;
lock (networkAutoCompleteLock)
{
pendingRequest = pendingNetworkAutoCompleteRequest;
pendingNetworkAutoCompleteRequest = null;
}
pendingRequest?.TrySetResult(result);
}
private void CancelPendingNetworkAutoComplete()
{
TaskCompletionSource<string[]>? pendingRequest;
lock (networkAutoCompleteLock)
{
pendingRequest = pendingNetworkAutoCompleteRequest;
pendingNetworkAutoCompleteRequest = null;
}
pendingRequest?.TrySetCanceled();
}
private void CompletePendingCommandListInitialization()
{
TaskCompletionSource<bool>? pendingInitialization;
lock (networkAutoCompleteLock)
{
pendingInitialization = pendingCommandListInitialization;
pendingCommandListInitialization = null;
}
pendingInitialization?.TrySetResult(true);
}
private void CancelPendingCommandListInitialization()
{
TaskCompletionSource<bool>? pendingInitialization;
lock (networkAutoCompleteLock)
{
pendingInitialization = pendingCommandListInitialization;
pendingCommandListInitialization = null;
}
pendingInitialization?.TrySetCanceled();
}
private async Task WaitForCommandListInitializationAsync(CancellationToken cancellationToken)
{
Task? initializationTask;
lock (networkAutoCompleteLock)
{
initializationTask = pendingCommandListInitialization?.Task;
}
if (initializationTask is null)
return;
try
{
await initializationTask.WaitAsync(TimeSpan.FromSeconds(1), cancellationToken);
}
catch (TimeoutException)
{
}
catch (OperationCanceledException)
{
}
}
/// <summary>
/// Disconnect the client from the server (initiated from MCC)
/// </summary>
@ -781,6 +1054,7 @@ namespace MinecraftClient
botsOnHold.Clear();
botsOnHold.AddRange(bots);
StopConsoleHandlers();
if (handler is not null)
{
@ -788,12 +1062,6 @@ namespace MinecraftClient
handler.Dispose();
}
if (cmdprompt is not null)
{
cmdprompt.Cancel();
cmdprompt = null;
}
if (timeoutdetector is not null)
{
timeoutdetector.Item2.Cancel();
@ -820,8 +1088,7 @@ namespace MinecraftClient
if (timeoutdetector is not null)
{
if (timeoutdetector is not null && Thread.CurrentThread != timeoutdetector.Item1)
timeoutdetector.Item2.Cancel();
timeoutdetector.Item2.Cancel();
timeoutdetector = null;
}
@ -872,9 +1139,7 @@ namespace MinecraftClient
if (!will_restart)
{
ConsoleIO.Backend.StopReadThread();
ConsoleIO.Backend.MessageReceived -= ConsoleReaderOnMessageReceived;
ConsoleIO.Backend.OnInputChange -= ConsoleIO.AutocompleteHandler;
StopConsoleHandlers();
Program.HandleFailure(null, false, reason);
}
}
@ -885,16 +1150,18 @@ namespace MinecraftClient
private void ConsoleReaderOnMessageReceived(object? sender, string e)
{
Channel<string>? activeChannel;
lock (consoleCommandProcessingLock)
{
activeChannel = consoleCommandChannel;
}
if (client.Client is null)
if (activeChannel is null || client.Client is null)
return;
if (client.Client.Connected)
{
new Thread(() =>
{
InvokeOnMainThread(() => HandleCommandPromptText(e));
}).Start();
activeChannel.Writer.TryWrite(e);
}
else
return;
@ -916,55 +1183,44 @@ namespace MinecraftClient
{
if (ConsoleIO.BasicIO && text.Length > 0 && text[0] == (char)0x00)
{
//Process a request from the GUI
string[] command = text[1..].Split((char)0x00);
switch (command[0].ToLower())
{
case "autocomplete":
int id = handler.AutoComplete(command[1]);
while (!ConsoleIO.AutoCompleteDone) { Thread.Sleep(100); }
if (command.Length > 1) { ConsoleIO.WriteLine((char)0x00 + "autocomplete" + (char)0x00 + ConsoleIO.AutoCompleteResult); }
else ConsoleIO.WriteLine((char)0x00 + "autocomplete" + (char)0x00);
break;
}
_ = HandleBasicIoAutocompleteRequestAsync(text, CancellationToken.None);
return;
}
else
{
text = text.Trim();
if (text.Length > 1
&& Config.Main.Advanced.InternalCmdChar == MainConfigHelper.MainConfig.AdvancedConfig.InternalCmdCharType.none
&& text[0] == '/')
text = text.Trim();
if (text.Length > 1
&& Config.Main.Advanced.InternalCmdChar == MainConfigHelper.MainConfig.AdvancedConfig.InternalCmdCharType.none
&& text[0] == '/')
{
SendText(text);
}
else if (text.Length > 2
&& Config.Main.Advanced.InternalCmdChar != MainConfigHelper.MainConfig.AdvancedConfig.InternalCmdCharType.none
&& text[0] == Config.Main.Advanced.InternalCmdChar.ToChar()
&& text[1] == '/')
{
SendText(text[1..]);
}
else if (text.Length > 0)
{
if (Config.Main.Advanced.InternalCmdChar == MainConfigHelper.MainConfig.AdvancedConfig.InternalCmdCharType.none
|| text[0] == Config.Main.Advanced.InternalCmdChar.ToChar())
{
SendText(text);
}
else if (text.Length > 2
&& Config.Main.Advanced.InternalCmdChar != MainConfigHelper.MainConfig.AdvancedConfig.InternalCmdCharType.none
&& text[0] == Config.Main.Advanced.InternalCmdChar.ToChar()
&& text[1] == '/')
{
SendText(text[1..]);
}
else if (text.Length > 0)
{
if (Config.Main.Advanced.InternalCmdChar == MainConfigHelper.MainConfig.AdvancedConfig.InternalCmdCharType.none
|| text[0] == Config.Main.Advanced.InternalCmdChar.ToChar())
{
CmdResult result = new();
string command = Config.Main.Advanced.InternalCmdChar.ToChar() == ' ' ? text : text[1..];
if (!PerformInternalCommand(Config.AppVar.ExpandVars(command), ref result, Settings.Config.AppVar.GetVariables()) && Config.Main.Advanced.InternalCmdChar.ToChar() == '/')
{
SendText(text);
}
else if (result.status != CmdResult.Status.NotRun && (result.status != CmdResult.Status.Done || !string.IsNullOrWhiteSpace(result.result)))
{
Log.Info(result);
}
}
else
CmdResult result = new();
string command = Config.Main.Advanced.InternalCmdChar.ToChar() == ' ' ? text : text[1..];
if (!PerformInternalCommand(Config.AppVar.ExpandVars(command), ref result, Settings.Config.AppVar.GetVariables()) && Config.Main.Advanced.InternalCmdChar.ToChar() == '/')
{
SendText(text);
}
else if (result.status != CmdResult.Status.NotRun && (result.status != CmdResult.Status.Done || !string.IsNullOrWhiteSpace(result.result)))
{
Log.Info(result);
}
}
else
{
SendText(text);
}
}
}
@ -1099,19 +1355,7 @@ namespace MinecraftClient
/// <typeparam name="T">Type of the return value</typeparam>
public T InvokeOnMainThread<T>(Func<T> task)
{
if (!InvokeRequired)
{
return task();
}
else
{
TaskWithResult<T> taskWithResult = new(task);
lock (threadTasksLock)
{
threadTasks.Enqueue(taskWithResult.ExecuteSynchronously);
}
return taskWithResult.WaitGetResult();
}
return InvokeOnMainThreadAsync(task).GetAwaiter().GetResult();
}
/// <summary>
@ -1126,6 +1370,37 @@ namespace MinecraftClient
InvokeOnMainThread(() => { task(); return true; });
}
private Task<T> InvokeOnMainThreadAsync<T>(Func<T> task)
{
if (!InvokeRequired)
{
try
{
return Task.FromResult(task());
}
catch (Exception e)
{
return Task.FromException<T>(e);
}
}
TaskWithResult<T> taskWithResult = new(task);
lock (threadTasksLock)
{
threadTasks.Enqueue(taskWithResult);
}
return taskWithResult.AsTask();
}
private Task InvokeOnMainThreadAsync(Action task)
{
return InvokeOnMainThreadAsync(() =>
{
task();
return true;
});
}
/// <summary>
/// Clear all tasks
/// </summary>
@ -1133,7 +1408,8 @@ namespace MinecraftClient
{
lock (threadTasksLock)
{
threadTasks.Clear();
while (threadTasks.Count > 0)
threadTasks.Dequeue().Cancel();
}
}
@ -1145,16 +1421,13 @@ namespace MinecraftClient
{
get
{
int callingThreadId = Environment.CurrentManagedThreadId;
if (handler is not null)
{
return handler.GetNetMainThreadId() != callingThreadId;
}
else
if (handler is null)
{
// net read thread (main thread) not yet ready
return false;
}
return !MainThreadExecutionScope.IsActive(this);
}
}
@ -3040,6 +3313,7 @@ namespace MinecraftClient
DispatchBotEvent(bot => bot.AfterGameJoined());
BeginCommandListInitialization();
ConsoleIO.InitCommandList(dispatcher);
}
@ -4445,6 +4719,8 @@ namespace MinecraftClient
public void OnAutoCompleteDone(int transactionId, string[] result)
{
ConsoleIO.OnAutoCompleteDone(transactionId, result);
CompletePendingNetworkAutoComplete(result);
CompletePendingCommandListInitialization();
}
public void SetCanSendMessage(bool canSendMessage)

View file

@ -661,7 +661,7 @@ namespace MinecraftClient
SessionCache.Store(loginLower, session);
if (result == ProtocolHandler.LoginResult.Success)
session.SessionPreCheckTask = Task.Factory.StartNew(() => session.SessionPreCheck(Config.Main.General.AccountType));
session.SessionPreCheckTask = session.SessionPreCheckAsync(Config.Main.General.AccountType);
}
if (result == ProtocolHandler.LoginResult.Success)

View file

@ -2,6 +2,8 @@ using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Inventory;
using MinecraftClient.Inventory.ItemPalettes;
using MinecraftClient.Mapping;
@ -313,6 +315,27 @@ namespace MinecraftClient.Protocol.Handlers
return i;
}
/// <summary>
/// Read an integer from the network asynchronously.
/// </summary>
/// <returns>The integer</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public async Task<int> ReadNextVarIntRAWAsync(SocketWrapper socket, CancellationToken cancellationToken)
{
int i = 0;
int j = 0;
byte b;
while (true)
{
b = (await socket.ReadDataRAWAsync(1, cancellationToken))[0];
i |= (b & 0x7F) << j++ * 7;
if (j > 5) throw new OverflowException("VarInt too big");
if ((b & 0x80) != 128) break;
}
return i;
}
/// <summary>
/// Read an integer from a cache of bytes and remove it from the cache
/// </summary>

View file

@ -7,6 +7,7 @@ using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Crypto;
using MinecraftClient.Inventory;
using MinecraftClient.Mapping;
@ -29,7 +30,9 @@ namespace MinecraftClient.Protocol.Handlers
readonly IMinecraftComHandler handler;
private bool encrypted = false;
private readonly int protocolversion;
private Tuple<Thread, CancellationTokenSource>? netRead = null;
private Task? netReadTask;
private CancellationTokenSource? netReadCancellationTokenSource;
private int netReadThreadId = -1;
Crypto.AesCfb8Stream? s;
readonly TcpClient c;
@ -69,15 +72,15 @@ namespace MinecraftClient.Protocol.Handlers
c = Client;
}
private void Updater(object? o)
private void Updater(CancellationToken cancelToken)
{
var cancelToken = (CancellationToken)o!;
if (cancelToken.IsCancellationRequested)
return;
try
{
netReadThreadId = Environment.CurrentManagedThreadId;
using IDisposable _ = MainThreadExecutionScope.Enter(handler);
Stopwatch stopWatch = Stopwatch.StartNew();
long nextUpdateDue = 0;
@ -104,6 +107,8 @@ namespace MinecraftClient.Protocol.Handlers
catch (SocketException) { }
catch (ObjectDisposedException) { }
catch (OperationCanceledException) { }
catch (Exception) { }
finally { netReadThreadId = -1; }
if (cancelToken.IsCancellationRequested)
return;
@ -240,9 +245,13 @@ namespace MinecraftClient.Protocol.Handlers
private void StartUpdating()
{
netRead = new(new Thread(new ParameterizedThreadStart(Updater)), new CancellationTokenSource());
netRead.Item1.Name = "ProtocolPacketHandler";
netRead.Item1.Start(netRead.Item2.Token);
CancellationTokenSource netReadCts = new();
netReadCancellationTokenSource = netReadCts;
netReadTask = Task.Factory.StartNew(
() => Updater(netReadCts.Token),
netReadCts.Token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
}
/// <summary>
@ -251,7 +260,7 @@ namespace MinecraftClient.Protocol.Handlers
/// <returns>Net read thread ID</returns>
public int GetNetMainThreadId()
{
return netRead is not null ? netRead.Item1.ManagedThreadId : -1;
return netReadThreadId;
}
public bool SendCookieResponse(string name, byte[]? data)
@ -268,9 +277,9 @@ namespace MinecraftClient.Protocol.Handlers
{
try
{
if (netRead is not null)
if (netReadCancellationTokenSource is not null)
{
netRead.Item2.Cancel();
netReadCancellationTokenSource.Cancel();
c.Close();
}
}
@ -519,7 +528,8 @@ namespace MinecraftClient.Protocol.Handlers
Receive(pid, 0, 1, SocketFlags.None);
while (pid[0] == 0xFA) //Skip some early plugin messages
{
ProcessPacket(pid[0]);
using (MainThreadExecutionScope.Enter(handler))
ProcessPacket(pid[0]);
Receive(pid, 0, 1, SocketFlags.None);
}
if (pid[0] == 0xFD)
@ -559,8 +569,7 @@ namespace MinecraftClient.Protocol.Handlers
if (session.ServerPublicKey is not null && session.SessionPreCheckTask is not null
&& serverIDhash == session.ServerIDhash && Enumerable.SequenceEqual(serverPublicKey, session.ServerPublicKey))
{
session.SessionPreCheckTask.Wait();
if (session.SessionPreCheckTask.Result) // PreCheck Successed
if (session.SessionPreCheckTask.IsCompletedSuccessfully && session.SessionPreCheckTask.Result)
needCheckSession = false;
}
@ -633,7 +642,8 @@ namespace MinecraftClient.Protocol.Handlers
Receive(pid, 0, 1, SocketFlags.None);
while (pid[0] >= 0xC0 && pid[0] != 0xFF) //Skip some early packets or plugin messages
{
ProcessPacket(pid[0]);
using (MainThreadExecutionScope.Enter(handler))
ProcessPacket(pid[0]);
Receive(pid, 0, 1, SocketFlags.None);
}
if (pid[0] == (byte)1)

View file

@ -9,6 +9,7 @@ using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Crypto;
using MinecraftClient.Inventory;
using MinecraftClient.Inventory.ItemPalettes;
@ -117,8 +118,11 @@ namespace MinecraftClient.Protocol.Handlers
readonly PacketTypePalette packetPalette;
readonly SocketWrapper socketWrapper;
readonly DataTypes dataTypes;
Tuple<Thread, CancellationTokenSource>? netMain = null; // main thread
Tuple<Thread, CancellationTokenSource>? netReader = null; // reader thread
private Task? netMainTask;
private CancellationTokenSource? netMainCancellationTokenSource;
private int netMainThreadId = -1;
private Task? netReaderTask;
private CancellationTokenSource? netReaderCancellationTokenSource;
readonly ILogger log;
readonly RandomNumberGenerator randomGen;
private bool legacyAchievementsInitialized;
@ -278,17 +282,17 @@ namespace MinecraftClient.Protocol.Handlers
}
/// <summary>
/// Separate thread. Network reading loop.
/// Serialized packet/tick loop.
/// </summary>
private void Updater(object? o)
private void Updater(CancellationToken cancelToken)
{
var cancelToken = (CancellationToken)o!;
if (cancelToken.IsCancellationRequested)
return;
try
{
netMainThreadId = Environment.CurrentManagedThreadId;
using IDisposable _ = MainThreadExecutionScope.Enter(handler);
Stopwatch stopWatch = Stopwatch.StartNew();
long nextUpdateDue = 0;
while (!packetQueue.IsAddingCompleted)
@ -330,6 +334,13 @@ namespace MinecraftClient.Protocol.Handlers
catch (System.IO.IOException)
{
}
catch (Exception)
{
}
finally
{
netMainThreadId = -1;
}
if (cancelToken.IsCancellationRequested)
return;
@ -340,20 +351,13 @@ namespace MinecraftClient.Protocol.Handlers
/// <summary>
/// Read and decompress packets.
/// </summary>
internal void PacketReader(object? o)
internal async Task PacketReaderAsync(CancellationToken cancelToken)
{
var cancelToken = (CancellationToken)o!;
while (socketWrapper.IsConnected() && !cancelToken.IsCancellationRequested)
while (!cancelToken.IsCancellationRequested)
{
try
{
while (socketWrapper.HasDataAvailable())
{
packetQueue.Add(ReadNextPacket(), cancelToken);
if (cancelToken.IsCancellationRequested)
break;
}
packetQueue.Add(await ReadNextPacketAsync(cancelToken), cancelToken);
}
catch (OperationCanceledException)
{
@ -375,11 +379,10 @@ namespace MinecraftClient.Protocol.Handlers
{
break;
}
if (cancelToken.IsCancellationRequested)
catch (Exception)
{
break;
Thread.Sleep(10);
}
}
packetQueue.CompleteAdding();
@ -415,6 +418,30 @@ namespace MinecraftClient.Protocol.Handlers
return new(packetId, packetData);
}
internal async Task<Tuple<int, Queue<byte>>> ReadNextPacketAsync(CancellationToken cancellationToken)
{
var size = await dataTypes.ReadNextVarIntRAWAsync(socketWrapper, cancellationToken); //Packet size
Queue<byte> packetData = new(await socketWrapper.ReadDataRAWAsync(size, cancellationToken)); //Packet contents
if (protocolVersion >= MC_1_8_Version
&& compression_treshold >= 0)
{
var sizeUncompressed = dataTypes.ReadNextVarInt(packetData);
if (sizeUncompressed != 0)
{
var toDecompress = packetData.ToArray();
var uncompressed = ZlibUtils.Decompress(toDecompress, sizeUncompressed);
packetData = new Queue<byte>(uncompressed);
}
}
var packetId = dataTypes.ReadNextVarInt(packetData);
if (handler.GetNetworkPacketCaptureEnabled())
handler.OnNetworkPacket(packetId, packetData.ToList(), currentState == CurrentState.Login, true);
return new(packetId, packetData);
}
/// <summary>
/// Handle the given packet
/// </summary>
@ -3844,19 +3871,17 @@ namespace MinecraftClient.Protocol.Handlers
/// </summary>
private void StartUpdating()
{
Thread threadUpdater = new(new ParameterizedThreadStart(Updater))
{
Name = "ProtocolPacketHandler"
};
netMain = new Tuple<Thread, CancellationTokenSource>(threadUpdater, new CancellationTokenSource());
threadUpdater.Start(netMain.Item2.Token);
CancellationTokenSource netMainCts = new();
netMainCancellationTokenSource = netMainCts;
netMainTask = Task.Factory.StartNew(
() => Updater(netMainCts.Token),
netMainCts.Token,
TaskCreationOptions.LongRunning,
TaskScheduler.Default);
Thread threadReader = new(new ParameterizedThreadStart(PacketReader))
{
Name = "ProtocolPacketReader"
};
netReader = new Tuple<Thread, CancellationTokenSource>(threadReader, new CancellationTokenSource());
threadReader.Start(netReader.Item2.Token);
CancellationTokenSource netReaderCts = new();
netReaderCancellationTokenSource = netReaderCts;
netReaderTask = PacketReaderAsync(netReaderCts.Token);
}
/// <summary>
@ -3865,7 +3890,7 @@ namespace MinecraftClient.Protocol.Handlers
/// <returns>Net read thread ID</returns>
public int GetNetMainThreadId()
{
return netMain is not null ? netMain.Item1.ManagedThreadId : -1;
return netMainThreadId;
}
/// <summary>
@ -3875,14 +3900,14 @@ namespace MinecraftClient.Protocol.Handlers
{
try
{
if (netMain is not null)
if (netMainCancellationTokenSource is not null)
{
netMain.Item2.Cancel();
netMainCancellationTokenSource.Cancel();
}
if (netReader is not null)
if (netReaderCancellationTokenSource is not null)
{
netReader.Item2.Cancel();
netReaderCancellationTokenSource.Cancel();
socketWrapper.Disconnect();
}
}
@ -4106,7 +4131,8 @@ namespace MinecraftClient.Protocol.Handlers
return true; //No need to check session or start encryption
}
default:
HandlePacket(packetId, packetData);
using (MainThreadExecutionScope.Enter(handler))
HandlePacket(packetId, packetData);
break;
}
}
@ -4133,8 +4159,7 @@ namespace MinecraftClient.Protocol.Handlers
&& serverIDhash == session.ServerIDhash &&
serverPublicKey.SequenceEqual(session.ServerPublicKey))
{
session.SessionPreCheckTask.Wait();
if (session.SessionPreCheckTask.Result) // PreCheck Success
if (session.SessionPreCheckTask.IsCompletedSuccessfully && session.SessionPreCheckTask.Result)
needCheckSession = false;
}
@ -4256,7 +4281,8 @@ namespace MinecraftClient.Protocol.Handlers
return true;
}
default:
HandlePacket(packetId, packetData);
using (MainThreadExecutionScope.Enter(handler))
HandlePacket(packetId, packetData);
break;
}
}

View file

@ -1,5 +1,8 @@
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Crypto;
namespace MinecraftClient.Protocol.Handlers
@ -68,6 +71,22 @@ namespace MinecraftClient.Protocol.Handlers
}
}
private async Task ReceiveAsync(Memory<byte> buffer, CancellationToken cancellationToken)
{
int read = 0;
while (read < buffer.Length)
{
int currentRead = encrypted
? await s!.ReadAsync(buffer[read..], cancellationToken)
: await c.GetStream().ReadAsync(buffer[read..], cancellationToken);
if (currentRead == 0)
throw new IOException("Connection closed.");
read += currentRead;
}
}
/// <summary>
/// Read some data from the server.
/// </summary>
@ -84,6 +103,18 @@ namespace MinecraftClient.Protocol.Handlers
return Array.Empty<byte>();
}
public async Task<byte[]> ReadDataRAWAsync(int length, CancellationToken cancellationToken)
{
if (length > 0)
{
byte[] cache = new byte[length];
await ReceiveAsync(cache, cancellationToken);
return cache;
}
return Array.Empty<byte>();
}
/// <summary>
/// Send raw data to the server.
/// </summary>
@ -99,6 +130,17 @@ namespace MinecraftClient.Protocol.Handlers
c.Client.Send(buffer);
}
public async Task SendDataRAWAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
{
if (!IsConnected())
throw new SocketException((int)SocketError.NotConnected);
if (encrypted)
await s!.WriteAsync(buffer, cancellationToken);
else
await c.GetStream().WriteAsync(buffer, cancellationToken);
}
/// <summary>
/// Disconnect from the server
/// </summary>

View file

@ -7,6 +7,7 @@ using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using static MinecraftClient.Settings;
@ -231,6 +232,8 @@ namespace MinecraftClient.Protocol.Message
/// Specify whether translation rules have been loaded
/// </summary>
private static bool RulesInitialized = false;
private static readonly Lock RulesInitializationLock = new();
private static Task? RulesRefreshTask = null;
/// <summary>
/// Set of translation rules for formatting text
@ -243,23 +246,25 @@ namespace MinecraftClient.Protocol.Message
/// </summary>
public static void InitTranslations()
{
if (!RulesInitialized)
lock (RulesInitializationLock)
{
InitRules();
if (RulesInitialized)
return;
RulesInitialized = true;
RulesRefreshTask = InitRulesAsync();
_ = ObserveInitRulesAsync(RulesRefreshTask);
}
}
/// <summary>
/// Internal rule initialization method. Looks for local rule file or download it from Mojang asset servers.
/// Internal rule initialization method. Looks for local rule file and refreshes it from Mojang asset servers if needed.
/// </summary>
private static void InitRules()
private static async Task InitRulesAsync()
{
if (Config.Main.Advanced.Language == "en_us")
{
TranslationRules =
JsonSerializer.Deserialize<Dictionary<string, string>>(
(byte[])MinecraftAssets.ResourceManager.GetObject("en_us.json")!)!;
TranslationRules = LoadEmbeddedTranslationRules();
return;
}
@ -269,21 +274,9 @@ namespace MinecraftClient.Protocol.Message
string languageFilePath = "lang" + Path.DirectorySeparatorChar + Config.Main.Advanced.Language + ".json";
// Load the external dictionary of translation rules or display an error message
if (File.Exists(languageFilePath))
{
try
{
TranslationRules =
JsonSerializer.Deserialize<Dictionary<string, string>>(File.OpenRead(languageFilePath))!;
}
catch (IOException)
{
}
catch (JsonException)
{
}
}
if (TryLoadTranslationRulesFromFile(languageFilePath, out Dictionary<string, string>? translationRules))
TranslationRules = translationRules;
else TranslationRules = LoadEmbeddedTranslationRules();
if (TranslationRules.TryGetValue("Version", out string? version) &&
version == Settings.TranslationsFile_Version)
@ -296,14 +289,12 @@ namespace MinecraftClient.Protocol.Message
// Try downloading language file from Mojang's servers?
ConsoleIO.WriteLineFormatted(
"§8" + string.Format(Translations.chat_download, Config.Main.Advanced.Language));
HttpClient httpClient = new();
using HttpClient httpClient = new();
try
{
Task<string> fetch_index = httpClient.GetStringAsync(TranslationsFile_Website_Index);
fetch_index.Wait();
Match match = Regex.Match(fetch_index.Result,
string fetchIndex = await httpClient.GetStringAsync(TranslationsFile_Website_Index);
Match match = Regex.Match(fetchIndex,
$"minecraft/lang/{Config.Main.Advanced.Language}.json" + @""":\s\{""hash"":\s""([\d\w]{40})""");
fetch_index.Dispose();
if (match.Success && match.Groups.Count == 2)
{
string hash = match.Groups[1].Value;
@ -312,22 +303,19 @@ namespace MinecraftClient.Protocol.Message
ConsoleIO.WriteLineFormatted(
string.Format(Translations.chat_request, translation_file_location));
Task<Dictionary<string, string>?> fetckFileTask =
httpClient.GetFromJsonAsync<Dictionary<string, string>>(translation_file_location);
fetckFileTask.Wait();
if (fetckFileTask.Result is not null && fetckFileTask.Result.Count > 0)
Dictionary<string, string>? fetchedFile =
await httpClient.GetFromJsonAsync<Dictionary<string, string>>(translation_file_location);
if (fetchedFile is not null && fetchedFile.Count > 0)
{
TranslationRules = fetckFileTask.Result;
TranslationRules = fetchedFile;
TranslationRules["Version"] = TranslationsFile_Version;
File.WriteAllText(languageFilePath,
await File.WriteAllTextAsync(languageFilePath,
JsonSerializer.Serialize(TranslationRules, typeof(Dictionary<string, string>)),
Encoding.UTF8);
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.chat_done, languageFilePath));
return;
}
fetckFileTask.Dispose();
}
else
{
@ -350,17 +338,52 @@ namespace MinecraftClient.Protocol.Message
if (Config.Logging.DebugMessages && !string.IsNullOrEmpty(e.StackTrace))
ConsoleIO.WriteLine(e.StackTrace);
}
finally
{
httpClient.Dispose();
}
TranslationRules =
JsonSerializer.Deserialize<Dictionary<string, string>>(
(byte[])MinecraftAssets.ResourceManager.GetObject("en_us.json")!)!;
TranslationRules = LoadEmbeddedTranslationRules();
ConsoleIO.WriteLine(Translations.chat_use_default);
}
private static async Task ObserveInitRulesAsync(Task initRulesTask)
{
try
{
await initRulesTask;
}
catch (Exception e)
{
TranslationRules = LoadEmbeddedTranslationRules();
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLine(e.ToString());
}
}
private static Dictionary<string, string> LoadEmbeddedTranslationRules()
{
return JsonSerializer.Deserialize<Dictionary<string, string>>(
(byte[])MinecraftAssets.ResourceManager.GetObject("en_us.json")!)!;
}
private static bool TryLoadTranslationRulesFromFile(string languageFilePath, out Dictionary<string, string>? translationRules)
{
translationRules = null;
if (!File.Exists(languageFilePath))
return false;
try
{
translationRules =
JsonSerializer.Deserialize<Dictionary<string, string>>(File.OpenRead(languageFilePath))!;
return translationRules is not null;
}
catch (IOException)
{
return false;
}
catch (JsonException)
{
return false;
}
}
public static string? TranslateString(string rulename)
{
if (TranslationRules.TryGetValue(rulename, out string? result))
@ -617,4 +640,4 @@ namespace MinecraftClient.Protocol.Message
return formatting + message + extraBuilder.ToString();
}
}
}
}

View file

@ -6,6 +6,7 @@ using System.Globalization;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace MinecraftClient.Protocol
{
@ -37,7 +38,13 @@ namespace MinecraftClient.Protocol
{
string postData = "client_id={0}&grant_type=authorization_code&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&code={1}";
postData = string.Format(postData, clientId, code);
return RequestToken(postData);
return RequestTokenAsync(postData).GetAwaiter().GetResult();
}
public static Task<LoginResponse> RequestAccessTokenAsync(string code)
{
string postData = "client_id={0}&grant_type=authorization_code&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&code={1}";
return RequestTokenAsync(string.Format(postData, clientId, code));
}
/// <summary>
@ -49,7 +56,13 @@ namespace MinecraftClient.Protocol
{
string postData = "client_id={0}&grant_type=refresh_token&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&refresh_token={1}";
postData = string.Format(postData, clientId, refreshToken);
return RequestToken(postData);
return RequestTokenAsync(postData).GetAwaiter().GetResult();
}
public static Task<LoginResponse> RefreshAccessTokenAsync(string refreshToken)
{
string postData = "client_id={0}&grant_type=refresh_token&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&refresh_token={1}";
return RequestTokenAsync(string.Format(postData, clientId, refreshToken));
}
/// <summary>
@ -58,6 +71,11 @@ namespace MinecraftClient.Protocol
/// </summary>
/// <returns>Device code response for user to complete authentication</returns>
public static DeviceCodeResponse RequestDeviceCode()
{
return RequestDeviceCodeAsync().GetAwaiter().GetResult();
}
public static async Task<DeviceCodeResponse> RequestDeviceCodeAsync(CancellationToken cancellationToken = default)
{
string postData = string.Format("client_id={0}&scope=XboxLive.signin%20offline_access%20openid%20email", clientId);
@ -65,7 +83,7 @@ namespace MinecraftClient.Protocol
{
UserAgent = "MCC/" + Program.Version
};
var response = request.Post("application/x-www-form-urlencoded", postData);
var response = await request.PostAsync("application/x-www-form-urlencoded", postData, cancellationToken);
var jsonData = Json.ParseJson(response.Body);
if (jsonData?["error"] is not null)
@ -93,6 +111,11 @@ namespace MinecraftClient.Protocol
/// <param name="interval">Polling interval in seconds</param>
/// <returns>Login response with access token and refresh token</returns>
public static LoginResponse PollDeviceCodeToken(string deviceCode, int expiresIn, int interval)
{
return PollDeviceCodeTokenAsync(deviceCode, expiresIn, interval).GetAwaiter().GetResult();
}
public static async Task<LoginResponse> PollDeviceCodeTokenAsync(string deviceCode, int expiresIn, int interval, CancellationToken cancellationToken = default)
{
// Per OAuth 2.0 device code spec, server may respond with "slow_down" requiring
// the client to increase its polling interval by this amount
@ -107,13 +130,13 @@ namespace MinecraftClient.Protocol
while (stopwatch.Elapsed.TotalSeconds < expiresIn)
{
Thread.Sleep(pollInterval * 1000);
await Task.Delay(TimeSpan.FromSeconds(pollInterval), cancellationToken);
var request = new ProxiedWebRequest(tokenUrl)
{
UserAgent = "MCC/" + Program.Version
};
var response = request.Post("application/x-www-form-urlencoded", postData);
var response = await request.PostAsync("application/x-www-form-urlencoded", postData, cancellationToken);
var jsonData = Json.ParseJson(response.Body);
if (jsonData?["error"] is not null)
@ -173,12 +196,17 @@ namespace MinecraftClient.Protocol
/// <param name="postData">Complete POST data for the request</param>
/// <returns></returns>
private static LoginResponse RequestToken(string postData)
{
return RequestTokenAsync(postData).GetAwaiter().GetResult();
}
private static async Task<LoginResponse> RequestTokenAsync(string postData, CancellationToken cancellationToken = default)
{
var request = new ProxiedWebRequest(tokenUrl)
{
UserAgent = "MCC/" + Program.Version
};
var response = request.Post("application/x-www-form-urlencoded", postData);
var response = await request.PostAsync("application/x-www-form-urlencoded", postData, cancellationToken);
var jsonData = Json.ParseJson(response.Body);
// Error handling
@ -271,6 +299,11 @@ namespace MinecraftClient.Protocol
/// <param name="loginResponse"></param>
/// <returns></returns>
public static XblAuthenticateResponse XblAuthenticate(Microsoft.LoginResponse loginResponse)
{
return XblAuthenticateAsync(loginResponse).GetAwaiter().GetResult();
}
public static async Task<XblAuthenticateResponse> XblAuthenticateAsync(Microsoft.LoginResponse loginResponse, CancellationToken cancellationToken = default)
{
var request = new ProxiedWebRequest(xbl)
{
@ -291,7 +324,7 @@ namespace MinecraftClient.Protocol
+ "\"RelyingParty\": \"http://auth.xboxlive.com\","
+ "\"TokenType\": \"JWT\""
+ "}";
var response = request.Post("application/json", payload);
var response = await request.PostAsync("application/json", payload, cancellationToken);
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLine(response.ToString());
@ -321,6 +354,11 @@ namespace MinecraftClient.Protocol
/// <param name="xblResponse"></param>
/// <returns></returns>
public static XSTSAuthenticateResponse XSTSAuthenticate(XblAuthenticateResponse xblResponse)
{
return XSTSAuthenticateAsync(xblResponse).GetAwaiter().GetResult();
}
public static async Task<XSTSAuthenticateResponse> XSTSAuthenticateAsync(XblAuthenticateResponse xblResponse, CancellationToken cancellationToken = default)
{
var request = new ProxiedWebRequest(xsts)
{
@ -339,7 +377,7 @@ namespace MinecraftClient.Protocol
+ "\"RelyingParty\": \"rp://api.minecraftservices.com/\","
+ "\"TokenType\": \"JWT\""
+ "}";
var response = request.Post("application/json", payload);
var response = await request.PostAsync("application/json", payload, cancellationToken);
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLine(response.ToString());
@ -404,6 +442,11 @@ namespace MinecraftClient.Protocol
/// <param name="xstsToken"></param>
/// <returns></returns>
public static string LoginWithXbox(string userHash, string xstsToken)
{
return LoginWithXboxAsync(userHash, xstsToken).GetAwaiter().GetResult();
}
public static async Task<string> LoginWithXboxAsync(string userHash, string xstsToken, CancellationToken cancellationToken = default)
{
var request = new ProxiedWebRequest(loginWithXbox)
{
@ -411,7 +454,7 @@ namespace MinecraftClient.Protocol
};
string payload = "{\"identityToken\": \"XBL3.0 x=" + userHash + ";" + xstsToken + "\"}";
var response = request.Post("application/json", payload);
var response = await request.PostAsync("application/json", payload, cancellationToken);
if (Settings.Config.Logging.DebugMessages)
{
@ -430,10 +473,15 @@ namespace MinecraftClient.Protocol
/// <param name="accessToken"></param>
/// <returns>True if the user own the game</returns>
public static bool UserHasGame(string accessToken)
{
return UserHasGameAsync(accessToken).GetAwaiter().GetResult();
}
public static async Task<bool> UserHasGameAsync(string accessToken, CancellationToken cancellationToken = default)
{
var request = new ProxiedWebRequest(ownership);
request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken));
var response = request.Get();
var response = await request.GetAsync(cancellationToken);
if (Settings.Config.Logging.DebugMessages)
{
@ -446,10 +494,15 @@ namespace MinecraftClient.Protocol
}
public static UserProfile GetUserProfile(string accessToken)
{
return GetUserProfileAsync(accessToken).GetAwaiter().GetResult();
}
public static async Task<UserProfile> GetUserProfileAsync(string accessToken, CancellationToken cancellationToken = default)
{
var request = new ProxiedWebRequest(profile);
request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken));
var response = request.Get();
var response = await request.GetAsync(cancellationToken);
if (Settings.Config.Logging.DebugMessages)
{

View file

@ -7,6 +7,8 @@ using System.Net.Http;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using DnsClient;
using MinecraftClient.Protocol.Handlers;
using MinecraftClient.Protocol.Handlers.Forge;
@ -1108,6 +1110,43 @@ namespace MinecraftClient.Protocol
}
}
public static async Task<bool> SessionCheckAsync(string uuid, string accesstoken, string serverhash, LoginType type)
{
try
{
string jsonRequest = "{\"accessToken\":\"" + accesstoken + "\",\"selectedProfile\":\"" + uuid +
"\",\"serverId\":\"" + serverhash + "\"}";
string host = type == LoginType.yggdrasil
? Config.Main.General.AuthServer.Host
: "sessionserver.mojang.com";
int port = type == LoginType.yggdrasil ? Config.Main.General.AuthServer.Port : 443;
string endpoint = type == LoginType.yggdrasil
? Config.Main.General.AuthServer.AuthlibInjectorAPIPath + "/sessionserver/session/minecraft/join"
: "/session/minecraft/join";
bool useHttps = type == LoginType.yggdrasil ? Config.Main.General.AuthServer.UseHttps : true;
var response = await DoHTTPSRequestAsync(
HttpMethod.Post,
host,
port,
endpoint,
new Dictionary<string, string>
{
{ "Accept", "application/json" },
{ "Content-Type", "application/json" }
},
jsonRequest,
useHttps,
CancellationToken.None);
return response.StatusCode >= 200 && response.StatusCode < 300;
}
catch
{
return false;
}
}
/// <summary>
/// Retrieve available Realms worlds of a player and display them
/// </summary>
@ -1349,6 +1388,57 @@ namespace MinecraftClient.Protocol
return statusCode;
}
private static async Task<(int StatusCode, string Result)> DoHTTPSRequestAsync(HttpMethod method, string host, int port, string path, Dictionary<string, string> headers, string? body, bool useHttps, CancellationToken cancellationToken)
{
if (Settings.Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.debug_request, host));
using SocketsHttpHandler handler = new();
handler.ConnectCallback = async (ctx, ct) =>
{
TcpClient client = ProxyHandler.NewTcpClient(host, port, true);
return client.GetStream();
};
using HttpClient client = new(handler);
string scheme = useHttps ? "https" : "http";
using HttpRequestMessage request = new(method, scheme + "://" + host + ":" + port + path);
string contentType = "text/plain";
foreach (var header in headers)
{
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
if (header.Key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase))
contentType = header.Value;
}
if (body is not null)
request.Content = new StringContent(body, Encoding.UTF8, contentType);
if (Settings.Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted("§8> " + request);
using CancellationTokenSource timeoutCancellationTokenSource =
CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(30));
using HttpResponseMessage response = await client.SendAsync(request, timeoutCancellationTokenSource.Token);
int statusCode = (int)response.StatusCode;
string responseBody = statusCode == 204
? "No Content"
: await response.Content.ReadAsStringAsync(timeoutCancellationTokenSource.Token);
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLine("");
foreach (string line in responseBody.Split('\n'))
ConsoleIO.WriteLineFormatted("§8< " + line);
}
return (statusCode, responseBody);
}
/// <summary>
/// Encode a string to a json string.
/// Will convert special chars to \u0000 unicode escape sequences.
@ -1389,4 +1479,4 @@ namespace MinecraftClient.Protocol
return dateTime;
}
}
}
}

View file

@ -3,6 +3,8 @@ using System.Collections.Specialized;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Proxy;
namespace MinecraftClient.Protocol
@ -72,6 +74,12 @@ namespace MinecraftClient.Protocol
/// </summary>
public Response Get() => Send(HttpMethod.Get);
/// <summary>
/// Perform GET request asynchronously. Proxy is handled automatically.
/// </summary>
public Task<Response> GetAsync(CancellationToken cancellationToken = default) =>
SendAsync(HttpMethod.Get, cancellationToken: cancellationToken);
/// <summary>
/// Perform POST request. Proxy is handled automatically.
/// </summary>
@ -79,6 +87,14 @@ namespace MinecraftClient.Protocol
/// <param name="body">Request body</param>
public Response Post(string contentType, string body) => Send(HttpMethod.Post, contentType, body);
/// <summary>
/// Perform POST request asynchronously. Proxy is handled automatically.
/// </summary>
/// <param name="contentType">The content type of request body</param>
/// <param name="body">Request body</param>
public Task<Response> PostAsync(string contentType, string body, CancellationToken cancellationToken = default) =>
SendAsync(HttpMethod.Post, contentType, body, cancellationToken);
/// <summary>
/// Send an HTTP request. Proxy is configured automatically from Settings.
/// </summary>
@ -144,6 +160,66 @@ namespace MinecraftClient.Protocol
}
}
/// <summary>
/// Send an HTTP request asynchronously. Proxy is configured automatically from Settings.
/// </summary>
private async Task<Response> SendAsync(HttpMethod method, string? contentType = null, string? body = null, CancellationToken cancellationToken = default)
{
using var handler = CreateHandler();
using var client = new HttpClient(handler);
using var request = new HttpRequestMessage(method, _uri);
foreach (string key in Headers)
{
if (key.Equals("Content-Type", StringComparison.OrdinalIgnoreCase) ||
key.Equals("Content-Length", StringComparison.OrdinalIgnoreCase) ||
key.Equals("Host", StringComparison.OrdinalIgnoreCase))
continue;
request.Headers.TryAddWithoutValidation(key, Headers[key]);
}
if (body is not null)
request.Content = new StringContent(body, Encoding.UTF8, contentType ?? "text/plain");
if (Debug)
{
ConsoleIO.WriteLine($"< {method} {_uri}");
foreach (string key in Headers)
ConsoleIO.WriteLine($"< {key}: {Headers[key]}");
}
try
{
using var httpResponse = await client.SendAsync(request, cancellationToken);
string responseBody = await httpResponse.Content.ReadAsStringAsync(cancellationToken);
var responseHeaders = new NameValueCollection();
foreach (var header in httpResponse.Headers)
foreach (var val in header.Value)
responseHeaders.Add(header.Key.ToLowerInvariant(), val);
foreach (var header in httpResponse.Content.Headers)
foreach (var val in header.Value)
responseHeaders.Add(header.Key.ToLowerInvariant(), val);
var cookies = new NameValueCollection();
foreach (Cookie cookie in handler.CookieContainer.GetCookies(_uri))
{
if (!cookie.Expired)
cookies.Add(cookie.Name, cookie.Value);
}
return new Response((int)httpResponse.StatusCode, responseBody, responseHeaders, cookies);
}
catch (HttpRequestException ex)
{
if (Debug)
ConsoleIO.WriteLine("HTTP error: " + ex.Message);
return Response.Empty();
}
}
/// <summary>
/// Create a SocketsHttpHandler with proxy support from ProxyHandler settings.
/// </summary>
@ -231,4 +307,4 @@ namespace MinecraftClient.Protocol
}
}
}
}
}

View file

@ -54,6 +54,16 @@ namespace MinecraftClient.Protocol.Session
return false;
}
public async Task<bool> SessionPreCheckAsync(LoginType type)
{
if (ID == string.Empty || PlayerID == String.Empty || ServerPublicKey is null)
return false;
Crypto.CryptoHandler.ClientAESPrivateKey ??= Crypto.CryptoHandler.GenerateAESPrivateKey();
string serverHash = Crypto.CryptoHandler.GetServerHash(ServerIDhash, ServerPublicKey, Crypto.CryptoHandler.ClientAESPrivateKey);
return await ProtocolHandler.SessionCheckAsync(PlayerID, ID, serverHash, type);
}
public override string ToString()
{
return String.Join(",", ID, PlayerName, PlayerID, ClientID, RefreshToken, ServerIDhash,

View file

@ -1,20 +1,24 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace MinecraftClient
{
internal interface IMainThreadTask
{
void ExecuteSynchronously();
void Cancel();
}
/// <summary>
/// Holds an asynchronous task with return value
/// </summary>
/// <typeparam name="T">Type of the return value</typeparam>
public class TaskWithResult<T>
public sealed class TaskWithResult<T> : IMainThreadTask
{
private readonly AutoResetEvent resultEvent = new(false);
private readonly Func<T> task;
private T? result = default;
private Exception? exception = null;
private bool taskRun = false;
private readonly Lock taskRunLock = new();
private readonly TaskCompletionSource<T> completionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
private int taskState;
/// <summary>
/// Create a new asynchronous task with return value
@ -28,13 +32,7 @@ namespace MinecraftClient
/// <summary>
/// Check whether the task has finished running
/// </summary>
public bool HasRun
{
get
{
return taskRun;
}
}
public bool HasRun => completionSource.Task.IsCompleted;
/// <summary>
/// Get the task result (return value of the inner delegate)
@ -44,10 +42,10 @@ namespace MinecraftClient
{
get
{
if (taskRun)
return result!;
else
if (!completionSource.Task.IsCompleted)
throw new InvalidOperationException("Attempting to retrieve the result of an unfinished task");
return completionSource.Task.GetAwaiter().GetResult();
}
}
@ -58,40 +56,39 @@ namespace MinecraftClient
{
get
{
return exception;
return completionSource.Task.Exception?.InnerException;
}
}
public Task<T> AsTask()
{
return completionSource.Task;
}
/// <summary>
/// Execute the task in the current thread and set the <see cref="Result"/> property or <see cref=""/>to the returned value
/// </summary>
public void ExecuteSynchronously()
{
// Make sur the task will not run twice
lock (taskRunLock)
{
if (taskRun)
{
throw new InvalidOperationException("Attempting to run a task twice");
}
}
if (Interlocked.CompareExchange(ref taskState, 1, 0) != 0)
throw new InvalidOperationException("Attempting to run a task twice");
// Run the task
try
{
result = task();
completionSource.TrySetResult(task());
}
catch (Exception e)
{
exception = e;
completionSource.TrySetException(e);
}
}
// Mark task as complete and release wait event
lock (taskRunLock)
{
taskRun = true;
}
resultEvent.Set();
public void Cancel()
{
if (Interlocked.CompareExchange(ref taskState, 1, 0) != 0)
return;
completionSource.TrySetException(new OperationCanceledException("Main-thread task was canceled before execution."));
}
/// <summary>
@ -101,22 +98,7 @@ namespace MinecraftClient
/// <exception cref="System.Exception">Any exception thrown by the task</exception>
public T WaitGetResult()
{
// Wait only if the result is not available yet
bool mustWait = false;
lock (taskRunLock)
{
mustWait = !taskRun;
}
if (mustWait)
{
resultEvent.WaitOne();
}
// Receive exception from task
if (exception is not null)
throw exception;
return result!;
return completionSource.Task.GetAwaiter().GetResult();
}
}
}