Refactoring to asynchronous. (partially completed)

This commit is contained in:
BruceChen 2022-12-20 22:41:14 +08:00
parent 7ee08092d4
commit 096ea0c70c
72 changed files with 6033 additions and 5080 deletions

View file

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using MinecraftClient.Protocol.ProfileKey;
using MinecraftClient.Protocol.Session;
namespace MinecraftClient
{
internal static class AsyncTaskHandler
{
private static readonly List<Task> Tasks = new();
internal static Task CheckUpdate = Task.CompletedTask;
internal static Task CacheSessionReader = Task.CompletedTask;
internal static Task SaveSessionToDisk = Task.CompletedTask;
internal static Task WritebackSettingFile = Task.CompletedTask;
internal static async void ExitCleanUp()
{
await WritebackSettingFile;
await CacheSessionReader;
await SaveSessionToDisk;
foreach (var task in Tasks)
{
await task;
}
Tasks.Clear();
}
}
}

View file

@ -437,10 +437,9 @@ namespace MinecraftClient.ChatBots
StopDigging();
}
public override bool OnDisconnect(DisconnectReason reason, string message)
public override int OnDisconnect(DisconnectReason reason, string message)
{
StopDigging();
return base.OnDisconnect(reason, message);
}
}

View file

@ -535,7 +535,7 @@ namespace MinecraftClient.ChatBots
StopFishing();
}
public override bool OnDisconnect(DisconnectReason reason, string message)
public override int OnDisconnect(DisconnectReason reason, string message)
{
StopFishing();

View file

@ -97,8 +97,9 @@ namespace MinecraftClient.ChatBots
}
}
public override bool OnDisconnect(DisconnectReason reason, string message)
public override int OnDisconnect(DisconnectReason reason, string message)
{
bool triggerReco = false;
if (reason == DisconnectReason.UserLogout)
{
LogDebugToConsole(Translations.bot_autoRelog_ignore_user_logout);
@ -113,44 +114,77 @@ namespace MinecraftClient.ChatBots
if (Config.Ignore_Kick_Message)
{
Configs._BotRecoAttempts++;
LaunchDelayedReconnection(null);
return true;
LogDebugToConsole(Translations.bot_autoRelog_reconnect_always);
triggerReco = true;
}
foreach (string msg in Config.Kick_Messages)
else
{
if (comp.Contains(msg))
foreach (string msg in Config.Kick_Messages)
{
Configs._BotRecoAttempts++;
LaunchDelayedReconnection(msg);
return true;
if (comp.Contains(msg))
{
Configs._BotRecoAttempts++;
LogDebugToConsole(string.Format(Translations.bot_autoRelog_reconnect, msg));
triggerReco = true;
break;
}
}
if (!triggerReco)
LogDebugToConsole(Translations.bot_autoRelog_reconnect_ignore);
}
}
if (triggerReco)
{
double delay = random.NextDouble() * (Config.Delay.max - Config.Delay.min) + Config.Delay.min;
LogToConsole(string.Format(Translations.bot_autoRelog_wait, delay));
return (int)Math.Floor(delay * 1000);
}
else
{
return -1;
}
}
public static int OnDisconnectStatic(DisconnectReason reason, string message)
{
bool triggerReco = false;
if (Config.Enabled
&& reason != DisconnectReason.UserLogout
&& (Config.Retries < 0 || Configs._BotRecoAttempts < Config.Retries))
{
message = GetVerbatim(message);
string comp = message.ToLower();
if (Config.Ignore_Kick_Message)
{
Configs._BotRecoAttempts++;
triggerReco = true;
}
else
{
foreach (string msg in Config.Kick_Messages)
{
if (comp.Contains(msg))
{
Configs._BotRecoAttempts++;
triggerReco = true;
break;
}
}
}
LogDebugToConsole(Translations.bot_autoRelog_reconnect_ignore);
}
return false;
}
private void LaunchDelayedReconnection(string? msg)
{
double delay = random.NextDouble() * (Config.Delay.max - Config.Delay.min) + Config.Delay.min;
LogDebugToConsole(string.Format(string.IsNullOrEmpty(msg) ? Translations.bot_autoRelog_reconnect_always : Translations.bot_autoRelog_reconnect, msg));
LogToConsole(string.Format(Translations.bot_autoRelog_wait, delay));
System.Threading.Thread.Sleep((int)Math.Floor(delay * 1000));
ReconnectToTheServer();
}
public static bool OnDisconnectStatic(DisconnectReason reason, string message)
{
if (Config.Enabled)
if (triggerReco)
{
AutoRelog bot = new();
bot.Initialize();
return bot.OnDisconnect(reason, message);
double delay = random.NextDouble() * (Config.Delay.max - Config.Delay.min) + Config.Delay.min;
return (int)Math.Floor(delay * 1000);
}
else
{
return -1;
}
return false;
}
}
}

View file

@ -244,10 +244,10 @@ namespace MinecraftClient.ChatBots
running = false;
}
public override bool OnDisconnect(DisconnectReason reason, string message)
public override int OnDisconnect(DisconnectReason reason, string message)
{
running = false;
return true;
return base.OnDisconnect(reason, message);
}
private void MainPorcess()

View file

@ -134,7 +134,7 @@ namespace MinecraftClient.ChatBots
}
}
public override bool OnDisconnect(DisconnectReason reason, string message)
public override int OnDisconnect(DisconnectReason reason, string message)
{
replay!.OnShutDown();
return base.OnDisconnect(reason, message);

View file

@ -236,10 +236,10 @@ namespace MinecraftClient.ChatBots
}
}
public override bool OnDisconnect(DisconnectReason reason, string message)
public override int OnDisconnect(DisconnectReason reason, string message)
{
serverlogin_done = false;
return false;
return base.OnDisconnect(reason, message);
}
private static string Task2String(TaskConfig task)

View file

@ -29,14 +29,17 @@ namespace MinecraftClient
/// Get the translated version of command description.
/// </summary>
/// <returns>Translated command description</returns>
public string GetCmdDescTranslated()
public string GetCmdDescTranslated(bool ListAllUsage = true)
{
char cmdChar = Settings.Config.Main.Advanced.InternalCmdChar.ToChar();
StringBuilder sb = new();
string s = (string.IsNullOrEmpty(CmdUsage) || string.IsNullOrEmpty(CmdDesc)) ? string.Empty : ": "; // If either one is empty, no colon :
sb.Append("§e").Append(cmdChar).Append(CmdUsage).Append("§r").Append(s).AppendLine(CmdDesc);
sb.Append(McClient.dispatcher.GetAllUsageString(CmdName, false));
sb.Append("§e").Append(cmdChar).Append(CmdUsage).Append("§r").Append(s);
if (ListAllUsage)
sb.AppendLine(CmdDesc).Append(McClient.dispatcher.GetAllUsageString(CmdName, false));
else
sb.Append(CmdDesc);
return sb.ToString();
}

View file

@ -49,7 +49,7 @@ namespace MinecraftClient.Commands
private static int DoAnimation(CmdResult r, bool mainhand)
{
McClient handler = CmdResult.currentHandler!;
return r.SetAndReturn(handler.DoAnimation(mainhand ? 1 : 0));
return r.SetAndReturn(handler.DoAnimation(mainhand ? 1 : 0).Result);
}
}
}

View file

@ -57,7 +57,7 @@ namespace MinecraftClient.Commands
private static int DoLeaveBed(CmdResult r)
{
McClient handler = CmdResult.currentHandler!;
return r.SetAndReturn(Translations.cmd_bed_leaving, handler.SendEntityAction(Protocol.EntityActionType.LeaveBed));
return r.SetAndReturn(Translations.cmd_bed_leaving, handler.SendEntityAction(Protocol.EntityActionType.LeaveBed).Result);
}
private static int DoSleepBedWithRadius(CmdResult r, double radius)
@ -137,7 +137,7 @@ namespace MinecraftClient.Commands
handler.Log.Info(string.Format(Translations.cmd_bed_moving, bedLocation.X, bedLocation.Y, bedLocation.Z));
bool res = handler.PlaceBlock(bedLocation, Direction.Down);
bool res = handler.PlaceBlock(bedLocation, Direction.Down).Result;
handler.Log.Info(string.Format(
Translations.cmd_bed_trying_to_use,
@ -174,7 +174,7 @@ namespace MinecraftClient.Commands
blockCenter.X,
blockCenter.Y,
blockCenter.Z,
handler.PlaceBlock(block, Direction.Down) ? Translations.cmd_bed_in : Translations.cmd_bed_not_in
handler.PlaceBlock(block, Direction.Down).Result ? Translations.cmd_bed_in : Translations.cmd_bed_not_in
));
}
}

View file

@ -77,7 +77,7 @@ namespace MinecraftClient.Commands
return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_bots_noloaded);
else
{
handler.UnloadAllBots();
handler.UnloadAllBots().Wait();
return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_bots_unloaded_all);
}
}
@ -88,7 +88,7 @@ namespace MinecraftClient.Commands
return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_bots_notfound, botName));
else
{
handler.BotUnLoad(bot);
handler.BotUnLoad(bot).Wait();
return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_bots_unloaded, botName));
}
}

View file

@ -44,7 +44,7 @@ namespace MinecraftClient.Commands
if (!handler.GetInventoryEnabled())
return r.SetAndReturn(Status.FailNeedInventory);
if (handler.ChangeSlot((short)(slot - 1)))
if (handler.ChangeSlot((short)(slot - 1)).Result)
return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_changeSlot_changed, slot));
else
return r.SetAndReturn(Status.Fail, Translations.cmd_changeSlot_fail);

View file

@ -54,7 +54,7 @@ namespace MinecraftClient.Commands
Block block = handler.GetWorld().GetBlock(blockToBreak);
if (block.Type == Material.Air)
return r.SetAndReturn(Status.Fail, Translations.cmd_dig_no_block);
else if (handler.DigBlock(blockToBreak))
else if (handler.DigBlock(blockToBreak).Result)
{
blockToBreak = blockToBreak.ToCenter();
return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_dig_dig, blockToBreak.X, blockToBreak.Y, blockToBreak.Z, block.GetTypeString()));
@ -74,7 +74,7 @@ namespace MinecraftClient.Commands
return r.SetAndReturn(Status.Fail, Translations.cmd_dig_too_far);
else if (block.Type == Material.Air)
return r.SetAndReturn(Status.Fail, Translations.cmd_dig_no_block);
else if (handler.DigBlock(blockLoc, lookAtBlock: false))
else if (handler.DigBlock(blockLoc, lookAtBlock: false).Result)
return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_dig_dig, blockLoc.X, blockLoc.Y, blockLoc.Z, block.GetTypeString()));
else
return r.SetAndReturn(Status.Fail, Translations.cmd_dig_fail);

View file

@ -58,7 +58,7 @@ namespace MinecraftClient.Commands
var p = inventories[inventoryId];
int[] targetItems = p.SearchItem(itemType);
foreach (int slot in targetItems)
handler.DoWindowAction(inventoryId, slot, WindowActionType.DropItemStack);
handler.DoWindowAction(inventoryId, slot, WindowActionType.DropItemStack).Wait();
return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_dropItem_dropped, Item.GetTypeString(itemType), inventoryId));
}

View file

@ -99,7 +99,7 @@ namespace MinecraftClient.Commands
return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_enchant_no_levels, handler.GetLevel(), requiredLevel));
else
{
if (handler.ClickContainerButton(enchantingTable.ID, slotId))
if (handler.ClickContainerButton(enchantingTable.ID, slotId).Result)
return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_enchant_clicked);
else
return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_enchant_not_clicked);

View file

@ -166,12 +166,12 @@ namespace MinecraftClient.Commands
{
if (action == ActionType.Attack)
{
handler.InteractEntity(entity2.Key, InteractType.Attack);
handler.InteractEntity(entity2.Key, InteractType.Attack).Wait();
actionst = Translations.cmd_entityCmd_attacked;
}
else if (action == ActionType.Use)
{
handler.InteractEntity(entity2.Key, InteractType.Interact);
handler.InteractEntity(entity2.Key, InteractType.Interact).Wait();
actionst = Translations.cmd_entityCmd_used;
}
actioncount++;
@ -311,10 +311,10 @@ namespace MinecraftClient.Commands
switch (action)
{
case ActionType.Attack:
handler.InteractEntity(entity.ID, InteractType.Attack);
handler.InteractEntity(entity.ID, InteractType.Attack).Wait();
return Translations.cmd_entityCmd_attacked;
case ActionType.Use:
handler.InteractEntity(entity.ID, InteractType.Interact);
handler.InteractEntity(entity.ID, InteractType.Interact).Wait();
return Translations.cmd_entityCmd_used;
case ActionType.List:
return GetEntityInfoDetailed(handler, entity);

View file

@ -48,11 +48,5 @@ namespace MinecraftClient.Commands
Program.Exit(code);
return r.SetAndReturn(CmdResult.Status.Done);
}
internal static string DoExit(string command)
{
Program.Exit();
return string.Empty;
}
}
}

View file

@ -159,7 +159,7 @@ namespace MinecraftClient.Commands
if (handler.GetGamemode() == 1)
{
if (handler.DoCreativeGive(slot, itemType, count, null))
if (handler.DoCreativeGive(slot, itemType, count, null).Result)
return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_inventory_creative_done, itemType, count, slot));
else
return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_inventory_creative_fail);
@ -178,7 +178,7 @@ namespace MinecraftClient.Commands
if (handler.GetGamemode() == 1)
{
if (handler.DoCreativeGive(slot, ItemType.Null, 0, null))
if (handler.DoCreativeGive(slot, ItemType.Null, 0, null).Result)
return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_inventory_creative_delete, slot));
else
return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_inventory_creative_fail);
@ -279,7 +279,7 @@ namespace MinecraftClient.Commands
if (inventory == null)
return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_inventory_not_exist, inventoryId));
if (handler.CloseInventory(inventoryId.Value))
if (handler.CloseInventory(inventoryId.Value).Result)
return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_inventory_close, inventoryId));
else
return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_inventory_close_fail, inventoryId));
@ -355,7 +355,9 @@ namespace MinecraftClient.Commands
};
handler.Log.Info(string.Format(Translations.cmd_inventory_clicking, keyName, slot, inventoryId));
return r.SetAndReturn(handler.DoWindowAction(inventoryId.Value, slot, actionType));
var task = handler.DoWindowAction(inventoryId.Value, slot, actionType);
task.Wait();
return r.SetAndReturn(task.Result);
}
private int DoDropAction(CmdResult r, int? inventoryId, int slot, WindowActionType actionType)
@ -379,7 +381,7 @@ namespace MinecraftClient.Commands
if (!inventory.Items.ContainsKey(slot))
return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_inventory_no_item, slot));
if (handler.DoWindowAction(inventoryId.Value, slot, actionType))
if (handler.DoWindowAction(inventoryId.Value, slot, actionType).Result)
{
if (actionType == WindowActionType.DropItemStack)
return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_inventory_drop_stack, slot));

View file

@ -63,7 +63,7 @@ namespace MinecraftClient.Commands
}
}
Program.Restart(keepAccountAndServerSettings: true);
return String.Empty;
return string.Empty;
}
}
}

View file

@ -40,7 +40,7 @@ namespace MinecraftClient.Commands
{
McClient handler = CmdResult.currentHandler!;
handler.Log.Info(Translations.cmd_reload_started);
handler.ReloadSettings();
handler.ReloadSettings().Wait();
handler.Log.Warn(Translations.cmd_reload_warning1);
handler.Log.Warn(Translations.cmd_reload_warning2);
handler.Log.Warn(Translations.cmd_reload_warning3);

View file

@ -39,7 +39,7 @@ namespace MinecraftClient.Commands
private int DoRespawn(CmdResult r)
{
McClient handler = CmdResult.currentHandler!;
handler.SendRespawnPacket();
handler.SendRespawnPacket().Wait();
return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_respawn_done);
}
}

View file

@ -37,7 +37,7 @@ namespace MinecraftClient.Commands
private int DoSendText(CmdResult r, string command)
{
McClient handler = CmdResult.currentHandler!;
handler.SendText(command);
handler.SendText(command).Wait();
return r.SetAndReturn(CmdResult.Status.Done);
}
}

View file

@ -42,7 +42,7 @@ namespace MinecraftClient.Commands
McClient handler = CmdResult.currentHandler!;
if (sneaking)
{
var result = handler.SendEntityAction(Protocol.EntityActionType.StopSneaking);
var result = handler.SendEntityAction(Protocol.EntityActionType.StopSneaking).Result;
if (result)
sneaking = false;
if (result)
@ -52,7 +52,7 @@ namespace MinecraftClient.Commands
}
else
{
var result = handler.SendEntityAction(Protocol.EntityActionType.StartSneaking);
var result = handler.SendEntityAction(Protocol.EntityActionType.StartSneaking).Result;
if (result)
sneaking = true;
if (result)

View file

@ -1,4 +1,5 @@
using Brigadier.NET;
using System.Threading.Tasks;
using Brigadier.NET;
using Brigadier.NET.Builder;
using MinecraftClient.CommandHandler;
@ -71,7 +72,7 @@ namespace MinecraftClient.Commands
private static int CheckUpdate(CmdResult r)
{
UpgradeHelper.CheckUpdate(forceUpdate: true);
Task.Run(async () => { await UpgradeHelper.CheckUpdate(forceUpdate: true); });
return r.SetAndReturn(CmdResult.Status.Done, Translations.mcc_update_start);
}
}

View file

@ -43,7 +43,7 @@ namespace MinecraftClient.Commands
if (!handler.GetInventoryEnabled())
return r.SetAndReturn(Status.FailNeedInventory);
handler.UseItemOnHand();
handler.UseItemOnHand().Wait();
return r.SetAndReturn(Status.Done, Translations.cmd_useitem_use);
}
}

View file

@ -48,7 +48,7 @@ namespace MinecraftClient.Commands
Location current = handler.GetCurrentLocation();
block = block.ToAbsolute(current).ToFloor();
Location blockCenter = block.ToCenter();
bool res = handler.PlaceBlock(block, Direction.Down);
bool res = handler.PlaceBlock(block, Direction.Down).Result;
return r.SetAndReturn(string.Format(Translations.cmd_useblock_use, blockCenter.X, blockCenter.Y, blockCenter.Z, res ? "succeeded" : "failed"), res);
}
}

View file

@ -61,6 +61,10 @@ namespace MinecraftClient
/// </summary>
public static string LogPrefix = "§8[Log] ";
private static bool SuppressOutput = false;
private static List<Tuple<bool, string>> MessageBuffer = new();
/// <summary>
/// Read a password from the standard input
/// </summary>
@ -101,15 +105,42 @@ namespace MinecraftClient
}
}
public static void SuppressPrinting(bool enable)
{
SuppressOutput = enable;
if (!enable)
{
lock (MessageBuffer)
{
foreach ((bool format, string message) in MessageBuffer)
{
if (format)
WriteLineFormatted(message, true, false);
else
WriteLine(message);
}
MessageBuffer.Clear();
}
}
}
/// <summary>
/// Write a string to the standard output with a trailing newline
/// </summary>
public static void WriteLine(string line)
public static void WriteLine(string line, bool ignoreSuppress = false)
{
if (BasicIO)
Console.WriteLine(line);
if (!ignoreSuppress && SuppressOutput)
{
lock(MessageBuffer)
MessageBuffer.Add(new(true, line));
}
else
ConsoleInteractive.ConsoleWriter.WriteLine(line);
{
if (BasicIO)
Console.WriteLine(line);
else
ConsoleInteractive.ConsoleWriter.WriteLine(line);
}
}
/// <summary>
@ -123,37 +154,42 @@ namespace MinecraftClient
/// If true, "hh-mm-ss" timestamp will be prepended.
/// If unspecified, value is retrieved from EnableTimestamps.
/// </param>
public static void WriteLineFormatted(string str, bool acceptnewlines = false, bool? displayTimestamp = null)
public static void WriteLineFormatted(string str, bool acceptnewlines = false, bool? displayTimestamp = null, bool ignoreSuppress = false)
{
StringBuilder output = new();
if (!String.IsNullOrEmpty(str))
if (!string.IsNullOrEmpty(str))
{
StringBuilder output = new();
displayTimestamp ??= EnableTimestamps;
if (displayTimestamp.Value)
{
int hour = DateTime.Now.Hour, minute = DateTime.Now.Minute, second = DateTime.Now.Second;
output.Append(String.Format("{0}:{1}:{2} ", hour.ToString("00"), minute.ToString("00"), second.ToString("00")));
}
if (!acceptnewlines)
{
str = str.Replace('\n', ' ');
}
if (BasicIO)
if (!ignoreSuppress && SuppressOutput)
{
if (BasicIO_NoColor)
lock (MessageBuffer)
MessageBuffer.Add(new(true, output.ToString()));
}
else
{
if (BasicIO)
{
output.Append(ChatBot.GetVerbatim(str));
if (BasicIO_NoColor)
output.Append(ChatBot.GetVerbatim(str));
else
output.Append(str);
Console.WriteLine(output.ToString());
}
else
{
output.Append(str);
ConsoleInteractive.ConsoleWriter.WriteLineFormatted(output.ToString());
}
Console.WriteLine(output.ToString());
return;
}
output.Append(str);
ConsoleInteractive.ConsoleWriter.WriteLineFormatted(output.ToString());
}
}
@ -178,6 +214,17 @@ namespace MinecraftClient
{
if (BasicIO) return;
ConsoleInteractive.ConsoleReader.ClearBuffer();
_cancellationTokenSource?.Cancel();
AutoCompleteDone = false;
AutoCompleteResult = Array.Empty<string>();
Commands.Clear();
CommandsFromAutoComplete = Array.Empty<string>();
CommandsFromDeclareCommands = Array.Empty<string>();
ConsoleInteractive.ConsoleSuggestion.ClearSuggestions();
}
@ -220,9 +267,7 @@ namespace MinecraftClient
string command = fullCommand[offset..];
if (command.Length == 0)
{
List<ConsoleInteractive.ConsoleSuggestion.Suggestion> sugList = new();
sugList.Add(new("/"));
List<ConsoleInteractive.ConsoleSuggestion.Suggestion> sugList = new() { new("/") };
var childs = McClient.dispatcher.GetRoot().Children;
if (childs != null)
@ -336,9 +381,9 @@ namespace MinecraftClient
MergeCommands();
}
public static void InitCommandList(CommandDispatcher<CmdResult> dispatcher)
public static async Task InitCommandList(CommandDispatcher<CmdResult> dispatcher)
{
autocomplete_engine!.AutoComplete("/");
await autocomplete_engine!.AutoComplete("/");
}
}
@ -353,6 +398,6 @@ namespace MinecraftClient
/// </summary>
/// <param name="BehindCursor">Text behind the cursor, e.g. "my input comm"</param>
/// <returns>List of auto-complete words, e.g. ["command", "comment"]</returns>
int AutoComplete(string BehindCursor);
Task<int> AutoComplete(string BehindCursor);
}
}

View file

@ -1,201 +0,0 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Threading.Tasks;
namespace MinecraftClient.Crypto
{
public class AesCfb8Stream : Stream
{
public const int blockSize = 16;
private readonly Aes? Aes = null;
private readonly FastAes? FastAes = null;
private bool inStreamEnded = false;
private readonly byte[] ReadStreamIV = new byte[16];
private readonly byte[] WriteStreamIV = new byte[16];
public Stream BaseStream { get; set; }
public AesCfb8Stream(Stream stream, byte[] key)
{
BaseStream = stream;
if (FastAes.IsSupported())
FastAes = new FastAes(key);
else
{
Aes = Aes.Create();
Aes.BlockSize = 128;
Aes.KeySize = 128;
Aes.Key = key;
Aes.Mode = CipherMode.ECB;
Aes.Padding = PaddingMode.None;
}
Array.Copy(key, ReadStreamIV, 16);
Array.Copy(key, WriteStreamIV, 16);
}
public override bool CanRead
{
get { return true; }
}
public override bool CanSeek
{
get { return false; }
}
public override bool CanWrite
{
get { return true; }
}
public override void Flush()
{
BaseStream.Flush();
}
public override long Length
{
get { throw new NotSupportedException(); }
}
public override long Position
{
get
{
throw new NotSupportedException();
}
set
{
throw new NotSupportedException();
}
}
public override int ReadByte()
{
if (inStreamEnded)
return -1;
int inputBuf = BaseStream.ReadByte();
if (inputBuf == -1)
{
inStreamEnded = true;
return -1;
}
Span<byte> blockOutput = stackalloc byte[blockSize];
if (FastAes != null)
FastAes.EncryptEcb(ReadStreamIV, blockOutput);
else
Aes!.EncryptEcb(ReadStreamIV, blockOutput, PaddingMode.None);
// Shift left
Array.Copy(ReadStreamIV, 1, ReadStreamIV, 0, blockSize - 1);
ReadStreamIV[blockSize - 1] = (byte)inputBuf;
return (byte)(blockOutput[0] ^ inputBuf);
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public override int Read(byte[] buffer, int outOffset, int required)
{
if (inStreamEnded)
return 0;
Span<byte> blockOutput = stackalloc byte[blockSize];
byte[] inputBuf = new byte[blockSize + required];
Array.Copy(ReadStreamIV, inputBuf, blockSize);
for (int readed = 0, curRead; readed < required; readed += curRead)
{
curRead = BaseStream.Read(inputBuf, blockSize + readed, required - readed);
if (curRead == 0)
{
inStreamEnded = true;
return readed;
}
int processEnd = readed + curRead;
if (FastAes != null)
{
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]);
}
}
}
Array.Copy(inputBuf, required, ReadStreamIV, 0, blockSize);
return required;
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void WriteByte(byte b)
{
Span<byte> blockOutput = stackalloc byte[blockSize];
if (FastAes != null)
FastAes.EncryptEcb(WriteStreamIV, blockOutput);
else
Aes!.EncryptEcb(WriteStreamIV, blockOutput, PaddingMode.None);
byte outputBuf = (byte)(blockOutput[0] ^ b);
BaseStream.WriteByte(outputBuf);
// Shift left
Array.Copy(WriteStreamIV, 1, WriteStreamIV, 0, blockSize - 1);
WriteStreamIV[blockSize - 1] = outputBuf;
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public override void Write(byte[] input, int offset, int required)
{
byte[] outputBuf = new byte[blockSize + required];
Array.Copy(WriteStreamIV, outputBuf, blockSize);
Span<byte> blockOutput = stackalloc byte[blockSize];
for (int wirtten = 0; wirtten < required; ++wirtten)
{
ReadOnlySpan<byte> blockInput = new(outputBuf, wirtten, blockSize);
if (FastAes != null)
FastAes.EncryptEcb(blockInput, blockOutput);
else
Aes!.EncryptEcb(blockInput, blockOutput, PaddingMode.None);
outputBuf[blockSize + wirtten] = (byte)(blockOutput[0] ^ input[offset + wirtten]);
}
BaseStream.WriteAsync(outputBuf, blockSize, required);
Array.Copy(outputBuf, required, WriteStreamIV, 0, blockSize);
}
}
}

View file

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace MinecraftClient.Crypto.AesHandler
{
public class BasicAes : IAesHandler
{
private readonly Aes Aes;
public BasicAes(byte[] key)
{
Aes = Aes.Create();
Aes.BlockSize = 128;
Aes.KeySize = 128;
Aes.Key = key;
Aes.Mode = CipherMode.ECB;
Aes.Padding = PaddingMode.None;
}
public override void EncryptEcb(Span<byte> plaintext, Span<byte> destination)
{
Aes.EncryptEcb(plaintext, destination, PaddingMode.None);
}
}
}

View file

@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Text;
using System.Threading.Tasks;
namespace MinecraftClient.Crypto.AesHandler
{
// https://github.com/Metalnem/aes-armv8
public class FasterAesArm : IAesHandler
{
private const int BlockSize = 16;
private const int Rounds = 10;
private readonly byte[] enc;
public FasterAesArm(Span<byte> key)
{
enc = new byte[(Rounds + 1) * BlockSize];
int[] intKey = GenerateKeyExpansion(key);
for (int i = 0; i < intKey.Length; ++i)
{
enc[i * 4 + 0] = (byte)((intKey[i] >> 0) & 0xff);
enc[i * 4 + 1] = (byte)((intKey[i] >> 8) & 0xff);
enc[i * 4 + 2] = (byte)((intKey[i] >> 16) & 0xff);
enc[i * 4 + 3] = (byte)((intKey[i] >> 24) & 0xff);
}
}
/// <summary>
/// Detects if the required instruction set is supported
/// </summary>
/// <returns>Is it supported</returns>
public static bool IsSupported()
{
return Aes.IsSupported && AdvSimd.IsSupported;
}
public override void EncryptEcb(Span<byte> plaintext, Span<byte> destination)
{
int position = 0;
int left = plaintext.Length;
var key0 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[0 * BlockSize]);
var key1 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[1 * BlockSize]);
var key2 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[2 * BlockSize]);
var key3 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[3 * BlockSize]);
var key4 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[4 * BlockSize]);
var key5 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[5 * BlockSize]);
var key6 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[6 * BlockSize]);
var key7 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[7 * BlockSize]);
var key8 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[8 * BlockSize]);
var key9 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[9 * BlockSize]);
var key10 = Unsafe.ReadUnaligned<Vector128<byte>>(ref enc[10 * BlockSize]);
while (left >= BlockSize)
{
var block = Unsafe.ReadUnaligned<Vector128<byte>>(ref plaintext[position]);
block = Aes.Encrypt(block, key0);
block = Aes.MixColumns(block);
block = Aes.Encrypt(block, key1);
block = Aes.MixColumns(block);
block = Aes.Encrypt(block, key2);
block = Aes.MixColumns(block);
block = Aes.Encrypt(block, key3);
block = Aes.MixColumns(block);
block = Aes.Encrypt(block, key4);
block = Aes.MixColumns(block);
block = Aes.Encrypt(block, key5);
block = Aes.MixColumns(block);
block = Aes.Encrypt(block, key6);
block = Aes.MixColumns(block);
block = Aes.Encrypt(block, key7);
block = Aes.MixColumns(block);
block = Aes.Encrypt(block, key8);
block = Aes.MixColumns(block);
block = Aes.Encrypt(block, key9);
block = AdvSimd.Xor(block, key10);
Unsafe.WriteUnaligned(ref destination[position], block);
position += BlockSize;
left -= BlockSize;
}
}
private int[] GenerateKeyExpansion(Span<byte> rgbKey)
{
var m_encryptKeyExpansion = new int[4 * (Rounds + 1)];
int index = 0;
for (int i = 0; i < 4; ++i)
{
int i0 = rgbKey[index++];
int i1 = rgbKey[index++];
int i2 = rgbKey[index++];
int i3 = rgbKey[index++];
m_encryptKeyExpansion[i] = i3 << 24 | i2 << 16 | i1 << 8 | i0;
}
for (int i = 4; i < 4 * (Rounds + 1); ++i)
{
int iTemp = m_encryptKeyExpansion[i - 1];
if (i % 4 == 0)
{
iTemp = SubWord(Rot3(iTemp));
iTemp ^= s_Rcon[(i / 4) - 1];
}
m_encryptKeyExpansion[i] = m_encryptKeyExpansion[i - 4] ^ iTemp;
}
return m_encryptKeyExpansion;
}
private static int SubWord(int a)
{
return s_Sbox[a & 0xFF] |
s_Sbox[a >> 8 & 0xFF] << 8 |
s_Sbox[a >> 16 & 0xFF] << 16 |
s_Sbox[a >> 24 & 0xFF] << 24;
}
private static int Rot3(int val)
{
return (val << 24 & unchecked((int)0xFF000000)) | (val >> 8 & unchecked((int)0x00FFFFFF));
}
private static readonly byte[] s_Sbox = new byte[] {
99, 124, 119, 123, 242, 107, 111, 197, 48, 1, 103, 43, 254, 215, 171, 118,
202, 130, 201, 125, 250, 89, 71, 240, 173, 212, 162, 175, 156, 164, 114, 192,
183, 253, 147, 38, 54, 63, 247, 204, 52, 165, 229, 241, 113, 216, 49, 21,
4, 199, 35, 195, 24, 150, 5, 154, 7, 18, 128, 226, 235, 39, 178, 117,
9, 131, 44, 26, 27, 110, 90, 160, 82, 59, 214, 179, 41, 227, 47, 132,
83, 209, 0, 237, 32, 252, 177, 91, 106, 203, 190, 57, 74, 76, 88, 207,
208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168,
81, 163, 64, 143, 146, 157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210,
205, 12, 19, 236, 95, 151, 68, 23, 196, 167, 126, 61, 100, 93, 25, 115,
96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94, 11, 219,
224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121,
231, 200, 55, 109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174, 8,
186, 120, 37, 46, 28, 166, 180, 198, 232, 221, 116, 31, 75, 189, 139, 138,
112, 62, 181, 102, 72, 3, 246, 14, 97, 53, 87, 185, 134, 193, 29, 158,
225, 248, 152, 17, 105, 217, 142, 148, 155, 30, 135, 233, 206, 85, 40, 223,
140, 161, 137, 13, 191, 230, 66, 104, 65, 153, 45, 15, 176, 84, 187, 22 };
private static readonly int[] s_Rcon = new int[] {
0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36,
0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6,
0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 };
}
}

View file

@ -4,15 +4,15 @@ using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
namespace MinecraftClient.Crypto
namespace MinecraftClient.Crypto.AesHandler
{
// Using the AES-NI instruction set
// https://gist.github.com/Thealexbarney/9f75883786a9f3100408ff795fb95d85
public class FastAes
public class FasterAesX86 : IAesHandler
{
private Vector128<byte>[] RoundKeys { get; }
public FastAes(Span<byte> key)
public FasterAesX86(Span<byte> key)
{
RoundKeys = KeyExpansion(key);
}
@ -23,11 +23,10 @@ namespace MinecraftClient.Crypto
/// <returns>Is it supported</returns>
public static bool IsSupported()
{
return Sse2.IsSupported && Aes.IsSupported;
return Aes.IsSupported && Sse2.IsSupported;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public void EncryptEcb(ReadOnlySpan<byte> plaintext, Span<byte> destination)
public override void EncryptEcb(Span<byte> plaintext, Span<byte> destination)
{
Vector128<byte>[] keys = RoundKeys;

View file

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MinecraftClient.Crypto
{
public abstract class IAesHandler
{
public abstract void EncryptEcb(Span<byte> plaintext, Span<byte> destination);
}
}

View file

@ -39,8 +39,8 @@ namespace MinecraftClient.Inventory
if (ValidateSlots(source, dest, destContainer) &&
HasItem(source) &&
((destContainer != null && !HasItem(dest, destContainer)) || (destContainer == null && !HasItem(dest))))
return mc.DoWindowAction(c.ID, source, WindowActionType.LeftClick)
&& mc.DoWindowAction(destContainer == null ? c.ID : destContainer.ID, dest, WindowActionType.LeftClick);
return mc.DoWindowAction(c.ID, source, WindowActionType.LeftClick).Result
&& mc.DoWindowAction(destContainer == null ? c.ID : destContainer.ID, dest, WindowActionType.LeftClick).Result;
else return false;
}
@ -57,9 +57,9 @@ namespace MinecraftClient.Inventory
if (ValidateSlots(slot1, slot2, destContainer) &&
HasItem(slot1) &&
(destContainer != null && HasItem(slot2, destContainer) || (destContainer == null && HasItem(slot2))))
return mc.DoWindowAction(c.ID, slot1, WindowActionType.LeftClick)
&& mc.DoWindowAction(destContainer == null ? c.ID : destContainer.ID, slot2, WindowActionType.LeftClick)
&& mc.DoWindowAction(c.ID, slot1, WindowActionType.LeftClick);
return mc.DoWindowAction(c.ID, slot1, WindowActionType.LeftClick).Result
&& mc.DoWindowAction(destContainer == null ? c.ID : destContainer.ID, slot2, WindowActionType.LeftClick).Result
&& mc.DoWindowAction(c.ID, slot1, WindowActionType.LeftClick).Result;
else return false;
}
@ -104,14 +104,14 @@ namespace MinecraftClient.Inventory
break;
}
}
mc.DoWindowAction(c.ID, source, WindowActionType.LeftClick); // grab item
mc.DoWindowAction(c.ID, -999, startDragging);
mc.DoWindowAction(c.ID, source, WindowActionType.LeftClick).Wait(); // grab item
mc.DoWindowAction(c.ID, -999, startDragging).Wait();
foreach (var slot in availableSlots)
{
mc.DoWindowAction(c.ID, slot, addDragging);
mc.DoWindowAction(c.ID, slot, addDragging).Wait();
}
mc.DoWindowAction(c.ID, -999, endDragging);
mc.DoWindowAction(c.ID, source, WindowActionType.LeftClick); // put down item left (if any)
mc.DoWindowAction(c.ID, -999, endDragging).Wait();
mc.DoWindowAction(c.ID, source, WindowActionType.LeftClick).Wait(); // put down item left (if any)
return true;
}
else return false;

File diff suppressed because it is too large Load diff

View file

@ -29,7 +29,6 @@
<ItemGroup>
<PackageReference Include="Brigadier.NET" Version="1.2.13" />
<PackageReference Include="DnsClient" Version="1.7.0" />
<PackageReference Include="DotNetZip" Version="1.16.0" />
<PackageReference Include="DSharpPlus" Version="4.2.0" />
<PackageReference Include="DynamicExpresso.Core" Version="2.13.0" />
<PackageReference Include="FuzzySharp" Version="2.0.2" />

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace MinecraftClient.Protocol.Handlers.Forge
{
@ -11,16 +12,22 @@ namespace MinecraftClient.Protocol.Handlers.Forge
/// <summary>
/// Represents an individual forge mod.
/// </summary>
public class ForgeMod
public record ForgeMod
{
public ForgeMod(String ModID, String Version)
public ForgeMod(string? modID, string? version)
{
this.ModID = ModID;
this.Version = Version;
ModID = modID;
Version = ModMarker = version;
}
public readonly String ModID;
public readonly String Version;
[JsonPropertyName("modId")]
public string? ModID { init; get; }
[JsonPropertyName("version")]
public string? Version { init; get; }
[JsonPropertyName("modmarker")]
public string? ModMarker { init; get; }
public override string ToString()
{
@ -138,5 +145,16 @@ namespace MinecraftClient.Protocol.Handlers.Forge
throw new NotImplementedException("FMLVersion '" + fmlVersion + "' not implemented!");
}
}
/// <summary>
/// Create a new ForgeInfo from the given data.
/// </summary>
/// <param name="data">The modinfo JSON tag.</param>
/// <param name="fmlVersion">Forge protocol version</param>
internal ForgeInfo(ForgeMod[] mods, FMLVersion fmlVersion)
{
Mods = new(mods);
Version = fmlVersion;
}
}
}

View file

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using MinecraftClient.Protocol.PacketPipeline;
namespace MinecraftClient.Protocol.Handlers.packet.s2c
{
@ -8,7 +10,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
private static int RootIdx;
private static CommandNode[] Nodes = Array.Empty<CommandNode>();
public static void Read(DataTypes dataTypes, Queue<byte> packetData)
public static async Task Read(DataTypes dataTypes, PacketStream packetData)
{
int count = dataTypes.ReadNextVarInt(packetData);
Nodes = new CommandNode[count];
@ -23,7 +25,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
int redirectNode = ((flags & 0x08) > 0) ? dataTypes.ReadNextVarInt(packetData) : -1;
string? name = ((flags & 0x03) == 1 || (flags & 0x03) == 2) ? dataTypes.ReadNextString(packetData) : null;
string? name = ((flags & 0x03) == 1 || (flags & 0x03) == 2) ? (await dataTypes.ReadNextStringAsync(packetData)) : null;
int paserId = ((flags & 0x03) == 2) ? dataTypes.ReadNextVarInt(packetData) : -1;
Paser? paser = null;
@ -50,7 +52,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
};
}
string? suggestionsType = ((flags & 0x10) > 0) ? dataTypes.ReadNextString(packetData) : null;
string? suggestionsType = ((flags & 0x10) > 0) ? (await dataTypes.ReadNextStringAsync(packetData)) : null;
Nodes[i] = new(flags, childs, redirectNode, name, paser, suggestionsType);
}
@ -158,7 +160,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
internal class PaserEmpty : Paser
{
public PaserEmpty(DataTypes dataTypes, Queue<byte> packetData) { }
public PaserEmpty(DataTypes dataTypes, PacketStream packetData) { }
public override bool Check(string text)
{
@ -181,7 +183,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
private byte Flags;
private float Min = float.MinValue, Max = float.MaxValue;
public PaserFloat(DataTypes dataTypes, Queue<byte> packetData)
public PaserFloat(DataTypes dataTypes, PacketStream packetData)
{
Flags = dataTypes.ReadNextByte(packetData);
if ((Flags & 0x01) > 0)
@ -211,7 +213,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
private byte Flags;
private double Min = double.MinValue, Max = double.MaxValue;
public PaserDouble(DataTypes dataTypes, Queue<byte> packetData)
public PaserDouble(DataTypes dataTypes, PacketStream packetData)
{
Flags = dataTypes.ReadNextByte(packetData);
if ((Flags & 0x01) > 0)
@ -241,7 +243,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
private byte Flags;
private int Min = int.MinValue, Max = int.MaxValue;
public PaserInteger(DataTypes dataTypes, Queue<byte> packetData)
public PaserInteger(DataTypes dataTypes, PacketStream packetData)
{
Flags = dataTypes.ReadNextByte(packetData);
if ((Flags & 0x01) > 0)
@ -271,7 +273,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
private byte Flags;
private long Min = long.MinValue, Max = long.MaxValue;
public PaserLong(DataTypes dataTypes, Queue<byte> packetData)
public PaserLong(DataTypes dataTypes, PacketStream packetData)
{
Flags = dataTypes.ReadNextByte(packetData);
if ((Flags & 0x01) > 0)
@ -302,7 +304,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
private enum StringType { SINGLE_WORD, QUOTABLE_PHRASE, GREEDY_PHRASE };
public PaserString(DataTypes dataTypes, Queue<byte> packetData)
public PaserString(DataTypes dataTypes, PacketStream packetData)
{
Type = (StringType)dataTypes.ReadNextVarInt(packetData);
}
@ -327,7 +329,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
{
private byte Flags;
public PaserEntity(DataTypes dataTypes, Queue<byte> packetData)
public PaserEntity(DataTypes dataTypes, PacketStream packetData)
{
Flags = dataTypes.ReadNextByte(packetData);
}
@ -351,7 +353,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
internal class PaserBlockPos : Paser
{
public PaserBlockPos(DataTypes dataTypes, Queue<byte> packetData) { }
public PaserBlockPos(DataTypes dataTypes, PacketStream packetData) { }
public override bool Check(string text)
{
@ -372,7 +374,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
internal class PaserColumnPos : Paser
{
public PaserColumnPos(DataTypes dataTypes, Queue<byte> packetData) { }
public PaserColumnPos(DataTypes dataTypes, PacketStream packetData) { }
public override bool Check(string text)
{
@ -393,7 +395,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
internal class PaserVec3 : Paser
{
public PaserVec3(DataTypes dataTypes, Queue<byte> packetData) { }
public PaserVec3(DataTypes dataTypes, PacketStream packetData) { }
public override bool Check(string text)
{
@ -414,7 +416,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
internal class PaserVec2 : Paser
{
public PaserVec2(DataTypes dataTypes, Queue<byte> packetData) { }
public PaserVec2(DataTypes dataTypes, PacketStream packetData) { }
public override bool Check(string text)
{
@ -435,7 +437,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
internal class PaserRotation : Paser
{
public PaserRotation(DataTypes dataTypes, Queue<byte> packetData) { }
public PaserRotation(DataTypes dataTypes, PacketStream packetData) { }
public override bool Check(string text)
{
@ -455,7 +457,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
internal class PaserMessage : Paser
{
public PaserMessage(DataTypes dataTypes, Queue<byte> packetData) { }
public PaserMessage(DataTypes dataTypes, PacketStream packetData) { }
public override bool Check(string text)
{
@ -477,7 +479,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
{
private byte Flags;
public PaserScoreHolder(DataTypes dataTypes, Queue<byte> packetData)
public PaserScoreHolder(DataTypes dataTypes, PacketStream packetData)
{
Flags = dataTypes.ReadNextByte(packetData);
}
@ -502,7 +504,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
{
private bool Decimals;
public PaserRange(DataTypes dataTypes, Queue<byte> packetData)
public PaserRange(DataTypes dataTypes, PacketStream packetData)
{
Decimals = dataTypes.ReadNextBool(packetData);
}
@ -527,9 +529,11 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
{
private string Registry;
public PaserResourceOrTag(DataTypes dataTypes, Queue<byte> packetData)
public PaserResourceOrTag(DataTypes dataTypes, PacketStream packetData)
{
Registry = dataTypes.ReadNextString(packetData);
var task = dataTypes.ReadNextStringAsync(packetData);
task.Wait();
Registry = task.Result;
}
public override bool Check(string text)
@ -552,9 +556,11 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
{
private string Registry;
public PaserResource(DataTypes dataTypes, Queue<byte> packetData)
public PaserResource(DataTypes dataTypes, PacketStream packetData)
{
Registry = dataTypes.ReadNextString(packetData);
var task = dataTypes.ReadNextStringAsync(packetData);
task.Wait();
Registry = task.Result;
}
public override bool Check(string text)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -3,9 +3,12 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Protocol.Handlers.Forge;
using MinecraftClient.Protocol.Message;
using MinecraftClient.Protocol.PacketPipeline;
using MinecraftClient.Scripting;
using static MinecraftClient.Protocol.Handlers.Protocol18Handler;
namespace MinecraftClient.Protocol.Handlers
{
@ -54,23 +57,23 @@ namespace MinecraftClient.Protocol.Handlers
/// Completes the Minecraft Forge handshake (Forge Protocol version 1: FML)
/// </summary>
/// <returns>Whether the handshake was successful.</returns>
public bool CompleteForgeHandshake()
public async Task<bool> CompleteForgeHandshake(SocketWrapper socketWrapper)
{
if (ForgeEnabled() && forgeInfo!.Version == FMLVersion.FML)
{
while (fmlHandshakeState != FMLHandshakeClientState.DONE)
{
(int packetID, Queue<byte> packetData) = protocol18.ReadNextPacket();
(int packetID, PacketStream packetStream) = await socketWrapper.GetNextPacket(handleCompress: true);
if (packetID == 0x40) // Disconnect
{
mcHandler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, ChatParser.ParseText(dataTypes.ReadNextString(packetData)));
mcHandler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, ChatParser.ParseText(await dataTypes.ReadNextStringAsync(packetStream)));
return false;
}
else
{
// Send back regular packet to the vanilla protocol handler
protocol18.HandlePacket(packetID, packetData);
await protocol18.HandlePacket(packetID, packetStream);
}
}
}
@ -82,7 +85,7 @@ namespace MinecraftClient.Protocol.Handlers
/// </summary>
/// <param name="packetData">Packet data to read from</param>
/// <returns>Length from packet data</returns>
public int ReadNextVarShort(Queue<byte> packetData)
public int ReadNextVarShort(PacketStream packetData)
{
if (ForgeEnabled())
{
@ -103,10 +106,11 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="packetData">Plugin message data</param>
/// <param name="currentDimension">Current world dimension</param>
/// <returns>TRUE if the plugin message was recognized and handled</returns>
public bool HandlePluginMessage(string channel, Queue<byte> packetData, ref int currentDimension)
public async Task<Tuple<bool, int>> HandlePluginMessage(string channel, byte[] packetDataArr, int currentDimension)
{
if (ForgeEnabled() && forgeInfo!.Version == FMLVersion.FML && fmlHandshakeState != FMLHandshakeClientState.DONE)
{
Queue<byte> packetData = new(packetDataArr);
if (channel == "FML|HS")
{
FMLHandshakeDiscriminator discriminator = (FMLHandshakeDiscriminator)dataTypes.ReadNextByte(packetData);
@ -114,21 +118,21 @@ namespace MinecraftClient.Protocol.Handlers
if (discriminator == FMLHandshakeDiscriminator.HandshakeReset)
{
fmlHandshakeState = FMLHandshakeClientState.START;
return true;
return new(true, currentDimension);
}
switch (fmlHandshakeState)
{
case FMLHandshakeClientState.START:
if (discriminator != FMLHandshakeDiscriminator.ServerHello)
return false;
return new(false, currentDimension);
// Send the plugin channel registration.
// REGISTER is somewhat special in that it doesn't actually include length information,
// and is also \0-separated.
// Also, yes, "FML" is there twice. Don't ask me why, but that's the way forge does it.
string[] channels = { "FML|HS", "FML", "FML|MP", "FML", "FORGE" };
protocol18.SendPluginChannelPacket("REGISTER", Encoding.UTF8.GetBytes(string.Join("\0", channels)));
await protocol18.SendPluginChannelPacket("REGISTER", Encoding.UTF8.GetBytes(string.Join("\0", channels)));
byte fmlProtocolVersion = dataTypes.ReadNextByte(packetData);
@ -139,7 +143,7 @@ namespace MinecraftClient.Protocol.Handlers
currentDimension = dataTypes.ReadNextInt(packetData);
// Tell the server we're running the same version.
SendForgeHandshakePacket(FMLHandshakeDiscriminator.ClientHello, new byte[] { fmlProtocolVersion });
await SendForgeHandshakePacket(FMLHandshakeDiscriminator.ClientHello, new byte[] { fmlProtocolVersion });
// Then tell the server that we're running the same mods.
if (Settings.Config.Logging.DebugMessages)
@ -148,17 +152,17 @@ namespace MinecraftClient.Protocol.Handlers
for (int i = 0; i < forgeInfo.Mods.Count; i++)
{
ForgeInfo.ForgeMod mod = forgeInfo.Mods[i];
mods[i] = dataTypes.ConcatBytes(dataTypes.GetString(mod.ModID), dataTypes.GetString(mod.Version));
mods[i] = dataTypes.ConcatBytes(dataTypes.GetString(mod.ModID!), dataTypes.GetString(mod.Version ?? mod.ModMarker ?? string.Empty));
}
SendForgeHandshakePacket(FMLHandshakeDiscriminator.ModList,
await SendForgeHandshakePacket(FMLHandshakeDiscriminator.ModList,
dataTypes.ConcatBytes(dataTypes.GetVarInt(forgeInfo.Mods.Count), dataTypes.ConcatBytes(mods)));
fmlHandshakeState = FMLHandshakeClientState.WAITINGSERVERDATA;
return true;
return new(true, currentDimension);
case FMLHandshakeClientState.WAITINGSERVERDATA:
if (discriminator != FMLHandshakeDiscriminator.ModList)
return false;
return new(false, currentDimension);
Thread.Sleep(2000);
@ -167,16 +171,16 @@ namespace MinecraftClient.Protocol.Handlers
// Tell the server that yes, we are OK with the mods it has
// even though we don't actually care what mods it has.
SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck,
await SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck,
new byte[] { (byte)FMLHandshakeClientState.WAITINGSERVERDATA });
fmlHandshakeState = FMLHandshakeClientState.WAITINGSERVERCOMPLETE;
return false;
return new(false, currentDimension);
case FMLHandshakeClientState.WAITINGSERVERCOMPLETE:
// The server now will tell us a bunch of registry information.
// We need to read it all, though, until it says that there is no more.
if (discriminator != FMLHandshakeDiscriminator.RegistryData)
return false;
return new(false, currentDimension);
if (protocolversion < Protocol18Handler.MC_1_8_Version)
{
@ -202,34 +206,34 @@ namespace MinecraftClient.Protocol.Handlers
fmlHandshakeState = FMLHandshakeClientState.PENDINGCOMPLETE;
}
return false;
return new(false, currentDimension);
case FMLHandshakeClientState.PENDINGCOMPLETE:
// The server will ask us to accept the registries.
// Just say yes.
if (discriminator != FMLHandshakeDiscriminator.HandshakeAck)
return false;
return new(false, currentDimension);
if (Settings.Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted("§8" + Translations.forge_accept_registry, acceptnewlines: true);
SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck,
await SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck,
new byte[] { (byte)FMLHandshakeClientState.PENDINGCOMPLETE });
fmlHandshakeState = FMLHandshakeClientState.COMPLETE;
return true;
return new(true, currentDimension);
case FMLHandshakeClientState.COMPLETE:
// One final "OK". On the actual forge source, a packet is sent from
// the client to the client saying that the connection was complete, but
// we don't need to do that.
SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck,
await SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck,
new byte[] { (byte)FMLHandshakeClientState.COMPLETE });
if (Settings.Config.Logging.DebugMessages)
ConsoleIO.WriteLine(Translations.forge_complete);
fmlHandshakeState = FMLHandshakeClientState.DONE;
return true;
return new(true, currentDimension);
}
}
}
return false;
return new(false, currentDimension);
}
/// <summary>
@ -239,8 +243,9 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="packetData">Plugin message data</param>
/// <param name="responseData">Response data to return to server</param>
/// <returns>TRUE/FALSE depending on whether the packet was understood or not</returns>
public bool HandleLoginPluginRequest(string channel, Queue<byte> packetData, ref List<byte> responseData)
public async Task<Tuple<bool, List<byte>>> HandleLoginPluginRequest(string channel, PacketStream packetData)
{
List<byte> responseData = new();
if (ForgeEnabled() && forgeInfo!.Version == FMLVersion.FML2 && channel == "fml:loginwrapper")
{
// Forge Handshake handler source code used to implement the FML2 packets:
@ -278,8 +283,8 @@ namespace MinecraftClient.Protocol.Handlers
// The content of each message is mapped into a class inside FMLHandshakeMessages.java
// FMLHandshakeHandler will then process the packet, e.g. handleServerModListOnClient() for Server Mod List.
string fmlChannel = dataTypes.ReadNextString(packetData);
dataTypes.ReadNextVarInt(packetData); // Packet length
string fmlChannel = await dataTypes.ReadNextStringAsync(packetData);
dataTypes.SkipNextVarInt(packetData); // Packet length
int packetID = dataTypes.ReadNextVarInt(packetData);
if (fmlChannel == "fml:handshake")
@ -308,17 +313,17 @@ namespace MinecraftClient.Protocol.Handlers
List<string> mods = new();
int modCount = dataTypes.ReadNextVarInt(packetData);
for (int i = 0; i < modCount; i++)
mods.Add(dataTypes.ReadNextString(packetData));
mods.Add(await dataTypes.ReadNextStringAsync(packetData));
Dictionary<string, string> channels = new();
int channelCount = dataTypes.ReadNextVarInt(packetData);
for (int i = 0; i < channelCount; i++)
channels.Add(dataTypes.ReadNextString(packetData), dataTypes.ReadNextString(packetData));
channels.Add(await dataTypes.ReadNextStringAsync(packetData), await dataTypes.ReadNextStringAsync(packetData));
List<string> registries = new();
int registryCount = dataTypes.ReadNextVarInt(packetData);
for (int i = 0; i < registryCount; i++)
registries.Add(dataTypes.ReadNextString(packetData));
registries.Add(await dataTypes.ReadNextStringAsync(packetData));
// Server Mod List Reply: FMLHandshakeMessages.java > C2SModListReply > encode()
//
@ -372,7 +377,7 @@ namespace MinecraftClient.Protocol.Handlers
if (Settings.Config.Logging.DebugMessages)
{
string registryName = dataTypes.ReadNextString(packetData);
string registryName = await dataTypes.ReadNextStringAsync(packetData);
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_fml2_registry, registryName));
}
@ -391,7 +396,7 @@ namespace MinecraftClient.Protocol.Handlers
if (Settings.Config.Logging.DebugMessages)
{
string configName = dataTypes.ReadNextString(packetData);
string configName = await dataTypes.ReadNextStringAsync(packetData);
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_fml2_config, configName));
}
@ -408,11 +413,10 @@ namespace MinecraftClient.Protocol.Handlers
if (fmlResponseReady)
{
// Wrap our FML packet into a LoginPluginResponse payload
responseData.Clear();
responseData.AddRange(dataTypes.GetString(fmlChannel));
responseData.AddRange(dataTypes.GetVarInt(fmlResponsePacket.Count));
responseData.AddRange(fmlResponsePacket);
return true;
return new(true, responseData);
}
}
else if (Settings.Config.Logging.DebugMessages)
@ -420,7 +424,7 @@ namespace MinecraftClient.Protocol.Handlers
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_fml2_unknown_channel, fmlChannel));
}
}
return false;
return new(false, responseData);
}
/// <summary>
@ -428,9 +432,9 @@ namespace MinecraftClient.Protocol.Handlers
/// </summary>
/// <param name="discriminator">Discriminator to use.</param>
/// <param name="data">packet Data</param>
private void SendForgeHandshakePacket(FMLHandshakeDiscriminator discriminator, byte[] data)
private async Task SendForgeHandshakePacket(FMLHandshakeDiscriminator discriminator, byte[] data)
{
protocol18.SendPluginChannelPacket("FML|HS", dataTypes.ConcatBytes(new byte[] { (byte)discriminator }, data));
await protocol18.SendPluginChannelPacket("FML|HS", dataTypes.ConcatBytes(new byte[] { (byte)discriminator }, data));
}
/// <summary>
@ -439,10 +443,10 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="jsonData">JSON data returned by the server</param>
/// <param name="forgeInfo">ForgeInfo to populate</param>
/// <returns>True if the server is running Forge</returns>
public static bool ServerInfoCheckForge(Json.JSONData jsonData, ref ForgeInfo? forgeInfo)
public static bool ServerInfoCheckForge(PingResult jsonData, ref ForgeInfo? forgeInfo)
{
return ServerInfoCheckForgeSub(jsonData, ref forgeInfo, FMLVersion.FML) // MC 1.12 and lower
|| ServerInfoCheckForgeSub(jsonData, ref forgeInfo, FMLVersion.FML2); // MC 1.13 and greater
return ServerInfoCheckForgeSubFML1(jsonData, ref forgeInfo) // MC 1.12 and lower
|| ServerInfoCheckForgeSubFML2(jsonData, ref forgeInfo); // MC 1.13 and greater
}
/// <summary>
@ -474,38 +478,21 @@ namespace MinecraftClient.Protocol.Handlers
/// </summary>
/// <param name="jsonData">JSON data returned by the server</param>
/// <param name="forgeInfo">ForgeInfo to populate</param>
/// <param name="fmlVersion">Forge protocol version</param>
/// <returns>True if the server is running Forge</returns>
private static bool ServerInfoCheckForgeSub(Json.JSONData jsonData, ref ForgeInfo? forgeInfo, FMLVersion fmlVersion)
private static bool ServerInfoCheckForgeSubFML1(PingResult jsonData, ref ForgeInfo? forgeInfo)
{
string forgeDataTag;
string versionField;
string versionString;
switch (fmlVersion)
if (jsonData.modinfo != null)
{
case FMLVersion.FML:
forgeDataTag = "modinfo";
versionField = "type";
versionString = "FML";
break;
case FMLVersion.FML2:
forgeDataTag = "forgeData";
versionField = "fmlNetworkVersion";
versionString = "2";
break;
default:
throw new NotImplementedException("FMLVersion '" + fmlVersion + "' not implemented!");
}
if (jsonData.Properties.ContainsKey(forgeDataTag) && jsonData.Properties[forgeDataTag].Type == Json.JSONData.DataType.Object)
{
Json.JSONData modData = jsonData.Properties[forgeDataTag];
if (modData.Properties.ContainsKey(versionField) && modData.Properties[versionField].StringValue == versionString)
if (jsonData.modinfo.type == "FML")
{
forgeInfo = new ForgeInfo(modData, fmlVersion);
if (forgeInfo.Mods.Any())
if (jsonData.modinfo.modList == null || jsonData.modinfo.modList.Length == 0)
{
forgeInfo = null;
ConsoleIO.WriteLineFormatted("§8" + Translations.forge_no_mod, acceptnewlines: true);
}
else
{
forgeInfo = new ForgeInfo(jsonData.modinfo.modList, FMLVersion.FML);
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_with_mod, forgeInfo.Mods.Count));
if (Settings.Config.Logging.DebugMessages)
{
@ -515,10 +502,39 @@ namespace MinecraftClient.Protocol.Handlers
}
return true;
}
}
}
return false;
}
/// <summary>
/// Server Info: Check for For Forge on a Minecraft server Ping result (Handles FML and FML2
/// </summary>
/// <param name="jsonData">JSON data returned by the server</param>
/// <param name="forgeInfo">ForgeInfo to populate</param>
/// <returns>True if the server is running Forge</returns>
private static bool ServerInfoCheckForgeSubFML2(PingResult jsonData, ref ForgeInfo? forgeInfo)
{
if (jsonData.forgeData != null)
{
if (jsonData.forgeData.fmlNetworkVersion == "2")
{
if (jsonData.forgeData.mods == null || jsonData.forgeData.mods.Length == 0)
{
forgeInfo = null;
ConsoleIO.WriteLineFormatted("§8" + Translations.forge_no_mod, acceptnewlines: true);
}
else
{
ConsoleIO.WriteLineFormatted("§8" + Translations.forge_no_mod, acceptnewlines: true);
forgeInfo = null;
forgeInfo = new ForgeInfo(jsonData.forgeData.mods, FMLVersion.FML2);
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_with_mod, forgeInfo.Mods.Count));
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLineFormatted("§8" + Translations.forge_mod_list, acceptnewlines: true);
foreach (ForgeInfo.ForgeMod mod in forgeInfo.Mods)
ConsoleIO.WriteLineFormatted("§8 " + mod.ToString());
}
return true;
}
}
}

View file

@ -3,9 +3,11 @@ using System.Collections.Generic;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
//using System.Linq;
//using System.Text;
using MinecraftClient.Mapping;
using MinecraftClient.Protocol.PacketPipeline;
namespace MinecraftClient.Protocol.Handlers
{
@ -33,21 +35,21 @@ namespace MinecraftClient.Protocol.Handlers
/// <summary>
/// Reading the "Block states" field: consists of 4096 entries, representing all the blocks in the chunk section.
/// </summary>
/// <param name="cache">Cache for reading data</param>
/// <param name="stream">Cache for reading data</param>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private Chunk? ReadBlockStatesField(Queue<byte> cache)
private async Task<Chunk?> ReadBlockStatesFieldAsync(PacketStream stream)
{
// read Block states (Type: Paletted Container)
byte bitsPerEntry = dataTypes.ReadNextByte(cache);
byte bitsPerEntry = await dataTypes.ReadNextByteAsync(stream);
// 1.18(1.18.1) add a pattle named "Single valued" to replace the vertical strip bitmask in the old
if (bitsPerEntry == 0 && protocolversion >= Protocol18Handler.MC_1_18_1_Version)
{
// Palettes: Single valued - 1.18(1.18.1) and above
ushort blockId = (ushort)dataTypes.ReadNextVarInt(cache);
ushort blockId = (ushort)(await dataTypes.ReadNextVarIntAsync(stream));
Block block = new(blockId);
dataTypes.SkipNextVarInt(cache); // Data Array Length will be zero
dataTypes.SkipNextVarInt(stream); // Data Array Length will be zero
// Empty chunks will not be stored
if (block.Type == Material.Air)
@ -73,16 +75,16 @@ namespace MinecraftClient.Protocol.Handlers
// EG, if bitsPerEntry = 5, valueMask = 00011111 in binary
uint valueMask = (uint)((1 << bitsPerEntry) - 1);
int paletteLength = usePalette ? dataTypes.ReadNextVarInt(cache) : 0; // Assume zero when length is absent
int paletteLength = usePalette ? await dataTypes.ReadNextVarIntAsync(stream) : 0; // Assume zero when length is absent
Span<uint> palette = paletteLength < 256 ? stackalloc uint[paletteLength] : new uint[paletteLength];
uint[] palette = new uint[paletteLength];
for (int i = 0; i < paletteLength; i++)
palette[i] = (uint)dataTypes.ReadNextVarInt(cache);
palette[i] = (uint)(await dataTypes.ReadNextVarIntAsync(stream));
//// Block IDs are packed in the array of 64-bits integers
dataTypes.SkipNextVarInt(cache); // Entry length
Span<byte> entryDataByte = stackalloc byte[8];
Span<long> entryDataLong = MemoryMarshal.Cast<byte, long>(entryDataByte); // Faster than MemoryMarshal.Read<long>
dataTypes.SkipNextVarInt(stream); // Entry length
long entryData = 0;
Chunk chunk = new();
int startOffset = 64; // Read the first data immediately
@ -101,10 +103,10 @@ namespace MinecraftClient.Protocol.Handlers
// When overlapping, move forward to the beginning of the next Long
startOffset = 0;
dataTypes.ReadDataReverse(cache, entryDataByte); // read long
entryData = await dataTypes.ReadNextLongAsync(stream);
}
uint blockId = (uint)(entryDataLong[0] >> startOffset) & valueMask;
uint blockId = (uint)(entryData >> startOffset) & valueMask;
// Map small IDs to actual larger block IDs
if (usePalette)
@ -141,10 +143,10 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="chunkX">Chunk X location</param>
/// <param name="chunkZ">Chunk Z location</param>
/// <param name="verticalStripBitmask">Chunk mask for reading data, store in bitset, used in 1.17 and 1.17.1</param>
/// <param name="cache">Cache for reading chunk data</param>
/// <param name="stream">Cache for reading chunk data</param>
/// <param name="cancellationToken">token to cancel the task</param>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public void ProcessChunkColumnData(int chunkX, int chunkZ, ulong[]? verticalStripBitmask, Queue<byte> cache)
public async Task ProcessChunkColumnData(int chunkX, int chunkZ, ulong[]? verticalStripBitmask, PacketStream stream)
{
World world = handler.GetWorld();
@ -181,10 +183,10 @@ namespace MinecraftClient.Protocol.Handlers
((verticalStripBitmask![chunkY / 64] & (1UL << (chunkY % 64))) != 0))
{
// Non-air block count inside chunk section, for lighting purposes
int blockCnt = dataTypes.ReadNextShort(cache);
int blockCnt = await dataTypes.ReadNextShortAsync(stream);
// Read Block states (Type: Paletted Container)
Chunk? chunk = ReadBlockStatesField(cache);
Chunk? chunk = await ReadBlockStatesFieldAsync(stream);
//We have our chunk, save the chunk into the world
world.StoreChunk(chunkX, chunkY, chunkZ, chunkColumnSize, chunk, chunkY == lastChunkY);
@ -192,23 +194,23 @@ namespace MinecraftClient.Protocol.Handlers
// Skip Read Biomes (Type: Paletted Container) - 1.18(1.18.1) and above
if (protocolversion >= Protocol18Handler.MC_1_18_1_Version)
{
byte bitsPerEntryBiome = dataTypes.ReadNextByte(cache); // Bits Per Entry
byte bitsPerEntryBiome = await dataTypes.ReadNextByteAsync(stream); // Bits Per Entry
if (bitsPerEntryBiome == 0)
{
dataTypes.SkipNextVarInt(cache); // Value
dataTypes.SkipNextVarInt(cache); // Data Array Length
dataTypes.SkipNextVarInt(stream); // Value
dataTypes.SkipNextVarInt(stream); // Data Array Length
// Data Array must be empty
}
else
{
if (bitsPerEntryBiome <= 3)
{
int paletteLength = dataTypes.ReadNextVarInt(cache); // Palette Length
int paletteLength = await dataTypes.ReadNextVarIntAsync(stream); // Palette Length
for (int i = 0; i < paletteLength; i++)
dataTypes.SkipNextVarInt(cache); // Palette
dataTypes.SkipNextVarInt(stream); // Palette
}
int dataArrayLength = dataTypes.ReadNextVarInt(cache); // Data Array Length
dataTypes.DropData(dataArrayLength * 8, cache); // Data Array
int dataArrayLength = await dataTypes.ReadNextVarIntAsync(stream); // Data Array Length
await dataTypes.DropDataAsync(dataArrayLength * 8, stream); // Data Array
}
}
}
@ -228,10 +230,10 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="hasSkyLight">Contains skylight info</param>
/// <param name="chunksContinuous">Are the chunk continuous</param>
/// <param name="currentDimension">Current dimension type (0 = overworld)</param>
/// <param name="cache">Cache for reading chunk data</param>
/// <param name="stream">Cache for reading chunk data</param>
/// <param name="cancellationToken">token to cancel the task</param>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public void ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ushort chunkMask2, bool hasSkyLight, bool chunksContinuous, int currentDimension, Queue<byte> cache)
public async Task ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ushort chunkMask2, bool hasSkyLight, bool chunksContinuous, int currentDimension, PacketStream stream)
{
World world = handler.GetWorld();
@ -247,9 +249,9 @@ namespace MinecraftClient.Protocol.Handlers
{
// 1.14 and above Non-air block count inside chunk section, for lighting purposes
if (protocolversion >= Protocol18Handler.MC_1_14_Version)
dataTypes.ReadNextShort(cache);
await dataTypes.SkipNextShortAsync(stream);
byte bitsPerBlock = dataTypes.ReadNextByte(cache);
byte bitsPerBlock = await dataTypes.ReadNextByteAsync(stream);
bool usePalette = (bitsPerBlock <= 8);
// Vanilla Minecraft will use at least 4 bits per block
@ -260,12 +262,12 @@ namespace MinecraftClient.Protocol.Handlers
// is not used, MC 1.13+ does not send the field at all in this case
int paletteLength = 0; // Assume zero when length is absent
if (usePalette || protocolversion < Protocol18Handler.MC_1_13_Version)
paletteLength = dataTypes.ReadNextVarInt(cache);
paletteLength = await dataTypes.ReadNextVarIntAsync(stream);
int[] palette = new int[paletteLength];
for (int i = 0; i < paletteLength; i++)
{
palette[i] = dataTypes.ReadNextVarInt(cache);
palette[i] = await dataTypes.ReadNextVarIntAsync(stream);
}
// Bit mask covering bitsPerBlock bits
@ -273,7 +275,7 @@ namespace MinecraftClient.Protocol.Handlers
uint valueMask = (uint)((1 << bitsPerBlock) - 1);
// Block IDs are packed in the array of 64-bits integers
ulong[] dataArray = dataTypes.ReadNextULongArray(cache);
ulong[] dataArray = await dataTypes.ReadNextULongArrayAsync(stream);
Chunk chunk = new();
@ -358,19 +360,19 @@ namespace MinecraftClient.Protocol.Handlers
}
}
//We have our chunk, save the chunk into the world
// We have our chunk, save the chunk into the world
world.StoreChunk(chunkX, chunkY, chunkZ, chunkColumnSize, chunk, chunkY == maxChunkY);
//Pre-1.14 Lighting data
// Pre-1.14 Lighting data
if (protocolversion < Protocol18Handler.MC_1_14_Version)
{
//Skip block light
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache);
// Skip block light
await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, stream);
//Skip sky light
// Skip sky light
if (currentDimension == 0)
// Sky light is not sent in the nether or the end
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache);
await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, stream);
}
}
}
@ -383,15 +385,12 @@ namespace MinecraftClient.Protocol.Handlers
// 1.8 chunk format
if (chunksContinuous && chunkMask == 0)
{
//Unload the entire chunk column
handler.InvokeOnMainThread(() =>
{
world[chunkX, chunkZ] = null;
});
// Unload the entire chunk column
world[chunkX, chunkZ] = null;
}
else
{
//Load chunk data from the server
// Load chunk data from the server
int maxChunkY = sizeof(int) * 8 - 1 - BitOperations.LeadingZeroCount(chunkMask);
for (int chunkY = 0; chunkY <= maxChunkY; chunkY++)
{
@ -399,35 +398,34 @@ namespace MinecraftClient.Protocol.Handlers
{
Chunk chunk = new();
//Read chunk data, all at once for performance reasons, and build the chunk object
Queue<ushort> queue = new(dataTypes.ReadNextUShortsLittleEndian(Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ, cache));
// Read chunk data, all at once for performance reasons, and build the chunk object
for (int blockY = 0; blockY < Chunk.SizeY; blockY++)
for (int blockZ = 0; blockZ < Chunk.SizeZ; blockZ++)
for (int blockX = 0; blockX < Chunk.SizeX; blockX++)
chunk.SetWithoutCheck(blockX, blockY, blockZ, new Block(queue.Dequeue()));
chunk.SetWithoutCheck(blockX, blockY, blockZ, new Block(await dataTypes.ReadNextUShortAsync(stream)));
//We have our chunk, save the chunk into the world
// We have our chunk, save the chunk into the world
world.StoreChunk(chunkX, chunkY, chunkZ, chunkColumnSize, chunk, chunkY == maxChunkY);
}
}
//Skip light information
// Skip light information
for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++)
{
if ((chunkMask & (1 << chunkY)) != 0)
{
//Skip block light
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache);
// Skip block light
await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, stream);
//Skip sky light
// Skip sky light
if (hasSkyLight)
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache);
await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, stream);
}
}
//Skip biome metadata
// Skip biome metadata
if (chunksContinuous)
dataTypes.DropData(Chunk.SizeX * Chunk.SizeZ, cache);
await dataTypes.DropDataAsync(Chunk.SizeX * Chunk.SizeZ, stream);
}
}
else
@ -435,15 +433,12 @@ namespace MinecraftClient.Protocol.Handlers
// 1.7 chunk format
if (chunksContinuous && chunkMask == 0)
{
//Unload the entire chunk column
handler.InvokeOnMainThread(() =>
{
world[chunkX, chunkZ] = null;
});
// Unload the entire chunk column
world[chunkX, chunkZ] = null;
}
else
{
//Count chunk sections
// Count chunk sections
int sectionCount = 0;
int addDataSectionCount = 0;
for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++)
@ -454,10 +449,10 @@ namespace MinecraftClient.Protocol.Handlers
addDataSectionCount++;
}
//Read chunk data, unpacking 4-bit values into 8-bit values for block metadata
Queue<byte> blockTypes = new(dataTypes.ReadData(Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount, cache));
// Read chunk data, unpacking 4-bit values into 8-bit values for block metadata
Queue<byte> blockTypes = new(await dataTypes.ReadDataAsync(Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount, stream));
Queue<byte> blockMeta = new();
foreach (byte packed in dataTypes.ReadData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, cache))
foreach (byte packed in await dataTypes.ReadDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, stream))
{
byte hig = (byte)(packed >> 4);
byte low = (byte)(packed & (byte)0x0F);
@ -465,15 +460,15 @@ namespace MinecraftClient.Protocol.Handlers
blockMeta.Enqueue(low);
}
//Skip data we don't need
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, cache); //Block light
// Skip data we don't need
await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, stream); //Block light
if (hasSkyLight)
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, cache); //Sky light
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * addDataSectionCount) / 2, cache); //BlockAdd
await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, stream); //Sky light
await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * addDataSectionCount) / 2, stream); //BlockAdd
if (chunksContinuous)
dataTypes.DropData(Chunk.SizeX * Chunk.SizeZ, cache); //Biomes
await dataTypes.DropDataAsync(Chunk.SizeX * Chunk.SizeZ, stream); //Biomes
//Load chunk data
// Load chunk data
int maxChunkY = sizeof(int) * 8 - 1 - BitOperations.LeadingZeroCount(chunkMask);
for (int chunkY = 0; chunkY <= maxChunkY; chunkY++)
{

View file

@ -1,114 +0,0 @@
using System;
using System.Net.Sockets;
using MinecraftClient.Crypto;
namespace MinecraftClient.Protocol.Handlers
{
/// <summary>
/// Wrapper for handling unencrypted & encrypted socket
/// </summary>
class SocketWrapper
{
readonly TcpClient c;
AesCfb8Stream? s;
bool encrypted = false;
/// <summary>
/// Initialize a new SocketWrapper
/// </summary>
/// <param name="client">TcpClient connected to the server</param>
public SocketWrapper(TcpClient client)
{
c = client;
}
/// <summary>
/// Check if the socket is still connected
/// </summary>
/// <returns>TRUE if still connected</returns>
/// <remarks>Silently dropped connection can only be detected by attempting to read/write data</remarks>
public bool IsConnected()
{
return c.Client != null && c.Connected;
}
/// <summary>
/// Check if the socket has data available to read
/// </summary>
/// <returns>TRUE if data is available to read</returns>
public bool HasDataAvailable()
{
return c.Client.Available > 0;
}
/// <summary>
/// Switch network reading/writing to an encrypted stream
/// </summary>
/// <param name="secretKey">AES secret key</param>
public void SwitchToEncrypted(byte[] secretKey)
{
if (encrypted)
throw new InvalidOperationException("Stream is already encrypted!?");
s = new AesCfb8Stream(c.GetStream(), secretKey);
encrypted = true;
}
/// <summary>
/// Network reading method. Read bytes from the socket or encrypted socket.
/// </summary>
private void Receive(byte[] buffer, int start, int offset, SocketFlags f)
{
int read = 0;
while (read < offset)
{
if (encrypted)
read += s!.Read(buffer, start + read, offset - read);
else
read += c.Client.Receive(buffer, start + read, offset - read, f);
}
}
/// <summary>
/// Read some data from the server.
/// </summary>
/// <param name="length">Amount of bytes to read</param>
/// <returns>The data read from the network as an array</returns>
public byte[] ReadDataRAW(int length)
{
if (length > 0)
{
byte[] cache = new byte[length];
Receive(cache, 0, length, SocketFlags.None);
return cache;
}
return Array.Empty<byte>();
}
/// <summary>
/// Send raw data to the server.
/// </summary>
/// <param name="buffer">data to send</param>
public void SendDataRAW(byte[] buffer)
{
if (encrypted)
s!.Write(buffer, 0, buffer.Length);
else
c.Client.Send(buffer);
}
/// <summary>
/// Disconnect from the server
/// </summary>
public void Disconnect()
{
try
{
c.Close();
}
catch (SocketException) { }
catch (System.IO.IOException) { }
catch (NullReferenceException) { }
catch (ObjectDisposedException) { }
}
}
}

View file

@ -1,63 +0,0 @@
using Ionic.Zlib;
namespace MinecraftClient.Protocol.Handlers
{
/// <summary>
/// Quick Zlib compression handling for network packet compression.
/// Note: Underlying compression handling is taken from the DotNetZip Library.
/// This library is open source and provided under the Microsoft Public License.
/// More info about DotNetZip at dotnetzip.codeplex.com.
/// </summary>
public static class ZlibUtils
{
/// <summary>
/// Compress a byte array into another bytes array using Zlib compression
/// </summary>
/// <param name="to_compress">Data to compress</param>
/// <returns>Compressed data as a byte array</returns>
public static byte[] Compress(byte[] to_compress)
{
byte[] data;
using (System.IO.MemoryStream memstream = new())
{
using (ZlibStream stream = new(memstream, CompressionMode.Compress))
{
stream.Write(to_compress, 0, to_compress.Length);
}
data = memstream.ToArray();
}
return data;
}
/// <summary>
/// Decompress a byte array into another byte array of the specified size
/// </summary>
/// <param name="to_decompress">Data to decompress</param>
/// <param name="size_uncompressed">Size of the data once decompressed</param>
/// <returns>Decompressed data as a byte array</returns>
public static byte[] Decompress(byte[] to_decompress, int size_uncompressed)
{
ZlibStream stream = new(new System.IO.MemoryStream(to_decompress, false), CompressionMode.Decompress);
byte[] packetData_decompressed = new byte[size_uncompressed];
stream.Read(packetData_decompressed, 0, size_uncompressed);
stream.Close();
return packetData_decompressed;
}
/// <summary>
/// Decompress a byte array into another byte array of a potentially unlimited size (!)
/// </summary>
/// <param name="to_decompress">Data to decompress</param>
/// <returns>Decompressed data as byte array</returns>
public static byte[] Decompress(byte[] to_decompress)
{
ZlibStream stream = new(new System.IO.MemoryStream(to_decompress, false), CompressionMode.Decompress);
byte[] buffer = new byte[16 * 1024];
using System.IO.MemoryStream decompressedBuffer = new();
int read;
while ((read = stream.Read(buffer, 0, buffer.Length)) > 0)
decompressedBuffer.Write(buffer, 0, read);
return decompressedBuffer.ToArray();
}
}
}

View file

@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Inventory;
using MinecraftClient.Mapping;
using MinecraftClient.Protocol.ProfileKey;
@ -19,7 +22,12 @@ namespace MinecraftClient.Protocol
/// Start the login procedure once connected to the server
/// </summary>
/// <returns>True if login was successful</returns>
bool Login(PlayerKeyPair? playerKeyPair, Session.SessionToken session);
Task<bool> Login(HttpClient httpClient, PlayerKeyPair? playerKeyPair, Session.SessionToken session);
/// <summary>
/// Start processing game packets.
/// </summary>
Task StartUpdating();
/// <summary>
/// Disconnect from the server
@ -46,20 +54,20 @@ namespace MinecraftClient.Protocol
/// </summary>
/// <param name="message">Text to send</param>
/// <returns>True if successfully sent</returns>
bool SendChatMessage(string message, PlayerKeyPair? playerKeyPair = null);
Task<bool> SendChatMessage(string message, PlayerKeyPair? playerKeyPair = null);
/// <summary>
/// Allow to respawn after death
/// </summary>
/// <returns>True if packet successfully sent</returns>
bool SendRespawnPacket();
Task<bool> SendRespawnPacket();
/// <summary>
/// Inform the server of the client being used to connect
/// </summary>
/// <param name="brandInfo">Client string describing the client</param>
/// <returns>True if brand info was successfully sent</returns>
bool SendBrandInfo(string brandInfo);
Task<bool> SendBrandInfo(string brandInfo);
/// <summary>
/// Inform the server of the client's Minecraft settings
@ -72,7 +80,7 @@ namespace MinecraftClient.Protocol
/// <param name="skinParts">Show skin layers</param>
/// <param name="mainHand">1.9+ main hand</param>
/// <returns>True if client settings were successfully sent</returns>
bool SendClientSettings(string language, byte viewDistance, byte difficulty, byte chatMode, bool chatColors, byte skinParts, byte mainHand);
Task<bool> SendClientSettings(string language, byte viewDistance, byte difficulty, byte chatMode, bool chatColors, byte skinParts, byte mainHand);
/// <summary>
/// Send a location update telling that we moved to that location
@ -82,7 +90,7 @@ namespace MinecraftClient.Protocol
/// <param name="yaw">The new yaw (optional)</param>
/// <param name="pitch">The new pitch (optional)</param>
/// <returns>True if packet was successfully sent</returns>
bool SendLocationUpdate(Location location, bool onGround, float? yaw, float? pitch);
Task<bool> SendLocationUpdate(Location location, bool onGround, float? yaw, float? pitch);
/// <summary>
/// Send a plugin channel packet to the server.
@ -91,7 +99,7 @@ namespace MinecraftClient.Protocol
/// <param name="channel">Channel to send packet on</param>
/// <param name="data">packet Data</param>
/// <returns>True if message was successfully sent</returns>
bool SendPluginChannelPacket(string channel, byte[] data);
Task<bool> SendPluginChannelPacket(string channel, byte[] data);
/// <summary>
/// Send Entity Action packet to the server.
@ -99,14 +107,14 @@ namespace MinecraftClient.Protocol
/// <param name="entityID">PlayerID</param>
/// <param name="type">Type of packet to send</param>
/// <returns>True if packet was successfully sent</returns>
bool SendEntityAction(int EntityID, int type);
Task<bool> SendEntityAction(int EntityID, int type);
/// <summary>
/// Send a held item change packet to the server.
/// </summary>
/// <param name="slot">New active slot in the inventory hotbar</param>
/// <returns>True if packet was successfully sent</returns>
bool SendHeldItemChange(short slot);
Task<bool> SendHeldItemChange(short slot);
/// <summary>
/// Send an entity interaction packet to the server.
@ -114,7 +122,7 @@ namespace MinecraftClient.Protocol
/// <param name="EntityID">Entity ID to interact with</param>
/// <param name="type">Type of interaction (0: interact, 1: attack, 2: interact at)</param>
/// <returns>True if packet was successfully sent</returns>
bool SendInteractEntity(int EntityID, int type);
Task<bool> SendInteractEntity(int EntityID, int type);
/// <summary>
/// Send an entity interaction packet to the server.
@ -126,7 +134,7 @@ namespace MinecraftClient.Protocol
/// <param name="Z">Z coordinate for "interact at"</param>
/// <param name="hand">Player hand (0: main hand, 1: off hand)</param>
/// <returns>True if packet was successfully sent</returns>
bool SendInteractEntity(int EntityID, int type, float X, float Y, float Z, int hand);
Task<bool> SendInteractEntity(int EntityID, int type, float X, float Y, float Z, int hand);
/// <summary>
/// Send an entity interaction packet to the server.
@ -137,7 +145,7 @@ namespace MinecraftClient.Protocol
/// <param name="Y">Y coordinate for "interact at"</param>
/// <param name="Z">Z coordinate for "interact at"</param>
/// <returns>True if packet was successfully sent</returns>
bool SendInteractEntity(int EntityID, int type, float X, float Y, float Z);
Task<bool> SendInteractEntity(int EntityID, int type, float X, float Y, float Z);
/// <summary>
/// Send an entity interaction packet to the server.
@ -146,7 +154,7 @@ namespace MinecraftClient.Protocol
/// <param name="type">Type of interaction (0: interact, 1: attack, 2: interact at)</param>
/// <param name="hand">Only if Type is interact or interact at; 0: main hand, 1: off hand</param>
/// <returns>True if packet was successfully sent</returns>
bool SendInteractEntity(int EntityID, int type, int hand);
Task<bool> SendInteractEntity(int EntityID, int type, int hand);
/// <summary>
/// Send a use item packet to the server
@ -154,7 +162,7 @@ namespace MinecraftClient.Protocol
/// <param name="hand">0: main hand, 1: off hand</param>
/// <param name="sequenceId">Sequence ID used for synchronization</param>
/// <returns>True if packet was successfully sent</returns>
bool SendUseItem(int hand, int sequenceId);
Task<bool> SendUseItem(int hand, int sequenceId);
/// <summary>
/// Send a click window slot packet to the server
@ -166,7 +174,7 @@ namespace MinecraftClient.Protocol
/// <param name="changedSlots">Slots that have been changed in this event: List<SlotID, Changed Items> </param>
/// <param name="stateId">Inventory's stateId</param>
/// <returns>True if packet was successfully sent</returns>
bool SendWindowAction(int windowId, int slotId, WindowActionType action, Item? item, List<Tuple<short, Item?>> changedSlots, int stateId);
Task<bool> SendWindowAction(int windowId, int slotId, WindowActionType action, Item? item, List<Tuple<short, Item?>> changedSlots, int stateId);
/// <summary>
/// Request Creative Mode item creation into regular/survival Player Inventory
@ -177,7 +185,7 @@ namespace MinecraftClient.Protocol
/// <param name="count">Item count</param>
/// <param name="nbt">Optional item NBT</param>
/// <returns>TRUE if item given successfully</returns>
bool SendCreativeInventoryAction(int slot, ItemType itemType, int count, Dictionary<string, object>? nbt);
Task<bool> SendCreativeInventoryAction(int slot, ItemType itemType, int count, Dictionary<string, object>? nbt);
/// <summary>
/// Send a click container button packet to the server.
@ -187,7 +195,7 @@ namespace MinecraftClient.Protocol
/// <param name="buttonId">Id of the clicked button</param>
/// <returns>True if packet was successfully sent</returns>
bool ClickContainerButton(int windowId, int buttonId);
Task<bool> ClickContainerButton(int windowId, int buttonId);
/// <summary>
/// Plays animation
@ -195,13 +203,13 @@ namespace MinecraftClient.Protocol
/// <param name="animation">0 for left arm, 1 for right arm</param>
/// <param name="playerid">Player Entity ID</param>
/// <returns>TRUE if item given successfully</returns>
bool SendAnimation(int animation, int playerid);
Task<bool> SendAnimation(int animation, int playerid);
/// <summary>
/// Send a close window packet to the server
/// </summary>
/// <param name="windowId">Id of the window being closed</param>
bool SendCloseWindow(int windowId);
Task<bool> SendCloseWindow(int windowId);
/// <summary>
/// Send player block placement packet to the server
@ -211,7 +219,7 @@ namespace MinecraftClient.Protocol
/// <param name="face">Block face</param>
/// <param name="sequenceId">Sequence ID (use for synchronization)</param>
/// <returns>True if packet was successfully sent</returns>
bool SendPlayerBlockPlacement(int hand, Location location, Direction face, int sequenceId);
Task<bool> SendPlayerBlockPlacement(int hand, Location location, Direction face, int sequenceId);
/// <summary>
/// Send player blog digging packet to the server. This packet needs to be called at least twice: Once to begin digging, then a second time to finish digging
@ -221,7 +229,7 @@ namespace MinecraftClient.Protocol
/// <param name="face">Block face</param>
/// <param name="sequenceId">Sequence ID (use for synchronization)</param>
/// <returns>True if packet was succcessfully sent</returns>
bool SendPlayerDigging(int status, Location location, Direction face, int sequenceId);
Task<bool> SendPlayerDigging(int status, Location location, Direction face, int sequenceId);
/// <summary>
/// Change text on a sign
@ -232,7 +240,7 @@ namespace MinecraftClient.Protocol
/// <param name="line3">New line 3</param>
/// <param name="line4">New line 4</param>
/// <returns>True if packet was succcessfully sent</returns>
bool SendUpdateSign(Location location, string line1, string line2, string line3, string line4);
Task<bool> SendUpdateSign(Location location, string line1, string line2, string line3, string line4);
/// <summary>
/// Update command block
@ -241,24 +249,18 @@ namespace MinecraftClient.Protocol
/// <param name="command">command</param>
/// <param name="mode">command block mode</param>
/// <param name="flags">command block flags</param>
bool UpdateCommandBlock(Location location, string command, CommandBlockMode mode, CommandBlockFlags flags);
Task<bool> UpdateCommandBlock(Location location, string command, CommandBlockMode mode, CommandBlockFlags flags);
/// <summary>
/// Select villager trade
/// </summary>
/// <param name="selectedSlot">The slot of the trade, starts at 0.</param>
bool SelectTrade(int selectedSlot);
Task<bool> SelectTrade(int selectedSlot);
/// <summary>
/// Spectate a player/entity
/// </summary>
/// <param name="uuid">The uuid of the player/entity to spectate/teleport to.</param>
bool SendSpectate(Guid uuid);
/// <summary>
/// Get net read thread (main thread) ID
/// </summary>
/// <returns>Net read thread ID</returns>
int GetNetMainThreadId();
Task<bool> SendSpectate(Guid uuid);
}
}

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using MinecraftClient.Inventory;
using MinecraftClient.Logger;
using MinecraftClient.Mapping;
@ -18,7 +19,6 @@ namespace MinecraftClient.Protocol
{
/* The MinecraftCom Handler must
* provide these getters */
int GetServerPort();
string GetServerHost();
string GetUsername();
@ -43,26 +43,6 @@ namespace MinecraftClient.Protocol
Container? GetInventory(int inventoryID);
ILogger GetLogger();
/// <summary>
/// Invoke a task on the main thread, wait for completion and retrieve return value.
/// </summary>
/// <param name="task">Task to run with any type or return value</param>
/// <returns>Any result returned from task, result type is inferred from the task</returns>
/// <example>bool result = InvokeOnMainThread(methodThatReturnsAbool);</example>
/// <example>bool result = InvokeOnMainThread(() => methodThatReturnsAbool(argument));</example>
/// <example>int result = InvokeOnMainThread(() => { yourCode(); return 42; });</example>
/// <typeparam name="T">Type of the return value</typeparam>
T InvokeOnMainThread<T>(Func<T> task);
/// <summary>
/// Invoke a task on the main thread and wait for completion
/// </summary>
/// <param name="task">Task to run without return value</param>
/// <example>InvokeOnMainThread(methodThatReturnsNothing);</example>
/// <example>InvokeOnMainThread(() => methodThatReturnsNothing(argument));</example>
/// <example>InvokeOnMainThread(() => { yourCode(); });</example>
void InvokeOnMainThread(Action task);
/// <summary>
/// Called when a network packet received or sent
/// </summary>
@ -78,7 +58,7 @@ namespace MinecraftClient.Protocol
/// <summary>
/// Called when a server was successfully joined
/// </summary>
void OnGameJoined();
Task OnGameJoined();
/// <summary>
/// Received chat/system message from the server
@ -184,21 +164,21 @@ namespace MinecraftClient.Protocol
/// Called ~10 times per second (10 ticks per second)
/// Useful for updating bots in other parts of the program
/// </summary>
void OnUpdate();
Task OnUpdate();
/// <summary>
/// Registers the given plugin channel for the given bot.
/// </summary>
/// <param name="channel">The channel to register.</param>
/// <param name="bot">The bot to register the channel for.</param>
void RegisterPluginChannel(string channel, ChatBot bot);
Task RegisterPluginChannel(string channel, ChatBot bot);
/// <summary>
/// Unregisters the given plugin channel for the given bot.
/// </summary>
/// <param name="channel">The channel to unregister.</param>
/// <param name="bot">The bot to unregister the channel for.</param>
void UnregisterPluginChannel(string channel, ChatBot bot);
Task UnregisterPluginChannel(string channel, ChatBot bot);
/// <summary>
/// Sends a plugin channel packet to the server.
@ -208,7 +188,7 @@ namespace MinecraftClient.Protocol
/// <param name="data">The payload for the packet.</param>
/// <param name="sendEvenIfNotRegistered">Whether the packet should be sent even if the server or the client hasn't registered it yet.</param>
/// <returns>Whether the packet was sent: true if it was sent, false if there was a connection error or it wasn't registered.</returns>
bool SendPluginChannelMessage(string channel, byte[] data, bool sendEvenIfNotRegistered = false);
Task<bool> SendPluginChannelMessage(string channel, byte[] data, bool sendEvenIfNotRegistered = false);
/// <summary>
/// Called when a plugin channel message was sent from the server.
@ -348,7 +328,7 @@ namespace MinecraftClient.Protocol
/// </summary>
/// <param name="uuid">Affected player's UUID</param>
/// <param name="gamemode">New game mode</param>
void OnGamemodeUpdate(Guid uuid, int gamemode);
Task OnGamemodeUpdate(Guid uuid, int gamemode);
/// <summary>
/// Called when a player's latency has changed
@ -472,6 +452,6 @@ namespace MinecraftClient.Protocol
/// <param name="buttonId">Id of the clicked button</param>
/// <returns>True if packet was successfully sent</returns>
bool ClickContainerButton(int windowId, int buttonId);
public Task<bool> ClickContainerButton(int windowId, int buttonId);
}
}

View file

@ -1,4 +1,5 @@
using System;
using System.IO;
using System.Text;
namespace MinecraftClient.Protocol
@ -6,11 +7,10 @@ namespace MinecraftClient.Protocol
// Thanks to https://stackoverflow.com/questions/60404612/parse-jwt-token-to-get-the-payload-content-only-without-external-library-in-c-sh
public static class JwtPayloadDecode
{
public static string GetPayload(string token)
public static MemoryStream GetPayload(string token)
{
var content = token.Split('.')[1];
var jsonPayload = Encoding.UTF8.GetString(Decode(content));
return jsonPayload;
return new MemoryStream(Decode(content));
}
private static byte[] Decode(string input)
@ -23,7 +23,7 @@ namespace MinecraftClient.Protocol
case 0: break; // No pad chars in this case
case 2: output += "=="; break; // Two pad chars
case 3: output += "="; break; // One pad char
default: throw new System.ArgumentOutOfRangeException(nameof(input), "Illegal base64url string!");
default: throw new ArgumentOutOfRangeException(nameof(input), "Illegal base64url string!");
}
var converted = Convert.FromBase64String(output); // Standard base64 decoder
return converted;

View file

@ -3,9 +3,18 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using MinecraftClient.Protocol.ProfileKey;
using static MinecraftClient.Settings;
using static MinecraftClient.Settings.MainConfigHealper.MainConfig.GeneralConfig;
@ -13,9 +22,10 @@ namespace MinecraftClient.Protocol
{
static class Microsoft
{
private static readonly string clientId = "54473e32-df8f-42e9-a649-9419b0dab9d3";
private static readonly string signinUrl = string.Format("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id={0}&response_type=code&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&scope=XboxLive.signin%20offline_access%20openid%20email&prompt=select_account&response_mode=fragment", clientId);
private static readonly string tokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
private const string clientId = "54473e32-df8f-42e9-a649-9419b0dab9d3";
private const string tokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
private const string signinUrl = $"https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id={clientId}&response_type=code&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&scope=XboxLive.signin%20offline_access%20openid%20email&prompt=select_account&response_mode=fragment";
private const string certificates = "https://api.minecraftservices.com/player/certificates";
public static string SignInUrl { get { return signinUrl; } }
@ -26,7 +36,7 @@ namespace MinecraftClient.Protocol
/// <returns>Sign-in URL with email pre-filled</returns>
public static string GetSignInUrlWithHint(string loginHint)
{
return SignInUrl + "&login_hint=" + Uri.EscapeDataString(loginHint);
return $"{SignInUrl}&login_hint={Uri.EscapeDataString(loginHint)}";
}
/// <summary>
@ -34,11 +44,16 @@ namespace MinecraftClient.Protocol
/// </summary>
/// <param name="code">Auth code obtained after user signing in</param>
/// <returns>Access token and refresh token</returns>
public static LoginResponse RequestAccessToken(string code)
public static async Task<LoginResponse> RequestAccessTokenAsync(HttpClient httpClient, string code)
{
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);
FormUrlEncodedContent postData = new(new KeyValuePair<string, string>[]
{
new("client_id", clientId),
new("grant_type", "authorization_code"),
new("redirect_uri", "https://mccteam.github.io/redirect.html"),
new("code", code),
});
return await RequestTokenAsync(httpClient, postData);
}
/// <summary>
@ -46,11 +61,43 @@ namespace MinecraftClient.Protocol
/// </summary>
/// <param name="refreshToken">Refresh token</param>
/// <returns>Access token and new refresh token</returns>
public static LoginResponse RefreshAccessToken(string refreshToken)
public static async Task<LoginResponse> RefreshAccessTokenAsync(HttpClient httpClient, string refreshToken)
{
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);
FormUrlEncodedContent postData = new(new KeyValuePair<string, string>[]
{
new("client_id", clientId),
new("grant_type", "refresh_token"),
new("redirect_uri", "https://mccteam.github.io/redirect.html"),
new("refresh_token", refreshToken),
});
return await RequestTokenAsync(httpClient, postData);
}
private record TokenInfo
{
public string? token_type { init; get; }
public string? scope { init; get; }
public int expires_in { init; get; }
public int ext_expires_in { init; get; }
public string? access_token { init; get; }
public string? refresh_token { init; get; }
public string? id_token { init; get; }
public string? error { init; get; }
public string? error_description { init; get; }
}
private record JwtPayloadInIdToken
{
public string? ver { init; get; }
public string? iss { init; get; }
public string? sub { init; get; }
public string? aud { init; get; }
public long exp { init; get; }
public long iat { init; get; }
public long nbf { init; get; }
public string? email { init; get; }
public string? tid { init; get; }
public string? aio { init; get; }
}
/// <summary>
@ -58,45 +105,88 @@ namespace MinecraftClient.Protocol
/// </summary>
/// <param name="postData">Complete POST data for the request</param>
/// <returns></returns>
private static LoginResponse RequestToken(string postData)
private static async Task<LoginResponse> RequestTokenAsync(HttpClient httpClient, FormUrlEncodedContent postData)
{
var request = new ProxiedWebRequest(tokenUrl)
{
UserAgent = "MCC/" + Program.Version
};
var response = request.Post("application/x-www-form-urlencoded", postData);
var jsonData = Json.ParseJson(response.Body);
using HttpResponseMessage response = await httpClient.PostAsync(tokenUrl, postData);
TokenInfo jsonData = (await response.Content.ReadFromJsonAsync<TokenInfo>())!;
// Error handling
if (jsonData.Properties.ContainsKey("error"))
if (!string.IsNullOrEmpty(jsonData.error))
{
throw new Exception(jsonData.Properties["error_description"].StringValue);
throw new Exception(jsonData.error_description);
}
else
{
string accessToken = jsonData.Properties["access_token"].StringValue;
string refreshToken = jsonData.Properties["refresh_token"].StringValue;
int expiresIn = int.Parse(jsonData.Properties["expires_in"].StringValue, NumberStyles.Any, CultureInfo.CurrentCulture);
// Extract email from JWT
string payload = JwtPayloadDecode.GetPayload(jsonData.Properties["id_token"].StringValue);
var jsonPayload = Json.ParseJson(payload);
string email = jsonPayload.Properties["email"].StringValue;
Stream payload = JwtPayloadDecode.GetPayload(jsonData.id_token!);
JwtPayloadInIdToken jsonPayload = (await JsonSerializer.DeserializeAsync<JwtPayloadInIdToken>(payload))!;
return new LoginResponse()
{
Email = email,
AccessToken = accessToken,
RefreshToken = refreshToken,
ExpiresIn = expiresIn
Email = jsonPayload.email!,
AccessToken = jsonData.access_token!,
RefreshToken = jsonData.refresh_token!,
ExpiresIn = jsonData.expires_in,
};
}
}
private record ProfileKeyResult
{
public KeyPair? keyPair { init; get; }
public string? publicKeySignature { init; get; }
public string? publicKeySignatureV2 { init; get; }
public DateTime expiresAt { init; get; }
public DateTime refreshedAfter { init; get; }
public record KeyPair
{
public string? privateKey { init; get; }
public string? publicKey { init; get; }
}
}
/// <summary>
/// Request the key to be used for message signing.
/// </summary>
/// <param name="accessToken">Access token in session</param>
/// <returns>Profile key</returns>
public static async Task<PlayerKeyPair?> RequestProfileKeyAsync(HttpClient httpClient, string accessToken)
{
try
{
using HttpRequestMessage request = new(HttpMethod.Post, certificates);
request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken));
using HttpResponseMessage response = await httpClient.SendAsync(request);
if (Settings.Config.Logging.DebugMessages)
ConsoleIO.WriteLine(response.ToString());
ProfileKeyResult jsonData = (await response.Content.ReadFromJsonAsync<ProfileKeyResult>())!;
PublicKey publicKey = new(jsonData.keyPair!.publicKey!, jsonData.publicKeySignature, jsonData.publicKeySignatureV2);
PrivateKey privateKey = new(jsonData.keyPair!.privateKey!);
return new PlayerKeyPair(publicKey, privateKey, jsonData.expiresAt, jsonData.refreshedAfter);
}
catch (HttpRequestException e)
{
ConsoleIO.WriteLineFormatted("§cFetch profile key failed: " + e.Message);
if (Settings.Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted("§c" + e.StackTrace);
return null;
}
}
public static void OpenBrowser(string link)
{
try
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
if (OperatingSystem.IsWindows())
{
var ps = new ProcessStartInfo(link)
{
@ -106,11 +196,11 @@ namespace MinecraftClient.Protocol
Process.Start(ps);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
else if (OperatingSystem.IsLinux())
{
Process.Start("xdg-open", link);
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
else if (OperatingSystem.IsMacOS())
{
Process.Start("open", link);
}
@ -134,53 +224,86 @@ namespace MinecraftClient.Protocol
}
}
static class XboxLive
static partial class XboxLive
{
private static readonly string authorize = "https://login.live.com/oauth20_authorize.srf?client_id=000000004C12AE6F&redirect_uri=https://login.live.com/oauth20_desktop.srf&scope=service::user.auth.xboxlive.com::MBI_SSL&display=touch&response_type=token&locale=en";
private static readonly string xbl = "https://user.auth.xboxlive.com/user/authenticate";
private static readonly string xsts = "https://xsts.auth.xboxlive.com/xsts/authorize";
internal const string UserAgent = "Mozilla/5.0 (XboxReplay; XboxLiveAuth/3.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36";
private static readonly string userAgent = "Mozilla/5.0 (XboxReplay; XboxLiveAuth/3.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36";
private const string xsts = "https://xsts.auth.xboxlive.com/xsts/authorize";
private const string xbl = "https://user.auth.xboxlive.com/user/authenticate";
private const string authorize = "https://login.live.com/oauth20_authorize.srf?client_id=000000004C12AE6F&redirect_uri=https://login.live.com/oauth20_desktop.srf&scope=service::user.auth.xboxlive.com::MBI_SSL&display=touch&response_type=token&locale=en";
private static readonly Regex ppft = new("sFTTag:'.*value=\"(.*)\"\\/>'");
private static readonly Regex urlPost = new("urlPost:'(.+?(?=\'))");
private static readonly Regex confirm = new("identity\\/confirm");
private static readonly Regex invalidAccount = new("Sign in to", RegexOptions.IgnoreCase);
private static readonly Regex twoFA = new("Help us protect your account", RegexOptions.IgnoreCase);
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.General)
{
AllowTrailingCommas = true,
PropertyNameCaseInsensitive = false,
ReadCommentHandling = JsonCommentHandling.Skip,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
public static string SignInUrl { get { return authorize; } }
private record AuthPayload
{
public Propertie? Properties { init; get; }
public string? RelyingParty { init; get; }
public string? TokenType { init; get; }
public record Propertie
{
public string? AuthMethod { init; get; }
public string? SiteName { init; get; }
public string? RpsTicket { init; get; }
public string? SandboxId { init; get; }
public string[]? UserTokens { init; get; }
}
}
private record AuthResult
{
public DateTime IssueInstant { init; get; }
public DateTime NotAfter { init; get; }
public string? Token { init; get; }
public DisplayClaim? DisplayClaims { init; get; }
public record DisplayClaim
{
public Dictionary<string, string>[]? xui { init; get; }
}
}
private record AuthError
{
public string? Identity { init; get; }
public long XErr { init; get; }
public string? Message { init; get; }
public string? Redirect { init; get; }
}
/// <summary>
/// Pre-authentication
/// </summary>
/// <remarks>This step is to get the login page for later use</remarks>
/// <returns></returns>
public static PreAuthResponse PreAuth()
public static async Task<PreAuthResponse> PreAuthAsync(HttpClient httpClient)
{
var request = new ProxiedWebRequest(authorize)
{
UserAgent = userAgent
};
var response = request.Get();
using HttpResponseMessage response = await httpClient.GetAsync(authorize);
string html = response.Body;
string html = await response.Content.ReadAsStringAsync();
string PPFT = ppft.Match(html).Groups[1].Value;
string urlPost = XboxLive.urlPost.Match(html).Groups[1].Value;
string PPFT = GetPpftRegex().Match(html).Groups[1].Value;
string urlPost = GetUrlPostRegex().Match(html).Groups[1].Value;
if (string.IsNullOrEmpty(PPFT) || string.IsNullOrEmpty(urlPost))
{
throw new Exception("Fail to extract PPFT or urlPost");
}
//Console.WriteLine("PPFT: {0}", PPFT);
//Console.WriteLine();
//Console.WriteLine("urlPost: {0}", urlPost);
return new PreAuthResponse()
{
UrlPost = urlPost,
PPFT = PPFT,
Cookie = response.Cookies
Cookie = new()// response.Cookies
};
}
@ -192,69 +315,54 @@ namespace MinecraftClient.Protocol
/// <param name="password">Account password</param>
/// <param name="preAuth"></param>
/// <returns></returns>
public static Microsoft.LoginResponse UserLogin(string email, string password, PreAuthResponse preAuth)
public static async Task<Microsoft.LoginResponse> UserLoginAsync(HttpClient httpClient, string email, string password, PreAuthResponse preAuth)
{
var request = new ProxiedWebRequest(preAuth.UrlPost, preAuth.Cookie)
FormUrlEncodedContent postData = new(new KeyValuePair<string, string>[]
{
UserAgent = userAgent
};
new("login", email),
new("loginfmt", email),
new("passwd", password),
new("PPFT", preAuth.PPFT),
});
string postData = "login=" + Uri.EscapeDataString(email)
+ "&loginfmt=" + Uri.EscapeDataString(email)
+ "&passwd=" + Uri.EscapeDataString(password)
+ "&PPFT=" + Uri.EscapeDataString(preAuth.PPFT);
using HttpResponseMessage response = await httpClient.PostAsync(preAuth.UrlPost, postData);
var response = request.Post("application/x-www-form-urlencoded", postData);
if (Settings.Config.Logging.DebugMessages)
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLine(response.ToString());
}
if (response.StatusCode >= 300 && response.StatusCode <= 399)
if (response.IsSuccessStatusCode)
{
string url = response.Headers.Get("Location")!;
string hash = url.Split('#')[1];
var request2 = new ProxiedWebRequest(url);
var response2 = request2.Get();
if (response2.StatusCode != 200)
{
throw new Exception("Authentication failed");
}
string hash = response.RequestMessage!.RequestUri!.Fragment[1..];
if (string.IsNullOrEmpty(hash))
{
throw new Exception("Cannot extract access token");
}
var dict = Request.ParseQueryString(hash);
//foreach (var pair in dict)
//{
// Console.WriteLine("{0}: {1}", pair.Key, pair.Value);
//}
var dict = Request.ParseQueryString(hash);
return new Microsoft.LoginResponse()
{
Email = email,
AccessToken = dict["access_token"],
RefreshToken = dict["refresh_token"],
ExpiresIn = int.Parse(dict["expires_in"], NumberStyles.Any, CultureInfo.CurrentCulture)
ExpiresIn = int.Parse(dict["expires_in"])
};
}
else
{
if (twoFA.IsMatch(response.Body))
string body = await response.Content.ReadAsStringAsync();
if (GetTwoFARegex().IsMatch(body))
{
// TODO: Handle 2FA
throw new Exception("2FA enabled but not supported yet. Use browser sign-in method or try to disable 2FA in Microsoft account settings");
}
else if (invalidAccount.IsMatch(response.Body))
else if (GetInvalidAccountRegex().IsMatch(body))
{
throw new Exception("Invalid credentials. Check your credentials");
}
else throw new Exception("Unexpected response. Check your credentials. Response code: " + response.StatusCode);
else
{
throw new Exception("Unexpected response. Check your credentials. Response code: " + response.StatusCode);
}
}
}
@ -263,54 +371,54 @@ namespace MinecraftClient.Protocol
/// </summary>
/// <param name="loginResponse"></param>
/// <returns></returns>
public static XblAuthenticateResponse XblAuthenticate(Microsoft.LoginResponse loginResponse)
public static async Task<XblAuthenticateResponse> XblAuthenticateAsync(HttpClient httpClient, Microsoft.LoginResponse loginResponse)
{
var request = new ProxiedWebRequest(xbl)
{
UserAgent = userAgent,
Accept = "application/json"
};
request.Headers.Add("x-xbl-contract-version", "0");
var accessToken = loginResponse.AccessToken;
string accessToken;
if (Config.Main.General.Method == LoginMethod.browser)
{
// Our own client ID must have d= in front of the token or HTTP status 400
// "Stolen" client ID must not have d= in front of the token or HTTP status 400
accessToken = "d=" + accessToken;
accessToken = "d=" + loginResponse.AccessToken;
}
else
{
accessToken = loginResponse.AccessToken;
}
string payload = "{"
+ "\"Properties\": {"
+ "\"AuthMethod\": \"RPS\","
+ "\"SiteName\": \"user.auth.xboxlive.com\","
+ "\"RpsTicket\": \"" + accessToken + "\""
+ "},"
+ "\"RelyingParty\": \"http://auth.xboxlive.com\","
+ "\"TokenType\": \"JWT\""
+ "}";
var response = request.Post("application/json", payload);
if (Settings.Config.Logging.DebugMessages)
AuthPayload payload = new()
{
Properties = new AuthPayload.Propertie()
{
AuthMethod = "RPS",
SiteName = "user.auth.xboxlive.com",
RpsTicket = accessToken,
},
RelyingParty = "http://auth.xboxlive.com",
TokenType = "JWT",
};
using StringContent httpContent = new(JsonSerializer.Serialize(payload, JsonOptions), Encoding.UTF8, "application/json");
httpContent.Headers.Add("x-xbl-contract-version", "0");
using HttpResponseMessage response = await httpClient.PostAsync(xbl, httpContent);
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLine(response.ToString());
}
if (response.StatusCode == 200)
{
string jsonString = response.Body;
//Console.WriteLine(jsonString);
Json.JSONData json = Json.ParseJson(jsonString);
string token = json.Properties["Token"].StringValue;
string userHash = json.Properties["DisplayClaims"].Properties["xui"].DataArray[0].Properties["uhs"].StringValue;
if (response.IsSuccessStatusCode)
{
AuthResult jsonData = (await response.Content.ReadFromJsonAsync<AuthResult>())!;
return new XblAuthenticateResponse()
{
Token = token,
UserHash = userHash
Token = jsonData.Token!,
UserHash = jsonData.DisplayClaims!.xui![0]["uhs"],
};
}
else
{
throw new Exception("XBL Authentication failed");
throw new Exception("XBL Authentication failed, code = " + response.StatusCode.ToString());
}
}
@ -320,56 +428,53 @@ namespace MinecraftClient.Protocol
/// <remarks>(Don't ask me what is XSTS, I DONT KNOW)</remarks>
/// <param name="xblResponse"></param>
/// <returns></returns>
public static XSTSAuthenticateResponse XSTSAuthenticate(XblAuthenticateResponse xblResponse)
public static async Task<XSTSAuthenticateResponse> XSTSAuthenticateAsync(HttpClient httpClient, XblAuthenticateResponse xblResponse)
{
var request = new ProxiedWebRequest(xsts)
AuthPayload payload = new()
{
UserAgent = userAgent,
Accept = "application/json"
Properties = new AuthPayload.Propertie()
{
SandboxId = "RETAIL",
UserTokens = new string[] { xblResponse.Token },
},
RelyingParty = "rp://api.minecraftservices.com/",
TokenType = "JWT",
};
request.Headers.Add("x-xbl-contract-version", "1");
string payload = "{"
+ "\"Properties\": {"
+ "\"SandboxId\": \"RETAIL\","
+ "\"UserTokens\": ["
+ "\"" + xblResponse.Token + "\""
+ "]"
+ "},"
+ "\"RelyingParty\": \"rp://api.minecraftservices.com/\","
+ "\"TokenType\": \"JWT\""
+ "}";
var response = request.Post("application/json", payload);
if (Settings.Config.Logging.DebugMessages)
{
using StringContent httpContent = new(JsonSerializer.Serialize(payload, JsonOptions), Encoding.UTF8, "application/json");
httpContent.Headers.Add("x-xbl-contract-version", "1");
using HttpResponseMessage response = await httpClient.PostAsync(xsts, httpContent);
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLine(response.ToString());
}
if (response.StatusCode == 200)
if (response.IsSuccessStatusCode)
{
string jsonString = response.Body;
Json.JSONData json = Json.ParseJson(jsonString);
string token = json.Properties["Token"].StringValue;
string userHash = json.Properties["DisplayClaims"].Properties["xui"].DataArray[0].Properties["uhs"].StringValue;
AuthResult jsonData = (await response.Content.ReadFromJsonAsync<AuthResult>())!;
return new XSTSAuthenticateResponse()
{
Token = token,
UserHash = userHash
Token = jsonData.Token!,
UserHash = jsonData.DisplayClaims!.xui![0]["uhs"],
};
}
else
{
if (response.StatusCode == 401)
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
Json.JSONData json = Json.ParseJson(response.Body);
if (json.Properties["XErr"].StringValue == "2148916233")
{
AuthError jsonData = (await response.Content.ReadFromJsonAsync<AuthError>())!;
if (jsonData.XErr == 2148916233)
throw new Exception("The account doesn't have an Xbox account");
}
else if (json.Properties["XErr"].StringValue == "2148916238")
{
else if (jsonData.XErr == 2148916235)
throw new Exception("The account is from a country where Xbox Live is not available/banned");
else if (jsonData.XErr == 2148916236 || jsonData.XErr == 2148916237)
throw new Exception("The account needs adult verification on Xbox page. (South Korea)");
else if (jsonData.XErr == 2148916238)
throw new Exception("The account is a child (under 18) and cannot proceed unless the account is added to a Family by an adult");
}
else throw new Exception("Unknown XSTS error code: " + json.Properties["XErr"].StringValue);
else
throw new Exception("Unknown XSTS error code: " + jsonData.XErr.ToString() + ", Check " + jsonData.Redirect);
}
else
{
@ -396,13 +501,63 @@ namespace MinecraftClient.Protocol
public string Token;
public string UserHash;
}
[GeneratedRegex("sFTTag:'.*value=\"(.*)\"\\/>'")]
private static partial Regex GetPpftRegex();
[GeneratedRegex("urlPost:'(.+?(?='))")]
private static partial Regex GetUrlPostRegex();
[GeneratedRegex("identity\\/confirm")]
private static partial Regex GetConfirmRegex();
[GeneratedRegex("Sign in to", RegexOptions.IgnoreCase, "zh-CN")]
private static partial Regex GetInvalidAccountRegex();
[GeneratedRegex("Help us protect your account", RegexOptions.IgnoreCase, "zh-CN")]
private static partial Regex GetTwoFARegex();
}
static class MinecraftWithXbox
{
private static readonly string loginWithXbox = "https://api.minecraftservices.com/authentication/login_with_xbox";
private static readonly string ownership = "https://api.minecraftservices.com/entitlements/mcstore";
private static readonly string profile = "https://api.minecraftservices.com/minecraft/profile";
private const string profile = "https://api.minecraftservices.com/minecraft/profile";
private const string ownership = "https://api.minecraftservices.com/entitlements/mcstore";
private const string loginWithXbox = "https://api.minecraftservices.com/authentication/login_with_xbox";
private record LoginPayload
{
public string? identityToken { init; get; }
}
private record LoginResult
{
public string? username { init; get; }
public string[]? roles { init; get; }
public string? access_token { init; get; }
public string? token_type { init; get; }
public int expires_in { init; get; }
}
private record GameOwnershipResult
{
public Dictionary<string, string>[]? items { init; get; }
public string? signature { init; get; }
public string? keyId { init; get; }
}
private record GameProfileResult
{
public string? id { init; get; }
public string? name { init; get; }
public Dictionary<string, string>[]? skins { init; get; }
public Dictionary<string, string>[]? capes { init; get; }
/* Error */
public string? path { init; get; }
public string? errorType { init; get; }
public string? error { init; get; }
public string? errorMessage { init; get; }
public string? developerMessage { init; get; }
}
/// <summary>
/// Login to Minecraft using the XSTS token and user hash obtained before
@ -410,25 +565,23 @@ namespace MinecraftClient.Protocol
/// <param name="userHash"></param>
/// <param name="xstsToken"></param>
/// <returns></returns>
public static string LoginWithXbox(string userHash, string xstsToken)
public static async Task<string> LoginWithXboxAsync(HttpClient httpClient, string userHash, string xstsToken)
{
var request = new ProxiedWebRequest(loginWithXbox)
LoginPayload payload = new()
{
Accept = "application/json"
identityToken = $"XBL3.0 x={userHash};{xstsToken}",
};
string payload = "{\"identityToken\": \"XBL3.0 x=" + userHash + ";" + xstsToken + "\"}";
var response = request.Post("application/json", payload);
using StringContent httpContent = new(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
if (Settings.Config.Logging.DebugMessages)
{
using HttpResponseMessage response = await httpClient.PostAsync(loginWithXbox, httpContent);
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLine(response.ToString());
}
string jsonString = response.Body;
Json.JSONData json = Json.ParseJson(jsonString);
LoginResult jsonData = (await response.Content.ReadFromJsonAsync<LoginResult>())!;
return json.Properties["access_token"].StringValue;
return jsonData.access_token!;
}
/// <summary>
@ -436,39 +589,40 @@ namespace MinecraftClient.Protocol
/// </summary>
/// <param name="accessToken"></param>
/// <returns>True if the user own the game</returns>
public static bool UserHasGame(string accessToken)
public static async Task<bool> CheckUserHasGameAsync(HttpClient httpClient, string accessToken)
{
var request = new ProxiedWebRequest(ownership);
using HttpRequestMessage request = new(HttpMethod.Get, ownership);
request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken));
var response = request.Get();
if (Settings.Config.Logging.DebugMessages)
{
using HttpResponseMessage response = await httpClient.SendAsync(request);
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLine(response.ToString());
}
string jsonString = response.Body;
Json.JSONData json = Json.ParseJson(jsonString);
return json.Properties["items"].DataArray.Count > 0;
GameOwnershipResult jsonData = (await response.Content.ReadFromJsonAsync<GameOwnershipResult>())!;
return jsonData.items!.Length > 0;
}
public static UserProfile GetUserProfile(string accessToken)
public static async Task<UserProfile> GetUserProfileAsync(HttpClient httpClient, string accessToken)
{
var request = new ProxiedWebRequest(profile);
using HttpRequestMessage request = new(HttpMethod.Get, profile);
request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken));
var response = request.Get();
if (Settings.Config.Logging.DebugMessages)
{
using HttpResponseMessage response = await httpClient.SendAsync(request);
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLine(response.ToString());
}
string jsonString = response.Body;
Json.JSONData json = Json.ParseJson(jsonString);
GameProfileResult jsonData = (await response.Content.ReadFromJsonAsync<GameProfileResult>())!;
if (!string.IsNullOrEmpty(jsonData.error))
throw new Exception($"{jsonData.errorType}: {jsonData.error}. {jsonData.errorMessage}");
return new UserProfile()
{
UUID = json.Properties["id"].StringValue,
UserName = json.Properties["name"].StringValue
UUID = jsonData.id!,
UserName = jsonData.name!,
};
}

View file

@ -0,0 +1,203 @@
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Crypto;
using MinecraftClient.Crypto.AesHandler;
using static ConsoleInteractive.ConsoleReader;
namespace MinecraftClient.Protocol.PacketPipeline
{
public class AesStream : Stream
{
public const int BlockSize = 16;
private const int BufferSize = 1024;
public Socket Client;
private bool inStreamEnded = false;
private readonly IAesHandler Aes;
private int InputBufPos = 0, OutputBufPos = 0;
private readonly Memory<byte> InputBuf, OutputBuf;
private readonly Memory<byte> AesBufRead, AesBufSend;
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
public AesStream(Socket socket, byte[] key)
{
Client = socket;
InputBuf = new byte[BufferSize + BlockSize];
OutputBuf = new byte[BufferSize + BlockSize];
AesBufRead = new byte[BlockSize];
AesBufSend = new byte[BlockSize];
if (FasterAesX86.IsSupported())
Aes = new FasterAesX86(key);
else if (FasterAesArm.IsSupported())
Aes = new FasterAesArm(key);
else
Aes = new BasicAes(key);
key.CopyTo(InputBuf.Slice(0, BlockSize));
key.CopyTo(OutputBuf.Slice(0, BlockSize));
}
public override void Flush()
{
throw new NotSupportedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
var task = ReadAsync(buffer.AsMemory(offset, count)).AsTask();
task.Wait();
return task.Result;
}
public override int ReadByte()
{
if (inStreamEnded)
return -1;
var task = Client.ReceiveAsync(InputBuf.Slice(InputBufPos + BlockSize, 1)).AsTask();
task.Wait();
if (task.Result == 0)
{
inStreamEnded = true;
return -1;
}
Aes.EncryptEcb(InputBuf.Slice(InputBufPos, BlockSize).Span, AesBufRead.Span);
byte result = (byte)(AesBufRead.Span[0] ^ InputBuf.Span[InputBufPos + BlockSize]);
InputBufPos++;
if (InputBufPos == BufferSize)
{
InputBuf.Slice(BufferSize, BlockSize).CopyTo(InputBuf[..BlockSize]);
InputBufPos = 0;
}
return result;
}
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
if (inStreamEnded)
return 0;
int readLimit = Math.Min(buffer.Length, BufferSize - InputBufPos);
int curRead = await Client.ReceiveAsync(InputBuf.Slice(InputBufPos + BlockSize, readLimit), cancellationToken);
if (curRead == 0 || cancellationToken.IsCancellationRequested)
{
if (curRead == 0)
inStreamEnded = true;
return curRead;
}
for (int idx = 0; idx < curRead; idx++)
{
Aes.EncryptEcb(InputBuf.Slice(InputBufPos + idx, BlockSize).Span, AesBufRead.Span);
buffer.Span[idx] = (byte)(AesBufRead.Span[0] ^ InputBuf.Span[InputBufPos + BlockSize + idx]);
}
InputBufPos += curRead;
if (InputBufPos == BufferSize)
{
InputBuf.Slice(BufferSize, BlockSize).CopyTo(InputBuf[..BlockSize]);
InputBufPos = 0;
}
return curRead;
}
public new async ValueTask ReadExactlyAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
if (inStreamEnded)
return;
for (int readed = 0, curRead; readed < buffer.Length; readed += curRead)
{
int readLimit = Math.Min(buffer.Length - readed, BufferSize - InputBufPos);
curRead = await Client.ReceiveAsync(InputBuf.Slice(InputBufPos + BlockSize, readLimit), cancellationToken);
if (curRead == 0 || cancellationToken.IsCancellationRequested)
{
if (curRead == 0)
inStreamEnded = true;
return;
}
for (int idx = 0; idx < curRead; idx++)
{
Aes.EncryptEcb(InputBuf.Slice(InputBufPos + idx, BlockSize).Span, AesBufRead.Span);
buffer.Span[readed + idx] = (byte)(AesBufRead.Span[0] ^ InputBuf.Span[InputBufPos + BlockSize + idx]);
}
InputBufPos += curRead;
if (InputBufPos == BufferSize)
{
InputBuf.Slice(BufferSize, BlockSize).CopyTo(InputBuf.Slice(0, BlockSize));
InputBufPos = 0;
}
}
}
public async ValueTask<int> ReadRawAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
return await Client.ReceiveAsync(buffer, cancellationToken);
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
WriteAsync(buffer.AsMemory(offset, count)).AsTask().Wait();
}
public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
{
int outputStartPos = OutputBufPos;
for (int wirtten = 0; wirtten < buffer.Length; ++wirtten)
{
if (cancellationToken.IsCancellationRequested)
return;
Aes.EncryptEcb(OutputBuf.Slice(OutputBufPos, BlockSize).Span, AesBufSend.Span);
OutputBuf.Span[OutputBufPos + BlockSize] = (byte)(AesBufSend.Span[0] ^ buffer.Span[wirtten]);
if (++OutputBufPos == BufferSize)
{
await Client.SendAsync(OutputBuf.Slice(outputStartPos + BlockSize, BufferSize - outputStartPos), cancellationToken);
OutputBuf.Slice(BufferSize, BlockSize).CopyTo(OutputBuf.Slice(0, BlockSize));
OutputBufPos = outputStartPos = 0;
}
}
if (OutputBufPos > outputStartPos)
await Client.SendAsync(OutputBuf.Slice(outputStartPos + BlockSize, OutputBufPos - outputStartPos), cancellationToken);
return;
}
}
}

View file

@ -0,0 +1,189 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static ConsoleInteractive.ConsoleReader;
namespace MinecraftClient.Protocol.PacketPipeline
{
internal class PacketStream : Stream
{
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
readonly CancellationToken CancelToken;
private readonly Stream baseStream;
private readonly AesStream? aesStream;
private ZLibStream? zlibStream;
private int packetSize, packetReaded;
internal const int DropBufSize = 1024;
internal static readonly Memory<byte> DropBuf = new byte[DropBufSize];
private static readonly byte[] SingleByteBuf = new byte[1];
public PacketStream(ZLibStream zlibStream, int packetSize, CancellationToken cancellationToken = default)
{
CancelToken = cancellationToken;
this.aesStream = null;
this.zlibStream = zlibStream;
this.baseStream = zlibStream;
this.packetReaded = 0;
this.packetSize = packetSize;
}
public PacketStream(AesStream aesStream, int packetSize, CancellationToken cancellationToken = default)
{
CancelToken = cancellationToken;
this.aesStream = aesStream;
this.zlibStream = null;
this.baseStream = aesStream;
this.packetReaded = 0;
this.packetSize = packetSize;
}
public PacketStream(Stream baseStream, int packetSize, CancellationToken cancellationToken = default)
{
CancelToken = cancellationToken;
this.aesStream = null;
this.zlibStream = null;
this.baseStream = baseStream;
this.packetReaded = 0;
this.packetSize = packetSize;
}
public override void Flush()
{
throw new NotSupportedException();
}
public new byte ReadByte()
{
++packetReaded;
if (packetReaded > packetSize)
throw new OverflowException("Reach the end of the packet!");
baseStream.Read(SingleByteBuf, 0, 1);
return SingleByteBuf[0];
}
public async Task<byte> ReadByteAsync()
{
++packetReaded;
if (packetReaded > packetSize)
throw new OverflowException("Reach the end of the packet!");
await baseStream.ReadExactlyAsync(SingleByteBuf, CancelToken);
return SingleByteBuf[0];
}
public override int Read(byte[] buffer, int offset, int count)
{
if (packetReaded + buffer.Length > packetSize)
throw new OverflowException("Reach the end of the packet!");
int readed = baseStream.Read(buffer, offset, count);
packetReaded += readed;
return readed;
}
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
if (packetReaded + buffer.Length > packetSize)
throw new OverflowException("Reach the end of the packet!");
int readed = await baseStream.ReadAsync(buffer, CancelToken);
packetReaded += readed;
return readed;
}
public new async ValueTask ReadExactlyAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
if (packetReaded + buffer.Length > packetSize)
throw new OverflowException("Reach the end of the packet!");
await baseStream.ReadExactlyAsync(buffer, CancelToken);
packetReaded += buffer.Length;
}
public async Task<byte[]> ReadFullPacket()
{
byte[] buffer = new byte[packetSize - packetReaded];
await ReadExactlyAsync(buffer);
packetReaded = packetSize;
return buffer;
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
public async Task Skip(int length)
{
if (zlibStream != null)
{
for (int readed = 0, curRead; readed < length; readed += curRead)
curRead = await zlibStream.ReadAsync(DropBuf[..Math.Min(DropBufSize, length - readed)]);
}
else if (aesStream != null)
{
int skipRaw = length - AesStream.BlockSize;
for (int readed = 0, curRead; readed < skipRaw; readed += curRead)
curRead = await aesStream.ReadRawAsync(DropBuf[..Math.Min(DropBufSize, skipRaw - readed)]);
await aesStream.ReadAsync(DropBuf[..Math.Min(length, AesStream.BlockSize)]);
}
else
{
for (int readed = 0, curRead; readed < length; readed += curRead)
curRead = await baseStream.ReadAsync(DropBuf[..Math.Min(DropBufSize, length - readed)]);
}
packetReaded += length;
}
public override async ValueTask DisposeAsync()
{
if (CancelToken.IsCancellationRequested)
return;
if (zlibStream != null)
{
await zlibStream.DisposeAsync();
zlibStream = null;
packetReaded = packetSize;
}
else
{
if (packetSize - packetReaded > 0)
{
// ConsoleIO.WriteLine("Plain readed " + packetReaded + ", last " + (packetSize - packetReaded));
await Skip(packetSize - packetReaded);
}
}
}
}
}

View file

@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Crypto;
namespace MinecraftClient.Protocol.PacketPipeline
{
/// <summary>
/// Wrapper for handling unencrypted & encrypted socket
/// </summary>
class SocketWrapper
{
private TcpClient tcpClient;
private AesStream? AesStream;
private PacketStream? packetStream = null;
private Stream ReadStream, WriteStream;
private bool Encrypted = false;
public int CompressionThreshold { get; set; } = 0;
private SemaphoreSlim SendSemaphore = new SemaphoreSlim(1, 1);
private Task LastSendTask = Task.CompletedTask;
/// <summary>
/// Initialize a new SocketWrapper
/// </summary>
/// <param name="client">TcpClient connected to the server</param>
public SocketWrapper(TcpClient client)
{
tcpClient = client;
ReadStream = WriteStream = client.GetStream();
}
/// <summary>
/// Check if the socket is still connected
/// </summary>
/// <returns>TRUE if still connected</returns>
/// <remarks>Silently dropped connection can only be detected by attempting to read/write data</remarks>
public bool IsConnected()
{
return tcpClient.Client != null && tcpClient.Connected;
}
/// <summary>
/// Check if the socket has data available to read
/// </summary>
/// <returns>TRUE if data is available to read</returns>
public bool HasDataAvailable()
{
return tcpClient.Client.Available > 0;
}
/// <summary>
/// Switch network reading/writing to an encrypted stream
/// </summary>
/// <param name="secretKey">AES secret key</param>
public void SwitchToEncrypted(byte[] secretKey)
{
if (Encrypted)
throw new InvalidOperationException("Stream is already encrypted!?");
Encrypted = true;
ReadStream = WriteStream = AesStream = new AesStream(tcpClient.Client, secretKey);
}
/// <summary>
/// Send raw data to the server.
/// </summary>
/// <param name="buffer">data to send</param>
public async Task SendAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
await SendSemaphore.WaitAsync();
await LastSendTask;
LastSendTask = WriteStream.WriteAsync(buffer, cancellationToken).AsTask();
SendSemaphore.Release();
}
public async Task<Tuple<int, PacketStream>> GetNextPacket(bool handleCompress, CancellationToken cancellationToken = default)
{
// ConsoleIO.WriteLine("GetNextPacket");
if (packetStream != null)
{
await packetStream.DisposeAsync();
packetStream = null;
}
int readed = 0;
(int packetSize, _) = await ReceiveVarIntRaw(ReadStream, cancellationToken);
int packetID;
if (handleCompress && CompressionThreshold > 0)
{
(int sizeUncompressed, readed) = await ReceiveVarIntRaw(ReadStream, cancellationToken);
if (sizeUncompressed != 0)
{
ZlibBaseStream zlibBaseStream = new(AesStream ?? ReadStream, packetSize: packetSize - readed);
ZLibStream zlibStream = new(zlibBaseStream, CompressionMode.Decompress, leaveOpen: false);
zlibBaseStream.BufferSize = 16;
(packetID, readed) = await ReceiveVarIntRaw(zlibStream, cancellationToken);
zlibBaseStream.BufferSize = 512;
// ConsoleIO.WriteLine("packetID = " + packetID + ", readed = " + zlibBaseStream.packetReaded + ", size = " + packetSize + " -> " + sizeUncompressed);
packetStream = new(zlibStream, sizeUncompressed - readed, cancellationToken);
return new(packetID, packetStream);
}
}
(packetID, int readed2) = await ReceiveVarIntRaw(ReadStream, cancellationToken);
packetStream = new(AesStream ?? ReadStream, packetSize - readed - readed2, cancellationToken);
return new(packetID, packetStream);
}
private async Task<Tuple<int, int>> ReceiveVarIntRaw(Stream stream, CancellationToken cancellationToken = default)
{
int i = 0;
int j = 0;
byte[] b = new byte[1];
while (true)
{
await stream.ReadAsync(b);
i |= (b[0] & 0x7F) << j++ * 7;
if (j > 5) throw new OverflowException("VarInt too big");
if ((b[0] & 0x80) != 128) break;
}
return new(i, j);
}
/// <summary>
/// Disconnect from the server
/// </summary>
public void Disconnect()
{
try
{
tcpClient.Close();
}
catch (SocketException) { }
catch (IOException) { }
catch (NullReferenceException) { }
catch (ObjectDisposedException) { }
}
}
}

View file

@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Crypto;
using static ConsoleInteractive.ConsoleReader;
namespace MinecraftClient.Protocol.PacketPipeline
{
internal class ZlibBaseStream : Stream
{
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
public int BufferSize { get; set; } = 16;
public int packetSize = 0, packetReaded = 0;
private Stream baseStream;
private AesStream? aesStream;
public ZlibBaseStream(Stream baseStream, int packetSize)
{
packetReaded = 0;
this.packetSize = packetSize;
this.baseStream = baseStream;
aesStream = null;
}
public ZlibBaseStream(AesStream aesStream, int packetSize)
{
packetReaded = 0;
this.packetSize = packetSize;
baseStream = this.aesStream = aesStream;
}
public override void Flush()
{
throw new NotSupportedException();
}
public override int Read(byte[] buffer, int offset, int count)
{
if (packetReaded == packetSize)
return 0;
int readed = baseStream.Read(buffer, offset, Math.Min(BufferSize, Math.Min(count, packetSize - packetReaded)));
packetReaded += readed;
return readed;
}
public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
int readLen = Math.Min(BufferSize, Math.Min(buffer.Length, packetSize - packetReaded));
if (packetReaded + readLen > packetSize)
throw new OverflowException("Reach the end of the packet!");
await baseStream.ReadExactlyAsync(buffer[..readLen], cancellationToken);
packetReaded += readLen;
return readLen;
}
public new async ValueTask ReadExactlyAsync(Memory<byte> buffer, CancellationToken cancellationToken = default)
{
if (packetReaded + buffer.Length > packetSize)
throw new OverflowException("Reach the end of the packet!");
await baseStream.ReadExactlyAsync(buffer, cancellationToken);
packetReaded += buffer.Length;
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
public async Task Skip(int length)
{
if (aesStream != null)
{
int skipRaw = length - AesStream.BlockSize;
for (int readed = 0, curRead; readed < skipRaw; readed += curRead)
curRead = await aesStream.ReadRawAsync(PacketStream.DropBuf[..Math.Min(PacketStream.DropBufSize, skipRaw - readed)]);
await aesStream.ReadAsync(PacketStream.DropBuf[..Math.Min(length, AesStream.BlockSize)]);
}
else
{
for (int readed = 0, curRead; readed < length; readed += curRead)
curRead = await baseStream.ReadAsync(PacketStream.DropBuf[..Math.Min(PacketStream.DropBufSize, length - readed)]);
}
packetReaded += length;
}
public override async ValueTask DisposeAsync()
{
if (packetSize - packetReaded > 0)
{
// ConsoleIO.WriteLine("Zlib readed " + packetReaded + ", last " + (packetSize - packetReaded));
await Skip(packetSize - packetReaded);
}
}
}
}

View file

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using MinecraftClient.Protocol.Message;
namespace MinecraftClient.Protocol.ProfileKey
@ -10,51 +11,6 @@ namespace MinecraftClient.Protocol.ProfileKey
{
private static readonly SHA256 sha256Hash = SHA256.Create();
private static readonly string certificates = "https://api.minecraftservices.com/player/certificates";
public static PlayerKeyPair? GetNewProfileKeys(string accessToken)
{
ProxiedWebRequest.Response? response = null;
try
{
var request = new ProxiedWebRequest(certificates)
{
Accept = "application/json"
};
request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken));
response = request.Post("application/json", "");
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLine(response.Body.ToString());
}
string jsonString = response.Body;
Json.JSONData json = Json.ParseJson(jsonString);
PublicKey publicKey = new(pemKey: json.Properties["keyPair"].Properties["publicKey"].StringValue,
sig: json.Properties["publicKeySignature"].StringValue,
sigV2: json.Properties["publicKeySignatureV2"].StringValue);
PrivateKey privateKey = new(pemKey: json.Properties["keyPair"].Properties["privateKey"].StringValue);
return new PlayerKeyPair(publicKey, privateKey,
expiresAt: json.Properties["expiresAt"].StringValue,
refreshedAfter: json.Properties["refreshedAfter"].StringValue);
}
catch (Exception e)
{
int code = response == null ? 0 : response.StatusCode;
ConsoleIO.WriteLineFormatted("§cFetch profile key failed: HttpCode = " + code + ", Error = " + e.Message);
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLineFormatted("§c" + e.StackTrace);
}
return null;
}
}
public static byte[] DecodePemKey(string key, string prefix, string suffix)
{
int i = key.IndexOf(prefix);

View file

@ -1,199 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Timers;
using static MinecraftClient.Settings;
using static MinecraftClient.Settings.MainConfigHealper.MainConfig.AdvancedConfig;
namespace MinecraftClient.Protocol.ProfileKey
{
/// <summary>
/// Handle keys caching and storage.
/// </summary>
public static class KeysCache
{
private const string KeysCacheFilePlaintext = "ProfileKeyCache.ini";
private static FileMonitor? cachemonitor;
private static readonly Dictionary<string, PlayerKeyPair> keys = new();
private static readonly Timer updatetimer = new(100);
private static readonly List<KeyValuePair<string, PlayerKeyPair>> pendingadds = new();
private static readonly BinaryFormatter formatter = new();
/// <summary>
/// Retrieve whether KeysCache contains a keys for the given login.
/// </summary>
/// <param name="login">User login used with Minecraft.net</param>
/// <returns>TRUE if keys are available</returns>
public static bool Contains(string login)
{
return keys.ContainsKey(login);
}
/// <summary>
/// Store keys and save it to disk if required.
/// </summary>
/// <param name="login">User login used with Minecraft.net</param>
/// <param name="playerKeyPair">User keys</param>
public static void Store(string login, PlayerKeyPair playerKeyPair)
{
if (Contains(login))
{
keys[login] = playerKeyPair;
}
else
{
keys.Add(login, playerKeyPair);
}
if (Config.Main.Advanced.ProfileKeyCache == CacheType.disk && updatetimer.Enabled == true)
{
pendingadds.Add(new KeyValuePair<string, PlayerKeyPair>(login, playerKeyPair));
}
else if (Config.Main.Advanced.ProfileKeyCache == CacheType.disk)
{
SaveToDisk();
}
}
/// <summary>
/// Retrieve keys for the given login.
/// </summary>
/// <param name="login">User login used with Minecraft.net</param>
/// <returns>PlayerKeyPair for given login</returns>
public static PlayerKeyPair Get(string login)
{
return keys[login];
}
/// <summary>
/// Initialize cache monitoring to keep cache updated with external changes.
/// </summary>
/// <returns>TRUE if keys are seeded from file</returns>
public static bool InitializeDiskCache()
{
cachemonitor = new FileMonitor(AppDomain.CurrentDomain.BaseDirectory, KeysCacheFilePlaintext, new FileSystemEventHandler(OnChanged));
updatetimer.Elapsed += HandlePending;
return LoadFromDisk();
}
/// <summary>
/// Reloads cache on external cache file change.
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event data</param>
private static void OnChanged(object sender, FileSystemEventArgs e)
{
updatetimer.Stop();
updatetimer.Start();
}
/// <summary>
/// Called after timer elapsed. Reads disk cache and adds new/modified keys back.
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event data</param>
private static void HandlePending(object? sender, ElapsedEventArgs e)
{
updatetimer.Stop();
LoadFromDisk();
foreach (KeyValuePair<string, PlayerKeyPair> pending in pendingadds.ToArray())
{
Store(pending.Key, pending.Value);
pendingadds.Remove(pending);
}
}
/// <summary>
/// Reads cache file and loads KeysInfos into KeysCache.
/// </summary>
/// <returns>True if data is successfully loaded</returns>
private static bool LoadFromDisk()
{
//User-editable keys cache file in text format
if (File.Exists(KeysCacheFilePlaintext))
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading_keys, KeysCacheFilePlaintext));
try
{
foreach (string line in FileMonitor.ReadAllLinesWithRetries(KeysCacheFilePlaintext))
{
if (!line.TrimStart().StartsWith("#"))
{
int separatorIdx = line.IndexOf('=');
if (separatorIdx >= 1 && line.Length > separatorIdx + 1)
{
string login = line[..separatorIdx];
string value = line[(separatorIdx + 1)..];
try
{
PlayerKeyPair playerKeyPair = PlayerKeyPair.FromString(value);
keys[login] = playerKeyPair;
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded_keys, playerKeyPair.ExpiresAt.ToString()));
}
catch (InvalidDataException e)
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_string_keys, value, e.Message));
}
catch (FormatException e)
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_string_keys, value, e.Message));
}
catch (ArgumentNullException e)
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_string_keys, value, e.Message));
}
}
else if (Config.Logging.DebugMessages)
{
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_line_keys, line));
}
}
}
}
catch (IOException e)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail_plain_keys, e.Message));
}
}
return keys.Count > 0;
}
/// <summary>
/// Saves player's keypair from KeysCache into cache file.
/// </summary>
private static void SaveToDisk()
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted("§8" + Translations.cache_saving_keys, acceptnewlines: true);
List<string> KeysCacheLines = new()
{
"# Generated by MCC v" + Program.Version + " - Keep it secret & Edit at own risk!",
"# ProfileKey=PublicKey(base64),PublicKeySignature(base64),PublicKeySignatureV2(base64),PrivateKey(base64),ExpiresAt,RefreshAfter"
};
foreach (KeyValuePair<string, PlayerKeyPair> entry in keys)
KeysCacheLines.Add(entry.Key + '=' + entry.Value.ToString());
try
{
FileMonitor.WriteAllLinesWithRetries(KeysCacheFilePlaintext, KeysCacheLines);
}
catch (IOException e)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_save_fail_keys, e.Message));
}
}
}
}

View file

@ -1,35 +1,38 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json.Serialization;
namespace MinecraftClient.Protocol.ProfileKey
{
public class PlayerKeyPair
{
[JsonInclude]
[JsonPropertyName("PublicKey")]
public PublicKey PublicKey;
[JsonInclude]
[JsonPropertyName("PrivateKey")]
public PrivateKey PrivateKey;
[JsonInclude]
[JsonPropertyName("ExpiresAt")]
public DateTime ExpiresAt;
public DateTime RefreshedAfter; // Todo: add a timer
[JsonInclude]
[JsonPropertyName("RefreshedAfter")]
public DateTime RefreshedAfter;
[JsonIgnore]
private const string DataTimeFormat = "yyyy-MM-ddTHH:mm:ss.ffffffZ";
public PlayerKeyPair(PublicKey keyPublic, PrivateKey keyPrivate, string expiresAt, string refreshedAfter)
[JsonConstructor]
public PlayerKeyPair(PublicKey PublicKey, PrivateKey PrivateKey, DateTime ExpiresAt, DateTime RefreshedAfter)
{
PublicKey = keyPublic;
PrivateKey = keyPrivate;
try
{
ExpiresAt = DateTime.ParseExact(expiresAt, DataTimeFormat, System.Globalization.CultureInfo.InvariantCulture).ToUniversalTime();
RefreshedAfter = DateTime.ParseExact(refreshedAfter, DataTimeFormat, System.Globalization.CultureInfo.InvariantCulture).ToUniversalTime();
}
catch
{
ExpiresAt = DateTime.Parse(expiresAt).ToUniversalTime();
RefreshedAfter = DateTime.Parse(refreshedAfter).ToUniversalTime();
}
this.PublicKey = PublicKey;
this.PrivateKey = PrivateKey;
this.ExpiresAt = ExpiresAt;
this.RefreshedAfter = RefreshedAfter;
}
public bool NeedRefresh()
@ -54,21 +57,6 @@ namespace MinecraftClient.Protocol.ProfileKey
return timeOffset.ToUnixTimeSeconds();
}
public static PlayerKeyPair FromString(string tokenString)
{
string[] fields = tokenString.Split(',');
if (fields.Length < 6)
throw new InvalidDataException("Invalid string format");
PublicKey publicKey = new(pemKey: fields[0].Trim(),
sig: fields[1].Trim(), sigV2: fields[2].Trim());
PrivateKey privateKey = new(pemKey: fields[3].Trim());
return new PlayerKeyPair(publicKey, privateKey, fields[4].Trim(), fields[5].Trim());
}
public override string ToString()
{
List<string> datas = new();

View file

@ -1,15 +1,20 @@
using System;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using MinecraftClient.Protocol.Message;
namespace MinecraftClient.Protocol.ProfileKey
{
public class PrivateKey
{
[JsonInclude]
[JsonPropertyName("Key")]
public byte[] Key { get; set; }
[JsonIgnore]
private readonly RSA rsa;
[JsonIgnore]
private byte[]? precedingSignature = null;
public PrivateKey(string pemKey)
@ -20,6 +25,14 @@ namespace MinecraftClient.Protocol.ProfileKey
rsa.ImportPkcs8PrivateKey(Key, out _);
}
[JsonConstructor]
public PrivateKey(byte[] Key)
{
this.Key = Key;
rsa = RSA.Create();
rsa.ImportPkcs8PrivateKey(Key, out _);
}
public byte[] SignData(byte[] data)
{
return rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

View file

@ -1,15 +1,26 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text.Json.Serialization;
using MinecraftClient.Protocol.Message;
namespace MinecraftClient.Protocol.ProfileKey
{
public class PublicKey
{
[JsonInclude]
[JsonPropertyName("Key")]
public byte[] Key { get; set; }
[JsonInclude]
[JsonPropertyName("Signature")]
public byte[]? Signature { get; set; }
[JsonInclude]
[JsonPropertyName("SignatureV2")]
public byte[]? SignatureV2 { get; set; }
[JsonIgnore]
private readonly RSA rsa;
public PublicKey(string pemKey, string? sig = null, string? sigV2 = null)
@ -36,6 +47,12 @@ namespace MinecraftClient.Protocol.ProfileKey
Signature = signature;
}
[JsonConstructor]
public PublicKey(byte[] Key, byte[]? Signature, byte[]? SignatureV2) : this(Key, Signature!)
{
this.SignatureV2 = SignatureV2;
}
public bool VerifyData(byte[] data, byte[] signature)
{
return rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,7 @@ using System.Net.Sockets;
using System.Security.Authentication;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Proxy;
namespace MinecraftClient.Protocol
@ -25,7 +26,7 @@ namespace MinecraftClient.Protocol
private readonly string httpVersion = "HTTP/1.1";
private ITcpFactory? tcpFactory;
private readonly ITcpFactory? tcpFactory;
private bool isProxied = false; // Send absolute Url in request if true
private readonly Uri uri;
@ -45,7 +46,7 @@ namespace MinecraftClient.Protocol
/// Set to true to tell the http client proxy is enabled
/// </summary>
public bool IsProxy { get { return isProxied; } set { isProxied = value; } }
public bool Debug { get { return Settings.Config.Logging.DebugMessages; } }
public static bool Debug { get { return Settings.Config.Logging.DebugMessages; } }
/// <summary>
/// Create a new http request
@ -105,9 +106,9 @@ namespace MinecraftClient.Protocol
/// Perform GET request and get the response. Proxy is handled automatically
/// </summary>
/// <returns></returns>
public Response Get()
public async Task<Response> Get()
{
return Send("GET");
return await Send("GET");
}
/// <summary>
@ -116,12 +117,12 @@ namespace MinecraftClient.Protocol
/// <param name="contentType">The content type of request body</param>
/// <param name="body">Request body</param>
/// <returns></returns>
public Response Post(string contentType, string body)
public async Task<Response> Post(string contentType, string body)
{
Headers.Add("Content-Type", contentType);
// Calculate length
Headers.Add("Content-Length", Encoding.UTF8.GetBytes(body).Length.ToString());
return Send("POST", body);
return await Send("POST", body);
}
/// <summary>
@ -130,35 +131,35 @@ namespace MinecraftClient.Protocol
/// <param name="method">Method in string representation</param>
/// <param name="body">Optional request body</param>
/// <returns></returns>
private Response Send(string method, string body = "")
private async Task<Response> Send(string method, string body = "")
{
List<string> requestMessage = new()
{
string.Format("{0} {1} {2}", method.ToUpper(), isProxied ? AbsoluteUrl : Path, httpVersion) // Request line
};
foreach (string key in Headers) // Headers
{
var value = Headers[key];
requestMessage.Add(string.Format("{0}: {1}", key, value));
}
requestMessage.Add(""); // <CR><LF>
if (body != "")
{
requestMessage.Add(body);
}
else requestMessage.Add(""); // <CR><LF>
else
requestMessage.Add(""); // <CR><LF>
if (Debug)
{
foreach (string l in requestMessage)
{
ConsoleIO.WriteLine("< " + l);
}
}
Response response = Response.Empty();
// FIXME: Use TcpFactory interface to avoid direct usage of the ProxyHandler class
// TcpClient client = tcpFactory.CreateTcpClient(Host, Port);
TcpClient client = ProxyHandler.NewTcpClient(Host, Port, true);
TcpClient client = ProxyHandler.NewTcpClient(Host, Port, ProxyHandler.ClientType.Login);
Stream stream;
if (IsSecure)
{
@ -171,35 +172,25 @@ namespace MinecraftClient.Protocol
}
string h = string.Join("\r\n", requestMessage.ToArray());
byte[] data = Encoding.ASCII.GetBytes(h);
stream.Write(data, 0, data.Length);
stream.Flush();
await stream.WriteAsync(data);
await stream.FlushAsync();
// Read response
int statusCode = ReadHttpStatus(stream);
var headers = ReadHeader(stream);
string? rbody;
if (headers.Get("transfer-encoding") == "chunked")
{
rbody = ReadBodyChunked(stream);
}
else
{
rbody = ReadBody(stream, int.Parse(headers.Get("content-length") ?? "0"));
}
int statusCode = await ReadHttpStatus(stream);
var headers = await ReadHeader(stream);
Task<string> rbody = (headers.Get("transfer-encoding") == "chunked") ?
ReadBodyChunked(stream) : ReadBody(stream, int.Parse(headers.Get("content-length") ?? "0"));
if (headers.Get("set-cookie") != null)
{
response.Cookies = ParseSetCookie(headers.GetValues("set-cookie") ?? Array.Empty<string>());
}
response.Body = rbody ?? "";
response.StatusCode = statusCode;
response.Headers = headers;
response.Body = await rbody;
try
{
stream.Close();
client.Close();
}
catch { }
try { stream.Close(); } catch { }
try { client.Close(); } catch { }
return response;
}
@ -210,9 +201,9 @@ namespace MinecraftClient.Protocol
/// <param name="s">Stream to read</param>
/// <returns></returns>
/// <exception cref="InvalidDataException">If server return unknown data</exception>
private static int ReadHttpStatus(Stream s)
private static async Task<int> ReadHttpStatus(Stream s)
{
var httpHeader = ReadLine(s); // http header line
var httpHeader = await ReadLine(s); // http header line
if (httpHeader.StartsWith("HTTP/1.1") || httpHeader.StartsWith("HTTP/1.0"))
{
return int.Parse(httpHeader.Split(' ')[1], NumberStyles.Any, CultureInfo.CurrentCulture);
@ -228,15 +219,15 @@ namespace MinecraftClient.Protocol
/// </summary>
/// <param name="s">Stream to read</param>
/// <returns>Headers in lower-case</returns>
private static NameValueCollection ReadHeader(Stream s)
private static async Task<NameValueCollection> ReadHeader(Stream s)
{
var headers = new NameValueCollection();
// Read headers
string header;
do
{
header = ReadLine(s);
if (!String.IsNullOrEmpty(header))
header = await ReadLine(s);
if (!string.IsNullOrEmpty(header))
{
var tmp = header.Split(new char[] { ':' }, 2);
var name = tmp[0].ToLower();
@ -244,7 +235,7 @@ namespace MinecraftClient.Protocol
headers.Add(name, value);
}
}
while (!String.IsNullOrEmpty(header));
while (!string.IsNullOrEmpty(header));
return headers;
}
@ -254,23 +245,19 @@ namespace MinecraftClient.Protocol
/// <param name="s">Stream to read</param>
/// <param name="length">Length of the body (the Content-Length header)</param>
/// <returns>Body or null if length is zero</returns>
private static string? ReadBody(Stream s, int length)
private static async Task<string> ReadBody(Stream s, int length)
{
if (length > 0)
{
byte[] buffer = new byte[length];
int r = 0;
while (r < length)
{
var read = s.Read(buffer, r, length - r);
r += read;
Thread.Sleep(50);
}
int readed = 0;
while (readed < length)
readed += await s.ReadAsync(buffer.AsMemory(readed, length - readed));
return Encoding.UTF8.GetString(buffer);
}
else
{
return null;
return string.Empty;
}
}
@ -279,13 +266,13 @@ namespace MinecraftClient.Protocol
/// </summary>
/// <param name="s">Stream to read</param>
/// <returns>Body or empty string if nothing is received</returns>
private static string ReadBodyChunked(Stream s)
private static async Task<string> ReadBodyChunked(Stream s)
{
List<byte> buffer1 = new();
while (true)
{
string l = ReadLine(s);
int size = Int32.Parse(l, NumberStyles.HexNumber);
string l = await ReadLine(s);
int size = int.Parse(l, NumberStyles.HexNumber);
if (size == 0)
break;
byte[] buffer2 = new byte[size];
@ -296,7 +283,7 @@ namespace MinecraftClient.Protocol
r += read;
Thread.Sleep(50);
}
ReadLine(s);
await ReadLine(s);
buffer1.AddRange(buffer2);
}
return Encoding.UTF8.GetString(buffer1.ToArray());
@ -366,17 +353,15 @@ namespace MinecraftClient.Protocol
/// </remarks>
/// <param name="s">Stream to read</param>
/// <returns>String</returns>
private static string ReadLine(Stream s)
private static async Task<string> ReadLine(Stream s)
{
List<byte> buffer = new();
byte c;
byte[] c = new byte[1];
while (true)
{
int b = s.ReadByte();
if (b == -1)
break;
c = (byte)b;
if (c == '\n')
try { await s.ReadExactlyAsync(c, 0, 1); }
catch { break; }
if (c[0] == '\n')
{
if (buffer.Last() == '\r')
{
@ -384,7 +369,7 @@ namespace MinecraftClient.Protocol
break;
}
}
buffer.Add(c);
buffer.Add(c[0]);
}
return Encoding.UTF8.GetString(buffer.ToArray());
}

View file

@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using Ionic.Zip;
using MinecraftClient.Mapping;
using MinecraftClient.Protocol.Handlers;
using MinecraftClient.Protocol.Handlers.PacketPalettes;
@ -135,15 +135,19 @@ namespace MinecraftClient.Protocol
MetaData.duration = Convert.ToInt32((lastPacketTime - recordStartTime).TotalMilliseconds);
MetaData.SaveToFile();
using (Stream recordingFile = new FileStream(Path.Combine(temporaryCache, recordingTmpFileName), FileMode.Open))
using (FileStream zipToOpen = new(Path.Combine(ReplayFileDirectory, replayFileName), FileMode.Open))
{
using ZipArchive archive = new(zipToOpen, ZipArchiveMode.Create);
using (Stream recordingFile = new FileStream(Path.Combine(temporaryCache, recordingTmpFileName), FileMode.Open))
{
ZipArchiveEntry recordingTmpFileEntry = archive.CreateEntry(recordingTmpFileName);
recordingFile.CopyTo(recordingTmpFileEntry.Open());
}
using Stream metaDataFile = new FileStream(Path.Combine(temporaryCache, MetaData.MetaDataFileName), FileMode.Open);
using ZipOutputStream zs = new(Path.Combine(ReplayFileDirectory, replayFileName));
zs.PutNextEntry(recordingTmpFileName);
recordingFile.CopyTo(zs);
zs.PutNextEntry(MetaData.MetaDataFileName);
metaDataFile.CopyTo(zs);
zs.Close();
ZipArchiveEntry metaDataFileEntry = archive.CreateEntry(MetaData.MetaDataFileName);
metaDataFile.CopyTo(metaDataFileEntry.Open());
}
File.Delete(Path.Combine(temporaryCache, recordingTmpFileName));
@ -165,20 +169,21 @@ namespace MinecraftClient.Protocol
MetaData.duration = Convert.ToInt32((lastPacketTime - recordStartTime).TotalMilliseconds);
MetaData.SaveToFile();
using (Stream metaDataFile = new FileStream(Path.Combine(temporaryCache, MetaData.MetaDataFileName), FileMode.Open))
using (FileStream zipToOpen = new(replayFileName, FileMode.OpenOrCreate))
{
using ZipOutputStream zs = new(replayFileName);
zs.PutNextEntry(recordingTmpFileName);
using ZipArchive archive = new(zipToOpen, ZipArchiveMode.Create);
ZipArchiveEntry recordingTmpFileEntry = archive.CreateEntry(recordingTmpFileName);
// .CopyTo() method start from stream current position
// We need to reset position in order to get full content
var lastPosition = recordStream!.BaseStream.Position;
recordStream.BaseStream.Position = 0;
recordStream.BaseStream.CopyTo(zs);
recordStream.BaseStream.CopyTo(recordingTmpFileEntry.Open());
recordStream.BaseStream.Position = lastPosition;
zs.PutNextEntry(MetaData.MetaDataFileName);
metaDataFile.CopyTo(zs);
zs.Close();
using Stream metaDataFile = new FileStream(Path.Combine(temporaryCache, MetaData.MetaDataFileName), FileMode.Open);
ZipArchiveEntry metaDataFileEntry = archive.CreateEntry(MetaData.MetaDataFileName);
metaDataFile.CopyTo(metaDataFileEntry.Open());
}
WriteDebugLog("Backup replay file created.");

View file

@ -1,9 +1,19 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.ServiceModel.Channels;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Timers;
using MinecraftClient.Protocol.ProfileKey;
using MinecraftClient.Scripting;
using static MinecraftClient.Settings;
using static MinecraftClient.Settings.MainConfigHealper.MainConfig.AdvancedConfig;
@ -12,58 +22,88 @@ namespace MinecraftClient.Protocol.Session
/// <summary>
/// Handle sessions caching and storage.
/// </summary>
public static class SessionCache
public static partial class SessionCache
{
private const string SessionCacheFilePlaintext = "SessionCache.ini";
private const string SessionCacheFileSerialized = "SessionCache.db";
private static readonly string SessionCacheFileMinecraft = String.Concat(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
Path.DirectorySeparatorChar,
".minecraft",
Path.DirectorySeparatorChar,
"launcher_profiles.json"
);
public class Cache
{
[JsonInclude]
public Dictionary<string, SessionToken> SessionTokens = new();
private static FileMonitor? cachemonitor;
private static readonly Dictionary<string, SessionToken> sessions = new();
private static readonly Timer updatetimer = new(100);
private static readonly List<KeyValuePair<string, SessionToken>> pendingadds = new();
private static readonly BinaryFormatter formatter = new();
[JsonInclude]
public Dictionary<string, PlayerKeyPair> ProfileKeys = new();
[JsonInclude]
public Dictionary<string, ServerInfo> ServerKeys = new();
public record ServerInfo
{
public ServerInfo(string serverIDhash, byte[] serverPublicKey)
{
ServerIDhash = serverIDhash;
ServerPublicKey = serverPublicKey;
}
public string? ServerIDhash { init; get; }
public byte[]? ServerPublicKey { init; get; }
}
}
private static Cache cache = new();
private const string SessionCacheFileJson = "SessionCache.json";
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.General)
{
WriteIndented = true,
AllowTrailingCommas = true,
PropertyNameCaseInsensitive = false,
ReadCommentHandling = JsonCommentHandling.Skip,
NumberHandling = JsonNumberHandling.AllowReadingFromString,
};
public static async Task ReadCacheSessionAsync()
{
if (File.Exists(SessionCacheFileJson))
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading_session, SessionCacheFileJson));
FileStream fileStream = File.OpenRead(SessionCacheFileJson);
try
{
Cache? diskCache = (Cache?)await JsonSerializer.DeserializeAsync(fileStream, typeof(Cache), JsonOptions);
if (diskCache != null)
{
cache = diskCache;
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, cache.SessionTokens.Count, cache.ProfileKeys.Count));
}
}
catch (IOException e)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail_plain, e.Message));
}
catch (JsonException e)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail_plain, e.Message));
}
await fileStream.DisposeAsync();
}
}
/// <summary>
/// Retrieve whether SessionCache contains a session for the given login.
/// </summary>
/// <param name="login">User login used with Minecraft.net</param>
/// <returns>TRUE if session is available</returns>
public static bool Contains(string login)
public static bool ContainsSession(string login)
{
return sessions.ContainsKey(login);
}
/// <summary>
/// Store a session and save it to disk if required.
/// </summary>
/// <param name="login">User login used with Minecraft.net</param>
/// <param name="session">User session token used with Minecraft.net</param>
public static void Store(string login, SessionToken session)
{
if (Contains(login))
{
sessions[login] = session;
}
else
{
sessions.Add(login, session);
}
if (Config.Main.Advanced.SessionCache == CacheType.disk && updatetimer.Enabled == true)
{
pendingadds.Add(new KeyValuePair<string, SessionToken>(login, session));
}
else if (Config.Main.Advanced.SessionCache == CacheType.disk)
{
SaveToDisk();
}
return cache.SessionTokens.ContainsKey(login);
}
/// <summary>
@ -71,202 +111,92 @@ namespace MinecraftClient.Protocol.Session
/// </summary>
/// <param name="login">User login used with Minecraft.net</param>
/// <returns>SessionToken for given login</returns>
public static SessionToken Get(string login)
public static Tuple<SessionToken?, PlayerKeyPair?> GetSession(string login)
{
return sessions[login];
cache.SessionTokens.TryGetValue(login, out SessionToken? sessionToken);
cache.ProfileKeys.TryGetValue(login, out PlayerKeyPair? playerKeyPair);
return new(sessionToken, playerKeyPair);
}
public static Cache.ServerInfo? GetServerInfo(string server)
{
if (cache.ServerKeys.TryGetValue(server, out Cache.ServerInfo? info))
return info;
else
return null;
}
/// <summary>
/// Initialize cache monitoring to keep cache updated with external changes.
/// Store a session and save it to disk if required.
/// </summary>
/// <returns>TRUE if session tokens are seeded from file</returns>
public static bool InitializeDiskCache()
/// <param name="login">User login used with Minecraft.net</param>
/// <param name="newSession">User session token used with Minecraft.net</param>
public static async Task StoreSessionAsync(string login, SessionToken? sessionToken, PlayerKeyPair? profileKey)
{
cachemonitor = new FileMonitor(AppDomain.CurrentDomain.BaseDirectory, SessionCacheFilePlaintext, new FileSystemEventHandler(OnChanged));
updatetimer.Elapsed += HandlePending;
return LoadFromDisk();
if (sessionToken != null)
cache.SessionTokens[login] = sessionToken;
if (profileKey != null)
cache.ProfileKeys[login] = profileKey;
if (Config.Main.Advanced.SessionCache == CacheType.disk)
await SaveToDisk();
}
/// <summary>
/// Reloads cache on external cache file change.
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event data</param>
private static void OnChanged(object sender, FileSystemEventArgs e)
public static void StoreServerInfo(string server, string ServerIDhash, byte[] ServerPublicKey)
{
updatetimer.Stop();
updatetimer.Start();
}
/// <summary>
/// Called after timer elapsed. Reads disk cache and adds new/modified sessions back.
/// </summary>
/// <param name="sender">Sender</param>
/// <param name="e">Event data</param>
private static void HandlePending(object? sender, ElapsedEventArgs e)
{
updatetimer.Stop();
LoadFromDisk();
foreach (KeyValuePair<string, SessionToken> pending in pendingadds.ToArray())
{
Store(pending.Key, pending.Value);
pendingadds.Remove(pending);
}
}
/// <summary>
/// Reads cache file and loads SessionTokens into SessionCache.
/// </summary>
/// <returns>True if data is successfully loaded</returns>
private static bool LoadFromDisk()
{
//Grab sessions in the Minecraft directory
if (File.Exists(SessionCacheFileMinecraft))
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading, Path.GetFileName(SessionCacheFileMinecraft)));
Json.JSONData mcSession = new(Json.JSONData.DataType.String);
try
{
mcSession = Json.ParseJson(File.ReadAllText(SessionCacheFileMinecraft));
}
catch (IOException) { /* Failed to read file from disk -- ignoring */ }
if (mcSession.Type == Json.JSONData.DataType.Object
&& mcSession.Properties.ContainsKey("clientToken")
&& mcSession.Properties.ContainsKey("authenticationDatabase"))
{
string clientID = mcSession.Properties["clientToken"].StringValue.Replace("-", "");
Dictionary<string, Json.JSONData> sessionItems = mcSession.Properties["authenticationDatabase"].Properties;
foreach (string key in sessionItems.Keys)
{
if (Guid.TryParseExact(key, "N", out Guid temp))
{
Dictionary<string, Json.JSONData> sessionItem = sessionItems[key].Properties;
if (sessionItem.ContainsKey("displayName")
&& sessionItem.ContainsKey("accessToken")
&& sessionItem.ContainsKey("username")
&& sessionItem.ContainsKey("uuid"))
{
string login = Settings.ToLowerIfNeed(sessionItem["username"].StringValue);
try
{
SessionToken session = SessionToken.FromString(String.Join(",",
sessionItem["accessToken"].StringValue,
sessionItem["displayName"].StringValue,
sessionItem["uuid"].StringValue.Replace("-", ""),
clientID
));
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, login, session.ID));
sessions[login] = session;
}
catch (InvalidDataException) { /* Not a valid session */ }
}
}
}
}
}
//Serialized session cache file in binary format
if (File.Exists(SessionCacheFileSerialized))
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_converting, SessionCacheFileSerialized));
try
{
using FileStream fs = new(SessionCacheFileSerialized, FileMode.Open, FileAccess.Read, FileShare.Read);
#pragma warning disable SYSLIB0011 // BinaryFormatter.Deserialize() is obsolete
// Possible risk of information disclosure or remote code execution. The impact of this vulnerability is limited to the user side only.
Dictionary<string, SessionToken> sessionsTemp = (Dictionary<string, SessionToken>)formatter.Deserialize(fs);
#pragma warning restore SYSLIB0011 // BinaryFormatter.Deserialize() is obsolete
foreach (KeyValuePair<string, SessionToken> item in sessionsTemp)
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, item.Key, item.Value.ID));
sessions[item.Key] = item.Value;
}
}
catch (IOException ex)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail, ex.Message));
}
catch (SerializationException ex2)
{
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_malformed, ex2.Message));
}
}
//User-editable session cache file in text format
if (File.Exists(SessionCacheFilePlaintext))
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading_session, SessionCacheFilePlaintext));
try
{
foreach (string line in FileMonitor.ReadAllLinesWithRetries(SessionCacheFilePlaintext))
{
if (!line.Trim().StartsWith("#"))
{
string[] keyValue = line.Split('=');
if (keyValue.Length == 2)
{
try
{
string login = Settings.ToLowerIfNeed(keyValue[0]);
SessionToken session = SessionToken.FromString(keyValue[1]);
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, login, session.ID));
sessions[login] = session;
}
catch (InvalidDataException e)
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_string, keyValue[1], e.Message));
}
}
else if (Config.Logging.DebugMessages)
{
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_line, line));
}
}
}
}
catch (IOException e)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail_plain, e.Message));
}
}
return sessions.Count > 0;
cache.ServerKeys[server] = new(ServerIDhash, ServerPublicKey);
}
/// <summary>
/// Saves SessionToken's from SessionCache into cache file.
/// </summary>
private static void SaveToDisk()
private static async Task SaveToDisk()
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted("§8" + Translations.cache_saving, acceptnewlines: true);
List<string> sessionCacheLines = new()
foreach ((string login, SessionToken session) in cache.SessionTokens)
{
"# Generated by MCC v" + Program.Version + " - Keep it secret & Edit at own risk!",
"# Login=SessionID,PlayerName,UUID,ClientID,RefreshToken,ServerIDhash,ServerPublicKey"
};
foreach (KeyValuePair<string, SessionToken> entry in sessions)
sessionCacheLines.Add(entry.Key + '=' + entry.Value.ToString());
if (!GetJwtRegex().IsMatch(session.ID))
cache.SessionTokens.Remove(login);
else if (!ChatBot.IsValidName(session.PlayerName))
cache.SessionTokens.Remove(login);
else if (!Guid.TryParseExact(session.PlayerID, "N", out _))
cache.SessionTokens.Remove(login);
else if (!Guid.TryParseExact(session.ClientID, "N", out _))
cache.SessionTokens.Remove(login);
// No validation on refresh token because it is custom format token (not Jwt)
}
foreach ((string login, PlayerKeyPair profileKey) in cache.ProfileKeys)
{
if (profileKey.NeedRefresh())
cache.ProfileKeys.Remove(login);
}
try
{
FileMonitor.WriteAllLinesWithRetries(SessionCacheFilePlaintext, sessionCacheLines);
FileStream fileStream = File.Open(SessionCacheFileJson, FileMode.Create);
await fileStream.WriteAsync(Encoding.UTF8.GetBytes($"/* Generated by MCC v{Program.Version} - Keep it secret & Edit at own risk! */{Environment.NewLine}"));
await JsonSerializer.SerializeAsync(fileStream, cache, typeof(Cache), JsonOptions);
await fileStream.FlushAsync();
fileStream.Close();
await fileStream.DisposeAsync();
}
catch (IOException e)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_save_fail, e.Message));
}
catch (JsonException e)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_save_fail, e.Message));
}
}
[GeneratedRegex("^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+$", RegexOptions.Compiled)]
private static partial Regex GetJwtRegex();
}
}

View file

@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Net.Http;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using MinecraftClient.Scripting;
@ -9,92 +11,39 @@ namespace MinecraftClient.Protocol.Session
[Serializable]
public class SessionToken
{
private static readonly Regex JwtRegex = new("^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+$");
[JsonInclude]
[JsonPropertyName("SessionID")]
public string ID { get; set; }
public string PlayerName { get; set; }
public string PlayerID { get; set; }
public string ClientID { get; set; }
public string RefreshToken { get; set; }
public string ServerIDhash { get; set; }
public byte[]? ServerPublicKey { get; set; }
[JsonInclude]
[JsonPropertyName("PlayerName")]
public string PlayerName { get; set; }
[JsonInclude]
[JsonPropertyName("PlayerID")]
public string PlayerID { get; set; }
[JsonInclude]
[JsonPropertyName("ClientID")]
public string ClientID { get; set; }
[JsonInclude]
[JsonPropertyName("RefreshToken")]
public string RefreshToken { get; set; }
[JsonIgnore]
public string? ServerInfoHash = null;
[JsonIgnore]
public Task<bool>? SessionPreCheckTask = null;
public SessionToken()
{
ID = String.Empty;
PlayerName = String.Empty;
PlayerID = String.Empty;
ClientID = String.Empty;
RefreshToken = String.Empty;
ServerIDhash = String.Empty;
ServerPublicKey = null;
}
public bool SessionPreCheck()
{
if (ID == string.Empty || PlayerID == String.Empty || ServerPublicKey == null)
return false;
Crypto.CryptoHandler.ClientAESPrivateKey ??= Crypto.CryptoHandler.GenerateAESPrivateKey();
string serverHash = Crypto.CryptoHandler.GetServerHash(ServerIDhash, ServerPublicKey, Crypto.CryptoHandler.ClientAESPrivateKey);
if (ProtocolHandler.SessionCheck(PlayerID, ID, serverHash))
return true;
return false;
}
public override string ToString()
{
return String.Join(",", ID, PlayerName, PlayerID, ClientID, RefreshToken, ServerIDhash,
(ServerPublicKey == null) ? String.Empty : Convert.ToBase64String(ServerPublicKey));
}
public static SessionToken FromString(string tokenString)
{
string[] fields = tokenString.Split(',');
if (fields.Length < 4)
throw new InvalidDataException("Invalid string format");
SessionToken session = new()
{
ID = fields[0],
PlayerName = fields[1],
PlayerID = fields[2],
ClientID = fields[3]
};
// Backward compatible with old session file without refresh token field
if (fields.Length > 4)
session.RefreshToken = fields[4];
else
session.RefreshToken = String.Empty;
if (fields.Length > 5)
session.ServerIDhash = fields[5];
else
session.ServerIDhash = String.Empty;
if (fields.Length > 6)
{
try
{
session.ServerPublicKey = Convert.FromBase64String(fields[6]);
}
catch
{
session.ServerPublicKey = null;
}
}
else
session.ServerPublicKey = null;
if (!JwtRegex.IsMatch(session.ID))
throw new InvalidDataException("Invalid session ID");
if (!ChatBot.IsValidName(session.PlayerName))
throw new InvalidDataException("Invalid player name");
if (!Guid.TryParseExact(session.PlayerID, "N", out _))
throw new InvalidDataException("Invalid player ID");
if (!Guid.TryParseExact(session.ClientID, "N", out _))
throw new InvalidDataException("Invalid client ID");
// No validation on refresh token because it is custom format token (not Jwt)
return session;
ID = string.Empty;
PlayerName = string.Empty;
PlayerID = string.Empty;
ClientID = string.Empty;
RefreshToken = string.Empty;
}
}
}

View file

@ -1,4 +1,7 @@
using System.Net.Sockets;
using System;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using Starksoft.Aspen.Proxy;
using Tomlet.Attributes;
@ -17,14 +20,17 @@ namespace MinecraftClient.Proxy
[TomlDoNotInlineObject]
public class Configs
{
[TomlInlineComment("$Proxy.Enabled_Update$")]
public bool Enabled_Update = false;
[NonSerialized] // Compatible with old settings.
public bool? Enabled_Login = false, Enabled_Ingame = false, Enabled_Update = false;
[TomlInlineComment("$Proxy.Enabled_Login$")]
public bool Enabled_Login = false;
[TomlInlineComment("$Proxy.Ingame_Proxy$")]
public ProxyPreferenceType Ingame_Proxy = ProxyPreferenceType.disable;
[TomlInlineComment("$Proxy.Enabled_Ingame$")]
public bool Enabled_Ingame = false;
[TomlInlineComment("$Proxy.Login_Proxy$")]
public ProxyPreferenceType Login_Proxy = ProxyPreferenceType.follow_system;
[TomlInlineComment("$Proxy.MCC_Update_Proxy$")]
public ProxyPreferenceType MCC_Update_Proxy = ProxyPreferenceType.follow_system;
[TomlInlineComment("$Proxy.Server$")]
public ProxyInfoConfig Server = new("0.0.0.0", 8080);
@ -33,12 +39,22 @@ namespace MinecraftClient.Proxy
public ProxyType Proxy_Type = ProxyType.HTTP;
[TomlInlineComment("$Proxy.Username$")]
public string Username = "";
public string Username = string.Empty;
[TomlInlineComment("$Proxy.Password$")]
public string Password = "";
public string Password = string.Empty;
public void OnSettingUpdate() { }
public void OnSettingUpdate()
{
{ // Compatible with old settings.
if (Enabled_Login.HasValue && Enabled_Login.Value)
Login_Proxy = ProxyPreferenceType.custom;
if (Enabled_Ingame.HasValue && Enabled_Ingame.Value)
Ingame_Proxy = ProxyPreferenceType.custom;
if (Enabled_Update.HasValue && Enabled_Update.Value)
MCC_Update_Proxy = ProxyPreferenceType.custom;
}
}
public struct ProxyInfoConfig
{
@ -53,8 +69,12 @@ namespace MinecraftClient.Proxy
}
public enum ProxyType { HTTP, SOCKS4, SOCKS4a, SOCKS5 };
public enum ProxyPreferenceType { custom, follow_system, disable };
}
public enum ClientType { Ingame, Login, Update };
private static readonly ProxyClientFactory factory = new();
private static IProxyClient? proxy;
private static bool proxy_ok = false;
@ -66,11 +86,14 @@ namespace MinecraftClient.Proxy
/// <param name="port">Target port</param>
/// <param name="login">True if the purpose is logging in to a Minecraft account</param>
public static TcpClient NewTcpClient(string host, int port, bool login = false)
public static TcpClient NewTcpClient(string host, int port, ClientType clientType)
{
if (clientType == ClientType.Update)
throw new NotSupportedException();
try
{
if (login ? Config.Enabled_Login : Config.Enabled_Ingame)
Configs.ProxyPreferenceType proxyPreference = clientType == ClientType.Ingame ? Config.Ingame_Proxy : Config.Login_Proxy;
if (proxyPreference == Configs.ProxyPreferenceType.custom)
{
ProxyType innerProxytype = ProxyType.Http;
@ -95,7 +118,30 @@ namespace MinecraftClient.Proxy
return proxy.CreateConnection(host, port);
}
else return new TcpClient(host, port);
else if (proxyPreference == Configs.ProxyPreferenceType.follow_system)
{
Uri? webProxy = WebRequest.GetSystemWebProxy().GetProxy(new("http://" + host));
if (webProxy != null)
{
proxy = factory.CreateProxyClient(ProxyType.Http, webProxy.Host, webProxy.Port);
if (!proxy_ok)
{
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.proxy_connected, webProxy.Host, webProxy.Port));
proxy_ok = true;
}
return proxy.CreateConnection(host, port);
}
else
{
return new TcpClient(host, port);
}
}
else
{
return new TcpClient(host, port);
}
}
catch (ProxyException e)
{
@ -104,5 +150,58 @@ namespace MinecraftClient.Proxy
throw new SocketException((int)SocketError.HostUnreachable);
}
}
public static HttpClient NewHttpClient(ClientType clientType, HttpClientHandler? httpClientHandler = null)
{
if (clientType == ClientType.Ingame)
throw new NotSupportedException();
httpClientHandler ??= new();
AddProxySettings(clientType, ref httpClientHandler);
return new HttpClient(httpClientHandler);
}
public static void AddProxySettings(ClientType clientType, ref HttpClientHandler httpClientHandler)
{
if (clientType == ClientType.Ingame)
throw new NotSupportedException();
Configs.ProxyPreferenceType proxyPreference = clientType == ClientType.Login ? Config.Login_Proxy : Config.MCC_Update_Proxy;
if (proxyPreference == Configs.ProxyPreferenceType.custom)
{
httpClientHandler ??= new();
string proxyAddress;
if (!string.IsNullOrWhiteSpace(Settings.Config.Proxy.Username) && !string.IsNullOrWhiteSpace(Settings.Config.Proxy.Password))
proxyAddress = string.Format("{0}://{3}:{4}@{1}:{2}",
Settings.Config.Proxy.Proxy_Type.ToString().ToLower(),
Settings.Config.Proxy.Server.Host,
Settings.Config.Proxy.Server.Port,
Settings.Config.Proxy.Username,
Settings.Config.Proxy.Password);
else
proxyAddress = string.Format("{0}://{1}:{2}",
Settings.Config.Proxy.Proxy_Type.ToString().ToLower(),
Settings.Config.Proxy.Server.Host, Settings.Config.Proxy.Server.Port);
httpClientHandler.Proxy = new WebProxy(proxyAddress, true);
httpClientHandler.UseProxy = true;
}
else if (proxyPreference == Configs.ProxyPreferenceType.follow_system)
{
httpClientHandler.Proxy = WebRequest.GetSystemWebProxy();
httpClientHandler.UseProxy = true;
}
else if (proxyPreference == Configs.ProxyPreferenceType.disable)
{
httpClientHandler ??= new();
httpClientHandler.UseProxy = false;
}
else
{
throw new NotSupportedException();
}
}
}
}

View file

@ -1699,7 +1699,8 @@ namespace MinecraftClient {
/// Looks up a localized string similar to Connect to a server via a proxy instead of connecting directly
///If Mojang session services are blocked on your network, set Enabled_Login=true to login using proxy.
///If the connection to the Minecraft game server is blocked by the firewall, set Enabled_Ingame=true to use a proxy to connect to the game server.
////!\ Make sure your server rules allow Proxies or VPNs before setting enabled=true, or you may face consequences!.
////!\ Make sure your server rules allow Proxies or VPNs before setting enabled=true, or you may face consequences!
///Use &quot;custom&quot;, &quot;follow_system&quot; or &quot;disable&quot;..
/// </summary>
internal static string Proxy {
get {
@ -1710,27 +1711,27 @@ namespace MinecraftClient {
/// <summary>
/// Looks up a localized string similar to Whether to connect to the game server through a proxy..
/// </summary>
internal static string Proxy_Enabled_Ingame {
internal static string Proxy_Ingame_Proxy {
get {
return ResourceManager.GetString("Proxy.Enabled_Ingame", resourceCulture);
return ResourceManager.GetString("Proxy.Ingame_Proxy", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Whether to connect to the login server through a proxy..
/// </summary>
internal static string Proxy_Enabled_Login {
internal static string Proxy_Login_Proxy {
get {
return ResourceManager.GetString("Proxy.Enabled_Login", resourceCulture);
return ResourceManager.GetString("Proxy.Login_Proxy", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Whether to download MCC updates via proxy..
/// </summary>
internal static string Proxy_Enabled_Update {
internal static string Proxy_MCC_Update_Proxy {
get {
return ResourceManager.GetString("Proxy.Enabled_Update", resourceCulture);
return ResourceManager.GetString("Proxy.MCC_Update_Proxy", resourceCulture);
}
}

View file

@ -751,15 +751,16 @@ Usage examples: "/tell &lt;mybot&gt; connect Server1", "/connect Server2"</value
<value>Connect to a server via a proxy instead of connecting directly
If Mojang session services are blocked on your network, set Enabled_Login=true to login using proxy.
If the connection to the Minecraft game server is blocked by the firewall, set Enabled_Ingame=true to use a proxy to connect to the game server.
/!\ Make sure your server rules allow Proxies or VPNs before setting enabled=true, or you may face consequences!</value>
/!\ Make sure your server rules allow Proxies or VPNs before setting enabled=true, or you may face consequences!
Use "custom", "follow_system" or "disable".</value>
</data>
<data name="Proxy.Enabled_Ingame" xml:space="preserve">
<data name="Proxy.Ingame_Proxy" xml:space="preserve">
<value>Whether to connect to the game server through a proxy.</value>
</data>
<data name="Proxy.Enabled_Login" xml:space="preserve">
<data name="Proxy.Login_Proxy" xml:space="preserve">
<value>Whether to connect to the login server through a proxy.</value>
</data>
<data name="Proxy.Enabled_Update" xml:space="preserve">
<data name="Proxy.MCC_Update_Proxy" xml:space="preserve">
<value>Whether to download MCC updates via proxy.</value>
</data>
<data name="Proxy.Password" xml:space="preserve">

View file

@ -2127,7 +2127,7 @@ namespace MinecraftClient {
}
/// <summary>
/// Looks up a localized string similar to Loaded session: {0}:{1}.
/// Looks up a localized string similar to Reads {0} session cache and {1} signature key caches from the disk cache..
/// </summary>
internal static string cache_loaded {
get {
@ -4130,6 +4130,24 @@ namespace MinecraftClient {
}
}
/// <summary>
/// Looks up a localized string similar to [Settings] The language code is invalid!.
/// </summary>
internal static string config_invaild_language {
get {
return ResourceManager.GetString("config.invaild_language", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to [Settings] Only Microsoft accounts support logging in using the browser method..
/// </summary>
internal static string config_invaild_login_method {
get {
return ResourceManager.GetString("config.invaild_login_method", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Settings have been loaded from {0}.
/// </summary>
@ -4148,15 +4166,6 @@ namespace MinecraftClient {
}
}
/// <summary>
/// Looks up a localized string similar to The language code is invalid!.
/// </summary>
internal static string config_Main_Advanced_language_invaild {
get {
return ResourceManager.GetString("config.Main.Advanced.language.invaild", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The current setting is saved as {0}.
/// </summary>
@ -5295,7 +5304,16 @@ namespace MinecraftClient {
}
/// <summary>
/// Looks up a localized string similar to Login :.
/// Looks up a localized string similar to Exceptions occur when applying settings from command line parameters ({0}), and some entries may be ignored..
/// </summary>
internal static string mcc_load_from_args_fail {
get {
return ResourceManager.GetString("mcc.load_from_args_fail", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Login:.
/// </summary>
internal static string mcc_login {
get {
@ -5312,6 +5330,15 @@ namespace MinecraftClient {
}
}
/// <summary>
/// Looks up a localized string similar to Timed out when requesting {0}..
/// </summary>
internal static string mcc_network_timeout {
get {
return ResourceManager.GetString("mcc.network_timeout", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to perform SRV lookup for {0}
///{1}: {2}.
@ -5531,7 +5558,7 @@ namespace MinecraftClient {
}
/// <summary>
/// Looks up a localized string similar to Settings file MinecraftClient.ini has been generated..
/// Looks up a localized string similar to The configuration file is saved as {0}.
/// </summary>
internal static string mcc_settings_generated {
get {
@ -5539,6 +5566,15 @@ namespace MinecraftClient {
}
}
/// <summary>
/// Looks up a localized string similar to Error: Unhandled exception:.
/// </summary>
internal static string mcc_unhandled_exception {
get {
return ResourceManager.GetString("mcc.unhandled_exception", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unknown or not supported MC version {0}.
///Switching to autodetection mode..

View file

@ -822,7 +822,7 @@ Add the ID of this chat to "Authorized_Chat_Ids" field in the configuration file
<value>Ignoring profile key token string '{0}': {1}</value>
</data>
<data name="cache.loaded" xml:space="preserve">
<value>Loaded session: {0}:{1}</value>
<value>Reads {0} session cache and {1} signature key caches from the disk cache.</value>
</data>
<data name="cache.loaded_keys" xml:space="preserve">
<value>Loaded profile key, it will be refresh at {0}</value>
@ -1499,8 +1499,8 @@ You can use "/chunk status {0:0.0} {1:0.0} {2:0.0}" to check the chunk loading s
<data name="config.load.fail" xml:space="preserve">
<value>Failed to load settings:</value>
</data>
<data name="config.Main.Advanced.language.invaild" xml:space="preserve">
<value>The language code is invalid!</value>
<data name="config.invaild_language" xml:space="preserve">
<value>[Settings] The language code is invalid!</value>
</data>
<data name="config.saving" xml:space="preserve">
<value>The current setting is saved as {0}</value>
@ -1884,7 +1884,7 @@ Type '{0}quit' to leave the server.</value>
<value>Link: {0}</value>
</data>
<data name="mcc.login" xml:space="preserve">
<value>Login :</value>
<value>Login:</value>
</data>
<data name="mcc.login_basic_io" xml:space="preserve">
<value>Please type the username or email of your choice.</value>
@ -1964,7 +1964,7 @@ MCC is running with default settings.</value>
<value>Cached session is still valid for {0}.</value>
</data>
<data name="mcc.settings_generated" xml:space="preserve">
<value>Settings file MinecraftClient.ini has been generated.</value>
<value>The configuration file is saved as {0}</value>
</data>
<data name="mcc.unknown_version" xml:space="preserve">
<value>Unknown or not supported MC version {0}.
@ -2028,4 +2028,16 @@ Logging in...</value>
<data name="proxy.connected" xml:space="preserve">
<value>Connected to proxy {0}:{1}</value>
</data>
<data name="mcc.load_from_args_fail" xml:space="preserve">
<value>Exceptions occur when applying settings from command line parameters ({0}), and some entries may be ignored.</value>
</data>
<data name="mcc.unhandled_exception" xml:space="preserve">
<value>Error: Unhandled exception:</value>
</data>
<data name="mcc.network_timeout" xml:space="preserve">
<value>Timed out when requesting {0}.</value>
</data>
<data name="config.invaild_login_method" xml:space="preserve">
<value>[Settings] Only Microsoft accounts support logging in using the browser method.</value>
</data>
</root>

View file

@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Brigadier.NET;
using MinecraftClient.CommandHandler;
using MinecraftClient.Inventory;
@ -36,14 +37,16 @@ namespace MinecraftClient.Scripting
//Handler will be automatically set on bot loading, don't worry about this
public void SetHandler(McClient handler) { _handler = handler; }
protected void SetMaster(ChatBot master) { this.master = master; }
protected void LoadBot(ChatBot bot) { Handler.BotUnLoad(bot); Handler.BotLoad(bot); }
protected void LoadBot(ChatBot bot) { Handler.BotUnLoad(bot).Wait(); Handler.BotLoad(bot); }
protected List<ChatBot> GetLoadedChatBots() { return Handler.GetLoadedChatBots(); }
protected void UnLoadBot(ChatBot bot) { Handler.BotUnLoad(bot); }
protected void UnLoadBot(ChatBot bot) { Handler.BotUnLoad(bot).Wait(); }
private McClient? _handler = null;
private ChatBot? master = null;
private readonly List<string> registeredPluginChannels = new();
private readonly object delayTasksLock = new();
private readonly List<TaskWithDelay> delayedTasks = new();
protected McClient Handler
{
get
@ -117,10 +120,15 @@ namespace MinecraftClient.Scripting
public virtual void AfterGameJoined() { }
/// <summary>
/// Will be called every ~100ms (10fps) if loaded in MinecraftCom
/// Will be called every ~100ms (10tps) if loaded in MinecraftCom
/// </summary>
public virtual void Update() { }
/// <summary>
/// Will be called every ~50ms (20tps) if loaded in MinecraftCom
/// </summary>
public virtual async Task OnClientTickAsync() { await Task.CompletedTask; }
/// <summary>
/// Will be called every player break block in gamemode 0
/// </summary>
@ -157,8 +165,8 @@ namespace MinecraftClient.Scripting
/// </summary>
/// <param name="reason">Disconnect Reason</param>
/// <param name="message">Kick message, if any</param>
/// <returns>Return TRUE if the client is about to restart</returns>
public virtual bool OnDisconnect(DisconnectReason reason, string message) { return false; }
/// <returns>A return value less than zero indicates no reconnection, otherwise it is the number of milliseconds to wait before reconnecting.</returns>
public virtual int OnDisconnect(DisconnectReason reason, string message) { return -1; }
/// <summary>
/// Called when a plugin channel message is received.
@ -252,6 +260,14 @@ namespace MinecraftClient.Scripting
/// <param name="gamemode">New Game Mode (0: Survival, 1: Creative, 2: Adventure, 3: Spectator).</param>
public virtual void OnGamemodeUpdate(string playername, Guid uuid, int gamemode) { }
/// <summary>
/// Called when the Game Mode has been updated for a player
/// </summary>
/// <param name="playername">Player Name</param>
/// <param name="uuid">Player UUID</param>
/// <param name="gamemode">New Game Mode (0: Survival, 1: Creative, 2: Adventure, 3: Spectator).</param>
public virtual async Task OnGamemodeUpdateAsync(string playername, Guid uuid, int gamemode) { await Task.CompletedTask; }
/// <summary>
/// Called when the Latency has been updated for a player
/// </summary>
@ -502,7 +518,7 @@ namespace MinecraftClient.Scripting
protected bool SendText(string text, bool sendImmediately = false)
{
LogToConsole("Sending '" + text + "'");
Handler.SendText(text);
Handler.SendText(text).Wait();
return true;
}
@ -929,7 +945,7 @@ namespace MinecraftClient.Scripting
ConsoleIO.WriteLogLine(string.Format(Translations.chatbot_reconnect, botName));
}
McClient.ReconnectionAttemptsLeft = ExtraAttempts;
Program.Restart(delaySeconds, keepAccountAndServerSettings);
Program.Restart(delaySeconds * 10, keepAccountAndServerSettings);
}
/// <summary>
@ -945,7 +961,7 @@ namespace MinecraftClient.Scripting
/// </summary>
protected void UnloadBot()
{
Handler.BotUnLoad(this);
Handler.BotUnLoad(this).Wait();
}
/// <summary>
@ -1021,7 +1037,15 @@ namespace MinecraftClient.Scripting
/// </summary>
private bool SendEntityAction(Protocol.EntityActionType entityAction)
{
return Handler.SendEntityAction(entityAction);
return Handler.SendEntityAction(entityAction).Result;
}
/// <summary>
/// Send Entity Action
/// </summary>
private async Task<bool> SendEntityActionAsync(Protocol.EntityActionType entityAction)
{
return await Handler.SendEntityAction(entityAction);
}
/// <summary>
@ -1032,15 +1056,15 @@ namespace MinecraftClient.Scripting
/// <param name="lookAtBlock">Also look at the block before digging</param>
protected bool DigBlock(Location location, bool swingArms = true, bool lookAtBlock = true)
{
return Handler.DigBlock(location, swingArms, lookAtBlock);
return Handler.DigBlock(location, swingArms, lookAtBlock).Result;
}
/// <summary>
/// SetSlot
/// </summary>
protected void SetSlot(int slotNum)
protected bool SetSlot(int slotNum)
{
Handler.ChangeSlot((short)slotNum);
return Handler.ChangeSlot((short)slotNum).Result;
}
/// <summary>
@ -1273,7 +1297,7 @@ namespace MinecraftClient.Scripting
protected void RegisterPluginChannel(string channel)
{
registeredPluginChannels.Add(channel);
Handler.RegisterPluginChannel(channel, this);
Handler.RegisterPluginChannel(channel, this).Wait();
}
/// <summary>
@ -1283,7 +1307,7 @@ namespace MinecraftClient.Scripting
protected void UnregisterPluginChannel(string channel)
{
registeredPluginChannels.RemoveAll(chan => chan == channel);
Handler.UnregisterPluginChannel(channel, this);
Handler.UnregisterPluginChannel(channel, this).Wait();
}
/// <summary>
@ -1303,7 +1327,7 @@ namespace MinecraftClient.Scripting
return false;
}
}
return Handler.SendPluginChannelMessage(channel, data, sendEvenIfNotRegistered);
return Handler.SendPluginChannelMessage(channel, data, sendEvenIfNotRegistered).Result;
}
/// <summary>
@ -1325,7 +1349,7 @@ namespace MinecraftClient.Scripting
[Obsolete("Prefer using InteractType enum instead of int for interaction type")]
protected bool InteractEntity(int EntityID, int type, Hand hand = Hand.MainHand)
{
return Handler.InteractEntity(EntityID, (InteractType)type, hand);
return Handler.InteractEntity(EntityID, (InteractType)type, hand).Result;
}
/// <summary>
@ -1337,7 +1361,7 @@ namespace MinecraftClient.Scripting
/// <returns>TRUE in case of success</returns>
protected bool InteractEntity(int EntityID, InteractType type, Hand hand = Hand.MainHand)
{
return Handler.InteractEntity(EntityID, type, hand);
return Handler.InteractEntity(EntityID, type, hand).Result;
}
/// <summary>
@ -1351,7 +1375,7 @@ namespace MinecraftClient.Scripting
/// <returns>TRUE if item given successfully</returns>
protected bool CreativeGive(int slot, ItemType itemType, int count, Dictionary<string, object>? nbt = null)
{
return Handler.DoCreativeGive(slot, itemType, count, nbt);
return Handler.DoCreativeGive(slot, itemType, count, nbt).Result;
}
/// <summary>
@ -1373,7 +1397,7 @@ namespace MinecraftClient.Scripting
/// <returns>TRUE if animation successfully done</returns>
public bool SendAnimation(Hand hand = Hand.MainHand)
{
return Handler.DoAnimation((int)hand);
return Handler.DoAnimation((int)hand).Result;
}
/// <summary>
@ -1382,7 +1406,7 @@ namespace MinecraftClient.Scripting
/// <returns>TRUE if successful</returns>
protected bool UseItemInHand()
{
return Handler.UseItemOnHand();
return Handler.UseItemOnHand().Result;
}
/// <summary>
@ -1391,7 +1415,7 @@ namespace MinecraftClient.Scripting
/// <returns>TRUE if successful</returns>
protected bool UseItemInLeftHand()
{
return Handler.UseItemOnLeftHand();
return Handler.UseItemOnLeftHand().Result;
}
/// <summary>
@ -1412,7 +1436,7 @@ namespace MinecraftClient.Scripting
/// <returns>TRUE if successfully placed</returns>
public bool SendPlaceBlock(Location location, Direction blockFace, Hand hand = Hand.MainHand)
{
return Handler.PlaceBlock(location, blockFace, hand);
return Handler.PlaceBlock(location, blockFace, hand).Result;
}
/// <summary>
@ -1443,7 +1467,7 @@ namespace MinecraftClient.Scripting
/// <returns>TRUE in case of success</returns>
protected bool WindowAction(int inventoryId, int slot, WindowActionType actionType)
{
return Handler.DoWindowAction(inventoryId, slot, actionType);
return Handler.DoWindowAction(inventoryId, slot, actionType).Result;
}
/// <summary>
@ -1463,7 +1487,7 @@ namespace MinecraftClient.Scripting
/// <returns>True if success</returns>
protected bool ChangeSlot(short slot)
{
return Handler.ChangeSlot(slot);
return Handler.ChangeSlot(slot).Result;
}
/// <summary>
@ -1494,7 +1518,7 @@ namespace MinecraftClient.Scripting
/// <param name="line4"> text1 four</param>
protected bool UpdateSign(Location location, string line1, string line2, string line3, string line4)
{
return Handler.UpdateSign(location, line1, line2, line3, line4);
return Handler.UpdateSign(location, line1, line2, line3, line4).Result;
}
/// <summary>
@ -1503,7 +1527,7 @@ namespace MinecraftClient.Scripting
/// <param name="selectedSlot">Trade slot to select, starts at 0.</param>
protected bool SelectTrade(int selectedSlot)
{
return Handler.SelectTrade(selectedSlot);
return Handler.SelectTrade(selectedSlot).Result;
}
/// <summary>
@ -1512,7 +1536,7 @@ namespace MinecraftClient.Scripting
/// <param name="entity">player to teleport to</param>
protected bool SpectatorTeleport(Entity entity)
{
return Handler.Spectate(entity);
return Handler.Spectate(entity).Result;
}
/// <summary>
@ -1521,7 +1545,7 @@ namespace MinecraftClient.Scripting
/// <param name="uuid">uuid of entity to teleport to</param>
protected bool SpectatorTeleport(Guid UUID)
{
return Handler.SpectateByUUID(UUID);
return Handler.SpectateByUUID(UUID).Result;
}
/// <summary>
@ -1533,7 +1557,7 @@ namespace MinecraftClient.Scripting
/// <param name="flags">command block flags</param>
protected bool UpdateCommandBlock(Location location, string command, CommandBlockMode mode, CommandBlockFlags flags)
{
return Handler.UpdateCommandBlock(location, command, mode, flags);
return Handler.UpdateCommandBlock(location, command, mode, flags).Result;
}
/// <summary>
@ -1543,7 +1567,7 @@ namespace MinecraftClient.Scripting
/// <returns>True if success</returns>
protected bool CloseInventory(int inventoryID)
{
return Handler.CloseInventory(inventoryID);
return Handler.CloseInventory(inventoryID).Result;
}
/// <summary>
@ -1561,7 +1585,7 @@ namespace MinecraftClient.Scripting
protected bool Respawn()
{
if (Handler.GetHealth() <= 0)
return Handler.SendRespawnPacket();
return Handler.SendRespawnPacket().Result;
else return false;
}
@ -1586,32 +1610,6 @@ namespace MinecraftClient.Scripting
return Handler.GetProtocolVersion();
}
/// <summary>
/// Invoke a task on the main thread, wait for completion and retrieve return value.
/// </summary>
/// <param name="task">Task to run with any type or return value</param>
/// <returns>Any result returned from task, result type is inferred from the task</returns>
/// <example>bool result = InvokeOnMainThread(methodThatReturnsAbool);</example>
/// <example>bool result = InvokeOnMainThread(() => methodThatReturnsAbool(argument));</example>
/// <example>int result = InvokeOnMainThread(() => { yourCode(); return 42; });</example>
/// <typeparam name="T">Type of the return value</typeparam>
protected T InvokeOnMainThread<T>(Func<T> task)
{
return Handler.InvokeOnMainThread(task);
}
/// <summary>
/// Invoke a task on the main thread and wait for completion
/// </summary>
/// <param name="task">Task to run without return value</param>
/// <example>InvokeOnMainThread(methodThatReturnsNothing);</example>
/// <example>InvokeOnMainThread(() => methodThatReturnsNothing(argument));</example>
/// <example>InvokeOnMainThread(() => { yourCode(); });</example>
protected void InvokeOnMainThread(Action task)
{
Handler.InvokeOnMainThread(task);
}
/// <summary>
/// Schedule a task to run on the main thread, and do not wait for completion
/// </summary>

View file

@ -8,11 +8,13 @@ using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Protocol;
using MinecraftClient.Proxy;
using Tomlet;
using Tomlet.Attributes;
using Tomlet.Models;
using static MinecraftClient.Protocol.ProtocolHandler;
using static MinecraftClient.Settings.AppVarConfigHelper;
using static MinecraftClient.Settings.ChatBotConfigHealper;
using static MinecraftClient.Settings.ChatFormatConfigHelper;
@ -27,10 +29,9 @@ using static MinecraftClient.Settings.SignatureConfigHelper;
namespace MinecraftClient
{
public static class Settings
public static partial class Settings
{
private const int CommentsAlignPosition = 45;
private readonly static Regex CommentRegex = new(@"^(.*)\s?#\s\$([\w\.]+)\$\s*$$", RegexOptions.Compiled);
// Other Settings
public const string TranslationsFile_Version = "1.19.3";
@ -43,7 +44,7 @@ namespace MinecraftClient
public static class InternalConfig
{
public static string ServerIP = String.Empty;
public static string ServerIP = string.Empty;
public static ushort ServerPort = 25565;
@ -180,7 +181,7 @@ namespace MinecraftClient
return new(true, false);
}
public static void WriteToFile(string filepath, bool backupOldFile)
public static async Task WriteToFileAsync(string filepath, bool backupOldFile)
{
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
string tomlString = TomletMain.TomlStringFrom(Config);
@ -190,7 +191,7 @@ namespace MinecraftClient
StringBuilder newConfig = new();
foreach (string line in tomlList)
{
Match matchComment = CommentRegex.Match(line);
Match matchComment = GetCommentRegex().Match(line);
if (matchComment.Success && matchComment.Groups.Count == 3)
{
string config = matchComment.Groups[1].Value, comment = matchComment.Groups[2].Value;
@ -215,7 +216,7 @@ namespace MinecraftClient
try
{
if (new FileInfo(filepath).Length == newConfigByte.Length)
if (File.ReadAllBytes(filepath).SequenceEqual(newConfigByte))
if ((await File.ReadAllBytesAsync(filepath)).SequenceEqual(newConfigByte))
needUpdate = false;
}
catch { }
@ -238,7 +239,7 @@ namespace MinecraftClient
if (backupSuccessed)
{
try { File.WriteAllBytes(filepath, newConfigByte); }
try { await File.WriteAllBytesAsync(filepath, newConfigByte); }
catch (Exception ex)
{
ConsoleIO.WriteLineFormatted("§c" + string.Format(Translations.config_write_fail, filepath));
@ -252,7 +253,7 @@ namespace MinecraftClient
/// Load settings from the command line
/// </summary>
/// <param name="args">Command-line arguments</param>
/// <exception cref="System.ArgumentException">Thrown on invalid arguments</exception>
/// <exception cref="ArgumentException">Thrown on invalid arguments</exception>
public static void LoadArguments(string[] args)
{
int positionalIndex = 0;
@ -293,7 +294,7 @@ namespace MinecraftClient
InternalConfig.Account.Password = argument;
break;
case 2:
Config.Main.SetServerIP(new MainConfig.ServerInfoConfig(argument), true);
Config.Main.SetServerIP(new ServerInfoConfig(argument), true);
InternalConfig.KeepServerSettings = true;
break;
case 3:
@ -326,12 +327,12 @@ namespace MinecraftClient
}
}
public static class MainConfigHealper
public static partial class MainConfigHealper
{
public static MainConfig Config = new();
[TomlDoNotInlineObject]
public class MainConfig
public partial class MainConfig
{
public GeneralConfig General = new();
@ -385,8 +386,12 @@ namespace MinecraftClient
//Server IP (IP or domain names contains at least a dot)
if (sip.Length == 1 && !serverInfo.Port.HasValue && host.Contains('.') && host.Any(c => char.IsLetter(c)) &&
Settings.Config.Main.Advanced.ResolveSrvRecords != ResolveSrvRecordType.no)
{
//Domain name without port may need Minecraft SRV Record lookup
ProtocolHandler.MinecraftServiceLookup(ref host, ref port);
var lookup = MinecraftServiceLookupAsync(host);
if (lookup.Result.Item1)
(host, port) = (lookup.Result.Item2, lookup.Result.Item3);
}
InternalConfig.ServerIP = host;
InternalConfig.ServerPort = port;
return true;
@ -417,6 +422,12 @@ namespace MinecraftClient
General.Server.Host ??= string.Empty;
if (General.AccountType == GeneralConfig.LoginType.mojang && General.Method == GeneralConfig.LoginMethod.browser)
{
General.Method = GeneralConfig.LoginMethod.mcc;
ConsoleIO.WriteLogLine(Translations.config_invaild_login_method);
}
if (Advanced.MessageCooldown < 0)
Advanced.MessageCooldown = 0;
@ -436,12 +447,12 @@ namespace MinecraftClient
Thread.CurrentThread.CurrentUICulture = culture;
}
Advanced.Language = Regex.Replace(Advanced.Language, @"[^-^_^\w^*\d]", string.Empty).Replace('-', '_');
Advanced.Language = GetLanguageCodeRegex().Replace(Advanced.Language, string.Empty).Replace('-', '_');
Advanced.Language = ToLowerIfNeed(Advanced.Language);
if (!AvailableLang.Contains(Advanced.Language))
{
Advanced.Language = GetDefaultGameLanguage();
ConsoleIO.WriteLogLine("[Settings] " + Translations.config_Main_Advanced_language_invaild);
ConsoleIO.WriteLogLine(Translations.config_invaild_language);
}
if (!InternalConfig.KeepServerSettings)
@ -510,7 +521,7 @@ namespace MinecraftClient
public InternalCmdCharType InternalCmdChar = InternalCmdCharType.slash;
[TomlInlineComment("$Main.Advanced.message_cooldown$")]
public double MessageCooldown = 1.0;
public double MessageCooldown = 0.4;
[TomlInlineComment("$Main.Advanced.bot_owners$")]
public List<string> BotOwners = new() { "Player1", "Player2" };
@ -519,7 +530,7 @@ namespace MinecraftClient
public string MinecraftVersion = "auto";
[TomlInlineComment("$Main.Advanced.mc_forge$")]
public ForgeConfigType EnableForge = ForgeConfigType.auto;
public ForgeConfigType EnableForge = ForgeConfigType.no;
[TomlInlineComment("$Main.Advanced.brand_info$")]
public BrandInfoType BrandInfo = BrandInfoType.mcc;
@ -646,7 +657,7 @@ namespace MinecraftClient
public AccountInfoConfig(string Login)
{
this.Login = Login;
this.Password = "-";
Password = "-";
}
public AccountInfoConfig(string Login, string Password)
@ -668,7 +679,7 @@ namespace MinecraftClient
if (sip.Length > 1)
{
try { this.Port = Convert.ToUInt16(sip[1]); }
try { Port = Convert.ToUInt16(sip[1]); }
catch (FormatException) { }
}
}
@ -679,6 +690,9 @@ namespace MinecraftClient
this.Port = Port;
}
}
[GeneratedRegex("[^-^_^\\w^*\\d]")]
private static partial Regex GetLanguageCodeRegex();
}
}
@ -955,7 +969,7 @@ namespace MinecraftClient
/// <returns>True if the parameters were valid</returns>
public bool SetVar(string varName, object varData)
{
varName = Settings.ToLowerIfNeed(new string(varName.TakeWhile(char.IsLetterOrDigit).ToArray()));
varName = ToLowerIfNeed(new string(varName.TakeWhile(char.IsLetterOrDigit).ToArray()));
if (varName.Length > 0)
{
bool isString = varData.GetType() == typeof(string);
@ -1063,7 +1077,7 @@ namespace MinecraftClient
if (varname_ok)
{
string varname = var_name.ToString();
string varname_lower = Settings.ToLowerIfNeed(varname);
string varname_lower = ToLowerIfNeed(varname);
i = i + varname.Length + 1;
switch (varname_lower)
@ -1074,7 +1088,7 @@ namespace MinecraftClient
case "serverport": result.Append(InternalConfig.ServerPort); break;
case "datetime":
DateTime time = DateTime.Now;
result.Append(String.Format("{0}-{1}-{2} {3}:{4}:{5}",
result.Append(string.Format("{0}-{1}-{2} {3}:{4}:{5}",
time.Year.ToString("0000"),
time.Month.ToString("00"),
time.Day.ToString("00"),
@ -1162,13 +1176,13 @@ namespace MinecraftClient
public byte GetByte()
{
return (byte)(
((Cape ? 1 : 0) << 0)
| ((Jacket ? 1 : 0) << 1)
| ((Sleeve_Left ? 1 : 0) << 2)
| ((Sleeve_Right ? 1 : 0) << 3)
| ((Pants_Left ? 1 : 0) << 4)
| ((Pants_Right ? 1 : 0) << 5)
| ((Hat ? 1 : 0) << 6)
(Cape ? 1 : 0) << 0
| (Jacket ? 1 : 0) << 1
| (Sleeve_Left ? 1 : 0) << 2
| (Sleeve_Right ? 1 : 0) << 3
| (Pants_Left ? 1 : 0) << 4
| (Pants_Right ? 1 : 0) << 5
| (Hat ? 1 : 0) << 6
);
}
}
@ -1821,9 +1835,9 @@ namespace MinecraftClient
const string lookupStringL = "---------------------------------!-#$%&-()*+,-./0123456789:;<=>?@abcdefghijklmnopqrstuvwxyz[-]^_`abcdefghijklmnopqrstuvwxyz{|}~-";
bool needLower = false;
foreach (Char c in str)
foreach (char c in str)
{
if (Char.IsUpper(c))
if (char.IsUpper(c))
{
needLower = true;
break;
@ -1849,6 +1863,10 @@ namespace MinecraftClient
time = Math.Min(int.MaxValue / 10, time);
return (int)Math.Round(time * 10);
}
[GeneratedRegex("^(.*)\\s?#\\s\\$([\\w\\.]+)\\$\\s*$$", RegexOptions.Compiled)]
private static partial Regex GetCommentRegex();
}
public static class InternalCmdCharTypeExtensions

View file

@ -8,12 +8,15 @@ using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Proxy;
namespace MinecraftClient
{
internal static class UpgradeHelper
internal static partial class UpgradeHelper
{
private const string GithubReleaseUrl = "https://github.com/MCCTeam/Minecraft-Console-Client/releases";
internal const string GithubReleaseUrl = "https://github.com/MCCTeam/Minecraft-Console-Client/releases";
// private static HttpClient? httpClient = null;
private static int running = 0; // Type: bool; 1 for running; 0 for stopped;
private static CancellationTokenSource cancellationTokenSource = new();
@ -23,29 +26,17 @@ namespace MinecraftClient
private static DateTime downloadStartTime = DateTime.Now, lastNotifyTime = DateTime.Now;
private static TimeSpan minNotifyInterval = TimeSpan.FromMilliseconds(3000);
public static void CheckUpdate(bool forceUpdate = false)
public static async Task CheckUpdate(bool forceUpdate = false)
{
bool needPromptUpdate = true;
if (!forceUpdate && CompareVersionInfo(Settings.Config.Head.CurrentVersion, Settings.Config.Head.LatestVersion))
await DoCheckUpdate(CancellationToken.None);
if (CompareVersionInfo(Settings.Config.Head.CurrentVersion, Settings.Config.Head.LatestVersion))
{
needPromptUpdate = false;
ConsoleIO.WriteLineFormatted("§e" + string.Format(Translations.mcc_has_update, GithubReleaseUrl), true);
}
Task.Run(() =>
else if (forceUpdate)
{
DoCheckUpdate(CancellationToken.None);
if (needPromptUpdate)
{
if (CompareVersionInfo(Settings.Config.Head.CurrentVersion, Settings.Config.Head.LatestVersion))
{
ConsoleIO.WriteLineFormatted("§e" + string.Format(Translations.mcc_has_update, GithubReleaseUrl), true);
}
else if (forceUpdate)
{
ConsoleIO.WriteLine(Translations.mcc_update_already_latest + ' ' + Translations.mcc_update_promote_force_cmd);
}
}
});
ConsoleIO.WriteLine(Translations.mcc_update_already_latest + ' ' + Translations.mcc_update_promote_force_cmd);
}
}
public static bool DownloadLatestBuild(bool forceUpdate, bool isCommandLine = false)
@ -71,7 +62,7 @@ namespace MinecraftClient
}
else
{
string latestVersion = DoCheckUpdate(cancellationToken);
string latestVersion = await DoCheckUpdate(cancellationToken);
if (cancellationToken.IsCancellationRequested)
{
}
@ -89,7 +80,7 @@ namespace MinecraftClient
ConsoleIO.WriteLine(string.Format(Translations.mcc_update_exist_update, latestVersion, OSInfo));
HttpClientHandler httpClientHandler = new() { AllowAutoRedirect = true };
AddProxySettings(httpClientHandler);
ProxyHandler.AddProxySettings(ProxyHandler.ClientType.Update, ref httpClientHandler);
ProgressMessageHandler progressMessageHandler = new(httpClientHandler);
progressMessageHandler.HttpReceiveProgress += (_, info) =>
@ -183,43 +174,33 @@ namespace MinecraftClient
Thread.Sleep(500);
}
private static string DoCheckUpdate(CancellationToken cancellationToken)
internal static async Task<string> DoCheckUpdate(CancellationToken cancellationToken)
{
string latestBuildInfo = string.Empty;
HttpClientHandler httpClientHandler = new() { AllowAutoRedirect = false };
AddProxySettings(httpClientHandler);
HttpClient httpClient = new(httpClientHandler);
Task<HttpResponseMessage>? httpWebRequest = null;
try
ProxyHandler.AddProxySettings(ProxyHandler.ClientType.Update, ref httpClientHandler);
using HttpClient httpClient = new(httpClientHandler);
using HttpRequestMessage request = new(HttpMethod.Head, GithubReleaseUrl + "/latest");
using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken);
if (!cancellationToken.IsCancellationRequested)
{
httpWebRequest = httpClient.GetAsync(GithubReleaseUrl + "/latest", HttpCompletionOption.ResponseHeadersRead, cancellationToken);
httpWebRequest.Wait();
if (!cancellationToken.IsCancellationRequested)
if (response.Headers.Location != null)
{
HttpResponseMessage res = httpWebRequest.Result;
if (res.Headers.Location != null)
Match match = GetReleaseUrlRegex().Match(response.Headers.Location.ToString());
if (match.Success && match.Groups.Count == 5)
{
Match match = Regex.Match(res.Headers.Location.ToString(), GithubReleaseUrl + @"/tag/(\d{4})(\d{2})(\d{2})-(\d+)");
if (match.Success && match.Groups.Count == 5)
string year = match.Groups[1].Value, month = match.Groups[2].Value, day = match.Groups[3].Value, run = match.Groups[4].Value;
string latestVersion = string.Format("GitHub build {0}, built on {1}-{2}-{3}", run, year, month, day);
latestBuildInfo = string.Format("{0}{1}{2}-{3}", year, month, day, run);
if (latestVersion != Settings.Config.Head.LatestVersion)
{
string year = match.Groups[1].Value, month = match.Groups[2].Value, day = match.Groups[3].Value, run = match.Groups[4].Value;
string latestVersion = string.Format("GitHub build {0}, built on {1}-{2}-{3}", run, year, month, day);
latestBuildInfo = string.Format("{0}{1}{2}-{3}", year, month, day, run);
if (latestVersion != Settings.Config.Head.LatestVersion)
{
Settings.Config.Head.LatestVersion = latestVersion;
Program.WriteBackSettings(false);
}
Settings.Config.Head.LatestVersion = latestVersion;
_ = Program.WriteBackSettings(false);
}
}
res.Dispose();
}
httpWebRequest.Dispose();
}
catch (Exception) { }
finally { httpWebRequest?.Dispose(); }
httpClient.Dispose();
httpClientHandler.Dispose();
return latestBuildInfo;
}
@ -247,12 +228,12 @@ namespace MinecraftClient
return string.Empty;
}
private static bool CompareVersionInfo(string? current, string? latest)
internal static bool CompareVersionInfo(string? current, string? latest)
{
if (current == null || latest == null)
return false;
Regex reg = new(@"\w+\sbuild\s(\d+),\sbuilt\son\s(\d{4})[-\/\.\s]?(\d{2})[-\/\.\s]?(\d{2}).*");
Regex reg2 = new(@"\w+\sbuild\s(\d+),\sbuilt\son\s\w+\s(\d{2})[-\/\.\s]?(\d{2})[-\/\.\s]?(\d{4}).*");
Regex reg = GetVersionRegex1();
Regex reg2 = GetVersionRegex2();
DateTime? curTime = null, latestTime = null;
@ -302,24 +283,13 @@ namespace MinecraftClient
return false;
}
private static void AddProxySettings(HttpClientHandler httpClientHandler)
{
if (Settings.Config.Proxy.Enabled_Update)
{
string proxyAddress;
if (!string.IsNullOrWhiteSpace(Settings.Config.Proxy.Username) && !string.IsNullOrWhiteSpace(Settings.Config.Proxy.Password))
proxyAddress = string.Format("{0}://{3}:{4}@{1}:{2}",
Settings.Config.Proxy.Proxy_Type.ToString().ToLower(),
Settings.Config.Proxy.Server.Host,
Settings.Config.Proxy.Server.Port,
Settings.Config.Proxy.Username,
Settings.Config.Proxy.Password);
else
proxyAddress = string.Format("{0}://{1}:{2}",
Settings.Config.Proxy.Proxy_Type.ToString().ToLower(),
Settings.Config.Proxy.Server.Host, Settings.Config.Proxy.Server.Port);
httpClientHandler.Proxy = new WebProxy(proxyAddress, true);
}
}
[GeneratedRegex("https://github.com/MCCTeam/Minecraft-Console-Client/releases/tag/(\\d{4})(\\d{2})(\\d{2})-(\\d+)")]
private static partial Regex GetReleaseUrlRegex();
[GeneratedRegex("\\w+\\sbuild\\s(\\d+),\\sbuilt\\son\\s(\\d{4})[-\\/\\.\\s]?(\\d{2})[-\\/\\.\\s]?(\\d{2}).*")]
private static partial Regex GetVersionRegex1();
[GeneratedRegex("\\w+\\sbuild\\s(\\d+),\\sbuilt\\son\\s\\w+\\s(\\d{2})[-\\/\\.\\s]?(\\d{2})[-\\/\\.\\s]?(\\d{4}).*")]
private static partial Regex GetVersionRegex2();
}
}

View file

@ -30,7 +30,7 @@ namespace MinecraftClient.WinAPI
SETICON = 0x0080,
}
private static void SetWindowIcon(System.Drawing.Icon icon)
private static void SetWindowIcon(Icon icon)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
@ -43,55 +43,40 @@ namespace MinecraftClient.WinAPI
/// <summary>
/// Asynchronously download the player's skin and set the head as console icon
/// </summary>
[SupportedOSPlatform("windows")]
public static void SetPlayerIconAsync(string playerName)
public static async Task SetPlayerIconAsync(HttpClient httpClient, string playerName)
{
Thread t = new(new ThreadStart(delegate
if (!OperatingSystem.IsWindows())
return;
try
{
HttpClient httpClient = new();
using Stream imageStream = await httpClient.GetStreamAsync($"https://minotar.net/helm/{playerName}/100.png");
try
{
Task<Stream> httpWebRequest = httpClient.GetStreamAsync("https://minotar.net/helm/" + playerName + "/100.png");
httpWebRequest.Wait();
Stream imageStream = httpWebRequest.Result;
try
{
Bitmap skin = new(Image.FromStream(imageStream)); //Read skin from network
SetWindowIcon(Icon.FromHandle(skin.GetHicon())); // Windows 10+ (New console)
SetConsoleIcon(skin.GetHicon()); // Windows 8 and lower (Older console)
}
catch (ArgumentException)
{
/* Invalid image in HTTP response */
}
imageStream.Dispose();
httpWebRequest.Dispose();
Bitmap skin = new(Image.FromStream(imageStream)); //Read skin from network
SetWindowIcon(Icon.FromHandle(skin.GetHicon())); // Windows 10+ (New console)
SetConsoleIcon(skin.GetHicon()); // Windows 8 and lower (Older console)
}
catch (AggregateException ae)
catch (ArgumentException)
{
bool needRevert = false;
foreach (var ex in ae.InnerExceptions)
{
if (ex is HttpRequestException || ex is TaskCanceledException) //Skin not found? Reset to default icon
needRevert = true;
}
if (needRevert)
RevertToMCCIcon();
}
catch (HttpRequestException) //Skin not found? Reset to default icon
{
RevertToMCCIcon();
}
finally
{
httpClient.Dispose();
/* Invalid image in HTTP response */
}
}
))
catch (AggregateException ae)
{
Name = "Player skin icon setter"
};
t.Start();
bool needRevert = false;
foreach (var ex in ae.InnerExceptions)
{
if (ex is HttpRequestException || ex is TaskCanceledException) //Skin not found? Reset to default icon
needRevert = true;
}
if (needRevert)
RevertToMCCIcon();
}
catch (HttpRequestException) //Skin not found? Reset to default icon
{
RevertToMCCIcon();
}
}
/// <summary>