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(); StopDigging();
} }
public override bool OnDisconnect(DisconnectReason reason, string message) public override int OnDisconnect(DisconnectReason reason, string message)
{ {
StopDigging(); StopDigging();
return base.OnDisconnect(reason, message); return base.OnDisconnect(reason, message);
} }
} }

View file

@ -535,7 +535,7 @@ namespace MinecraftClient.ChatBots
StopFishing(); StopFishing();
} }
public override bool OnDisconnect(DisconnectReason reason, string message) public override int OnDisconnect(DisconnectReason reason, string message)
{ {
StopFishing(); 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) if (reason == DisconnectReason.UserLogout)
{ {
LogDebugToConsole(Translations.bot_autoRelog_ignore_user_logout); LogDebugToConsole(Translations.bot_autoRelog_ignore_user_logout);
@ -113,44 +114,77 @@ namespace MinecraftClient.ChatBots
if (Config.Ignore_Kick_Message) if (Config.Ignore_Kick_Message)
{ {
Configs._BotRecoAttempts++; Configs._BotRecoAttempts++;
LaunchDelayedReconnection(null); LogDebugToConsole(Translations.bot_autoRelog_reconnect_always);
return true; triggerReco = true;
} }
else
foreach (string msg in Config.Kick_Messages)
{ {
if (comp.Contains(msg)) foreach (string msg in Config.Kick_Messages)
{ {
Configs._BotRecoAttempts++; if (comp.Contains(msg))
LaunchDelayedReconnection(msg); {
return true; 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; if (triggerReco)
}
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)
{ {
AutoRelog bot = new(); double delay = random.NextDouble() * (Config.Delay.max - Config.Delay.min) + Config.Delay.min;
bot.Initialize(); return (int)Math.Floor(delay * 1000);
return bot.OnDisconnect(reason, message); }
else
{
return -1;
} }
return false;
} }
} }
} }

View file

@ -244,10 +244,10 @@ namespace MinecraftClient.ChatBots
running = false; running = false;
} }
public override bool OnDisconnect(DisconnectReason reason, string message) public override int OnDisconnect(DisconnectReason reason, string message)
{ {
running = false; running = false;
return true; return base.OnDisconnect(reason, message);
} }
private void MainPorcess() 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(); replay!.OnShutDown();
return base.OnDisconnect(reason, message); 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; serverlogin_done = false;
return false; return base.OnDisconnect(reason, message);
} }
private static string Task2String(TaskConfig task) private static string Task2String(TaskConfig task)

View file

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

View file

@ -49,7 +49,7 @@ namespace MinecraftClient.Commands
private static int DoAnimation(CmdResult r, bool mainhand) private static int DoAnimation(CmdResult r, bool mainhand)
{ {
McClient handler = CmdResult.currentHandler!; 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) private static int DoLeaveBed(CmdResult r)
{ {
McClient handler = CmdResult.currentHandler!; 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) 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)); 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( handler.Log.Info(string.Format(
Translations.cmd_bed_trying_to_use, Translations.cmd_bed_trying_to_use,
@ -174,7 +174,7 @@ namespace MinecraftClient.Commands
blockCenter.X, blockCenter.X,
blockCenter.Y, blockCenter.Y,
blockCenter.Z, 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); return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_bots_noloaded);
else else
{ {
handler.UnloadAllBots(); handler.UnloadAllBots().Wait();
return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_bots_unloaded_all); 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)); return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_bots_notfound, botName));
else else
{ {
handler.BotUnLoad(bot); handler.BotUnLoad(bot).Wait();
return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_bots_unloaded, botName)); 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()) if (!handler.GetInventoryEnabled())
return r.SetAndReturn(Status.FailNeedInventory); 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)); return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_changeSlot_changed, slot));
else else
return r.SetAndReturn(Status.Fail, Translations.cmd_changeSlot_fail); return r.SetAndReturn(Status.Fail, Translations.cmd_changeSlot_fail);

View file

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

View file

@ -58,7 +58,7 @@ namespace MinecraftClient.Commands
var p = inventories[inventoryId]; var p = inventories[inventoryId];
int[] targetItems = p.SearchItem(itemType); int[] targetItems = p.SearchItem(itemType);
foreach (int slot in targetItems) 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)); 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)); return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_enchant_no_levels, handler.GetLevel(), requiredLevel));
else else
{ {
if (handler.ClickContainerButton(enchantingTable.ID, slotId)) if (handler.ClickContainerButton(enchantingTable.ID, slotId).Result)
return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_enchant_clicked); return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_enchant_clicked);
else else
return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_enchant_not_clicked); return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_enchant_not_clicked);

View file

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

View file

@ -48,11 +48,5 @@ namespace MinecraftClient.Commands
Program.Exit(code); Program.Exit(code);
return r.SetAndReturn(CmdResult.Status.Done); 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.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)); return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_inventory_creative_done, itemType, count, slot));
else else
return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_inventory_creative_fail); return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_inventory_creative_fail);
@ -178,7 +178,7 @@ namespace MinecraftClient.Commands
if (handler.GetGamemode() == 1) 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)); return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_inventory_creative_delete, slot));
else else
return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_inventory_creative_fail); return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_inventory_creative_fail);
@ -279,7 +279,7 @@ namespace MinecraftClient.Commands
if (inventory == null) if (inventory == null)
return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_inventory_not_exist, inventoryId)); 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)); return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_inventory_close, inventoryId));
else else
return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_inventory_close_fail, inventoryId)); 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)); 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) private int DoDropAction(CmdResult r, int? inventoryId, int slot, WindowActionType actionType)
@ -379,7 +381,7 @@ namespace MinecraftClient.Commands
if (!inventory.Items.ContainsKey(slot)) if (!inventory.Items.ContainsKey(slot))
return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_inventory_no_item, 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) if (actionType == WindowActionType.DropItemStack)
return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_inventory_drop_stack, slot)); 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); Program.Restart(keepAccountAndServerSettings: true);
return String.Empty; return string.Empty;
} }
} }
} }

View file

@ -40,7 +40,7 @@ namespace MinecraftClient.Commands
{ {
McClient handler = CmdResult.currentHandler!; McClient handler = CmdResult.currentHandler!;
handler.Log.Info(Translations.cmd_reload_started); 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_warning1);
handler.Log.Warn(Translations.cmd_reload_warning2); handler.Log.Warn(Translations.cmd_reload_warning2);
handler.Log.Warn(Translations.cmd_reload_warning3); handler.Log.Warn(Translations.cmd_reload_warning3);

View file

@ -39,7 +39,7 @@ namespace MinecraftClient.Commands
private int DoRespawn(CmdResult r) private int DoRespawn(CmdResult r)
{ {
McClient handler = CmdResult.currentHandler!; McClient handler = CmdResult.currentHandler!;
handler.SendRespawnPacket(); handler.SendRespawnPacket().Wait();
return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_respawn_done); 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) private int DoSendText(CmdResult r, string command)
{ {
McClient handler = CmdResult.currentHandler!; McClient handler = CmdResult.currentHandler!;
handler.SendText(command); handler.SendText(command).Wait();
return r.SetAndReturn(CmdResult.Status.Done); return r.SetAndReturn(CmdResult.Status.Done);
} }
} }

View file

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

View file

@ -1,4 +1,5 @@
using Brigadier.NET; using System.Threading.Tasks;
using Brigadier.NET;
using Brigadier.NET.Builder; using Brigadier.NET.Builder;
using MinecraftClient.CommandHandler; using MinecraftClient.CommandHandler;
@ -71,7 +72,7 @@ namespace MinecraftClient.Commands
private static int CheckUpdate(CmdResult r) 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); return r.SetAndReturn(CmdResult.Status.Done, Translations.mcc_update_start);
} }
} }

View file

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

View file

@ -48,7 +48,7 @@ namespace MinecraftClient.Commands
Location current = handler.GetCurrentLocation(); Location current = handler.GetCurrentLocation();
block = block.ToAbsolute(current).ToFloor(); block = block.ToAbsolute(current).ToFloor();
Location blockCenter = block.ToCenter(); 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); 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> /// </summary>
public static string LogPrefix = "§8[Log] "; public static string LogPrefix = "§8[Log] ";
private static bool SuppressOutput = false;
private static List<Tuple<bool, string>> MessageBuffer = new();
/// <summary> /// <summary>
/// Read a password from the standard input /// Read a password from the standard input
/// </summary> /// </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> /// <summary>
/// Write a string to the standard output with a trailing newline /// Write a string to the standard output with a trailing newline
/// </summary> /// </summary>
public static void WriteLine(string line) public static void WriteLine(string line, bool ignoreSuppress = false)
{ {
if (BasicIO) if (!ignoreSuppress && SuppressOutput)
Console.WriteLine(line); {
lock(MessageBuffer)
MessageBuffer.Add(new(true, line));
}
else else
ConsoleInteractive.ConsoleWriter.WriteLine(line); {
if (BasicIO)
Console.WriteLine(line);
else
ConsoleInteractive.ConsoleWriter.WriteLine(line);
}
} }
/// <summary> /// <summary>
@ -123,37 +154,42 @@ namespace MinecraftClient
/// If true, "hh-mm-ss" timestamp will be prepended. /// If true, "hh-mm-ss" timestamp will be prepended.
/// If unspecified, value is retrieved from EnableTimestamps. /// If unspecified, value is retrieved from EnableTimestamps.
/// </param> /// </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; displayTimestamp ??= EnableTimestamps;
if (displayTimestamp.Value) if (displayTimestamp.Value)
{ {
int hour = DateTime.Now.Hour, minute = DateTime.Now.Minute, second = DateTime.Now.Second; 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"))); output.Append(String.Format("{0}:{1}:{2} ", hour.ToString("00"), minute.ToString("00"), second.ToString("00")));
} }
if (!acceptnewlines) if (!acceptnewlines)
{
str = str.Replace('\n', ' '); 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 else
{ {
output.Append(str); 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; if (BasicIO) return;
ConsoleInteractive.ConsoleReader.ClearBuffer(); 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..]; string command = fullCommand[offset..];
if (command.Length == 0) if (command.Length == 0)
{ {
List<ConsoleInteractive.ConsoleSuggestion.Suggestion> sugList = new(); List<ConsoleInteractive.ConsoleSuggestion.Suggestion> sugList = new() { new("/") };
sugList.Add(new("/"));
var childs = McClient.dispatcher.GetRoot().Children; var childs = McClient.dispatcher.GetRoot().Children;
if (childs != null) if (childs != null)
@ -336,9 +381,9 @@ namespace MinecraftClient
MergeCommands(); 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> /// </summary>
/// <param name="BehindCursor">Text behind the cursor, e.g. "my input comm"</param> /// <param name="BehindCursor">Text behind the cursor, e.g. "my input comm"</param>
/// <returns>List of auto-complete words, e.g. ["command", "comment"]</returns> /// <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;
using System.Runtime.Intrinsics.X86; using System.Runtime.Intrinsics.X86;
namespace MinecraftClient.Crypto namespace MinecraftClient.Crypto.AesHandler
{ {
// Using the AES-NI instruction set // Using the AES-NI instruction set
// https://gist.github.com/Thealexbarney/9f75883786a9f3100408ff795fb95d85 // https://gist.github.com/Thealexbarney/9f75883786a9f3100408ff795fb95d85
public class FastAes public class FasterAesX86 : IAesHandler
{ {
private Vector128<byte>[] RoundKeys { get; } private Vector128<byte>[] RoundKeys { get; }
public FastAes(Span<byte> key) public FasterAesX86(Span<byte> key)
{ {
RoundKeys = KeyExpansion(key); RoundKeys = KeyExpansion(key);
} }
@ -23,11 +23,10 @@ namespace MinecraftClient.Crypto
/// <returns>Is it supported</returns> /// <returns>Is it supported</returns>
public static bool IsSupported() public static bool IsSupported()
{ {
return Sse2.IsSupported && Aes.IsSupported; return Aes.IsSupported && Sse2.IsSupported;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public override void EncryptEcb(Span<byte> plaintext, Span<byte> destination)
public void EncryptEcb(ReadOnlySpan<byte> plaintext, Span<byte> destination)
{ {
Vector128<byte>[] keys = RoundKeys; 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) && if (ValidateSlots(source, dest, destContainer) &&
HasItem(source) && HasItem(source) &&
((destContainer != null && !HasItem(dest, destContainer)) || (destContainer == null && !HasItem(dest)))) ((destContainer != null && !HasItem(dest, destContainer)) || (destContainer == null && !HasItem(dest))))
return mc.DoWindowAction(c.ID, source, WindowActionType.LeftClick) return mc.DoWindowAction(c.ID, source, WindowActionType.LeftClick).Result
&& mc.DoWindowAction(destContainer == null ? c.ID : destContainer.ID, dest, WindowActionType.LeftClick); && mc.DoWindowAction(destContainer == null ? c.ID : destContainer.ID, dest, WindowActionType.LeftClick).Result;
else return false; else return false;
} }
@ -57,9 +57,9 @@ namespace MinecraftClient.Inventory
if (ValidateSlots(slot1, slot2, destContainer) && if (ValidateSlots(slot1, slot2, destContainer) &&
HasItem(slot1) && HasItem(slot1) &&
(destContainer != null && HasItem(slot2, destContainer) || (destContainer == null && HasItem(slot2)))) (destContainer != null && HasItem(slot2, destContainer) || (destContainer == null && HasItem(slot2))))
return 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) && mc.DoWindowAction(destContainer == null ? c.ID : destContainer.ID, slot2, WindowActionType.LeftClick).Result
&& mc.DoWindowAction(c.ID, slot1, WindowActionType.LeftClick); && mc.DoWindowAction(c.ID, slot1, WindowActionType.LeftClick).Result;
else return false; else return false;
} }
@ -104,14 +104,14 @@ namespace MinecraftClient.Inventory
break; break;
} }
} }
mc.DoWindowAction(c.ID, source, WindowActionType.LeftClick); // grab item mc.DoWindowAction(c.ID, source, WindowActionType.LeftClick).Wait(); // grab item
mc.DoWindowAction(c.ID, -999, startDragging); mc.DoWindowAction(c.ID, -999, startDragging).Wait();
foreach (var slot in availableSlots) 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, -999, endDragging).Wait();
mc.DoWindowAction(c.ID, source, WindowActionType.LeftClick); // put down item left (if any) mc.DoWindowAction(c.ID, source, WindowActionType.LeftClick).Wait(); // put down item left (if any)
return true; return true;
} }
else return false; else return false;

File diff suppressed because it is too large Load diff

View file

@ -29,7 +29,6 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Brigadier.NET" Version="1.2.13" /> <PackageReference Include="Brigadier.NET" Version="1.2.13" />
<PackageReference Include="DnsClient" Version="1.7.0" /> <PackageReference Include="DnsClient" Version="1.7.0" />
<PackageReference Include="DotNetZip" Version="1.16.0" />
<PackageReference Include="DSharpPlus" Version="4.2.0" /> <PackageReference Include="DSharpPlus" Version="4.2.0" />
<PackageReference Include="DynamicExpresso.Core" Version="2.13.0" /> <PackageReference Include="DynamicExpresso.Core" Version="2.13.0" />
<PackageReference Include="FuzzySharp" Version="2.0.2" /> <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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace MinecraftClient.Protocol.Handlers.Forge namespace MinecraftClient.Protocol.Handlers.Forge
{ {
@ -11,16 +12,22 @@ namespace MinecraftClient.Protocol.Handlers.Forge
/// <summary> /// <summary>
/// Represents an individual forge mod. /// Represents an individual forge mod.
/// </summary> /// </summary>
public class ForgeMod public record ForgeMod
{ {
public ForgeMod(String ModID, String Version) public ForgeMod(string? modID, string? version)
{ {
this.ModID = ModID; ModID = modID;
this.Version = Version; Version = ModMarker = version;
} }
public readonly String ModID; [JsonPropertyName("modId")]
public readonly String Version; public string? ModID { init; get; }
[JsonPropertyName("version")]
public string? Version { init; get; }
[JsonPropertyName("modmarker")]
public string? ModMarker { init; get; }
public override string ToString() public override string ToString()
{ {
@ -138,5 +145,16 @@ namespace MinecraftClient.Protocol.Handlers.Forge
throw new NotImplementedException("FMLVersion '" + fmlVersion + "' not implemented!"); 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
using MinecraftClient.Protocol.PacketPipeline;
namespace MinecraftClient.Protocol.Handlers.packet.s2c namespace MinecraftClient.Protocol.Handlers.packet.s2c
{ {
@ -8,7 +10,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
private static int RootIdx; private static int RootIdx;
private static CommandNode[] Nodes = Array.Empty<CommandNode>(); 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); int count = dataTypes.ReadNextVarInt(packetData);
Nodes = new CommandNode[count]; Nodes = new CommandNode[count];
@ -23,7 +25,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
int redirectNode = ((flags & 0x08) > 0) ? dataTypes.ReadNextVarInt(packetData) : -1; 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; int paserId = ((flags & 0x03) == 2) ? dataTypes.ReadNextVarInt(packetData) : -1;
Paser? paser = null; 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); Nodes[i] = new(flags, childs, redirectNode, name, paser, suggestionsType);
} }
@ -158,7 +160,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
internal class PaserEmpty : Paser internal class PaserEmpty : Paser
{ {
public PaserEmpty(DataTypes dataTypes, Queue<byte> packetData) { } public PaserEmpty(DataTypes dataTypes, PacketStream packetData) { }
public override bool Check(string text) public override bool Check(string text)
{ {
@ -181,7 +183,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
private byte Flags; private byte Flags;
private float Min = float.MinValue, Max = float.MaxValue; 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); Flags = dataTypes.ReadNextByte(packetData);
if ((Flags & 0x01) > 0) if ((Flags & 0x01) > 0)
@ -211,7 +213,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
private byte Flags; private byte Flags;
private double Min = double.MinValue, Max = double.MaxValue; 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); Flags = dataTypes.ReadNextByte(packetData);
if ((Flags & 0x01) > 0) if ((Flags & 0x01) > 0)
@ -241,7 +243,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
private byte Flags; private byte Flags;
private int Min = int.MinValue, Max = int.MaxValue; 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); Flags = dataTypes.ReadNextByte(packetData);
if ((Flags & 0x01) > 0) if ((Flags & 0x01) > 0)
@ -271,7 +273,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
private byte Flags; private byte Flags;
private long Min = long.MinValue, Max = long.MaxValue; 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); Flags = dataTypes.ReadNextByte(packetData);
if ((Flags & 0x01) > 0) if ((Flags & 0x01) > 0)
@ -302,7 +304,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
private enum StringType { SINGLE_WORD, QUOTABLE_PHRASE, GREEDY_PHRASE }; 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); Type = (StringType)dataTypes.ReadNextVarInt(packetData);
} }
@ -327,7 +329,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
{ {
private byte Flags; private byte Flags;
public PaserEntity(DataTypes dataTypes, Queue<byte> packetData) public PaserEntity(DataTypes dataTypes, PacketStream packetData)
{ {
Flags = dataTypes.ReadNextByte(packetData); Flags = dataTypes.ReadNextByte(packetData);
} }
@ -351,7 +353,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
internal class PaserBlockPos : Paser internal class PaserBlockPos : Paser
{ {
public PaserBlockPos(DataTypes dataTypes, Queue<byte> packetData) { } public PaserBlockPos(DataTypes dataTypes, PacketStream packetData) { }
public override bool Check(string text) public override bool Check(string text)
{ {
@ -372,7 +374,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
internal class PaserColumnPos : Paser internal class PaserColumnPos : Paser
{ {
public PaserColumnPos(DataTypes dataTypes, Queue<byte> packetData) { } public PaserColumnPos(DataTypes dataTypes, PacketStream packetData) { }
public override bool Check(string text) public override bool Check(string text)
{ {
@ -393,7 +395,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
internal class PaserVec3 : Paser internal class PaserVec3 : Paser
{ {
public PaserVec3(DataTypes dataTypes, Queue<byte> packetData) { } public PaserVec3(DataTypes dataTypes, PacketStream packetData) { }
public override bool Check(string text) public override bool Check(string text)
{ {
@ -414,7 +416,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
internal class PaserVec2 : Paser internal class PaserVec2 : Paser
{ {
public PaserVec2(DataTypes dataTypes, Queue<byte> packetData) { } public PaserVec2(DataTypes dataTypes, PacketStream packetData) { }
public override bool Check(string text) public override bool Check(string text)
{ {
@ -435,7 +437,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
internal class PaserRotation : Paser internal class PaserRotation : Paser
{ {
public PaserRotation(DataTypes dataTypes, Queue<byte> packetData) { } public PaserRotation(DataTypes dataTypes, PacketStream packetData) { }
public override bool Check(string text) public override bool Check(string text)
{ {
@ -455,7 +457,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
internal class PaserMessage : Paser internal class PaserMessage : Paser
{ {
public PaserMessage(DataTypes dataTypes, Queue<byte> packetData) { } public PaserMessage(DataTypes dataTypes, PacketStream packetData) { }
public override bool Check(string text) public override bool Check(string text)
{ {
@ -477,7 +479,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
{ {
private byte Flags; private byte Flags;
public PaserScoreHolder(DataTypes dataTypes, Queue<byte> packetData) public PaserScoreHolder(DataTypes dataTypes, PacketStream packetData)
{ {
Flags = dataTypes.ReadNextByte(packetData); Flags = dataTypes.ReadNextByte(packetData);
} }
@ -502,7 +504,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
{ {
private bool Decimals; private bool Decimals;
public PaserRange(DataTypes dataTypes, Queue<byte> packetData) public PaserRange(DataTypes dataTypes, PacketStream packetData)
{ {
Decimals = dataTypes.ReadNextBool(packetData); Decimals = dataTypes.ReadNextBool(packetData);
} }
@ -527,9 +529,11 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
{ {
private string Registry; 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) public override bool Check(string text)
@ -552,9 +556,11 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
{ {
private string Registry; 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) 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.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Protocol.Handlers.Forge; using MinecraftClient.Protocol.Handlers.Forge;
using MinecraftClient.Protocol.Message; using MinecraftClient.Protocol.Message;
using MinecraftClient.Protocol.PacketPipeline;
using MinecraftClient.Scripting; using MinecraftClient.Scripting;
using static MinecraftClient.Protocol.Handlers.Protocol18Handler;
namespace MinecraftClient.Protocol.Handlers namespace MinecraftClient.Protocol.Handlers
{ {
@ -54,23 +57,23 @@ namespace MinecraftClient.Protocol.Handlers
/// Completes the Minecraft Forge handshake (Forge Protocol version 1: FML) /// Completes the Minecraft Forge handshake (Forge Protocol version 1: FML)
/// </summary> /// </summary>
/// <returns>Whether the handshake was successful.</returns> /// <returns>Whether the handshake was successful.</returns>
public bool CompleteForgeHandshake() public async Task<bool> CompleteForgeHandshake(SocketWrapper socketWrapper)
{ {
if (ForgeEnabled() && forgeInfo!.Version == FMLVersion.FML) if (ForgeEnabled() && forgeInfo!.Version == FMLVersion.FML)
{ {
while (fmlHandshakeState != FMLHandshakeClientState.DONE) while (fmlHandshakeState != FMLHandshakeClientState.DONE)
{ {
(int packetID, Queue<byte> packetData) = protocol18.ReadNextPacket(); (int packetID, PacketStream packetStream) = await socketWrapper.GetNextPacket(handleCompress: true);
if (packetID == 0x40) // Disconnect 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; return false;
} }
else else
{ {
// Send back regular packet to the vanilla protocol handler // 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> /// </summary>
/// <param name="packetData">Packet data to read from</param> /// <param name="packetData">Packet data to read from</param>
/// <returns>Length from packet data</returns> /// <returns>Length from packet data</returns>
public int ReadNextVarShort(Queue<byte> packetData) public int ReadNextVarShort(PacketStream packetData)
{ {
if (ForgeEnabled()) if (ForgeEnabled())
{ {
@ -103,10 +106,11 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="packetData">Plugin message data</param> /// <param name="packetData">Plugin message data</param>
/// <param name="currentDimension">Current world dimension</param> /// <param name="currentDimension">Current world dimension</param>
/// <returns>TRUE if the plugin message was recognized and handled</returns> /// <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) if (ForgeEnabled() && forgeInfo!.Version == FMLVersion.FML && fmlHandshakeState != FMLHandshakeClientState.DONE)
{ {
Queue<byte> packetData = new(packetDataArr);
if (channel == "FML|HS") if (channel == "FML|HS")
{ {
FMLHandshakeDiscriminator discriminator = (FMLHandshakeDiscriminator)dataTypes.ReadNextByte(packetData); FMLHandshakeDiscriminator discriminator = (FMLHandshakeDiscriminator)dataTypes.ReadNextByte(packetData);
@ -114,21 +118,21 @@ namespace MinecraftClient.Protocol.Handlers
if (discriminator == FMLHandshakeDiscriminator.HandshakeReset) if (discriminator == FMLHandshakeDiscriminator.HandshakeReset)
{ {
fmlHandshakeState = FMLHandshakeClientState.START; fmlHandshakeState = FMLHandshakeClientState.START;
return true; return new(true, currentDimension);
} }
switch (fmlHandshakeState) switch (fmlHandshakeState)
{ {
case FMLHandshakeClientState.START: case FMLHandshakeClientState.START:
if (discriminator != FMLHandshakeDiscriminator.ServerHello) if (discriminator != FMLHandshakeDiscriminator.ServerHello)
return false; return new(false, currentDimension);
// Send the plugin channel registration. // Send the plugin channel registration.
// REGISTER is somewhat special in that it doesn't actually include length information, // REGISTER is somewhat special in that it doesn't actually include length information,
// and is also \0-separated. // and is also \0-separated.
// Also, yes, "FML" is there twice. Don't ask me why, but that's the way forge does it. // 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" }; 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); byte fmlProtocolVersion = dataTypes.ReadNextByte(packetData);
@ -139,7 +143,7 @@ namespace MinecraftClient.Protocol.Handlers
currentDimension = dataTypes.ReadNextInt(packetData); currentDimension = dataTypes.ReadNextInt(packetData);
// Tell the server we're running the same version. // 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. // Then tell the server that we're running the same mods.
if (Settings.Config.Logging.DebugMessages) if (Settings.Config.Logging.DebugMessages)
@ -148,17 +152,17 @@ namespace MinecraftClient.Protocol.Handlers
for (int i = 0; i < forgeInfo.Mods.Count; i++) for (int i = 0; i < forgeInfo.Mods.Count; i++)
{ {
ForgeInfo.ForgeMod mod = forgeInfo.Mods[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))); dataTypes.ConcatBytes(dataTypes.GetVarInt(forgeInfo.Mods.Count), dataTypes.ConcatBytes(mods)));
fmlHandshakeState = FMLHandshakeClientState.WAITINGSERVERDATA; fmlHandshakeState = FMLHandshakeClientState.WAITINGSERVERDATA;
return true; return new(true, currentDimension);
case FMLHandshakeClientState.WAITINGSERVERDATA: case FMLHandshakeClientState.WAITINGSERVERDATA:
if (discriminator != FMLHandshakeDiscriminator.ModList) if (discriminator != FMLHandshakeDiscriminator.ModList)
return false; return new(false, currentDimension);
Thread.Sleep(2000); Thread.Sleep(2000);
@ -167,16 +171,16 @@ namespace MinecraftClient.Protocol.Handlers
// Tell the server that yes, we are OK with the mods it has // Tell the server that yes, we are OK with the mods it has
// even though we don't actually care what 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 }); new byte[] { (byte)FMLHandshakeClientState.WAITINGSERVERDATA });
fmlHandshakeState = FMLHandshakeClientState.WAITINGSERVERCOMPLETE; fmlHandshakeState = FMLHandshakeClientState.WAITINGSERVERCOMPLETE;
return false; return new(false, currentDimension);
case FMLHandshakeClientState.WAITINGSERVERCOMPLETE: case FMLHandshakeClientState.WAITINGSERVERCOMPLETE:
// The server now will tell us a bunch of registry information. // 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. // We need to read it all, though, until it says that there is no more.
if (discriminator != FMLHandshakeDiscriminator.RegistryData) if (discriminator != FMLHandshakeDiscriminator.RegistryData)
return false; return new(false, currentDimension);
if (protocolversion < Protocol18Handler.MC_1_8_Version) if (protocolversion < Protocol18Handler.MC_1_8_Version)
{ {
@ -202,34 +206,34 @@ namespace MinecraftClient.Protocol.Handlers
fmlHandshakeState = FMLHandshakeClientState.PENDINGCOMPLETE; fmlHandshakeState = FMLHandshakeClientState.PENDINGCOMPLETE;
} }
return false; return new(false, currentDimension);
case FMLHandshakeClientState.PENDINGCOMPLETE: case FMLHandshakeClientState.PENDINGCOMPLETE:
// The server will ask us to accept the registries. // The server will ask us to accept the registries.
// Just say yes. // Just say yes.
if (discriminator != FMLHandshakeDiscriminator.HandshakeAck) if (discriminator != FMLHandshakeDiscriminator.HandshakeAck)
return false; return new(false, currentDimension);
if (Settings.Config.Logging.DebugMessages) if (Settings.Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted("§8" + Translations.forge_accept_registry, acceptnewlines: true); ConsoleIO.WriteLineFormatted("§8" + Translations.forge_accept_registry, acceptnewlines: true);
SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck, await SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck,
new byte[] { (byte)FMLHandshakeClientState.PENDINGCOMPLETE }); new byte[] { (byte)FMLHandshakeClientState.PENDINGCOMPLETE });
fmlHandshakeState = FMLHandshakeClientState.COMPLETE; fmlHandshakeState = FMLHandshakeClientState.COMPLETE;
return true; return new(true, currentDimension);
case FMLHandshakeClientState.COMPLETE: case FMLHandshakeClientState.COMPLETE:
// One final "OK". On the actual forge source, a packet is sent from // 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 // the client to the client saying that the connection was complete, but
// we don't need to do that. // we don't need to do that.
SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck, await SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck,
new byte[] { (byte)FMLHandshakeClientState.COMPLETE }); new byte[] { (byte)FMLHandshakeClientState.COMPLETE });
if (Settings.Config.Logging.DebugMessages) if (Settings.Config.Logging.DebugMessages)
ConsoleIO.WriteLine(Translations.forge_complete); ConsoleIO.WriteLine(Translations.forge_complete);
fmlHandshakeState = FMLHandshakeClientState.DONE; fmlHandshakeState = FMLHandshakeClientState.DONE;
return true; return new(true, currentDimension);
} }
} }
} }
return false; return new(false, currentDimension);
} }
/// <summary> /// <summary>
@ -239,8 +243,9 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="packetData">Plugin message data</param> /// <param name="packetData">Plugin message data</param>
/// <param name="responseData">Response data to return to server</param> /// <param name="responseData">Response data to return to server</param>
/// <returns>TRUE/FALSE depending on whether the packet was understood or not</returns> /// <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") if (ForgeEnabled() && forgeInfo!.Version == FMLVersion.FML2 && channel == "fml:loginwrapper")
{ {
// Forge Handshake handler source code used to implement the FML2 packets: // 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 // 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. // FMLHandshakeHandler will then process the packet, e.g. handleServerModListOnClient() for Server Mod List.
string fmlChannel = dataTypes.ReadNextString(packetData); string fmlChannel = await dataTypes.ReadNextStringAsync(packetData);
dataTypes.ReadNextVarInt(packetData); // Packet length dataTypes.SkipNextVarInt(packetData); // Packet length
int packetID = dataTypes.ReadNextVarInt(packetData); int packetID = dataTypes.ReadNextVarInt(packetData);
if (fmlChannel == "fml:handshake") if (fmlChannel == "fml:handshake")
@ -308,17 +313,17 @@ namespace MinecraftClient.Protocol.Handlers
List<string> mods = new(); List<string> mods = new();
int modCount = dataTypes.ReadNextVarInt(packetData); int modCount = dataTypes.ReadNextVarInt(packetData);
for (int i = 0; i < modCount; i++) for (int i = 0; i < modCount; i++)
mods.Add(dataTypes.ReadNextString(packetData)); mods.Add(await dataTypes.ReadNextStringAsync(packetData));
Dictionary<string, string> channels = new(); Dictionary<string, string> channels = new();
int channelCount = dataTypes.ReadNextVarInt(packetData); int channelCount = dataTypes.ReadNextVarInt(packetData);
for (int i = 0; i < channelCount; i++) 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(); List<string> registries = new();
int registryCount = dataTypes.ReadNextVarInt(packetData); int registryCount = dataTypes.ReadNextVarInt(packetData);
for (int i = 0; i < registryCount; i++) 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() // Server Mod List Reply: FMLHandshakeMessages.java > C2SModListReply > encode()
// //
@ -372,7 +377,7 @@ namespace MinecraftClient.Protocol.Handlers
if (Settings.Config.Logging.DebugMessages) 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)); ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_fml2_registry, registryName));
} }
@ -391,7 +396,7 @@ namespace MinecraftClient.Protocol.Handlers
if (Settings.Config.Logging.DebugMessages) 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)); ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_fml2_config, configName));
} }
@ -408,11 +413,10 @@ namespace MinecraftClient.Protocol.Handlers
if (fmlResponseReady) if (fmlResponseReady)
{ {
// Wrap our FML packet into a LoginPluginResponse payload // Wrap our FML packet into a LoginPluginResponse payload
responseData.Clear();
responseData.AddRange(dataTypes.GetString(fmlChannel)); responseData.AddRange(dataTypes.GetString(fmlChannel));
responseData.AddRange(dataTypes.GetVarInt(fmlResponsePacket.Count)); responseData.AddRange(dataTypes.GetVarInt(fmlResponsePacket.Count));
responseData.AddRange(fmlResponsePacket); responseData.AddRange(fmlResponsePacket);
return true; return new(true, responseData);
} }
} }
else if (Settings.Config.Logging.DebugMessages) 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)); ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_fml2_unknown_channel, fmlChannel));
} }
} }
return false; return new(false, responseData);
} }
/// <summary> /// <summary>
@ -428,9 +432,9 @@ namespace MinecraftClient.Protocol.Handlers
/// </summary> /// </summary>
/// <param name="discriminator">Discriminator to use.</param> /// <param name="discriminator">Discriminator to use.</param>
/// <param name="data">packet Data</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> /// <summary>
@ -439,10 +443,10 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="jsonData">JSON data returned by the server</param> /// <param name="jsonData">JSON data returned by the server</param>
/// <param name="forgeInfo">ForgeInfo to populate</param> /// <param name="forgeInfo">ForgeInfo to populate</param>
/// <returns>True if the server is running Forge</returns> /// <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 return ServerInfoCheckForgeSubFML1(jsonData, ref forgeInfo) // MC 1.12 and lower
|| ServerInfoCheckForgeSub(jsonData, ref forgeInfo, FMLVersion.FML2); // MC 1.13 and greater || ServerInfoCheckForgeSubFML2(jsonData, ref forgeInfo); // MC 1.13 and greater
} }
/// <summary> /// <summary>
@ -474,38 +478,21 @@ namespace MinecraftClient.Protocol.Handlers
/// </summary> /// </summary>
/// <param name="jsonData">JSON data returned by the server</param> /// <param name="jsonData">JSON data returned by the server</param>
/// <param name="forgeInfo">ForgeInfo to populate</param> /// <param name="forgeInfo">ForgeInfo to populate</param>
/// <param name="fmlVersion">Forge protocol version</param>
/// <returns>True if the server is running Forge</returns> /// <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; if (jsonData.modinfo != null)
string versionField;
string versionString;
switch (fmlVersion)
{ {
case FMLVersion.FML: if (jsonData.modinfo.type == "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)
{ {
forgeInfo = new ForgeInfo(modData, fmlVersion); if (jsonData.modinfo.modList == null || jsonData.modinfo.modList.Length == 0)
if (forgeInfo.Mods.Any())
{ {
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)); ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_with_mod, forgeInfo.Mods.Count));
if (Settings.Config.Logging.DebugMessages) if (Settings.Config.Logging.DebugMessages)
{ {
@ -515,10 +502,39 @@ namespace MinecraftClient.Protocol.Handlers
} }
return true; 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 else
{ {
ConsoleIO.WriteLineFormatted("§8" + Translations.forge_no_mod, acceptnewlines: true); forgeInfo = new ForgeInfo(jsonData.forgeData.mods, FMLVersion.FML2);
forgeInfo = null; 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.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks;
//using System.Linq; //using System.Linq;
//using System.Text; //using System.Text;
using MinecraftClient.Mapping; using MinecraftClient.Mapping;
using MinecraftClient.Protocol.PacketPipeline;
namespace MinecraftClient.Protocol.Handlers namespace MinecraftClient.Protocol.Handlers
{ {
@ -33,21 +35,21 @@ namespace MinecraftClient.Protocol.Handlers
/// <summary> /// <summary>
/// Reading the "Block states" field: consists of 4096 entries, representing all the blocks in the chunk section. /// Reading the "Block states" field: consists of 4096 entries, representing all the blocks in the chunk section.
/// </summary> /// </summary>
/// <param name="cache">Cache for reading data</param> /// <param name="stream">Cache for reading data</param>
[MethodImpl(MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveOptimization)]
private Chunk? ReadBlockStatesField(Queue<byte> cache) private async Task<Chunk?> ReadBlockStatesFieldAsync(PacketStream stream)
{ {
// read Block states (Type: Paletted Container) // 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 // 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) if (bitsPerEntry == 0 && protocolversion >= Protocol18Handler.MC_1_18_1_Version)
{ {
// Palettes: Single valued - 1.18(1.18.1) and above // 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); 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 // Empty chunks will not be stored
if (block.Type == Material.Air) if (block.Type == Material.Air)
@ -73,16 +75,16 @@ namespace MinecraftClient.Protocol.Handlers
// EG, if bitsPerEntry = 5, valueMask = 00011111 in binary // EG, if bitsPerEntry = 5, valueMask = 00011111 in binary
uint valueMask = (uint)((1 << bitsPerEntry) - 1); 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++) 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 //// Block IDs are packed in the array of 64-bits integers
dataTypes.SkipNextVarInt(cache); // Entry length dataTypes.SkipNextVarInt(stream); // Entry length
Span<byte> entryDataByte = stackalloc byte[8];
Span<long> entryDataLong = MemoryMarshal.Cast<byte, long>(entryDataByte); // Faster than MemoryMarshal.Read<long> long entryData = 0;
Chunk chunk = new(); Chunk chunk = new();
int startOffset = 64; // Read the first data immediately 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 // When overlapping, move forward to the beginning of the next Long
startOffset = 0; 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 // Map small IDs to actual larger block IDs
if (usePalette) if (usePalette)
@ -141,10 +143,10 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="chunkX">Chunk X location</param> /// <param name="chunkX">Chunk X location</param>
/// <param name="chunkZ">Chunk Z 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="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> /// <param name="cancellationToken">token to cancel the task</param>
[MethodImpl(MethodImplOptions.AggressiveOptimization)] [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(); World world = handler.GetWorld();
@ -181,10 +183,10 @@ namespace MinecraftClient.Protocol.Handlers
((verticalStripBitmask![chunkY / 64] & (1UL << (chunkY % 64))) != 0)) ((verticalStripBitmask![chunkY / 64] & (1UL << (chunkY % 64))) != 0))
{ {
// Non-air block count inside chunk section, for lighting purposes // 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) // 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 //We have our chunk, save the chunk into the world
world.StoreChunk(chunkX, chunkY, chunkZ, chunkColumnSize, chunk, chunkY == lastChunkY); 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 // Skip Read Biomes (Type: Paletted Container) - 1.18(1.18.1) and above
if (protocolversion >= Protocol18Handler.MC_1_18_1_Version) 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) if (bitsPerEntryBiome == 0)
{ {
dataTypes.SkipNextVarInt(cache); // Value dataTypes.SkipNextVarInt(stream); // Value
dataTypes.SkipNextVarInt(cache); // Data Array Length dataTypes.SkipNextVarInt(stream); // Data Array Length
// Data Array must be empty // Data Array must be empty
} }
else else
{ {
if (bitsPerEntryBiome <= 3) 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++) for (int i = 0; i < paletteLength; i++)
dataTypes.SkipNextVarInt(cache); // Palette dataTypes.SkipNextVarInt(stream); // Palette
} }
int dataArrayLength = dataTypes.ReadNextVarInt(cache); // Data Array Length int dataArrayLength = await dataTypes.ReadNextVarIntAsync(stream); // Data Array Length
dataTypes.DropData(dataArrayLength * 8, cache); // Data Array 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="hasSkyLight">Contains skylight info</param>
/// <param name="chunksContinuous">Are the chunk continuous</param> /// <param name="chunksContinuous">Are the chunk continuous</param>
/// <param name="currentDimension">Current dimension type (0 = overworld)</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> /// <param name="cancellationToken">token to cancel the task</param>
[MethodImpl(MethodImplOptions.AggressiveOptimization)] [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(); 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 // 1.14 and above Non-air block count inside chunk section, for lighting purposes
if (protocolversion >= Protocol18Handler.MC_1_14_Version) 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); bool usePalette = (bitsPerBlock <= 8);
// Vanilla Minecraft will use at least 4 bits per block // 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 // 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 int paletteLength = 0; // Assume zero when length is absent
if (usePalette || protocolversion < Protocol18Handler.MC_1_13_Version) if (usePalette || protocolversion < Protocol18Handler.MC_1_13_Version)
paletteLength = dataTypes.ReadNextVarInt(cache); paletteLength = await dataTypes.ReadNextVarIntAsync(stream);
int[] palette = new int[paletteLength]; int[] palette = new int[paletteLength];
for (int i = 0; i < paletteLength; i++) for (int i = 0; i < paletteLength; i++)
{ {
palette[i] = dataTypes.ReadNextVarInt(cache); palette[i] = await dataTypes.ReadNextVarIntAsync(stream);
} }
// Bit mask covering bitsPerBlock bits // Bit mask covering bitsPerBlock bits
@ -273,7 +275,7 @@ namespace MinecraftClient.Protocol.Handlers
uint valueMask = (uint)((1 << bitsPerBlock) - 1); uint valueMask = (uint)((1 << bitsPerBlock) - 1);
// Block IDs are packed in the array of 64-bits integers // 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(); 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); 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) if (protocolversion < Protocol18Handler.MC_1_14_Version)
{ {
//Skip block light // Skip block light
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache); await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, stream);
//Skip sky light // Skip sky light
if (currentDimension == 0) if (currentDimension == 0)
// Sky light is not sent in the nether or the end // 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 // 1.8 chunk format
if (chunksContinuous && chunkMask == 0) if (chunksContinuous && chunkMask == 0)
{ {
//Unload the entire chunk column // Unload the entire chunk column
handler.InvokeOnMainThread(() => world[chunkX, chunkZ] = null;
{
world[chunkX, chunkZ] = null;
});
} }
else else
{ {
//Load chunk data from the server // Load chunk data from the server
int maxChunkY = sizeof(int) * 8 - 1 - BitOperations.LeadingZeroCount(chunkMask); int maxChunkY = sizeof(int) * 8 - 1 - BitOperations.LeadingZeroCount(chunkMask);
for (int chunkY = 0; chunkY <= maxChunkY; chunkY++) for (int chunkY = 0; chunkY <= maxChunkY; chunkY++)
{ {
@ -399,35 +398,34 @@ namespace MinecraftClient.Protocol.Handlers
{ {
Chunk chunk = new(); Chunk chunk = new();
//Read chunk data, all at once for performance reasons, and build the chunk object // 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));
for (int blockY = 0; blockY < Chunk.SizeY; blockY++) for (int blockY = 0; blockY < Chunk.SizeY; blockY++)
for (int blockZ = 0; blockZ < Chunk.SizeZ; blockZ++) for (int blockZ = 0; blockZ < Chunk.SizeZ; blockZ++)
for (int blockX = 0; blockX < Chunk.SizeX; blockX++) 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); world.StoreChunk(chunkX, chunkY, chunkZ, chunkColumnSize, chunk, chunkY == maxChunkY);
} }
} }
//Skip light information // Skip light information
for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++) for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++)
{ {
if ((chunkMask & (1 << chunkY)) != 0) if ((chunkMask & (1 << chunkY)) != 0)
{ {
//Skip block light // Skip block light
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache); await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, stream);
//Skip sky light // Skip sky light
if (hasSkyLight) 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) if (chunksContinuous)
dataTypes.DropData(Chunk.SizeX * Chunk.SizeZ, cache); await dataTypes.DropDataAsync(Chunk.SizeX * Chunk.SizeZ, stream);
} }
} }
else else
@ -435,15 +433,12 @@ namespace MinecraftClient.Protocol.Handlers
// 1.7 chunk format // 1.7 chunk format
if (chunksContinuous && chunkMask == 0) if (chunksContinuous && chunkMask == 0)
{ {
//Unload the entire chunk column // Unload the entire chunk column
handler.InvokeOnMainThread(() => world[chunkX, chunkZ] = null;
{
world[chunkX, chunkZ] = null;
});
} }
else else
{ {
//Count chunk sections // Count chunk sections
int sectionCount = 0; int sectionCount = 0;
int addDataSectionCount = 0; int addDataSectionCount = 0;
for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++) for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++)
@ -454,10 +449,10 @@ namespace MinecraftClient.Protocol.Handlers
addDataSectionCount++; addDataSectionCount++;
} }
//Read chunk data, unpacking 4-bit values into 8-bit values for block metadata // 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)); Queue<byte> blockTypes = new(await dataTypes.ReadDataAsync(Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount, stream));
Queue<byte> blockMeta = new(); 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 hig = (byte)(packed >> 4);
byte low = (byte)(packed & (byte)0x0F); byte low = (byte)(packed & (byte)0x0F);
@ -465,15 +460,15 @@ namespace MinecraftClient.Protocol.Handlers
blockMeta.Enqueue(low); blockMeta.Enqueue(low);
} }
//Skip data we don't need // Skip data we don't need
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, cache); //Block light await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, stream); //Block light
if (hasSkyLight) if (hasSkyLight)
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, cache); //Sky light await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, stream); //Sky light
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * addDataSectionCount) / 2, cache); //BlockAdd await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * addDataSectionCount) / 2, stream); //BlockAdd
if (chunksContinuous) 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); int maxChunkY = sizeof(int) * 8 - 1 - BitOperations.LeadingZeroCount(chunkMask);
for (int chunkY = 0; chunkY <= maxChunkY; chunkY++) 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using MinecraftClient.Inventory; using MinecraftClient.Inventory;
using MinecraftClient.Mapping; using MinecraftClient.Mapping;
using MinecraftClient.Protocol.ProfileKey; using MinecraftClient.Protocol.ProfileKey;
@ -19,7 +22,12 @@ namespace MinecraftClient.Protocol
/// Start the login procedure once connected to the server /// Start the login procedure once connected to the server
/// </summary> /// </summary>
/// <returns>True if login was successful</returns> /// <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> /// <summary>
/// Disconnect from the server /// Disconnect from the server
@ -46,20 +54,20 @@ namespace MinecraftClient.Protocol
/// </summary> /// </summary>
/// <param name="message">Text to send</param> /// <param name="message">Text to send</param>
/// <returns>True if successfully sent</returns> /// <returns>True if successfully sent</returns>
bool SendChatMessage(string message, PlayerKeyPair? playerKeyPair = null); Task<bool> SendChatMessage(string message, PlayerKeyPair? playerKeyPair = null);
/// <summary> /// <summary>
/// Allow to respawn after death /// Allow to respawn after death
/// </summary> /// </summary>
/// <returns>True if packet successfully sent</returns> /// <returns>True if packet successfully sent</returns>
bool SendRespawnPacket(); Task<bool> SendRespawnPacket();
/// <summary> /// <summary>
/// Inform the server of the client being used to connect /// Inform the server of the client being used to connect
/// </summary> /// </summary>
/// <param name="brandInfo">Client string describing the client</param> /// <param name="brandInfo">Client string describing the client</param>
/// <returns>True if brand info was successfully sent</returns> /// <returns>True if brand info was successfully sent</returns>
bool SendBrandInfo(string brandInfo); Task<bool> SendBrandInfo(string brandInfo);
/// <summary> /// <summary>
/// Inform the server of the client's Minecraft settings /// 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="skinParts">Show skin layers</param>
/// <param name="mainHand">1.9+ main hand</param> /// <param name="mainHand">1.9+ main hand</param>
/// <returns>True if client settings were successfully sent</returns> /// <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> /// <summary>
/// Send a location update telling that we moved to that location /// 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="yaw">The new yaw (optional)</param>
/// <param name="pitch">The new pitch (optional)</param> /// <param name="pitch">The new pitch (optional)</param>
/// <returns>True if packet was successfully sent</returns> /// <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> /// <summary>
/// Send a plugin channel packet to the server. /// 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="channel">Channel to send packet on</param>
/// <param name="data">packet Data</param> /// <param name="data">packet Data</param>
/// <returns>True if message was successfully sent</returns> /// <returns>True if message was successfully sent</returns>
bool SendPluginChannelPacket(string channel, byte[] data); Task<bool> SendPluginChannelPacket(string channel, byte[] data);
/// <summary> /// <summary>
/// Send Entity Action packet to the server. /// Send Entity Action packet to the server.
@ -99,14 +107,14 @@ namespace MinecraftClient.Protocol
/// <param name="entityID">PlayerID</param> /// <param name="entityID">PlayerID</param>
/// <param name="type">Type of packet to send</param> /// <param name="type">Type of packet to send</param>
/// <returns>True if packet was successfully sent</returns> /// <returns>True if packet was successfully sent</returns>
bool SendEntityAction(int EntityID, int type); Task<bool> SendEntityAction(int EntityID, int type);
/// <summary> /// <summary>
/// Send a held item change packet to the server. /// Send a held item change packet to the server.
/// </summary> /// </summary>
/// <param name="slot">New active slot in the inventory hotbar</param> /// <param name="slot">New active slot in the inventory hotbar</param>
/// <returns>True if packet was successfully sent</returns> /// <returns>True if packet was successfully sent</returns>
bool SendHeldItemChange(short slot); Task<bool> SendHeldItemChange(short slot);
/// <summary> /// <summary>
/// Send an entity interaction packet to the server. /// 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="EntityID">Entity ID to interact with</param>
/// <param name="type">Type of interaction (0: interact, 1: attack, 2: interact at)</param> /// <param name="type">Type of interaction (0: interact, 1: attack, 2: interact at)</param>
/// <returns>True if packet was successfully sent</returns> /// <returns>True if packet was successfully sent</returns>
bool SendInteractEntity(int EntityID, int type); Task<bool> SendInteractEntity(int EntityID, int type);
/// <summary> /// <summary>
/// Send an entity interaction packet to the server. /// 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="Z">Z coordinate for "interact at"</param>
/// <param name="hand">Player hand (0: main hand, 1: off hand)</param> /// <param name="hand">Player hand (0: main hand, 1: off hand)</param>
/// <returns>True if packet was successfully sent</returns> /// <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> /// <summary>
/// Send an entity interaction packet to the server. /// 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="Y">Y coordinate for "interact at"</param>
/// <param name="Z">Z coordinate for "interact at"</param> /// <param name="Z">Z coordinate for "interact at"</param>
/// <returns>True if packet was successfully sent</returns> /// <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> /// <summary>
/// Send an entity interaction packet to the server. /// 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="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> /// <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> /// <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> /// <summary>
/// Send a use item packet to the server /// 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="hand">0: main hand, 1: off hand</param>
/// <param name="sequenceId">Sequence ID used for synchronization</param> /// <param name="sequenceId">Sequence ID used for synchronization</param>
/// <returns>True if packet was successfully sent</returns> /// <returns>True if packet was successfully sent</returns>
bool SendUseItem(int hand, int sequenceId); Task<bool> SendUseItem(int hand, int sequenceId);
/// <summary> /// <summary>
/// Send a click window slot packet to the server /// 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="changedSlots">Slots that have been changed in this event: List<SlotID, Changed Items> </param>
/// <param name="stateId">Inventory's stateId</param> /// <param name="stateId">Inventory's stateId</param>
/// <returns>True if packet was successfully sent</returns> /// <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> /// <summary>
/// Request Creative Mode item creation into regular/survival Player Inventory /// 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="count">Item count</param>
/// <param name="nbt">Optional item NBT</param> /// <param name="nbt">Optional item NBT</param>
/// <returns>TRUE if item given successfully</returns> /// <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> /// <summary>
/// Send a click container button packet to the server. /// 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> /// <param name="buttonId">Id of the clicked button</param>
/// <returns>True if packet was successfully sent</returns> /// <returns>True if packet was successfully sent</returns>
bool ClickContainerButton(int windowId, int buttonId); Task<bool> ClickContainerButton(int windowId, int buttonId);
/// <summary> /// <summary>
/// Plays animation /// Plays animation
@ -195,13 +203,13 @@ namespace MinecraftClient.Protocol
/// <param name="animation">0 for left arm, 1 for right arm</param> /// <param name="animation">0 for left arm, 1 for right arm</param>
/// <param name="playerid">Player Entity ID</param> /// <param name="playerid">Player Entity ID</param>
/// <returns>TRUE if item given successfully</returns> /// <returns>TRUE if item given successfully</returns>
bool SendAnimation(int animation, int playerid); Task<bool> SendAnimation(int animation, int playerid);
/// <summary> /// <summary>
/// Send a close window packet to the server /// Send a close window packet to the server
/// </summary> /// </summary>
/// <param name="windowId">Id of the window being closed</param> /// <param name="windowId">Id of the window being closed</param>
bool SendCloseWindow(int windowId); Task<bool> SendCloseWindow(int windowId);
/// <summary> /// <summary>
/// Send player block placement packet to the server /// Send player block placement packet to the server
@ -211,7 +219,7 @@ namespace MinecraftClient.Protocol
/// <param name="face">Block face</param> /// <param name="face">Block face</param>
/// <param name="sequenceId">Sequence ID (use for synchronization)</param> /// <param name="sequenceId">Sequence ID (use for synchronization)</param>
/// <returns>True if packet was successfully sent</returns> /// <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> /// <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 /// 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="face">Block face</param>
/// <param name="sequenceId">Sequence ID (use for synchronization)</param> /// <param name="sequenceId">Sequence ID (use for synchronization)</param>
/// <returns>True if packet was succcessfully sent</returns> /// <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> /// <summary>
/// Change text on a sign /// Change text on a sign
@ -232,7 +240,7 @@ namespace MinecraftClient.Protocol
/// <param name="line3">New line 3</param> /// <param name="line3">New line 3</param>
/// <param name="line4">New line 4</param> /// <param name="line4">New line 4</param>
/// <returns>True if packet was succcessfully sent</returns> /// <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> /// <summary>
/// Update command block /// Update command block
@ -241,24 +249,18 @@ namespace MinecraftClient.Protocol
/// <param name="command">command</param> /// <param name="command">command</param>
/// <param name="mode">command block mode</param> /// <param name="mode">command block mode</param>
/// <param name="flags">command block flags</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> /// <summary>
/// Select villager trade /// Select villager trade
/// </summary> /// </summary>
/// <param name="selectedSlot">The slot of the trade, starts at 0.</param> /// <param name="selectedSlot">The slot of the trade, starts at 0.</param>
bool SelectTrade(int selectedSlot); Task<bool> SelectTrade(int selectedSlot);
/// <summary> /// <summary>
/// Spectate a player/entity /// Spectate a player/entity
/// </summary> /// </summary>
/// <param name="uuid">The uuid of the player/entity to spectate/teleport to.</param> /// <param name="uuid">The uuid of the player/entity to spectate/teleport to.</param>
bool SendSpectate(Guid uuid); Task<bool> SendSpectate(Guid uuid);
/// <summary>
/// Get net read thread (main thread) ID
/// </summary>
/// <returns>Net read thread ID</returns>
int GetNetMainThreadId();
} }
} }

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
using MinecraftClient.Inventory; using MinecraftClient.Inventory;
using MinecraftClient.Logger; using MinecraftClient.Logger;
using MinecraftClient.Mapping; using MinecraftClient.Mapping;
@ -18,7 +19,6 @@ namespace MinecraftClient.Protocol
{ {
/* The MinecraftCom Handler must /* The MinecraftCom Handler must
* provide these getters */ * provide these getters */
int GetServerPort(); int GetServerPort();
string GetServerHost(); string GetServerHost();
string GetUsername(); string GetUsername();
@ -43,26 +43,6 @@ namespace MinecraftClient.Protocol
Container? GetInventory(int inventoryID); Container? GetInventory(int inventoryID);
ILogger GetLogger(); 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> /// <summary>
/// Called when a network packet received or sent /// Called when a network packet received or sent
/// </summary> /// </summary>
@ -78,7 +58,7 @@ namespace MinecraftClient.Protocol
/// <summary> /// <summary>
/// Called when a server was successfully joined /// Called when a server was successfully joined
/// </summary> /// </summary>
void OnGameJoined(); Task OnGameJoined();
/// <summary> /// <summary>
/// Received chat/system message from the server /// Received chat/system message from the server
@ -184,21 +164,21 @@ namespace MinecraftClient.Protocol
/// Called ~10 times per second (10 ticks per second) /// Called ~10 times per second (10 ticks per second)
/// Useful for updating bots in other parts of the program /// Useful for updating bots in other parts of the program
/// </summary> /// </summary>
void OnUpdate(); Task OnUpdate();
/// <summary> /// <summary>
/// Registers the given plugin channel for the given bot. /// Registers the given plugin channel for the given bot.
/// </summary> /// </summary>
/// <param name="channel">The channel to register.</param> /// <param name="channel">The channel to register.</param>
/// <param name="bot">The bot to register the channel for.</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> /// <summary>
/// Unregisters the given plugin channel for the given bot. /// Unregisters the given plugin channel for the given bot.
/// </summary> /// </summary>
/// <param name="channel">The channel to unregister.</param> /// <param name="channel">The channel to unregister.</param>
/// <param name="bot">The bot to unregister the channel for.</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> /// <summary>
/// Sends a plugin channel packet to the server. /// 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="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> /// <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> /// <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> /// <summary>
/// Called when a plugin channel message was sent from the server. /// Called when a plugin channel message was sent from the server.
@ -348,7 +328,7 @@ namespace MinecraftClient.Protocol
/// </summary> /// </summary>
/// <param name="uuid">Affected player's UUID</param> /// <param name="uuid">Affected player's UUID</param>
/// <param name="gamemode">New game mode</param> /// <param name="gamemode">New game mode</param>
void OnGamemodeUpdate(Guid uuid, int gamemode); Task OnGamemodeUpdate(Guid uuid, int gamemode);
/// <summary> /// <summary>
/// Called when a player's latency has changed /// Called when a player's latency has changed
@ -472,6 +452,6 @@ namespace MinecraftClient.Protocol
/// <param name="buttonId">Id of the clicked button</param> /// <param name="buttonId">Id of the clicked button</param>
/// <returns>True if packet was successfully sent</returns> /// <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;
using System.IO;
using System.Text; using System.Text;
namespace MinecraftClient.Protocol 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 // 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 class JwtPayloadDecode
{ {
public static string GetPayload(string token) public static MemoryStream GetPayload(string token)
{ {
var content = token.Split('.')[1]; var content = token.Split('.')[1];
var jsonPayload = Encoding.UTF8.GetString(Decode(content)); return new MemoryStream(Decode(content));
return jsonPayload;
} }
private static byte[] Decode(string input) private static byte[] Decode(string input)
@ -23,7 +23,7 @@ namespace MinecraftClient.Protocol
case 0: break; // No pad chars in this case case 0: break; // No pad chars in this case
case 2: output += "=="; break; // Two pad chars case 2: output += "=="; break; // Two pad chars
case 3: output += "="; break; // One pad char 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 var converted = Convert.FromBase64String(output); // Standard base64 decoder
return converted; return converted;

View file

@ -3,9 +3,18 @@ using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Runtime.InteropServices; 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.Text.RegularExpressions;
using System.Threading.Tasks;
using MinecraftClient.Protocol.ProfileKey;
using static MinecraftClient.Settings; using static MinecraftClient.Settings;
using static MinecraftClient.Settings.MainConfigHealper.MainConfig.GeneralConfig; using static MinecraftClient.Settings.MainConfigHealper.MainConfig.GeneralConfig;
@ -13,9 +22,10 @@ namespace MinecraftClient.Protocol
{ {
static class Microsoft static class Microsoft
{ {
private static readonly string clientId = "54473e32-df8f-42e9-a649-9419b0dab9d3"; private const 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 const string tokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
private static readonly 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; } } public static string SignInUrl { get { return signinUrl; } }
@ -26,7 +36,7 @@ namespace MinecraftClient.Protocol
/// <returns>Sign-in URL with email pre-filled</returns> /// <returns>Sign-in URL with email pre-filled</returns>
public static string GetSignInUrlWithHint(string loginHint) public static string GetSignInUrlWithHint(string loginHint)
{ {
return SignInUrl + "&login_hint=" + Uri.EscapeDataString(loginHint); return $"{SignInUrl}&login_hint={Uri.EscapeDataString(loginHint)}";
} }
/// <summary> /// <summary>
@ -34,11 +44,16 @@ namespace MinecraftClient.Protocol
/// </summary> /// </summary>
/// <param name="code">Auth code obtained after user signing in</param> /// <param name="code">Auth code obtained after user signing in</param>
/// <returns>Access token and refresh token</returns> /// <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}"; FormUrlEncodedContent postData = new(new KeyValuePair<string, string>[]
postData = string.Format(postData, clientId, code); {
return RequestToken(postData); 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> /// <summary>
@ -46,11 +61,43 @@ namespace MinecraftClient.Protocol
/// </summary> /// </summary>
/// <param name="refreshToken">Refresh token</param> /// <param name="refreshToken">Refresh token</param>
/// <returns>Access token and new refresh token</returns> /// <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}"; FormUrlEncodedContent postData = new(new KeyValuePair<string, string>[]
postData = string.Format(postData, clientId, refreshToken); {
return RequestToken(postData); 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> /// <summary>
@ -58,45 +105,88 @@ namespace MinecraftClient.Protocol
/// </summary> /// </summary>
/// <param name="postData">Complete POST data for the request</param> /// <param name="postData">Complete POST data for the request</param>
/// <returns></returns> /// <returns></returns>
private static LoginResponse RequestToken(string postData) private static async Task<LoginResponse> RequestTokenAsync(HttpClient httpClient, FormUrlEncodedContent postData)
{ {
var request = new ProxiedWebRequest(tokenUrl) using HttpResponseMessage response = await httpClient.PostAsync(tokenUrl, postData);
{
UserAgent = "MCC/" + Program.Version TokenInfo jsonData = (await response.Content.ReadFromJsonAsync<TokenInfo>())!;
};
var response = request.Post("application/x-www-form-urlencoded", postData);
var jsonData = Json.ParseJson(response.Body);
// Error handling // 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 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 // Extract email from JWT
string payload = JwtPayloadDecode.GetPayload(jsonData.Properties["id_token"].StringValue); Stream payload = JwtPayloadDecode.GetPayload(jsonData.id_token!);
var jsonPayload = Json.ParseJson(payload); JwtPayloadInIdToken jsonPayload = (await JsonSerializer.DeserializeAsync<JwtPayloadInIdToken>(payload))!;
string email = jsonPayload.Properties["email"].StringValue;
return new LoginResponse() return new LoginResponse()
{ {
Email = email, Email = jsonPayload.email!,
AccessToken = accessToken, AccessToken = jsonData.access_token!,
RefreshToken = refreshToken, RefreshToken = jsonData.refresh_token!,
ExpiresIn = expiresIn 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) public static void OpenBrowser(string link)
{ {
try try
{ {
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) if (OperatingSystem.IsWindows())
{ {
var ps = new ProcessStartInfo(link) var ps = new ProcessStartInfo(link)
{ {
@ -106,11 +196,11 @@ namespace MinecraftClient.Protocol
Process.Start(ps); Process.Start(ps);
} }
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) else if (OperatingSystem.IsLinux())
{ {
Process.Start("xdg-open", link); Process.Start("xdg-open", link);
} }
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) else if (OperatingSystem.IsMacOS())
{ {
Process.Start("open", link); 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"; 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 xbl = "https://user.auth.xboxlive.com/user/authenticate";
private static readonly string xsts = "https://xsts.auth.xboxlive.com/xsts/authorize";
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 JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.General)
private static readonly Regex urlPost = new("urlPost:'(.+?(?=\'))"); {
private static readonly Regex confirm = new("identity\\/confirm"); AllowTrailingCommas = true,
private static readonly Regex invalidAccount = new("Sign in to", RegexOptions.IgnoreCase); PropertyNameCaseInsensitive = false,
private static readonly Regex twoFA = new("Help us protect your account", RegexOptions.IgnoreCase); ReadCommentHandling = JsonCommentHandling.Skip,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
public static string SignInUrl { get { return authorize; } } 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> /// <summary>
/// Pre-authentication /// Pre-authentication
/// </summary> /// </summary>
/// <remarks>This step is to get the login page for later use</remarks> /// <remarks>This step is to get the login page for later use</remarks>
/// <returns></returns> /// <returns></returns>
public static PreAuthResponse PreAuth() public static async Task<PreAuthResponse> PreAuthAsync(HttpClient httpClient)
{ {
var request = new ProxiedWebRequest(authorize) using HttpResponseMessage response = await httpClient.GetAsync(authorize);
{
UserAgent = userAgent
};
var response = request.Get();
string html = response.Body; string html = await response.Content.ReadAsStringAsync();
string PPFT = ppft.Match(html).Groups[1].Value; string PPFT = GetPpftRegex().Match(html).Groups[1].Value;
string urlPost = XboxLive.urlPost.Match(html).Groups[1].Value;
string urlPost = GetUrlPostRegex().Match(html).Groups[1].Value;
if (string.IsNullOrEmpty(PPFT) || string.IsNullOrEmpty(urlPost)) if (string.IsNullOrEmpty(PPFT) || string.IsNullOrEmpty(urlPost))
{ {
throw new Exception("Fail to extract PPFT or 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() return new PreAuthResponse()
{ {
UrlPost = urlPost, UrlPost = urlPost,
PPFT = PPFT, PPFT = PPFT,
Cookie = response.Cookies Cookie = new()// response.Cookies
}; };
} }
@ -192,69 +315,54 @@ namespace MinecraftClient.Protocol
/// <param name="password">Account password</param> /// <param name="password">Account password</param>
/// <param name="preAuth"></param> /// <param name="preAuth"></param>
/// <returns></returns> /// <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) using HttpResponseMessage response = await httpClient.PostAsync(preAuth.UrlPost, postData);
+ "&loginfmt=" + Uri.EscapeDataString(email)
+ "&passwd=" + Uri.EscapeDataString(password)
+ "&PPFT=" + Uri.EscapeDataString(preAuth.PPFT);
var response = request.Post("application/x-www-form-urlencoded", postData); if (Config.Logging.DebugMessages)
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLine(response.ToString()); ConsoleIO.WriteLine(response.ToString());
}
if (response.StatusCode >= 300 && response.StatusCode <= 399) if (response.IsSuccessStatusCode)
{ {
string url = response.Headers.Get("Location")!; string hash = response.RequestMessage!.RequestUri!.Fragment[1..];
string hash = url.Split('#')[1];
var request2 = new ProxiedWebRequest(url);
var response2 = request2.Get();
if (response2.StatusCode != 200)
{
throw new Exception("Authentication failed");
}
if (string.IsNullOrEmpty(hash)) if (string.IsNullOrEmpty(hash))
{
throw new Exception("Cannot extract access token"); throw new Exception("Cannot extract access token");
}
var dict = Request.ParseQueryString(hash);
//foreach (var pair in dict) var dict = Request.ParseQueryString(hash);
//{
// Console.WriteLine("{0}: {1}", pair.Key, pair.Value);
//}
return new Microsoft.LoginResponse() return new Microsoft.LoginResponse()
{ {
Email = email, Email = email,
AccessToken = dict["access_token"], AccessToken = dict["access_token"],
RefreshToken = dict["refresh_token"], RefreshToken = dict["refresh_token"],
ExpiresIn = int.Parse(dict["expires_in"], NumberStyles.Any, CultureInfo.CurrentCulture) ExpiresIn = int.Parse(dict["expires_in"])
}; };
} }
else else
{ {
if (twoFA.IsMatch(response.Body)) string body = await response.Content.ReadAsStringAsync();
if (GetTwoFARegex().IsMatch(body))
{ {
// TODO: Handle 2FA // 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"); 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"); 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> /// </summary>
/// <param name="loginResponse"></param> /// <param name="loginResponse"></param>
/// <returns></returns> /// <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) string accessToken;
{
UserAgent = userAgent,
Accept = "application/json"
};
request.Headers.Add("x-xbl-contract-version", "0");
var accessToken = loginResponse.AccessToken;
if (Config.Main.General.Method == LoginMethod.browser) if (Config.Main.General.Method == LoginMethod.browser)
{ {
// Our own client ID must have d= in front of the token or HTTP status 400 // 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 // "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 = "{" AuthPayload payload = new()
+ "\"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)
{ {
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()); ConsoleIO.WriteLine(response.ToString());
}
if (response.StatusCode == 200)
{
string jsonString = response.Body;
//Console.WriteLine(jsonString);
Json.JSONData json = Json.ParseJson(jsonString); if (response.IsSuccessStatusCode)
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 XblAuthenticateResponse() return new XblAuthenticateResponse()
{ {
Token = token, Token = jsonData.Token!,
UserHash = userHash UserHash = jsonData.DisplayClaims!.xui![0]["uhs"],
}; };
} }
else 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> /// <remarks>(Don't ask me what is XSTS, I DONT KNOW)</remarks>
/// <param name="xblResponse"></param> /// <param name="xblResponse"></param>
/// <returns></returns> /// <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, Properties = new AuthPayload.Propertie()
Accept = "application/json" {
SandboxId = "RETAIL",
UserTokens = new string[] { xblResponse.Token },
},
RelyingParty = "rp://api.minecraftservices.com/",
TokenType = "JWT",
}; };
request.Headers.Add("x-xbl-contract-version", "1");
string payload = "{" using StringContent httpContent = new(JsonSerializer.Serialize(payload, JsonOptions), Encoding.UTF8, "application/json");
+ "\"Properties\": {"
+ "\"SandboxId\": \"RETAIL\"," httpContent.Headers.Add("x-xbl-contract-version", "1");
+ "\"UserTokens\": ["
+ "\"" + xblResponse.Token + "\"" using HttpResponseMessage response = await httpClient.PostAsync(xsts, httpContent);
+ "]"
+ "}," if (Config.Logging.DebugMessages)
+ "\"RelyingParty\": \"rp://api.minecraftservices.com/\","
+ "\"TokenType\": \"JWT\""
+ "}";
var response = request.Post("application/json", payload);
if (Settings.Config.Logging.DebugMessages)
{
ConsoleIO.WriteLine(response.ToString()); ConsoleIO.WriteLine(response.ToString());
}
if (response.StatusCode == 200) if (response.IsSuccessStatusCode)
{ {
string jsonString = response.Body; AuthResult jsonData = (await response.Content.ReadFromJsonAsync<AuthResult>())!;
Json.JSONData json = Json.ParseJson(jsonString);
string token = json.Properties["Token"].StringValue;
string userHash = json.Properties["DisplayClaims"].Properties["xui"].DataArray[0].Properties["uhs"].StringValue;
return new XSTSAuthenticateResponse() return new XSTSAuthenticateResponse()
{ {
Token = token, Token = jsonData.Token!,
UserHash = userHash UserHash = jsonData.DisplayClaims!.xui![0]["uhs"],
}; };
} }
else else
{ {
if (response.StatusCode == 401) if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{ {
Json.JSONData json = Json.ParseJson(response.Body); AuthError jsonData = (await response.Content.ReadFromJsonAsync<AuthError>())!;
if (json.Properties["XErr"].StringValue == "2148916233") if (jsonData.XErr == 2148916233)
{
throw new Exception("The account doesn't have an Xbox account"); throw new Exception("The account doesn't have an Xbox account");
} else if (jsonData.XErr == 2148916235)
else if (json.Properties["XErr"].StringValue == "2148916238") 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"); 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
else throw new Exception("Unknown XSTS error code: " + json.Properties["XErr"].StringValue); throw new Exception("Unknown XSTS error code: " + jsonData.XErr.ToString() + ", Check " + jsonData.Redirect);
} }
else else
{ {
@ -396,13 +501,63 @@ namespace MinecraftClient.Protocol
public string Token; public string Token;
public string UserHash; 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 static class MinecraftWithXbox
{ {
private static readonly string loginWithXbox = "https://api.minecraftservices.com/authentication/login_with_xbox"; private const string profile = "https://api.minecraftservices.com/minecraft/profile";
private static readonly string ownership = "https://api.minecraftservices.com/entitlements/mcstore"; private const string ownership = "https://api.minecraftservices.com/entitlements/mcstore";
private static readonly string profile = "https://api.minecraftservices.com/minecraft/profile"; 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> /// <summary>
/// Login to Minecraft using the XSTS token and user hash obtained before /// 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="userHash"></param>
/// <param name="xstsToken"></param> /// <param name="xstsToken"></param>
/// <returns></returns> /// <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 + "\"}"; using StringContent httpContent = new(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
var response = request.Post("application/json", payload);
if (Settings.Config.Logging.DebugMessages) using HttpResponseMessage response = await httpClient.PostAsync(loginWithXbox, httpContent);
{
if (Config.Logging.DebugMessages)
ConsoleIO.WriteLine(response.ToString()); ConsoleIO.WriteLine(response.ToString());
}
string jsonString = response.Body; LoginResult jsonData = (await response.Content.ReadFromJsonAsync<LoginResult>())!;
Json.JSONData json = Json.ParseJson(jsonString);
return json.Properties["access_token"].StringValue; return jsonData.access_token!;
} }
/// <summary> /// <summary>
@ -436,39 +589,40 @@ namespace MinecraftClient.Protocol
/// </summary> /// </summary>
/// <param name="accessToken"></param> /// <param name="accessToken"></param>
/// <returns>True if the user own the game</returns> /// <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)); 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()); ConsoleIO.WriteLine(response.ToString());
}
string jsonString = response.Body; GameOwnershipResult jsonData = (await response.Content.ReadFromJsonAsync<GameOwnershipResult>())!;
Json.JSONData json = Json.ParseJson(jsonString);
return json.Properties["items"].DataArray.Count > 0; 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)); 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()); ConsoleIO.WriteLine(response.ToString());
}
string jsonString = response.Body; GameProfileResult jsonData = (await response.Content.ReadFromJsonAsync<GameProfileResult>())!;
Json.JSONData json = Json.ParseJson(jsonString);
if (!string.IsNullOrEmpty(jsonData.error))
throw new Exception($"{jsonData.errorType}: {jsonData.error}. {jsonData.errorMessage}");
return new UserProfile() return new UserProfile()
{ {
UUID = json.Properties["id"].StringValue, UUID = jsonData.id!,
UserName = json.Properties["name"].StringValue 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.Collections.Generic;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading.Tasks;
using MinecraftClient.Protocol.Message; using MinecraftClient.Protocol.Message;
namespace MinecraftClient.Protocol.ProfileKey namespace MinecraftClient.Protocol.ProfileKey
@ -10,51 +11,6 @@ namespace MinecraftClient.Protocol.ProfileKey
{ {
private static readonly SHA256 sha256Hash = SHA256.Create(); 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) public static byte[] DecodePemKey(string key, string prefix, string suffix)
{ {
int i = key.IndexOf(prefix); 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text.Json.Serialization;
namespace MinecraftClient.Protocol.ProfileKey namespace MinecraftClient.Protocol.ProfileKey
{ {
public class PlayerKeyPair public class PlayerKeyPair
{ {
[JsonInclude]
[JsonPropertyName("PublicKey")]
public PublicKey PublicKey; public PublicKey PublicKey;
[JsonInclude]
[JsonPropertyName("PrivateKey")]
public PrivateKey PrivateKey; public PrivateKey PrivateKey;
[JsonInclude]
[JsonPropertyName("ExpiresAt")]
public DateTime 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"; 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; this.PublicKey = PublicKey;
PrivateKey = keyPrivate; this.PrivateKey = PrivateKey;
try this.ExpiresAt = ExpiresAt;
{ this.RefreshedAfter = RefreshedAfter;
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();
}
} }
public bool NeedRefresh() public bool NeedRefresh()
@ -54,21 +57,6 @@ namespace MinecraftClient.Protocol.ProfileKey
return timeOffset.ToUnixTimeSeconds(); 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() public override string ToString()
{ {
List<string> datas = new(); List<string> datas = new();

View file

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

View file

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

View file

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

View file

@ -1,9 +1,19 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Reflection;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary; 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 System.Timers;
using MinecraftClient.Protocol.ProfileKey;
using MinecraftClient.Scripting;
using static MinecraftClient.Settings; using static MinecraftClient.Settings;
using static MinecraftClient.Settings.MainConfigHealper.MainConfig.AdvancedConfig; using static MinecraftClient.Settings.MainConfigHealper.MainConfig.AdvancedConfig;
@ -12,58 +22,88 @@ namespace MinecraftClient.Protocol.Session
/// <summary> /// <summary>
/// Handle sessions caching and storage. /// Handle sessions caching and storage.
/// </summary> /// </summary>
public static class SessionCache public static partial class SessionCache
{ {
private const string SessionCacheFilePlaintext = "SessionCache.ini"; public class Cache
private const string SessionCacheFileSerialized = "SessionCache.db"; {
private static readonly string SessionCacheFileMinecraft = String.Concat( [JsonInclude]
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), public Dictionary<string, SessionToken> SessionTokens = new();
Path.DirectorySeparatorChar,
".minecraft",
Path.DirectorySeparatorChar,
"launcher_profiles.json"
);
private static FileMonitor? cachemonitor; [JsonInclude]
private static readonly Dictionary<string, SessionToken> sessions = new(); public Dictionary<string, PlayerKeyPair> ProfileKeys = new();
private static readonly Timer updatetimer = new(100);
private static readonly List<KeyValuePair<string, SessionToken>> pendingadds = new(); [JsonInclude]
private static readonly BinaryFormatter formatter = new(); 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> /// <summary>
/// Retrieve whether SessionCache contains a session for the given login. /// Retrieve whether SessionCache contains a session for the given login.
/// </summary> /// </summary>
/// <param name="login">User login used with Minecraft.net</param> /// <param name="login">User login used with Minecraft.net</param>
/// <returns>TRUE if session is available</returns> /// <returns>TRUE if session is available</returns>
public static bool Contains(string login) public static bool ContainsSession(string login)
{ {
return sessions.ContainsKey(login); return cache.SessionTokens.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();
}
} }
/// <summary> /// <summary>
@ -71,202 +111,92 @@ namespace MinecraftClient.Protocol.Session
/// </summary> /// </summary>
/// <param name="login">User login used with Minecraft.net</param> /// <param name="login">User login used with Minecraft.net</param>
/// <returns>SessionToken for given login</returns> /// <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> /// <summary>
/// Initialize cache monitoring to keep cache updated with external changes. /// Store a session and save it to disk if required.
/// </summary> /// </summary>
/// <returns>TRUE if session tokens are seeded from file</returns> /// <param name="login">User login used with Minecraft.net</param>
public static bool InitializeDiskCache() /// <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)); if (sessionToken != null)
updatetimer.Elapsed += HandlePending; cache.SessionTokens[login] = sessionToken;
return LoadFromDisk(); if (profileKey != null)
cache.ProfileKeys[login] = profileKey;
if (Config.Main.Advanced.SessionCache == CacheType.disk)
await SaveToDisk();
} }
/// <summary> public static void StoreServerInfo(string server, string ServerIDhash, byte[] ServerPublicKey)
/// 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(); cache.ServerKeys[server] = new(ServerIDhash, ServerPublicKey);
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;
} }
/// <summary> /// <summary>
/// Saves SessionToken's from SessionCache into cache file. /// Saves SessionToken's from SessionCache into cache file.
/// </summary> /// </summary>
private static void SaveToDisk() private static async Task SaveToDisk()
{ {
if (Config.Logging.DebugMessages) if (Config.Logging.DebugMessages)
ConsoleIO.WriteLineFormatted("§8" + Translations.cache_saving, acceptnewlines: true); 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!", if (!GetJwtRegex().IsMatch(session.ID))
"# Login=SessionID,PlayerName,UUID,ClientID,RefreshToken,ServerIDhash,ServerPublicKey" cache.SessionTokens.Remove(login);
}; else if (!ChatBot.IsValidName(session.PlayerName))
foreach (KeyValuePair<string, SessionToken> entry in sessions) cache.SessionTokens.Remove(login);
sessionCacheLines.Add(entry.Key + '=' + entry.Value.ToString()); 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 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) catch (IOException e)
{ {
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_save_fail, e.Message)); 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;
using System.IO; using System.IO;
using System.Net.Http;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using MinecraftClient.Scripting; using MinecraftClient.Scripting;
@ -9,92 +11,39 @@ namespace MinecraftClient.Protocol.Session
[Serializable] [Serializable]
public class SessionToken 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 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 Task<bool>? SessionPreCheckTask = null;
public SessionToken() public SessionToken()
{ {
ID = String.Empty; ID = string.Empty;
PlayerName = String.Empty; PlayerName = string.Empty;
PlayerID = String.Empty; PlayerID = string.Empty;
ClientID = String.Empty; ClientID = string.Empty;
RefreshToken = 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;
} }
} }
} }

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 Starksoft.Aspen.Proxy;
using Tomlet.Attributes; using Tomlet.Attributes;
@ -17,14 +20,17 @@ namespace MinecraftClient.Proxy
[TomlDoNotInlineObject] [TomlDoNotInlineObject]
public class Configs public class Configs
{ {
[TomlInlineComment("$Proxy.Enabled_Update$")] [NonSerialized] // Compatible with old settings.
public bool Enabled_Update = false; public bool? Enabled_Login = false, Enabled_Ingame = false, Enabled_Update = false;
[TomlInlineComment("$Proxy.Enabled_Login$")] [TomlInlineComment("$Proxy.Ingame_Proxy$")]
public bool Enabled_Login = false; public ProxyPreferenceType Ingame_Proxy = ProxyPreferenceType.disable;
[TomlInlineComment("$Proxy.Enabled_Ingame$")] [TomlInlineComment("$Proxy.Login_Proxy$")]
public bool Enabled_Ingame = false; public ProxyPreferenceType Login_Proxy = ProxyPreferenceType.follow_system;
[TomlInlineComment("$Proxy.MCC_Update_Proxy$")]
public ProxyPreferenceType MCC_Update_Proxy = ProxyPreferenceType.follow_system;
[TomlInlineComment("$Proxy.Server$")] [TomlInlineComment("$Proxy.Server$")]
public ProxyInfoConfig Server = new("0.0.0.0", 8080); public ProxyInfoConfig Server = new("0.0.0.0", 8080);
@ -33,12 +39,22 @@ namespace MinecraftClient.Proxy
public ProxyType Proxy_Type = ProxyType.HTTP; public ProxyType Proxy_Type = ProxyType.HTTP;
[TomlInlineComment("$Proxy.Username$")] [TomlInlineComment("$Proxy.Username$")]
public string Username = ""; public string Username = string.Empty;
[TomlInlineComment("$Proxy.Password$")] [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 public struct ProxyInfoConfig
{ {
@ -53,8 +69,12 @@ namespace MinecraftClient.Proxy
} }
public enum ProxyType { HTTP, SOCKS4, SOCKS4a, SOCKS5 }; 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 readonly ProxyClientFactory factory = new();
private static IProxyClient? proxy; private static IProxyClient? proxy;
private static bool proxy_ok = false; private static bool proxy_ok = false;
@ -66,11 +86,14 @@ namespace MinecraftClient.Proxy
/// <param name="port">Target port</param> /// <param name="port">Target port</param>
/// <param name="login">True if the purpose is logging in to a Minecraft account</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 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; ProxyType innerProxytype = ProxyType.Http;
@ -95,7 +118,30 @@ namespace MinecraftClient.Proxy
return proxy.CreateConnection(host, port); 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) catch (ProxyException e)
{ {
@ -104,5 +150,58 @@ namespace MinecraftClient.Proxy
throw new SocketException((int)SocketError.HostUnreachable); 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 /// 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 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. ///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> /// </summary>
internal static string Proxy { internal static string Proxy {
get { get {
@ -1710,27 +1711,27 @@ namespace MinecraftClient {
/// <summary> /// <summary>
/// Looks up a localized string similar to Whether to connect to the game server through a proxy.. /// Looks up a localized string similar to Whether to connect to the game server through a proxy..
/// </summary> /// </summary>
internal static string Proxy_Enabled_Ingame { internal static string Proxy_Ingame_Proxy {
get { get {
return ResourceManager.GetString("Proxy.Enabled_Ingame", resourceCulture); return ResourceManager.GetString("Proxy.Ingame_Proxy", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Whether to connect to the login server through a proxy.. /// Looks up a localized string similar to Whether to connect to the login server through a proxy..
/// </summary> /// </summary>
internal static string Proxy_Enabled_Login { internal static string Proxy_Login_Proxy {
get { get {
return ResourceManager.GetString("Proxy.Enabled_Login", resourceCulture); return ResourceManager.GetString("Proxy.Login_Proxy", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Whether to download MCC updates via proxy.. /// Looks up a localized string similar to Whether to download MCC updates via proxy..
/// </summary> /// </summary>
internal static string Proxy_Enabled_Update { internal static string Proxy_MCC_Update_Proxy {
get { 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 <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 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. 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>
<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> <value>Whether to connect to the game server through a proxy.</value>
</data> </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> <value>Whether to connect to the login server through a proxy.</value>
</data> </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> <value>Whether to download MCC updates via proxy.</value>
</data> </data>
<data name="Proxy.Password" xml:space="preserve"> <data name="Proxy.Password" xml:space="preserve">

View file

@ -2127,7 +2127,7 @@ namespace MinecraftClient {
} }
/// <summary> /// <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> /// </summary>
internal static string cache_loaded { internal static string cache_loaded {
get { 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> /// <summary>
/// Looks up a localized string similar to Settings have been loaded from {0}. /// Looks up a localized string similar to Settings have been loaded from {0}.
/// </summary> /// </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> /// <summary>
/// Looks up a localized string similar to The current setting is saved as {0}. /// Looks up a localized string similar to The current setting is saved as {0}.
/// </summary> /// </summary>
@ -5295,7 +5304,16 @@ namespace MinecraftClient {
} }
/// <summary> /// <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> /// </summary>
internal static string mcc_login { internal static string mcc_login {
get { 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> /// <summary>
/// Looks up a localized string similar to Failed to perform SRV lookup for {0} /// Looks up a localized string similar to Failed to perform SRV lookup for {0}
///{1}: {2}. ///{1}: {2}.
@ -5531,7 +5558,7 @@ namespace MinecraftClient {
} }
/// <summary> /// <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> /// </summary>
internal static string mcc_settings_generated { internal static string mcc_settings_generated {
get { 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> /// <summary>
/// Looks up a localized string similar to Unknown or not supported MC version {0}. /// Looks up a localized string similar to Unknown or not supported MC version {0}.
///Switching to autodetection mode.. ///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> <value>Ignoring profile key token string '{0}': {1}</value>
</data> </data>
<data name="cache.loaded" xml:space="preserve"> <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>
<data name="cache.loaded_keys" xml:space="preserve"> <data name="cache.loaded_keys" xml:space="preserve">
<value>Loaded profile key, it will be refresh at {0}</value> <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"> <data name="config.load.fail" xml:space="preserve">
<value>Failed to load settings:</value> <value>Failed to load settings:</value>
</data> </data>
<data name="config.Main.Advanced.language.invaild" xml:space="preserve"> <data name="config.invaild_language" xml:space="preserve">
<value>The language code is invalid!</value> <value>[Settings] The language code is invalid!</value>
</data> </data>
<data name="config.saving" xml:space="preserve"> <data name="config.saving" xml:space="preserve">
<value>The current setting is saved as {0}</value> <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> <value>Link: {0}</value>
</data> </data>
<data name="mcc.login" xml:space="preserve"> <data name="mcc.login" xml:space="preserve">
<value>Login :</value> <value>Login:</value>
</data> </data>
<data name="mcc.login_basic_io" xml:space="preserve"> <data name="mcc.login_basic_io" xml:space="preserve">
<value>Please type the username or email of your choice.</value> <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> <value>Cached session is still valid for {0}.</value>
</data> </data>
<data name="mcc.settings_generated" xml:space="preserve"> <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>
<data name="mcc.unknown_version" xml:space="preserve"> <data name="mcc.unknown_version" xml:space="preserve">
<value>Unknown or not supported MC version {0}. <value>Unknown or not supported MC version {0}.
@ -2028,4 +2028,16 @@ Logging in...</value>
<data name="proxy.connected" xml:space="preserve"> <data name="proxy.connected" xml:space="preserve">
<value>Connected to proxy {0}:{1}</value> <value>Connected to proxy {0}:{1}</value>
</data> </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> </root>

View file

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

View file

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

View file

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

View file

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