mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Stage 1
This commit is contained in:
parent
1ca023be36
commit
3a4d8951d5
20 changed files with 1511 additions and 389 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
45
MinecraftClient/MainThreadExecutionScope.cs
Normal file
45
MinecraftClient/MainThreadExecutionScope.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue