mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2025-11-07 17:36:07 +00:00
Refactoring to asynchronous. (partially completed)
This commit is contained in:
parent
7ee08092d4
commit
096ea0c70c
72 changed files with 6033 additions and 5080 deletions
39
MinecraftClient/AsyncTaskHandler.cs
Normal file
39
MinecraftClient/AsyncTaskHandler.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -437,10 +437,9 @@ namespace MinecraftClient.ChatBots
|
|||
StopDigging();
|
||||
}
|
||||
|
||||
public override bool OnDisconnect(DisconnectReason reason, string message)
|
||||
public override int OnDisconnect(DisconnectReason reason, string message)
|
||||
{
|
||||
StopDigging();
|
||||
|
||||
return base.OnDisconnect(reason, message);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -535,7 +535,7 @@ namespace MinecraftClient.ChatBots
|
|||
StopFishing();
|
||||
}
|
||||
|
||||
public override bool OnDisconnect(DisconnectReason reason, string message)
|
||||
public override int OnDisconnect(DisconnectReason reason, string message)
|
||||
{
|
||||
StopFishing();
|
||||
|
||||
|
|
|
|||
|
|
@ -97,8 +97,9 @@ namespace MinecraftClient.ChatBots
|
|||
}
|
||||
}
|
||||
|
||||
public override bool OnDisconnect(DisconnectReason reason, string message)
|
||||
public override int OnDisconnect(DisconnectReason reason, string message)
|
||||
{
|
||||
bool triggerReco = false;
|
||||
if (reason == DisconnectReason.UserLogout)
|
||||
{
|
||||
LogDebugToConsole(Translations.bot_autoRelog_ignore_user_logout);
|
||||
|
|
@ -113,44 +114,77 @@ namespace MinecraftClient.ChatBots
|
|||
if (Config.Ignore_Kick_Message)
|
||||
{
|
||||
Configs._BotRecoAttempts++;
|
||||
LaunchDelayedReconnection(null);
|
||||
return true;
|
||||
LogDebugToConsole(Translations.bot_autoRelog_reconnect_always);
|
||||
triggerReco = true;
|
||||
}
|
||||
|
||||
foreach (string msg in Config.Kick_Messages)
|
||||
else
|
||||
{
|
||||
if (comp.Contains(msg))
|
||||
foreach (string msg in Config.Kick_Messages)
|
||||
{
|
||||
Configs._BotRecoAttempts++;
|
||||
LaunchDelayedReconnection(msg);
|
||||
return true;
|
||||
if (comp.Contains(msg))
|
||||
{
|
||||
Configs._BotRecoAttempts++;
|
||||
LogDebugToConsole(string.Format(Translations.bot_autoRelog_reconnect, msg));
|
||||
triggerReco = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!triggerReco)
|
||||
LogDebugToConsole(Translations.bot_autoRelog_reconnect_ignore);
|
||||
}
|
||||
}
|
||||
|
||||
if (triggerReco)
|
||||
{
|
||||
double delay = random.NextDouble() * (Config.Delay.max - Config.Delay.min) + Config.Delay.min;
|
||||
LogToConsole(string.Format(Translations.bot_autoRelog_wait, delay));
|
||||
return (int)Math.Floor(delay * 1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public static int OnDisconnectStatic(DisconnectReason reason, string message)
|
||||
{
|
||||
bool triggerReco = false;
|
||||
if (Config.Enabled
|
||||
&& reason != DisconnectReason.UserLogout
|
||||
&& (Config.Retries < 0 || Configs._BotRecoAttempts < Config.Retries))
|
||||
{
|
||||
message = GetVerbatim(message);
|
||||
string comp = message.ToLower();
|
||||
|
||||
if (Config.Ignore_Kick_Message)
|
||||
{
|
||||
Configs._BotRecoAttempts++;
|
||||
triggerReco = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (string msg in Config.Kick_Messages)
|
||||
{
|
||||
if (comp.Contains(msg))
|
||||
{
|
||||
Configs._BotRecoAttempts++;
|
||||
triggerReco = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogDebugToConsole(Translations.bot_autoRelog_reconnect_ignore);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void LaunchDelayedReconnection(string? msg)
|
||||
{
|
||||
double delay = random.NextDouble() * (Config.Delay.max - Config.Delay.min) + Config.Delay.min;
|
||||
LogDebugToConsole(string.Format(string.IsNullOrEmpty(msg) ? Translations.bot_autoRelog_reconnect_always : Translations.bot_autoRelog_reconnect, msg));
|
||||
LogToConsole(string.Format(Translations.bot_autoRelog_wait, delay));
|
||||
System.Threading.Thread.Sleep((int)Math.Floor(delay * 1000));
|
||||
ReconnectToTheServer();
|
||||
}
|
||||
|
||||
public static bool OnDisconnectStatic(DisconnectReason reason, string message)
|
||||
{
|
||||
if (Config.Enabled)
|
||||
if (triggerReco)
|
||||
{
|
||||
AutoRelog bot = new();
|
||||
bot.Initialize();
|
||||
return bot.OnDisconnect(reason, message);
|
||||
double delay = random.NextDouble() * (Config.Delay.max - Config.Delay.min) + Config.Delay.min;
|
||||
return (int)Math.Floor(delay * 1000);
|
||||
}
|
||||
else
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -244,10 +244,10 @@ namespace MinecraftClient.ChatBots
|
|||
running = false;
|
||||
}
|
||||
|
||||
public override bool OnDisconnect(DisconnectReason reason, string message)
|
||||
public override int OnDisconnect(DisconnectReason reason, string message)
|
||||
{
|
||||
running = false;
|
||||
return true;
|
||||
return base.OnDisconnect(reason, message);
|
||||
}
|
||||
|
||||
private void MainPorcess()
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ namespace MinecraftClient.ChatBots
|
|||
}
|
||||
}
|
||||
|
||||
public override bool OnDisconnect(DisconnectReason reason, string message)
|
||||
public override int OnDisconnect(DisconnectReason reason, string message)
|
||||
{
|
||||
replay!.OnShutDown();
|
||||
return base.OnDisconnect(reason, message);
|
||||
|
|
|
|||
|
|
@ -236,10 +236,10 @@ namespace MinecraftClient.ChatBots
|
|||
}
|
||||
}
|
||||
|
||||
public override bool OnDisconnect(DisconnectReason reason, string message)
|
||||
public override int OnDisconnect(DisconnectReason reason, string message)
|
||||
{
|
||||
serverlogin_done = false;
|
||||
return false;
|
||||
return base.OnDisconnect(reason, message);
|
||||
}
|
||||
|
||||
private static string Task2String(TaskConfig task)
|
||||
|
|
|
|||
|
|
@ -29,14 +29,17 @@ namespace MinecraftClient
|
|||
/// Get the translated version of command description.
|
||||
/// </summary>
|
||||
/// <returns>Translated command description</returns>
|
||||
public string GetCmdDescTranslated()
|
||||
public string GetCmdDescTranslated(bool ListAllUsage = true)
|
||||
{
|
||||
char cmdChar = Settings.Config.Main.Advanced.InternalCmdChar.ToChar();
|
||||
|
||||
StringBuilder sb = new();
|
||||
string s = (string.IsNullOrEmpty(CmdUsage) || string.IsNullOrEmpty(CmdDesc)) ? string.Empty : ": "; // If either one is empty, no colon :
|
||||
sb.Append("§e").Append(cmdChar).Append(CmdUsage).Append("§r").Append(s).AppendLine(CmdDesc);
|
||||
sb.Append(McClient.dispatcher.GetAllUsageString(CmdName, false));
|
||||
sb.Append("§e").Append(cmdChar).Append(CmdUsage).Append("§r").Append(s);
|
||||
if (ListAllUsage)
|
||||
sb.AppendLine(CmdDesc).Append(McClient.dispatcher.GetAllUsageString(CmdName, false));
|
||||
else
|
||||
sb.Append(CmdDesc);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ namespace MinecraftClient.Commands
|
|||
private static int DoAnimation(CmdResult r, bool mainhand)
|
||||
{
|
||||
McClient handler = CmdResult.currentHandler!;
|
||||
return r.SetAndReturn(handler.DoAnimation(mainhand ? 1 : 0));
|
||||
return r.SetAndReturn(handler.DoAnimation(mainhand ? 1 : 0).Result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ namespace MinecraftClient.Commands
|
|||
private static int DoLeaveBed(CmdResult r)
|
||||
{
|
||||
McClient handler = CmdResult.currentHandler!;
|
||||
return r.SetAndReturn(Translations.cmd_bed_leaving, handler.SendEntityAction(Protocol.EntityActionType.LeaveBed));
|
||||
return r.SetAndReturn(Translations.cmd_bed_leaving, handler.SendEntityAction(Protocol.EntityActionType.LeaveBed).Result);
|
||||
}
|
||||
|
||||
private static int DoSleepBedWithRadius(CmdResult r, double radius)
|
||||
|
|
@ -137,7 +137,7 @@ namespace MinecraftClient.Commands
|
|||
|
||||
handler.Log.Info(string.Format(Translations.cmd_bed_moving, bedLocation.X, bedLocation.Y, bedLocation.Z));
|
||||
|
||||
bool res = handler.PlaceBlock(bedLocation, Direction.Down);
|
||||
bool res = handler.PlaceBlock(bedLocation, Direction.Down).Result;
|
||||
|
||||
handler.Log.Info(string.Format(
|
||||
Translations.cmd_bed_trying_to_use,
|
||||
|
|
@ -174,7 +174,7 @@ namespace MinecraftClient.Commands
|
|||
blockCenter.X,
|
||||
blockCenter.Y,
|
||||
blockCenter.Z,
|
||||
handler.PlaceBlock(block, Direction.Down) ? Translations.cmd_bed_in : Translations.cmd_bed_not_in
|
||||
handler.PlaceBlock(block, Direction.Down).Result ? Translations.cmd_bed_in : Translations.cmd_bed_not_in
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ namespace MinecraftClient.Commands
|
|||
return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_bots_noloaded);
|
||||
else
|
||||
{
|
||||
handler.UnloadAllBots();
|
||||
handler.UnloadAllBots().Wait();
|
||||
return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_bots_unloaded_all);
|
||||
}
|
||||
}
|
||||
|
|
@ -88,7 +88,7 @@ namespace MinecraftClient.Commands
|
|||
return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_bots_notfound, botName));
|
||||
else
|
||||
{
|
||||
handler.BotUnLoad(bot);
|
||||
handler.BotUnLoad(bot).Wait();
|
||||
return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_bots_unloaded, botName));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ namespace MinecraftClient.Commands
|
|||
if (!handler.GetInventoryEnabled())
|
||||
return r.SetAndReturn(Status.FailNeedInventory);
|
||||
|
||||
if (handler.ChangeSlot((short)(slot - 1)))
|
||||
if (handler.ChangeSlot((short)(slot - 1)).Result)
|
||||
return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_changeSlot_changed, slot));
|
||||
else
|
||||
return r.SetAndReturn(Status.Fail, Translations.cmd_changeSlot_fail);
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ namespace MinecraftClient.Commands
|
|||
Block block = handler.GetWorld().GetBlock(blockToBreak);
|
||||
if (block.Type == Material.Air)
|
||||
return r.SetAndReturn(Status.Fail, Translations.cmd_dig_no_block);
|
||||
else if (handler.DigBlock(blockToBreak))
|
||||
else if (handler.DigBlock(blockToBreak).Result)
|
||||
{
|
||||
blockToBreak = blockToBreak.ToCenter();
|
||||
return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_dig_dig, blockToBreak.X, blockToBreak.Y, blockToBreak.Z, block.GetTypeString()));
|
||||
|
|
@ -74,7 +74,7 @@ namespace MinecraftClient.Commands
|
|||
return r.SetAndReturn(Status.Fail, Translations.cmd_dig_too_far);
|
||||
else if (block.Type == Material.Air)
|
||||
return r.SetAndReturn(Status.Fail, Translations.cmd_dig_no_block);
|
||||
else if (handler.DigBlock(blockLoc, lookAtBlock: false))
|
||||
else if (handler.DigBlock(blockLoc, lookAtBlock: false).Result)
|
||||
return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_dig_dig, blockLoc.X, blockLoc.Y, blockLoc.Z, block.GetTypeString()));
|
||||
else
|
||||
return r.SetAndReturn(Status.Fail, Translations.cmd_dig_fail);
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ namespace MinecraftClient.Commands
|
|||
var p = inventories[inventoryId];
|
||||
int[] targetItems = p.SearchItem(itemType);
|
||||
foreach (int slot in targetItems)
|
||||
handler.DoWindowAction(inventoryId, slot, WindowActionType.DropItemStack);
|
||||
handler.DoWindowAction(inventoryId, slot, WindowActionType.DropItemStack).Wait();
|
||||
|
||||
return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_dropItem_dropped, Item.GetTypeString(itemType), inventoryId));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ namespace MinecraftClient.Commands
|
|||
return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_enchant_no_levels, handler.GetLevel(), requiredLevel));
|
||||
else
|
||||
{
|
||||
if (handler.ClickContainerButton(enchantingTable.ID, slotId))
|
||||
if (handler.ClickContainerButton(enchantingTable.ID, slotId).Result)
|
||||
return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_enchant_clicked);
|
||||
else
|
||||
return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_enchant_not_clicked);
|
||||
|
|
|
|||
|
|
@ -166,12 +166,12 @@ namespace MinecraftClient.Commands
|
|||
{
|
||||
if (action == ActionType.Attack)
|
||||
{
|
||||
handler.InteractEntity(entity2.Key, InteractType.Attack);
|
||||
handler.InteractEntity(entity2.Key, InteractType.Attack).Wait();
|
||||
actionst = Translations.cmd_entityCmd_attacked;
|
||||
}
|
||||
else if (action == ActionType.Use)
|
||||
{
|
||||
handler.InteractEntity(entity2.Key, InteractType.Interact);
|
||||
handler.InteractEntity(entity2.Key, InteractType.Interact).Wait();
|
||||
actionst = Translations.cmd_entityCmd_used;
|
||||
}
|
||||
actioncount++;
|
||||
|
|
@ -311,10 +311,10 @@ namespace MinecraftClient.Commands
|
|||
switch (action)
|
||||
{
|
||||
case ActionType.Attack:
|
||||
handler.InteractEntity(entity.ID, InteractType.Attack);
|
||||
handler.InteractEntity(entity.ID, InteractType.Attack).Wait();
|
||||
return Translations.cmd_entityCmd_attacked;
|
||||
case ActionType.Use:
|
||||
handler.InteractEntity(entity.ID, InteractType.Interact);
|
||||
handler.InteractEntity(entity.ID, InteractType.Interact).Wait();
|
||||
return Translations.cmd_entityCmd_used;
|
||||
case ActionType.List:
|
||||
return GetEntityInfoDetailed(handler, entity);
|
||||
|
|
|
|||
|
|
@ -48,11 +48,5 @@ namespace MinecraftClient.Commands
|
|||
Program.Exit(code);
|
||||
return r.SetAndReturn(CmdResult.Status.Done);
|
||||
}
|
||||
|
||||
internal static string DoExit(string command)
|
||||
{
|
||||
Program.Exit();
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ namespace MinecraftClient.Commands
|
|||
|
||||
if (handler.GetGamemode() == 1)
|
||||
{
|
||||
if (handler.DoCreativeGive(slot, itemType, count, null))
|
||||
if (handler.DoCreativeGive(slot, itemType, count, null).Result)
|
||||
return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_inventory_creative_done, itemType, count, slot));
|
||||
else
|
||||
return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_inventory_creative_fail);
|
||||
|
|
@ -178,7 +178,7 @@ namespace MinecraftClient.Commands
|
|||
|
||||
if (handler.GetGamemode() == 1)
|
||||
{
|
||||
if (handler.DoCreativeGive(slot, ItemType.Null, 0, null))
|
||||
if (handler.DoCreativeGive(slot, ItemType.Null, 0, null).Result)
|
||||
return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_inventory_creative_delete, slot));
|
||||
else
|
||||
return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_inventory_creative_fail);
|
||||
|
|
@ -279,7 +279,7 @@ namespace MinecraftClient.Commands
|
|||
if (inventory == null)
|
||||
return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_inventory_not_exist, inventoryId));
|
||||
|
||||
if (handler.CloseInventory(inventoryId.Value))
|
||||
if (handler.CloseInventory(inventoryId.Value).Result)
|
||||
return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_inventory_close, inventoryId));
|
||||
else
|
||||
return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_inventory_close_fail, inventoryId));
|
||||
|
|
@ -355,7 +355,9 @@ namespace MinecraftClient.Commands
|
|||
};
|
||||
|
||||
handler.Log.Info(string.Format(Translations.cmd_inventory_clicking, keyName, slot, inventoryId));
|
||||
return r.SetAndReturn(handler.DoWindowAction(inventoryId.Value, slot, actionType));
|
||||
var task = handler.DoWindowAction(inventoryId.Value, slot, actionType);
|
||||
task.Wait();
|
||||
return r.SetAndReturn(task.Result);
|
||||
}
|
||||
|
||||
private int DoDropAction(CmdResult r, int? inventoryId, int slot, WindowActionType actionType)
|
||||
|
|
@ -379,7 +381,7 @@ namespace MinecraftClient.Commands
|
|||
if (!inventory.Items.ContainsKey(slot))
|
||||
return r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_inventory_no_item, slot));
|
||||
|
||||
if (handler.DoWindowAction(inventoryId.Value, slot, actionType))
|
||||
if (handler.DoWindowAction(inventoryId.Value, slot, actionType).Result)
|
||||
{
|
||||
if (actionType == WindowActionType.DropItemStack)
|
||||
return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.cmd_inventory_drop_stack, slot));
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ namespace MinecraftClient.Commands
|
|||
}
|
||||
}
|
||||
Program.Restart(keepAccountAndServerSettings: true);
|
||||
return String.Empty;
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ namespace MinecraftClient.Commands
|
|||
{
|
||||
McClient handler = CmdResult.currentHandler!;
|
||||
handler.Log.Info(Translations.cmd_reload_started);
|
||||
handler.ReloadSettings();
|
||||
handler.ReloadSettings().Wait();
|
||||
handler.Log.Warn(Translations.cmd_reload_warning1);
|
||||
handler.Log.Warn(Translations.cmd_reload_warning2);
|
||||
handler.Log.Warn(Translations.cmd_reload_warning3);
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ namespace MinecraftClient.Commands
|
|||
private int DoRespawn(CmdResult r)
|
||||
{
|
||||
McClient handler = CmdResult.currentHandler!;
|
||||
handler.SendRespawnPacket();
|
||||
handler.SendRespawnPacket().Wait();
|
||||
return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_respawn_done);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ namespace MinecraftClient.Commands
|
|||
private int DoSendText(CmdResult r, string command)
|
||||
{
|
||||
McClient handler = CmdResult.currentHandler!;
|
||||
handler.SendText(command);
|
||||
handler.SendText(command).Wait();
|
||||
return r.SetAndReturn(CmdResult.Status.Done);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ namespace MinecraftClient.Commands
|
|||
McClient handler = CmdResult.currentHandler!;
|
||||
if (sneaking)
|
||||
{
|
||||
var result = handler.SendEntityAction(Protocol.EntityActionType.StopSneaking);
|
||||
var result = handler.SendEntityAction(Protocol.EntityActionType.StopSneaking).Result;
|
||||
if (result)
|
||||
sneaking = false;
|
||||
if (result)
|
||||
|
|
@ -52,7 +52,7 @@ namespace MinecraftClient.Commands
|
|||
}
|
||||
else
|
||||
{
|
||||
var result = handler.SendEntityAction(Protocol.EntityActionType.StartSneaking);
|
||||
var result = handler.SendEntityAction(Protocol.EntityActionType.StartSneaking).Result;
|
||||
if (result)
|
||||
sneaking = true;
|
||||
if (result)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Brigadier.NET;
|
||||
using System.Threading.Tasks;
|
||||
using Brigadier.NET;
|
||||
using Brigadier.NET.Builder;
|
||||
using MinecraftClient.CommandHandler;
|
||||
|
||||
|
|
@ -71,7 +72,7 @@ namespace MinecraftClient.Commands
|
|||
|
||||
private static int CheckUpdate(CmdResult r)
|
||||
{
|
||||
UpgradeHelper.CheckUpdate(forceUpdate: true);
|
||||
Task.Run(async () => { await UpgradeHelper.CheckUpdate(forceUpdate: true); });
|
||||
return r.SetAndReturn(CmdResult.Status.Done, Translations.mcc_update_start);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ namespace MinecraftClient.Commands
|
|||
if (!handler.GetInventoryEnabled())
|
||||
return r.SetAndReturn(Status.FailNeedInventory);
|
||||
|
||||
handler.UseItemOnHand();
|
||||
handler.UseItemOnHand().Wait();
|
||||
return r.SetAndReturn(Status.Done, Translations.cmd_useitem_use);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ namespace MinecraftClient.Commands
|
|||
Location current = handler.GetCurrentLocation();
|
||||
block = block.ToAbsolute(current).ToFloor();
|
||||
Location blockCenter = block.ToCenter();
|
||||
bool res = handler.PlaceBlock(block, Direction.Down);
|
||||
bool res = handler.PlaceBlock(block, Direction.Down).Result;
|
||||
return r.SetAndReturn(string.Format(Translations.cmd_useblock_use, blockCenter.X, blockCenter.Y, blockCenter.Z, res ? "succeeded" : "failed"), res);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,10 @@ namespace MinecraftClient
|
|||
/// </summary>
|
||||
public static string LogPrefix = "§8[Log] ";
|
||||
|
||||
private static bool SuppressOutput = false;
|
||||
|
||||
private static List<Tuple<bool, string>> MessageBuffer = new();
|
||||
|
||||
/// <summary>
|
||||
/// Read a password from the standard input
|
||||
/// </summary>
|
||||
|
|
@ -101,15 +105,42 @@ namespace MinecraftClient
|
|||
}
|
||||
}
|
||||
|
||||
public static void SuppressPrinting(bool enable)
|
||||
{
|
||||
SuppressOutput = enable;
|
||||
if (!enable)
|
||||
{
|
||||
lock (MessageBuffer)
|
||||
{
|
||||
foreach ((bool format, string message) in MessageBuffer)
|
||||
{
|
||||
if (format)
|
||||
WriteLineFormatted(message, true, false);
|
||||
else
|
||||
WriteLine(message);
|
||||
}
|
||||
MessageBuffer.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write a string to the standard output with a trailing newline
|
||||
/// </summary>
|
||||
public static void WriteLine(string line)
|
||||
public static void WriteLine(string line, bool ignoreSuppress = false)
|
||||
{
|
||||
if (BasicIO)
|
||||
Console.WriteLine(line);
|
||||
if (!ignoreSuppress && SuppressOutput)
|
||||
{
|
||||
lock(MessageBuffer)
|
||||
MessageBuffer.Add(new(true, line));
|
||||
}
|
||||
else
|
||||
ConsoleInteractive.ConsoleWriter.WriteLine(line);
|
||||
{
|
||||
if (BasicIO)
|
||||
Console.WriteLine(line);
|
||||
else
|
||||
ConsoleInteractive.ConsoleWriter.WriteLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -123,37 +154,42 @@ namespace MinecraftClient
|
|||
/// If true, "hh-mm-ss" timestamp will be prepended.
|
||||
/// If unspecified, value is retrieved from EnableTimestamps.
|
||||
/// </param>
|
||||
public static void WriteLineFormatted(string str, bool acceptnewlines = false, bool? displayTimestamp = null)
|
||||
public static void WriteLineFormatted(string str, bool acceptnewlines = false, bool? displayTimestamp = null, bool ignoreSuppress = false)
|
||||
{
|
||||
StringBuilder output = new();
|
||||
|
||||
if (!String.IsNullOrEmpty(str))
|
||||
if (!string.IsNullOrEmpty(str))
|
||||
{
|
||||
StringBuilder output = new();
|
||||
displayTimestamp ??= EnableTimestamps;
|
||||
if (displayTimestamp.Value)
|
||||
{
|
||||
int hour = DateTime.Now.Hour, minute = DateTime.Now.Minute, second = DateTime.Now.Second;
|
||||
output.Append(String.Format("{0}:{1}:{2} ", hour.ToString("00"), minute.ToString("00"), second.ToString("00")));
|
||||
}
|
||||
|
||||
if (!acceptnewlines)
|
||||
{
|
||||
str = str.Replace('\n', ' ');
|
||||
}
|
||||
if (BasicIO)
|
||||
|
||||
if (!ignoreSuppress && SuppressOutput)
|
||||
{
|
||||
if (BasicIO_NoColor)
|
||||
lock (MessageBuffer)
|
||||
MessageBuffer.Add(new(true, output.ToString()));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (BasicIO)
|
||||
{
|
||||
output.Append(ChatBot.GetVerbatim(str));
|
||||
if (BasicIO_NoColor)
|
||||
output.Append(ChatBot.GetVerbatim(str));
|
||||
else
|
||||
output.Append(str);
|
||||
Console.WriteLine(output.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
output.Append(str);
|
||||
ConsoleInteractive.ConsoleWriter.WriteLineFormatted(output.ToString());
|
||||
}
|
||||
Console.WriteLine(output.ToString());
|
||||
return;
|
||||
}
|
||||
output.Append(str);
|
||||
ConsoleInteractive.ConsoleWriter.WriteLineFormatted(output.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -178,6 +214,17 @@ namespace MinecraftClient
|
|||
{
|
||||
if (BasicIO) return;
|
||||
ConsoleInteractive.ConsoleReader.ClearBuffer();
|
||||
|
||||
_cancellationTokenSource?.Cancel();
|
||||
|
||||
AutoCompleteDone = false;
|
||||
AutoCompleteResult = Array.Empty<string>();
|
||||
|
||||
Commands.Clear();
|
||||
CommandsFromAutoComplete = Array.Empty<string>();
|
||||
CommandsFromDeclareCommands = Array.Empty<string>();
|
||||
|
||||
ConsoleInteractive.ConsoleSuggestion.ClearSuggestions();
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -220,9 +267,7 @@ namespace MinecraftClient
|
|||
string command = fullCommand[offset..];
|
||||
if (command.Length == 0)
|
||||
{
|
||||
List<ConsoleInteractive.ConsoleSuggestion.Suggestion> sugList = new();
|
||||
|
||||
sugList.Add(new("/"));
|
||||
List<ConsoleInteractive.ConsoleSuggestion.Suggestion> sugList = new() { new("/") };
|
||||
|
||||
var childs = McClient.dispatcher.GetRoot().Children;
|
||||
if (childs != null)
|
||||
|
|
@ -336,9 +381,9 @@ namespace MinecraftClient
|
|||
MergeCommands();
|
||||
}
|
||||
|
||||
public static void InitCommandList(CommandDispatcher<CmdResult> dispatcher)
|
||||
public static async Task InitCommandList(CommandDispatcher<CmdResult> dispatcher)
|
||||
{
|
||||
autocomplete_engine!.AutoComplete("/");
|
||||
await autocomplete_engine!.AutoComplete("/");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -353,6 +398,6 @@ namespace MinecraftClient
|
|||
/// </summary>
|
||||
/// <param name="BehindCursor">Text behind the cursor, e.g. "my input comm"</param>
|
||||
/// <returns>List of auto-complete words, e.g. ["command", "comment"]</returns>
|
||||
int AutoComplete(string BehindCursor);
|
||||
Task<int> AutoComplete(string BehindCursor);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
MinecraftClient/Crypto/AesHandler/BasicAes.cs
Normal file
29
MinecraftClient/Crypto/AesHandler/BasicAes.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
167
MinecraftClient/Crypto/AesHandler/FasterAesArm.cs
Normal file
167
MinecraftClient/Crypto/AesHandler/FasterAesArm.cs
Normal 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 };
|
||||
}
|
||||
}
|
||||
|
|
@ -4,15 +4,15 @@ using System.Runtime.InteropServices;
|
|||
using System.Runtime.Intrinsics;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
|
||||
namespace MinecraftClient.Crypto
|
||||
namespace MinecraftClient.Crypto.AesHandler
|
||||
{
|
||||
// Using the AES-NI instruction set
|
||||
// https://gist.github.com/Thealexbarney/9f75883786a9f3100408ff795fb95d85
|
||||
public class FastAes
|
||||
public class FasterAesX86 : IAesHandler
|
||||
{
|
||||
private Vector128<byte>[] RoundKeys { get; }
|
||||
|
||||
public FastAes(Span<byte> key)
|
||||
public FasterAesX86(Span<byte> key)
|
||||
{
|
||||
RoundKeys = KeyExpansion(key);
|
||||
}
|
||||
|
|
@ -23,11 +23,10 @@ namespace MinecraftClient.Crypto
|
|||
/// <returns>Is it supported</returns>
|
||||
public static bool IsSupported()
|
||||
{
|
||||
return Sse2.IsSupported && Aes.IsSupported;
|
||||
return Aes.IsSupported && Sse2.IsSupported;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public void EncryptEcb(ReadOnlySpan<byte> plaintext, Span<byte> destination)
|
||||
public override void EncryptEcb(Span<byte> plaintext, Span<byte> destination)
|
||||
{
|
||||
Vector128<byte>[] keys = RoundKeys;
|
||||
|
||||
13
MinecraftClient/Crypto/IAesHandler.cs
Normal file
13
MinecraftClient/Crypto/IAesHandler.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -39,8 +39,8 @@ namespace MinecraftClient.Inventory
|
|||
if (ValidateSlots(source, dest, destContainer) &&
|
||||
HasItem(source) &&
|
||||
((destContainer != null && !HasItem(dest, destContainer)) || (destContainer == null && !HasItem(dest))))
|
||||
return mc.DoWindowAction(c.ID, source, WindowActionType.LeftClick)
|
||||
&& mc.DoWindowAction(destContainer == null ? c.ID : destContainer.ID, dest, WindowActionType.LeftClick);
|
||||
return mc.DoWindowAction(c.ID, source, WindowActionType.LeftClick).Result
|
||||
&& mc.DoWindowAction(destContainer == null ? c.ID : destContainer.ID, dest, WindowActionType.LeftClick).Result;
|
||||
else return false;
|
||||
}
|
||||
|
||||
|
|
@ -57,9 +57,9 @@ namespace MinecraftClient.Inventory
|
|||
if (ValidateSlots(slot1, slot2, destContainer) &&
|
||||
HasItem(slot1) &&
|
||||
(destContainer != null && HasItem(slot2, destContainer) || (destContainer == null && HasItem(slot2))))
|
||||
return mc.DoWindowAction(c.ID, slot1, WindowActionType.LeftClick)
|
||||
&& mc.DoWindowAction(destContainer == null ? c.ID : destContainer.ID, slot2, WindowActionType.LeftClick)
|
||||
&& mc.DoWindowAction(c.ID, slot1, WindowActionType.LeftClick);
|
||||
return mc.DoWindowAction(c.ID, slot1, WindowActionType.LeftClick).Result
|
||||
&& mc.DoWindowAction(destContainer == null ? c.ID : destContainer.ID, slot2, WindowActionType.LeftClick).Result
|
||||
&& mc.DoWindowAction(c.ID, slot1, WindowActionType.LeftClick).Result;
|
||||
else return false;
|
||||
}
|
||||
|
||||
|
|
@ -104,14 +104,14 @@ namespace MinecraftClient.Inventory
|
|||
break;
|
||||
}
|
||||
}
|
||||
mc.DoWindowAction(c.ID, source, WindowActionType.LeftClick); // grab item
|
||||
mc.DoWindowAction(c.ID, -999, startDragging);
|
||||
mc.DoWindowAction(c.ID, source, WindowActionType.LeftClick).Wait(); // grab item
|
||||
mc.DoWindowAction(c.ID, -999, startDragging).Wait();
|
||||
foreach (var slot in availableSlots)
|
||||
{
|
||||
mc.DoWindowAction(c.ID, slot, addDragging);
|
||||
mc.DoWindowAction(c.ID, slot, addDragging).Wait();
|
||||
}
|
||||
mc.DoWindowAction(c.ID, -999, endDragging);
|
||||
mc.DoWindowAction(c.ID, source, WindowActionType.LeftClick); // put down item left (if any)
|
||||
mc.DoWindowAction(c.ID, -999, endDragging).Wait();
|
||||
mc.DoWindowAction(c.ID, source, WindowActionType.LeftClick).Wait(); // put down item left (if any)
|
||||
return true;
|
||||
}
|
||||
else return false;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -29,7 +29,6 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="Brigadier.NET" Version="1.2.13" />
|
||||
<PackageReference Include="DnsClient" Version="1.7.0" />
|
||||
<PackageReference Include="DotNetZip" Version="1.16.0" />
|
||||
<PackageReference Include="DSharpPlus" Version="4.2.0" />
|
||||
<PackageReference Include="DynamicExpresso.Core" Version="2.13.0" />
|
||||
<PackageReference Include="FuzzySharp" Version="2.0.2" />
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MinecraftClient.Protocol.Handlers.Forge
|
||||
{
|
||||
|
|
@ -11,16 +12,22 @@ namespace MinecraftClient.Protocol.Handlers.Forge
|
|||
/// <summary>
|
||||
/// Represents an individual forge mod.
|
||||
/// </summary>
|
||||
public class ForgeMod
|
||||
public record ForgeMod
|
||||
{
|
||||
public ForgeMod(String ModID, String Version)
|
||||
public ForgeMod(string? modID, string? version)
|
||||
{
|
||||
this.ModID = ModID;
|
||||
this.Version = Version;
|
||||
ModID = modID;
|
||||
Version = ModMarker = version;
|
||||
}
|
||||
|
||||
public readonly String ModID;
|
||||
public readonly String Version;
|
||||
[JsonPropertyName("modId")]
|
||||
public string? ModID { init; get; }
|
||||
|
||||
[JsonPropertyName("version")]
|
||||
public string? Version { init; get; }
|
||||
|
||||
[JsonPropertyName("modmarker")]
|
||||
public string? ModMarker { init; get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
|
|
@ -138,5 +145,16 @@ namespace MinecraftClient.Protocol.Handlers.Forge
|
|||
throw new NotImplementedException("FMLVersion '" + fmlVersion + "' not implemented!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new ForgeInfo from the given data.
|
||||
/// </summary>
|
||||
/// <param name="data">The modinfo JSON tag.</param>
|
||||
/// <param name="fmlVersion">Forge protocol version</param>
|
||||
internal ForgeInfo(ForgeMod[] mods, FMLVersion fmlVersion)
|
||||
{
|
||||
Mods = new(mods);
|
||||
Version = fmlVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using MinecraftClient.Protocol.PacketPipeline;
|
||||
|
||||
namespace MinecraftClient.Protocol.Handlers.packet.s2c
|
||||
{
|
||||
|
|
@ -8,7 +10,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
|
|||
private static int RootIdx;
|
||||
private static CommandNode[] Nodes = Array.Empty<CommandNode>();
|
||||
|
||||
public static void Read(DataTypes dataTypes, Queue<byte> packetData)
|
||||
public static async Task Read(DataTypes dataTypes, PacketStream packetData)
|
||||
{
|
||||
int count = dataTypes.ReadNextVarInt(packetData);
|
||||
Nodes = new CommandNode[count];
|
||||
|
|
@ -23,7 +25,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
|
|||
|
||||
int redirectNode = ((flags & 0x08) > 0) ? dataTypes.ReadNextVarInt(packetData) : -1;
|
||||
|
||||
string? name = ((flags & 0x03) == 1 || (flags & 0x03) == 2) ? dataTypes.ReadNextString(packetData) : null;
|
||||
string? name = ((flags & 0x03) == 1 || (flags & 0x03) == 2) ? (await dataTypes.ReadNextStringAsync(packetData)) : null;
|
||||
|
||||
int paserId = ((flags & 0x03) == 2) ? dataTypes.ReadNextVarInt(packetData) : -1;
|
||||
Paser? paser = null;
|
||||
|
|
@ -50,7 +52,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
|
|||
};
|
||||
}
|
||||
|
||||
string? suggestionsType = ((flags & 0x10) > 0) ? dataTypes.ReadNextString(packetData) : null;
|
||||
string? suggestionsType = ((flags & 0x10) > 0) ? (await dataTypes.ReadNextStringAsync(packetData)) : null;
|
||||
|
||||
Nodes[i] = new(flags, childs, redirectNode, name, paser, suggestionsType);
|
||||
}
|
||||
|
|
@ -158,7 +160,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
|
|||
internal class PaserEmpty : Paser
|
||||
{
|
||||
|
||||
public PaserEmpty(DataTypes dataTypes, Queue<byte> packetData) { }
|
||||
public PaserEmpty(DataTypes dataTypes, PacketStream packetData) { }
|
||||
|
||||
public override bool Check(string text)
|
||||
{
|
||||
|
|
@ -181,7 +183,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
|
|||
private byte Flags;
|
||||
private float Min = float.MinValue, Max = float.MaxValue;
|
||||
|
||||
public PaserFloat(DataTypes dataTypes, Queue<byte> packetData)
|
||||
public PaserFloat(DataTypes dataTypes, PacketStream packetData)
|
||||
{
|
||||
Flags = dataTypes.ReadNextByte(packetData);
|
||||
if ((Flags & 0x01) > 0)
|
||||
|
|
@ -211,7 +213,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
|
|||
private byte Flags;
|
||||
private double Min = double.MinValue, Max = double.MaxValue;
|
||||
|
||||
public PaserDouble(DataTypes dataTypes, Queue<byte> packetData)
|
||||
public PaserDouble(DataTypes dataTypes, PacketStream packetData)
|
||||
{
|
||||
Flags = dataTypes.ReadNextByte(packetData);
|
||||
if ((Flags & 0x01) > 0)
|
||||
|
|
@ -241,7 +243,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
|
|||
private byte Flags;
|
||||
private int Min = int.MinValue, Max = int.MaxValue;
|
||||
|
||||
public PaserInteger(DataTypes dataTypes, Queue<byte> packetData)
|
||||
public PaserInteger(DataTypes dataTypes, PacketStream packetData)
|
||||
{
|
||||
Flags = dataTypes.ReadNextByte(packetData);
|
||||
if ((Flags & 0x01) > 0)
|
||||
|
|
@ -271,7 +273,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
|
|||
private byte Flags;
|
||||
private long Min = long.MinValue, Max = long.MaxValue;
|
||||
|
||||
public PaserLong(DataTypes dataTypes, Queue<byte> packetData)
|
||||
public PaserLong(DataTypes dataTypes, PacketStream packetData)
|
||||
{
|
||||
Flags = dataTypes.ReadNextByte(packetData);
|
||||
if ((Flags & 0x01) > 0)
|
||||
|
|
@ -302,7 +304,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
|
|||
|
||||
private enum StringType { SINGLE_WORD, QUOTABLE_PHRASE, GREEDY_PHRASE };
|
||||
|
||||
public PaserString(DataTypes dataTypes, Queue<byte> packetData)
|
||||
public PaserString(DataTypes dataTypes, PacketStream packetData)
|
||||
{
|
||||
Type = (StringType)dataTypes.ReadNextVarInt(packetData);
|
||||
}
|
||||
|
|
@ -327,7 +329,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
|
|||
{
|
||||
private byte Flags;
|
||||
|
||||
public PaserEntity(DataTypes dataTypes, Queue<byte> packetData)
|
||||
public PaserEntity(DataTypes dataTypes, PacketStream packetData)
|
||||
{
|
||||
Flags = dataTypes.ReadNextByte(packetData);
|
||||
}
|
||||
|
|
@ -351,7 +353,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
|
|||
internal class PaserBlockPos : Paser
|
||||
{
|
||||
|
||||
public PaserBlockPos(DataTypes dataTypes, Queue<byte> packetData) { }
|
||||
public PaserBlockPos(DataTypes dataTypes, PacketStream packetData) { }
|
||||
|
||||
public override bool Check(string text)
|
||||
{
|
||||
|
|
@ -372,7 +374,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
|
|||
internal class PaserColumnPos : Paser
|
||||
{
|
||||
|
||||
public PaserColumnPos(DataTypes dataTypes, Queue<byte> packetData) { }
|
||||
public PaserColumnPos(DataTypes dataTypes, PacketStream packetData) { }
|
||||
|
||||
public override bool Check(string text)
|
||||
{
|
||||
|
|
@ -393,7 +395,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
|
|||
internal class PaserVec3 : Paser
|
||||
{
|
||||
|
||||
public PaserVec3(DataTypes dataTypes, Queue<byte> packetData) { }
|
||||
public PaserVec3(DataTypes dataTypes, PacketStream packetData) { }
|
||||
|
||||
public override bool Check(string text)
|
||||
{
|
||||
|
|
@ -414,7 +416,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
|
|||
internal class PaserVec2 : Paser
|
||||
{
|
||||
|
||||
public PaserVec2(DataTypes dataTypes, Queue<byte> packetData) { }
|
||||
public PaserVec2(DataTypes dataTypes, PacketStream packetData) { }
|
||||
|
||||
public override bool Check(string text)
|
||||
{
|
||||
|
|
@ -435,7 +437,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
|
|||
internal class PaserRotation : Paser
|
||||
{
|
||||
|
||||
public PaserRotation(DataTypes dataTypes, Queue<byte> packetData) { }
|
||||
public PaserRotation(DataTypes dataTypes, PacketStream packetData) { }
|
||||
|
||||
public override bool Check(string text)
|
||||
{
|
||||
|
|
@ -455,7 +457,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
|
|||
|
||||
internal class PaserMessage : Paser
|
||||
{
|
||||
public PaserMessage(DataTypes dataTypes, Queue<byte> packetData) { }
|
||||
public PaserMessage(DataTypes dataTypes, PacketStream packetData) { }
|
||||
|
||||
public override bool Check(string text)
|
||||
{
|
||||
|
|
@ -477,7 +479,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
|
|||
{
|
||||
private byte Flags;
|
||||
|
||||
public PaserScoreHolder(DataTypes dataTypes, Queue<byte> packetData)
|
||||
public PaserScoreHolder(DataTypes dataTypes, PacketStream packetData)
|
||||
{
|
||||
Flags = dataTypes.ReadNextByte(packetData);
|
||||
}
|
||||
|
|
@ -502,7 +504,7 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
|
|||
{
|
||||
private bool Decimals;
|
||||
|
||||
public PaserRange(DataTypes dataTypes, Queue<byte> packetData)
|
||||
public PaserRange(DataTypes dataTypes, PacketStream packetData)
|
||||
{
|
||||
Decimals = dataTypes.ReadNextBool(packetData);
|
||||
}
|
||||
|
|
@ -527,9 +529,11 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
|
|||
{
|
||||
private string Registry;
|
||||
|
||||
public PaserResourceOrTag(DataTypes dataTypes, Queue<byte> packetData)
|
||||
public PaserResourceOrTag(DataTypes dataTypes, PacketStream packetData)
|
||||
{
|
||||
Registry = dataTypes.ReadNextString(packetData);
|
||||
var task = dataTypes.ReadNextStringAsync(packetData);
|
||||
task.Wait();
|
||||
Registry = task.Result;
|
||||
}
|
||||
|
||||
public override bool Check(string text)
|
||||
|
|
@ -552,9 +556,11 @@ namespace MinecraftClient.Protocol.Handlers.packet.s2c
|
|||
{
|
||||
private string Registry;
|
||||
|
||||
public PaserResource(DataTypes dataTypes, Queue<byte> packetData)
|
||||
public PaserResource(DataTypes dataTypes, PacketStream packetData)
|
||||
{
|
||||
Registry = dataTypes.ReadNextString(packetData);
|
||||
var task = dataTypes.ReadNextStringAsync(packetData);
|
||||
task.Wait();
|
||||
Registry = task.Result;
|
||||
}
|
||||
|
||||
public override bool Check(string text)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -3,9 +3,12 @@ using System.Collections.Generic;
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MinecraftClient.Protocol.Handlers.Forge;
|
||||
using MinecraftClient.Protocol.Message;
|
||||
using MinecraftClient.Protocol.PacketPipeline;
|
||||
using MinecraftClient.Scripting;
|
||||
using static MinecraftClient.Protocol.Handlers.Protocol18Handler;
|
||||
|
||||
namespace MinecraftClient.Protocol.Handlers
|
||||
{
|
||||
|
|
@ -54,23 +57,23 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
/// Completes the Minecraft Forge handshake (Forge Protocol version 1: FML)
|
||||
/// </summary>
|
||||
/// <returns>Whether the handshake was successful.</returns>
|
||||
public bool CompleteForgeHandshake()
|
||||
public async Task<bool> CompleteForgeHandshake(SocketWrapper socketWrapper)
|
||||
{
|
||||
if (ForgeEnabled() && forgeInfo!.Version == FMLVersion.FML)
|
||||
{
|
||||
while (fmlHandshakeState != FMLHandshakeClientState.DONE)
|
||||
{
|
||||
(int packetID, Queue<byte> packetData) = protocol18.ReadNextPacket();
|
||||
(int packetID, PacketStream packetStream) = await socketWrapper.GetNextPacket(handleCompress: true);
|
||||
|
||||
if (packetID == 0x40) // Disconnect
|
||||
{
|
||||
mcHandler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, ChatParser.ParseText(dataTypes.ReadNextString(packetData)));
|
||||
mcHandler.OnConnectionLost(ChatBot.DisconnectReason.LoginRejected, ChatParser.ParseText(await dataTypes.ReadNextStringAsync(packetStream)));
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Send back regular packet to the vanilla protocol handler
|
||||
protocol18.HandlePacket(packetID, packetData);
|
||||
await protocol18.HandlePacket(packetID, packetStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -82,7 +85,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
/// </summary>
|
||||
/// <param name="packetData">Packet data to read from</param>
|
||||
/// <returns>Length from packet data</returns>
|
||||
public int ReadNextVarShort(Queue<byte> packetData)
|
||||
public int ReadNextVarShort(PacketStream packetData)
|
||||
{
|
||||
if (ForgeEnabled())
|
||||
{
|
||||
|
|
@ -103,10 +106,11 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
/// <param name="packetData">Plugin message data</param>
|
||||
/// <param name="currentDimension">Current world dimension</param>
|
||||
/// <returns>TRUE if the plugin message was recognized and handled</returns>
|
||||
public bool HandlePluginMessage(string channel, Queue<byte> packetData, ref int currentDimension)
|
||||
public async Task<Tuple<bool, int>> HandlePluginMessage(string channel, byte[] packetDataArr, int currentDimension)
|
||||
{
|
||||
if (ForgeEnabled() && forgeInfo!.Version == FMLVersion.FML && fmlHandshakeState != FMLHandshakeClientState.DONE)
|
||||
{
|
||||
Queue<byte> packetData = new(packetDataArr);
|
||||
if (channel == "FML|HS")
|
||||
{
|
||||
FMLHandshakeDiscriminator discriminator = (FMLHandshakeDiscriminator)dataTypes.ReadNextByte(packetData);
|
||||
|
|
@ -114,21 +118,21 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
if (discriminator == FMLHandshakeDiscriminator.HandshakeReset)
|
||||
{
|
||||
fmlHandshakeState = FMLHandshakeClientState.START;
|
||||
return true;
|
||||
return new(true, currentDimension);
|
||||
}
|
||||
|
||||
switch (fmlHandshakeState)
|
||||
{
|
||||
case FMLHandshakeClientState.START:
|
||||
if (discriminator != FMLHandshakeDiscriminator.ServerHello)
|
||||
return false;
|
||||
return new(false, currentDimension);
|
||||
|
||||
// Send the plugin channel registration.
|
||||
// REGISTER is somewhat special in that it doesn't actually include length information,
|
||||
// and is also \0-separated.
|
||||
// Also, yes, "FML" is there twice. Don't ask me why, but that's the way forge does it.
|
||||
string[] channels = { "FML|HS", "FML", "FML|MP", "FML", "FORGE" };
|
||||
protocol18.SendPluginChannelPacket("REGISTER", Encoding.UTF8.GetBytes(string.Join("\0", channels)));
|
||||
await protocol18.SendPluginChannelPacket("REGISTER", Encoding.UTF8.GetBytes(string.Join("\0", channels)));
|
||||
|
||||
byte fmlProtocolVersion = dataTypes.ReadNextByte(packetData);
|
||||
|
||||
|
|
@ -139,7 +143,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
currentDimension = dataTypes.ReadNextInt(packetData);
|
||||
|
||||
// Tell the server we're running the same version.
|
||||
SendForgeHandshakePacket(FMLHandshakeDiscriminator.ClientHello, new byte[] { fmlProtocolVersion });
|
||||
await SendForgeHandshakePacket(FMLHandshakeDiscriminator.ClientHello, new byte[] { fmlProtocolVersion });
|
||||
|
||||
// Then tell the server that we're running the same mods.
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
|
|
@ -148,17 +152,17 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
for (int i = 0; i < forgeInfo.Mods.Count; i++)
|
||||
{
|
||||
ForgeInfo.ForgeMod mod = forgeInfo.Mods[i];
|
||||
mods[i] = dataTypes.ConcatBytes(dataTypes.GetString(mod.ModID), dataTypes.GetString(mod.Version));
|
||||
mods[i] = dataTypes.ConcatBytes(dataTypes.GetString(mod.ModID!), dataTypes.GetString(mod.Version ?? mod.ModMarker ?? string.Empty));
|
||||
}
|
||||
SendForgeHandshakePacket(FMLHandshakeDiscriminator.ModList,
|
||||
await SendForgeHandshakePacket(FMLHandshakeDiscriminator.ModList,
|
||||
dataTypes.ConcatBytes(dataTypes.GetVarInt(forgeInfo.Mods.Count), dataTypes.ConcatBytes(mods)));
|
||||
|
||||
fmlHandshakeState = FMLHandshakeClientState.WAITINGSERVERDATA;
|
||||
|
||||
return true;
|
||||
return new(true, currentDimension);
|
||||
case FMLHandshakeClientState.WAITINGSERVERDATA:
|
||||
if (discriminator != FMLHandshakeDiscriminator.ModList)
|
||||
return false;
|
||||
return new(false, currentDimension);
|
||||
|
||||
Thread.Sleep(2000);
|
||||
|
||||
|
|
@ -167,16 +171,16 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
// Tell the server that yes, we are OK with the mods it has
|
||||
// even though we don't actually care what mods it has.
|
||||
|
||||
SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck,
|
||||
await SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck,
|
||||
new byte[] { (byte)FMLHandshakeClientState.WAITINGSERVERDATA });
|
||||
|
||||
fmlHandshakeState = FMLHandshakeClientState.WAITINGSERVERCOMPLETE;
|
||||
return false;
|
||||
return new(false, currentDimension);
|
||||
case FMLHandshakeClientState.WAITINGSERVERCOMPLETE:
|
||||
// The server now will tell us a bunch of registry information.
|
||||
// We need to read it all, though, until it says that there is no more.
|
||||
if (discriminator != FMLHandshakeDiscriminator.RegistryData)
|
||||
return false;
|
||||
return new(false, currentDimension);
|
||||
|
||||
if (protocolversion < Protocol18Handler.MC_1_8_Version)
|
||||
{
|
||||
|
|
@ -202,34 +206,34 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
fmlHandshakeState = FMLHandshakeClientState.PENDINGCOMPLETE;
|
||||
}
|
||||
|
||||
return false;
|
||||
return new(false, currentDimension);
|
||||
case FMLHandshakeClientState.PENDINGCOMPLETE:
|
||||
// The server will ask us to accept the registries.
|
||||
// Just say yes.
|
||||
if (discriminator != FMLHandshakeDiscriminator.HandshakeAck)
|
||||
return false;
|
||||
return new(false, currentDimension);
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted("§8" + Translations.forge_accept_registry, acceptnewlines: true);
|
||||
SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck,
|
||||
await SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck,
|
||||
new byte[] { (byte)FMLHandshakeClientState.PENDINGCOMPLETE });
|
||||
fmlHandshakeState = FMLHandshakeClientState.COMPLETE;
|
||||
|
||||
return true;
|
||||
return new(true, currentDimension);
|
||||
case FMLHandshakeClientState.COMPLETE:
|
||||
// One final "OK". On the actual forge source, a packet is sent from
|
||||
// the client to the client saying that the connection was complete, but
|
||||
// we don't need to do that.
|
||||
|
||||
SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck,
|
||||
await SendForgeHandshakePacket(FMLHandshakeDiscriminator.HandshakeAck,
|
||||
new byte[] { (byte)FMLHandshakeClientState.COMPLETE });
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLine(Translations.forge_complete);
|
||||
fmlHandshakeState = FMLHandshakeClientState.DONE;
|
||||
return true;
|
||||
return new(true, currentDimension);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return new(false, currentDimension);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -239,8 +243,9 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
/// <param name="packetData">Plugin message data</param>
|
||||
/// <param name="responseData">Response data to return to server</param>
|
||||
/// <returns>TRUE/FALSE depending on whether the packet was understood or not</returns>
|
||||
public bool HandleLoginPluginRequest(string channel, Queue<byte> packetData, ref List<byte> responseData)
|
||||
public async Task<Tuple<bool, List<byte>>> HandleLoginPluginRequest(string channel, PacketStream packetData)
|
||||
{
|
||||
List<byte> responseData = new();
|
||||
if (ForgeEnabled() && forgeInfo!.Version == FMLVersion.FML2 && channel == "fml:loginwrapper")
|
||||
{
|
||||
// Forge Handshake handler source code used to implement the FML2 packets:
|
||||
|
|
@ -278,8 +283,8 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
// The content of each message is mapped into a class inside FMLHandshakeMessages.java
|
||||
// FMLHandshakeHandler will then process the packet, e.g. handleServerModListOnClient() for Server Mod List.
|
||||
|
||||
string fmlChannel = dataTypes.ReadNextString(packetData);
|
||||
dataTypes.ReadNextVarInt(packetData); // Packet length
|
||||
string fmlChannel = await dataTypes.ReadNextStringAsync(packetData);
|
||||
dataTypes.SkipNextVarInt(packetData); // Packet length
|
||||
int packetID = dataTypes.ReadNextVarInt(packetData);
|
||||
|
||||
if (fmlChannel == "fml:handshake")
|
||||
|
|
@ -308,17 +313,17 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
List<string> mods = new();
|
||||
int modCount = dataTypes.ReadNextVarInt(packetData);
|
||||
for (int i = 0; i < modCount; i++)
|
||||
mods.Add(dataTypes.ReadNextString(packetData));
|
||||
mods.Add(await dataTypes.ReadNextStringAsync(packetData));
|
||||
|
||||
Dictionary<string, string> channels = new();
|
||||
int channelCount = dataTypes.ReadNextVarInt(packetData);
|
||||
for (int i = 0; i < channelCount; i++)
|
||||
channels.Add(dataTypes.ReadNextString(packetData), dataTypes.ReadNextString(packetData));
|
||||
channels.Add(await dataTypes.ReadNextStringAsync(packetData), await dataTypes.ReadNextStringAsync(packetData));
|
||||
|
||||
List<string> registries = new();
|
||||
int registryCount = dataTypes.ReadNextVarInt(packetData);
|
||||
for (int i = 0; i < registryCount; i++)
|
||||
registries.Add(dataTypes.ReadNextString(packetData));
|
||||
registries.Add(await dataTypes.ReadNextStringAsync(packetData));
|
||||
|
||||
// Server Mod List Reply: FMLHandshakeMessages.java > C2SModListReply > encode()
|
||||
//
|
||||
|
|
@ -372,7 +377,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
{
|
||||
string registryName = dataTypes.ReadNextString(packetData);
|
||||
string registryName = await dataTypes.ReadNextStringAsync(packetData);
|
||||
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_fml2_registry, registryName));
|
||||
}
|
||||
|
||||
|
|
@ -391,7 +396,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
{
|
||||
string configName = dataTypes.ReadNextString(packetData);
|
||||
string configName = await dataTypes.ReadNextStringAsync(packetData);
|
||||
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_fml2_config, configName));
|
||||
}
|
||||
|
||||
|
|
@ -408,11 +413,10 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
if (fmlResponseReady)
|
||||
{
|
||||
// Wrap our FML packet into a LoginPluginResponse payload
|
||||
responseData.Clear();
|
||||
responseData.AddRange(dataTypes.GetString(fmlChannel));
|
||||
responseData.AddRange(dataTypes.GetVarInt(fmlResponsePacket.Count));
|
||||
responseData.AddRange(fmlResponsePacket);
|
||||
return true;
|
||||
return new(true, responseData);
|
||||
}
|
||||
}
|
||||
else if (Settings.Config.Logging.DebugMessages)
|
||||
|
|
@ -420,7 +424,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_fml2_unknown_channel, fmlChannel));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return new(false, responseData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -428,9 +432,9 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
/// </summary>
|
||||
/// <param name="discriminator">Discriminator to use.</param>
|
||||
/// <param name="data">packet Data</param>
|
||||
private void SendForgeHandshakePacket(FMLHandshakeDiscriminator discriminator, byte[] data)
|
||||
private async Task SendForgeHandshakePacket(FMLHandshakeDiscriminator discriminator, byte[] data)
|
||||
{
|
||||
protocol18.SendPluginChannelPacket("FML|HS", dataTypes.ConcatBytes(new byte[] { (byte)discriminator }, data));
|
||||
await protocol18.SendPluginChannelPacket("FML|HS", dataTypes.ConcatBytes(new byte[] { (byte)discriminator }, data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -439,10 +443,10 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
/// <param name="jsonData">JSON data returned by the server</param>
|
||||
/// <param name="forgeInfo">ForgeInfo to populate</param>
|
||||
/// <returns>True if the server is running Forge</returns>
|
||||
public static bool ServerInfoCheckForge(Json.JSONData jsonData, ref ForgeInfo? forgeInfo)
|
||||
public static bool ServerInfoCheckForge(PingResult jsonData, ref ForgeInfo? forgeInfo)
|
||||
{
|
||||
return ServerInfoCheckForgeSub(jsonData, ref forgeInfo, FMLVersion.FML) // MC 1.12 and lower
|
||||
|| ServerInfoCheckForgeSub(jsonData, ref forgeInfo, FMLVersion.FML2); // MC 1.13 and greater
|
||||
return ServerInfoCheckForgeSubFML1(jsonData, ref forgeInfo) // MC 1.12 and lower
|
||||
|| ServerInfoCheckForgeSubFML2(jsonData, ref forgeInfo); // MC 1.13 and greater
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -474,38 +478,21 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
/// </summary>
|
||||
/// <param name="jsonData">JSON data returned by the server</param>
|
||||
/// <param name="forgeInfo">ForgeInfo to populate</param>
|
||||
/// <param name="fmlVersion">Forge protocol version</param>
|
||||
/// <returns>True if the server is running Forge</returns>
|
||||
private static bool ServerInfoCheckForgeSub(Json.JSONData jsonData, ref ForgeInfo? forgeInfo, FMLVersion fmlVersion)
|
||||
private static bool ServerInfoCheckForgeSubFML1(PingResult jsonData, ref ForgeInfo? forgeInfo)
|
||||
{
|
||||
string forgeDataTag;
|
||||
string versionField;
|
||||
string versionString;
|
||||
|
||||
switch (fmlVersion)
|
||||
if (jsonData.modinfo != null)
|
||||
{
|
||||
case FMLVersion.FML:
|
||||
forgeDataTag = "modinfo";
|
||||
versionField = "type";
|
||||
versionString = "FML";
|
||||
break;
|
||||
case FMLVersion.FML2:
|
||||
forgeDataTag = "forgeData";
|
||||
versionField = "fmlNetworkVersion";
|
||||
versionString = "2";
|
||||
break;
|
||||
default:
|
||||
throw new NotImplementedException("FMLVersion '" + fmlVersion + "' not implemented!");
|
||||
}
|
||||
|
||||
if (jsonData.Properties.ContainsKey(forgeDataTag) && jsonData.Properties[forgeDataTag].Type == Json.JSONData.DataType.Object)
|
||||
{
|
||||
Json.JSONData modData = jsonData.Properties[forgeDataTag];
|
||||
if (modData.Properties.ContainsKey(versionField) && modData.Properties[versionField].StringValue == versionString)
|
||||
if (jsonData.modinfo.type == "FML")
|
||||
{
|
||||
forgeInfo = new ForgeInfo(modData, fmlVersion);
|
||||
if (forgeInfo.Mods.Any())
|
||||
if (jsonData.modinfo.modList == null || jsonData.modinfo.modList.Length == 0)
|
||||
{
|
||||
forgeInfo = null;
|
||||
ConsoleIO.WriteLineFormatted("§8" + Translations.forge_no_mod, acceptnewlines: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
forgeInfo = new ForgeInfo(jsonData.modinfo.modList, FMLVersion.FML);
|
||||
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_with_mod, forgeInfo.Mods.Count));
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
{
|
||||
|
|
@ -515,10 +502,39 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server Info: Check for For Forge on a Minecraft server Ping result (Handles FML and FML2
|
||||
/// </summary>
|
||||
/// <param name="jsonData">JSON data returned by the server</param>
|
||||
/// <param name="forgeInfo">ForgeInfo to populate</param>
|
||||
/// <returns>True if the server is running Forge</returns>
|
||||
private static bool ServerInfoCheckForgeSubFML2(PingResult jsonData, ref ForgeInfo? forgeInfo)
|
||||
{
|
||||
if (jsonData.forgeData != null)
|
||||
{
|
||||
if (jsonData.forgeData.fmlNetworkVersion == "2")
|
||||
{
|
||||
if (jsonData.forgeData.mods == null || jsonData.forgeData.mods.Length == 0)
|
||||
{
|
||||
forgeInfo = null;
|
||||
ConsoleIO.WriteLineFormatted("§8" + Translations.forge_no_mod, acceptnewlines: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§8" + Translations.forge_no_mod, acceptnewlines: true);
|
||||
forgeInfo = null;
|
||||
forgeInfo = new ForgeInfo(jsonData.forgeData.mods, FMLVersion.FML2);
|
||||
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.forge_with_mod, forgeInfo.Mods.Count));
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§8" + Translations.forge_mod_list, acceptnewlines: true);
|
||||
foreach (ForgeInfo.ForgeMod mod in forgeInfo.Mods)
|
||||
ConsoleIO.WriteLineFormatted("§8 " + mod.ToString());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ using System.Collections.Generic;
|
|||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
//using System.Linq;
|
||||
//using System.Text;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Protocol.PacketPipeline;
|
||||
|
||||
namespace MinecraftClient.Protocol.Handlers
|
||||
{
|
||||
|
|
@ -33,21 +35,21 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
/// <summary>
|
||||
/// Reading the "Block states" field: consists of 4096 entries, representing all the blocks in the chunk section.
|
||||
/// </summary>
|
||||
/// <param name="cache">Cache for reading data</param>
|
||||
/// <param name="stream">Cache for reading data</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
private Chunk? ReadBlockStatesField(Queue<byte> cache)
|
||||
private async Task<Chunk?> ReadBlockStatesFieldAsync(PacketStream stream)
|
||||
{
|
||||
// read Block states (Type: Paletted Container)
|
||||
byte bitsPerEntry = dataTypes.ReadNextByte(cache);
|
||||
byte bitsPerEntry = await dataTypes.ReadNextByteAsync(stream);
|
||||
|
||||
// 1.18(1.18.1) add a pattle named "Single valued" to replace the vertical strip bitmask in the old
|
||||
if (bitsPerEntry == 0 && protocolversion >= Protocol18Handler.MC_1_18_1_Version)
|
||||
{
|
||||
// Palettes: Single valued - 1.18(1.18.1) and above
|
||||
ushort blockId = (ushort)dataTypes.ReadNextVarInt(cache);
|
||||
ushort blockId = (ushort)(await dataTypes.ReadNextVarIntAsync(stream));
|
||||
Block block = new(blockId);
|
||||
|
||||
dataTypes.SkipNextVarInt(cache); // Data Array Length will be zero
|
||||
dataTypes.SkipNextVarInt(stream); // Data Array Length will be zero
|
||||
|
||||
// Empty chunks will not be stored
|
||||
if (block.Type == Material.Air)
|
||||
|
|
@ -73,16 +75,16 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
// EG, if bitsPerEntry = 5, valueMask = 00011111 in binary
|
||||
uint valueMask = (uint)((1 << bitsPerEntry) - 1);
|
||||
|
||||
int paletteLength = usePalette ? dataTypes.ReadNextVarInt(cache) : 0; // Assume zero when length is absent
|
||||
int paletteLength = usePalette ? await dataTypes.ReadNextVarIntAsync(stream) : 0; // Assume zero when length is absent
|
||||
|
||||
Span<uint> palette = paletteLength < 256 ? stackalloc uint[paletteLength] : new uint[paletteLength];
|
||||
uint[] palette = new uint[paletteLength];
|
||||
for (int i = 0; i < paletteLength; i++)
|
||||
palette[i] = (uint)dataTypes.ReadNextVarInt(cache);
|
||||
palette[i] = (uint)(await dataTypes.ReadNextVarIntAsync(stream));
|
||||
|
||||
//// Block IDs are packed in the array of 64-bits integers
|
||||
dataTypes.SkipNextVarInt(cache); // Entry length
|
||||
Span<byte> entryDataByte = stackalloc byte[8];
|
||||
Span<long> entryDataLong = MemoryMarshal.Cast<byte, long>(entryDataByte); // Faster than MemoryMarshal.Read<long>
|
||||
dataTypes.SkipNextVarInt(stream); // Entry length
|
||||
|
||||
long entryData = 0;
|
||||
|
||||
Chunk chunk = new();
|
||||
int startOffset = 64; // Read the first data immediately
|
||||
|
|
@ -101,10 +103,10 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
|
||||
// When overlapping, move forward to the beginning of the next Long
|
||||
startOffset = 0;
|
||||
dataTypes.ReadDataReverse(cache, entryDataByte); // read long
|
||||
entryData = await dataTypes.ReadNextLongAsync(stream);
|
||||
}
|
||||
|
||||
uint blockId = (uint)(entryDataLong[0] >> startOffset) & valueMask;
|
||||
uint blockId = (uint)(entryData >> startOffset) & valueMask;
|
||||
|
||||
// Map small IDs to actual larger block IDs
|
||||
if (usePalette)
|
||||
|
|
@ -141,10 +143,10 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
/// <param name="chunkX">Chunk X location</param>
|
||||
/// <param name="chunkZ">Chunk Z location</param>
|
||||
/// <param name="verticalStripBitmask">Chunk mask for reading data, store in bitset, used in 1.17 and 1.17.1</param>
|
||||
/// <param name="cache">Cache for reading chunk data</param>
|
||||
/// <param name="stream">Cache for reading chunk data</param>
|
||||
/// <param name="cancellationToken">token to cancel the task</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
public void ProcessChunkColumnData(int chunkX, int chunkZ, ulong[]? verticalStripBitmask, Queue<byte> cache)
|
||||
public async Task ProcessChunkColumnData(int chunkX, int chunkZ, ulong[]? verticalStripBitmask, PacketStream stream)
|
||||
{
|
||||
World world = handler.GetWorld();
|
||||
|
||||
|
|
@ -181,10 +183,10 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
((verticalStripBitmask![chunkY / 64] & (1UL << (chunkY % 64))) != 0))
|
||||
{
|
||||
// Non-air block count inside chunk section, for lighting purposes
|
||||
int blockCnt = dataTypes.ReadNextShort(cache);
|
||||
int blockCnt = await dataTypes.ReadNextShortAsync(stream);
|
||||
|
||||
// Read Block states (Type: Paletted Container)
|
||||
Chunk? chunk = ReadBlockStatesField(cache);
|
||||
Chunk? chunk = await ReadBlockStatesFieldAsync(stream);
|
||||
|
||||
//We have our chunk, save the chunk into the world
|
||||
world.StoreChunk(chunkX, chunkY, chunkZ, chunkColumnSize, chunk, chunkY == lastChunkY);
|
||||
|
|
@ -192,23 +194,23 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
// Skip Read Biomes (Type: Paletted Container) - 1.18(1.18.1) and above
|
||||
if (protocolversion >= Protocol18Handler.MC_1_18_1_Version)
|
||||
{
|
||||
byte bitsPerEntryBiome = dataTypes.ReadNextByte(cache); // Bits Per Entry
|
||||
byte bitsPerEntryBiome = await dataTypes.ReadNextByteAsync(stream); // Bits Per Entry
|
||||
if (bitsPerEntryBiome == 0)
|
||||
{
|
||||
dataTypes.SkipNextVarInt(cache); // Value
|
||||
dataTypes.SkipNextVarInt(cache); // Data Array Length
|
||||
dataTypes.SkipNextVarInt(stream); // Value
|
||||
dataTypes.SkipNextVarInt(stream); // Data Array Length
|
||||
// Data Array must be empty
|
||||
}
|
||||
else
|
||||
{
|
||||
if (bitsPerEntryBiome <= 3)
|
||||
{
|
||||
int paletteLength = dataTypes.ReadNextVarInt(cache); // Palette Length
|
||||
int paletteLength = await dataTypes.ReadNextVarIntAsync(stream); // Palette Length
|
||||
for (int i = 0; i < paletteLength; i++)
|
||||
dataTypes.SkipNextVarInt(cache); // Palette
|
||||
dataTypes.SkipNextVarInt(stream); // Palette
|
||||
}
|
||||
int dataArrayLength = dataTypes.ReadNextVarInt(cache); // Data Array Length
|
||||
dataTypes.DropData(dataArrayLength * 8, cache); // Data Array
|
||||
int dataArrayLength = await dataTypes.ReadNextVarIntAsync(stream); // Data Array Length
|
||||
await dataTypes.DropDataAsync(dataArrayLength * 8, stream); // Data Array
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -228,10 +230,10 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
/// <param name="hasSkyLight">Contains skylight info</param>
|
||||
/// <param name="chunksContinuous">Are the chunk continuous</param>
|
||||
/// <param name="currentDimension">Current dimension type (0 = overworld)</param>
|
||||
/// <param name="cache">Cache for reading chunk data</param>
|
||||
/// <param name="stream">Cache for reading chunk data</param>
|
||||
/// <param name="cancellationToken">token to cancel the task</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
public void ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ushort chunkMask2, bool hasSkyLight, bool chunksContinuous, int currentDimension, Queue<byte> cache)
|
||||
public async Task ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ushort chunkMask2, bool hasSkyLight, bool chunksContinuous, int currentDimension, PacketStream stream)
|
||||
{
|
||||
World world = handler.GetWorld();
|
||||
|
||||
|
|
@ -247,9 +249,9 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
{
|
||||
// 1.14 and above Non-air block count inside chunk section, for lighting purposes
|
||||
if (protocolversion >= Protocol18Handler.MC_1_14_Version)
|
||||
dataTypes.ReadNextShort(cache);
|
||||
await dataTypes.SkipNextShortAsync(stream);
|
||||
|
||||
byte bitsPerBlock = dataTypes.ReadNextByte(cache);
|
||||
byte bitsPerBlock = await dataTypes.ReadNextByteAsync(stream);
|
||||
bool usePalette = (bitsPerBlock <= 8);
|
||||
|
||||
// Vanilla Minecraft will use at least 4 bits per block
|
||||
|
|
@ -260,12 +262,12 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
// is not used, MC 1.13+ does not send the field at all in this case
|
||||
int paletteLength = 0; // Assume zero when length is absent
|
||||
if (usePalette || protocolversion < Protocol18Handler.MC_1_13_Version)
|
||||
paletteLength = dataTypes.ReadNextVarInt(cache);
|
||||
paletteLength = await dataTypes.ReadNextVarIntAsync(stream);
|
||||
|
||||
int[] palette = new int[paletteLength];
|
||||
for (int i = 0; i < paletteLength; i++)
|
||||
{
|
||||
palette[i] = dataTypes.ReadNextVarInt(cache);
|
||||
palette[i] = await dataTypes.ReadNextVarIntAsync(stream);
|
||||
}
|
||||
|
||||
// Bit mask covering bitsPerBlock bits
|
||||
|
|
@ -273,7 +275,7 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
uint valueMask = (uint)((1 << bitsPerBlock) - 1);
|
||||
|
||||
// Block IDs are packed in the array of 64-bits integers
|
||||
ulong[] dataArray = dataTypes.ReadNextULongArray(cache);
|
||||
ulong[] dataArray = await dataTypes.ReadNextULongArrayAsync(stream);
|
||||
|
||||
Chunk chunk = new();
|
||||
|
||||
|
|
@ -358,19 +360,19 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
}
|
||||
}
|
||||
|
||||
//We have our chunk, save the chunk into the world
|
||||
// We have our chunk, save the chunk into the world
|
||||
world.StoreChunk(chunkX, chunkY, chunkZ, chunkColumnSize, chunk, chunkY == maxChunkY);
|
||||
|
||||
//Pre-1.14 Lighting data
|
||||
// Pre-1.14 Lighting data
|
||||
if (protocolversion < Protocol18Handler.MC_1_14_Version)
|
||||
{
|
||||
//Skip block light
|
||||
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache);
|
||||
// Skip block light
|
||||
await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, stream);
|
||||
|
||||
//Skip sky light
|
||||
// Skip sky light
|
||||
if (currentDimension == 0)
|
||||
// Sky light is not sent in the nether or the end
|
||||
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache);
|
||||
await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -383,15 +385,12 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
// 1.8 chunk format
|
||||
if (chunksContinuous && chunkMask == 0)
|
||||
{
|
||||
//Unload the entire chunk column
|
||||
handler.InvokeOnMainThread(() =>
|
||||
{
|
||||
world[chunkX, chunkZ] = null;
|
||||
});
|
||||
// Unload the entire chunk column
|
||||
world[chunkX, chunkZ] = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Load chunk data from the server
|
||||
// Load chunk data from the server
|
||||
int maxChunkY = sizeof(int) * 8 - 1 - BitOperations.LeadingZeroCount(chunkMask);
|
||||
for (int chunkY = 0; chunkY <= maxChunkY; chunkY++)
|
||||
{
|
||||
|
|
@ -399,35 +398,34 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
{
|
||||
Chunk chunk = new();
|
||||
|
||||
//Read chunk data, all at once for performance reasons, and build the chunk object
|
||||
Queue<ushort> queue = new(dataTypes.ReadNextUShortsLittleEndian(Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ, cache));
|
||||
// Read chunk data, all at once for performance reasons, and build the chunk object
|
||||
for (int blockY = 0; blockY < Chunk.SizeY; blockY++)
|
||||
for (int blockZ = 0; blockZ < Chunk.SizeZ; blockZ++)
|
||||
for (int blockX = 0; blockX < Chunk.SizeX; blockX++)
|
||||
chunk.SetWithoutCheck(blockX, blockY, blockZ, new Block(queue.Dequeue()));
|
||||
chunk.SetWithoutCheck(blockX, blockY, blockZ, new Block(await dataTypes.ReadNextUShortAsync(stream)));
|
||||
|
||||
//We have our chunk, save the chunk into the world
|
||||
// We have our chunk, save the chunk into the world
|
||||
world.StoreChunk(chunkX, chunkY, chunkZ, chunkColumnSize, chunk, chunkY == maxChunkY);
|
||||
}
|
||||
}
|
||||
|
||||
//Skip light information
|
||||
// Skip light information
|
||||
for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++)
|
||||
{
|
||||
if ((chunkMask & (1 << chunkY)) != 0)
|
||||
{
|
||||
//Skip block light
|
||||
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache);
|
||||
// Skip block light
|
||||
await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, stream);
|
||||
|
||||
//Skip sky light
|
||||
// Skip sky light
|
||||
if (hasSkyLight)
|
||||
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, cache);
|
||||
await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ) / 2, stream);
|
||||
}
|
||||
}
|
||||
|
||||
//Skip biome metadata
|
||||
// Skip biome metadata
|
||||
if (chunksContinuous)
|
||||
dataTypes.DropData(Chunk.SizeX * Chunk.SizeZ, cache);
|
||||
await dataTypes.DropDataAsync(Chunk.SizeX * Chunk.SizeZ, stream);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
@ -435,15 +433,12 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
// 1.7 chunk format
|
||||
if (chunksContinuous && chunkMask == 0)
|
||||
{
|
||||
//Unload the entire chunk column
|
||||
handler.InvokeOnMainThread(() =>
|
||||
{
|
||||
world[chunkX, chunkZ] = null;
|
||||
});
|
||||
// Unload the entire chunk column
|
||||
world[chunkX, chunkZ] = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Count chunk sections
|
||||
// Count chunk sections
|
||||
int sectionCount = 0;
|
||||
int addDataSectionCount = 0;
|
||||
for (int chunkY = 0; chunkY < chunkColumnSize; chunkY++)
|
||||
|
|
@ -454,10 +449,10 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
addDataSectionCount++;
|
||||
}
|
||||
|
||||
//Read chunk data, unpacking 4-bit values into 8-bit values for block metadata
|
||||
Queue<byte> blockTypes = new(dataTypes.ReadData(Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount, cache));
|
||||
// Read chunk data, unpacking 4-bit values into 8-bit values for block metadata
|
||||
Queue<byte> blockTypes = new(await dataTypes.ReadDataAsync(Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount, stream));
|
||||
Queue<byte> blockMeta = new();
|
||||
foreach (byte packed in dataTypes.ReadData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, cache))
|
||||
foreach (byte packed in await dataTypes.ReadDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, stream))
|
||||
{
|
||||
byte hig = (byte)(packed >> 4);
|
||||
byte low = (byte)(packed & (byte)0x0F);
|
||||
|
|
@ -465,15 +460,15 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
blockMeta.Enqueue(low);
|
||||
}
|
||||
|
||||
//Skip data we don't need
|
||||
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, cache); //Block light
|
||||
// Skip data we don't need
|
||||
await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, stream); //Block light
|
||||
if (hasSkyLight)
|
||||
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, cache); //Sky light
|
||||
dataTypes.DropData((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * addDataSectionCount) / 2, cache); //BlockAdd
|
||||
await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * sectionCount) / 2, stream); //Sky light
|
||||
await dataTypes.DropDataAsync((Chunk.SizeX * Chunk.SizeY * Chunk.SizeZ * addDataSectionCount) / 2, stream); //BlockAdd
|
||||
if (chunksContinuous)
|
||||
dataTypes.DropData(Chunk.SizeX * Chunk.SizeZ, cache); //Biomes
|
||||
await dataTypes.DropDataAsync(Chunk.SizeX * Chunk.SizeZ, stream); //Biomes
|
||||
|
||||
//Load chunk data
|
||||
// Load chunk data
|
||||
int maxChunkY = sizeof(int) * 8 - 1 - BitOperations.LeadingZeroCount(chunkMask);
|
||||
for (int chunkY = 0; chunkY <= maxChunkY; chunkY++)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MinecraftClient.Inventory;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Protocol.ProfileKey;
|
||||
|
|
@ -19,7 +22,12 @@ namespace MinecraftClient.Protocol
|
|||
/// Start the login procedure once connected to the server
|
||||
/// </summary>
|
||||
/// <returns>True if login was successful</returns>
|
||||
bool Login(PlayerKeyPair? playerKeyPair, Session.SessionToken session);
|
||||
Task<bool> Login(HttpClient httpClient, PlayerKeyPair? playerKeyPair, Session.SessionToken session);
|
||||
|
||||
/// <summary>
|
||||
/// Start processing game packets.
|
||||
/// </summary>
|
||||
Task StartUpdating();
|
||||
|
||||
/// <summary>
|
||||
/// Disconnect from the server
|
||||
|
|
@ -46,20 +54,20 @@ namespace MinecraftClient.Protocol
|
|||
/// </summary>
|
||||
/// <param name="message">Text to send</param>
|
||||
/// <returns>True if successfully sent</returns>
|
||||
bool SendChatMessage(string message, PlayerKeyPair? playerKeyPair = null);
|
||||
Task<bool> SendChatMessage(string message, PlayerKeyPair? playerKeyPair = null);
|
||||
|
||||
/// <summary>
|
||||
/// Allow to respawn after death
|
||||
/// </summary>
|
||||
/// <returns>True if packet successfully sent</returns>
|
||||
bool SendRespawnPacket();
|
||||
Task<bool> SendRespawnPacket();
|
||||
|
||||
/// <summary>
|
||||
/// Inform the server of the client being used to connect
|
||||
/// </summary>
|
||||
/// <param name="brandInfo">Client string describing the client</param>
|
||||
/// <returns>True if brand info was successfully sent</returns>
|
||||
bool SendBrandInfo(string brandInfo);
|
||||
Task<bool> SendBrandInfo(string brandInfo);
|
||||
|
||||
/// <summary>
|
||||
/// Inform the server of the client's Minecraft settings
|
||||
|
|
@ -72,7 +80,7 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="skinParts">Show skin layers</param>
|
||||
/// <param name="mainHand">1.9+ main hand</param>
|
||||
/// <returns>True if client settings were successfully sent</returns>
|
||||
bool SendClientSettings(string language, byte viewDistance, byte difficulty, byte chatMode, bool chatColors, byte skinParts, byte mainHand);
|
||||
Task<bool> SendClientSettings(string language, byte viewDistance, byte difficulty, byte chatMode, bool chatColors, byte skinParts, byte mainHand);
|
||||
|
||||
/// <summary>
|
||||
/// Send a location update telling that we moved to that location
|
||||
|
|
@ -82,7 +90,7 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="yaw">The new yaw (optional)</param>
|
||||
/// <param name="pitch">The new pitch (optional)</param>
|
||||
/// <returns>True if packet was successfully sent</returns>
|
||||
bool SendLocationUpdate(Location location, bool onGround, float? yaw, float? pitch);
|
||||
Task<bool> SendLocationUpdate(Location location, bool onGround, float? yaw, float? pitch);
|
||||
|
||||
/// <summary>
|
||||
/// Send a plugin channel packet to the server.
|
||||
|
|
@ -91,7 +99,7 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="channel">Channel to send packet on</param>
|
||||
/// <param name="data">packet Data</param>
|
||||
/// <returns>True if message was successfully sent</returns>
|
||||
bool SendPluginChannelPacket(string channel, byte[] data);
|
||||
Task<bool> SendPluginChannelPacket(string channel, byte[] data);
|
||||
|
||||
/// <summary>
|
||||
/// Send Entity Action packet to the server.
|
||||
|
|
@ -99,14 +107,14 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="entityID">PlayerID</param>
|
||||
/// <param name="type">Type of packet to send</param>
|
||||
/// <returns>True if packet was successfully sent</returns>
|
||||
bool SendEntityAction(int EntityID, int type);
|
||||
Task<bool> SendEntityAction(int EntityID, int type);
|
||||
|
||||
/// <summary>
|
||||
/// Send a held item change packet to the server.
|
||||
/// </summary>
|
||||
/// <param name="slot">New active slot in the inventory hotbar</param>
|
||||
/// <returns>True if packet was successfully sent</returns>
|
||||
bool SendHeldItemChange(short slot);
|
||||
Task<bool> SendHeldItemChange(short slot);
|
||||
|
||||
/// <summary>
|
||||
/// Send an entity interaction packet to the server.
|
||||
|
|
@ -114,7 +122,7 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="EntityID">Entity ID to interact with</param>
|
||||
/// <param name="type">Type of interaction (0: interact, 1: attack, 2: interact at)</param>
|
||||
/// <returns>True if packet was successfully sent</returns>
|
||||
bool SendInteractEntity(int EntityID, int type);
|
||||
Task<bool> SendInteractEntity(int EntityID, int type);
|
||||
|
||||
/// <summary>
|
||||
/// Send an entity interaction packet to the server.
|
||||
|
|
@ -126,7 +134,7 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="Z">Z coordinate for "interact at"</param>
|
||||
/// <param name="hand">Player hand (0: main hand, 1: off hand)</param>
|
||||
/// <returns>True if packet was successfully sent</returns>
|
||||
bool SendInteractEntity(int EntityID, int type, float X, float Y, float Z, int hand);
|
||||
Task<bool> SendInteractEntity(int EntityID, int type, float X, float Y, float Z, int hand);
|
||||
|
||||
/// <summary>
|
||||
/// Send an entity interaction packet to the server.
|
||||
|
|
@ -137,7 +145,7 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="Y">Y coordinate for "interact at"</param>
|
||||
/// <param name="Z">Z coordinate for "interact at"</param>
|
||||
/// <returns>True if packet was successfully sent</returns>
|
||||
bool SendInteractEntity(int EntityID, int type, float X, float Y, float Z);
|
||||
Task<bool> SendInteractEntity(int EntityID, int type, float X, float Y, float Z);
|
||||
|
||||
/// <summary>
|
||||
/// Send an entity interaction packet to the server.
|
||||
|
|
@ -146,7 +154,7 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="type">Type of interaction (0: interact, 1: attack, 2: interact at)</param>
|
||||
/// <param name="hand">Only if Type is interact or interact at; 0: main hand, 1: off hand</param>
|
||||
/// <returns>True if packet was successfully sent</returns>
|
||||
bool SendInteractEntity(int EntityID, int type, int hand);
|
||||
Task<bool> SendInteractEntity(int EntityID, int type, int hand);
|
||||
|
||||
/// <summary>
|
||||
/// Send a use item packet to the server
|
||||
|
|
@ -154,7 +162,7 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="hand">0: main hand, 1: off hand</param>
|
||||
/// <param name="sequenceId">Sequence ID used for synchronization</param>
|
||||
/// <returns>True if packet was successfully sent</returns>
|
||||
bool SendUseItem(int hand, int sequenceId);
|
||||
Task<bool> SendUseItem(int hand, int sequenceId);
|
||||
|
||||
/// <summary>
|
||||
/// Send a click window slot packet to the server
|
||||
|
|
@ -166,7 +174,7 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="changedSlots">Slots that have been changed in this event: List<SlotID, Changed Items> </param>
|
||||
/// <param name="stateId">Inventory's stateId</param>
|
||||
/// <returns>True if packet was successfully sent</returns>
|
||||
bool SendWindowAction(int windowId, int slotId, WindowActionType action, Item? item, List<Tuple<short, Item?>> changedSlots, int stateId);
|
||||
Task<bool> SendWindowAction(int windowId, int slotId, WindowActionType action, Item? item, List<Tuple<short, Item?>> changedSlots, int stateId);
|
||||
|
||||
/// <summary>
|
||||
/// Request Creative Mode item creation into regular/survival Player Inventory
|
||||
|
|
@ -177,7 +185,7 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="count">Item count</param>
|
||||
/// <param name="nbt">Optional item NBT</param>
|
||||
/// <returns>TRUE if item given successfully</returns>
|
||||
bool SendCreativeInventoryAction(int slot, ItemType itemType, int count, Dictionary<string, object>? nbt);
|
||||
Task<bool> SendCreativeInventoryAction(int slot, ItemType itemType, int count, Dictionary<string, object>? nbt);
|
||||
|
||||
/// <summary>
|
||||
/// Send a click container button packet to the server.
|
||||
|
|
@ -187,7 +195,7 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="buttonId">Id of the clicked button</param>
|
||||
/// <returns>True if packet was successfully sent</returns>
|
||||
|
||||
bool ClickContainerButton(int windowId, int buttonId);
|
||||
Task<bool> ClickContainerButton(int windowId, int buttonId);
|
||||
|
||||
/// <summary>
|
||||
/// Plays animation
|
||||
|
|
@ -195,13 +203,13 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="animation">0 for left arm, 1 for right arm</param>
|
||||
/// <param name="playerid">Player Entity ID</param>
|
||||
/// <returns>TRUE if item given successfully</returns>
|
||||
bool SendAnimation(int animation, int playerid);
|
||||
Task<bool> SendAnimation(int animation, int playerid);
|
||||
|
||||
/// <summary>
|
||||
/// Send a close window packet to the server
|
||||
/// </summary>
|
||||
/// <param name="windowId">Id of the window being closed</param>
|
||||
bool SendCloseWindow(int windowId);
|
||||
Task<bool> SendCloseWindow(int windowId);
|
||||
|
||||
/// <summary>
|
||||
/// Send player block placement packet to the server
|
||||
|
|
@ -211,7 +219,7 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="face">Block face</param>
|
||||
/// <param name="sequenceId">Sequence ID (use for synchronization)</param>
|
||||
/// <returns>True if packet was successfully sent</returns>
|
||||
bool SendPlayerBlockPlacement(int hand, Location location, Direction face, int sequenceId);
|
||||
Task<bool> SendPlayerBlockPlacement(int hand, Location location, Direction face, int sequenceId);
|
||||
|
||||
/// <summary>
|
||||
/// Send player blog digging packet to the server. This packet needs to be called at least twice: Once to begin digging, then a second time to finish digging
|
||||
|
|
@ -221,7 +229,7 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="face">Block face</param>
|
||||
/// <param name="sequenceId">Sequence ID (use for synchronization)</param>
|
||||
/// <returns>True if packet was succcessfully sent</returns>
|
||||
bool SendPlayerDigging(int status, Location location, Direction face, int sequenceId);
|
||||
Task<bool> SendPlayerDigging(int status, Location location, Direction face, int sequenceId);
|
||||
|
||||
/// <summary>
|
||||
/// Change text on a sign
|
||||
|
|
@ -232,7 +240,7 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="line3">New line 3</param>
|
||||
/// <param name="line4">New line 4</param>
|
||||
/// <returns>True if packet was succcessfully sent</returns>
|
||||
bool SendUpdateSign(Location location, string line1, string line2, string line3, string line4);
|
||||
Task<bool> SendUpdateSign(Location location, string line1, string line2, string line3, string line4);
|
||||
|
||||
/// <summary>
|
||||
/// Update command block
|
||||
|
|
@ -241,24 +249,18 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="command">command</param>
|
||||
/// <param name="mode">command block mode</param>
|
||||
/// <param name="flags">command block flags</param>
|
||||
bool UpdateCommandBlock(Location location, string command, CommandBlockMode mode, CommandBlockFlags flags);
|
||||
Task<bool> UpdateCommandBlock(Location location, string command, CommandBlockMode mode, CommandBlockFlags flags);
|
||||
|
||||
/// <summary>
|
||||
/// Select villager trade
|
||||
/// </summary>
|
||||
/// <param name="selectedSlot">The slot of the trade, starts at 0.</param>
|
||||
bool SelectTrade(int selectedSlot);
|
||||
Task<bool> SelectTrade(int selectedSlot);
|
||||
|
||||
/// <summary>
|
||||
/// Spectate a player/entity
|
||||
/// </summary>
|
||||
/// <param name="uuid">The uuid of the player/entity to spectate/teleport to.</param>
|
||||
bool SendSpectate(Guid uuid);
|
||||
|
||||
/// <summary>
|
||||
/// Get net read thread (main thread) ID
|
||||
/// </summary>
|
||||
/// <returns>Net read thread ID</returns>
|
||||
int GetNetMainThreadId();
|
||||
Task<bool> SendSpectate(Guid uuid);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using MinecraftClient.Inventory;
|
||||
using MinecraftClient.Logger;
|
||||
using MinecraftClient.Mapping;
|
||||
|
|
@ -18,7 +19,6 @@ namespace MinecraftClient.Protocol
|
|||
{
|
||||
/* The MinecraftCom Handler must
|
||||
* provide these getters */
|
||||
|
||||
int GetServerPort();
|
||||
string GetServerHost();
|
||||
string GetUsername();
|
||||
|
|
@ -43,26 +43,6 @@ namespace MinecraftClient.Protocol
|
|||
Container? GetInventory(int inventoryID);
|
||||
ILogger GetLogger();
|
||||
|
||||
/// <summary>
|
||||
/// Invoke a task on the main thread, wait for completion and retrieve return value.
|
||||
/// </summary>
|
||||
/// <param name="task">Task to run with any type or return value</param>
|
||||
/// <returns>Any result returned from task, result type is inferred from the task</returns>
|
||||
/// <example>bool result = InvokeOnMainThread(methodThatReturnsAbool);</example>
|
||||
/// <example>bool result = InvokeOnMainThread(() => methodThatReturnsAbool(argument));</example>
|
||||
/// <example>int result = InvokeOnMainThread(() => { yourCode(); return 42; });</example>
|
||||
/// <typeparam name="T">Type of the return value</typeparam>
|
||||
T InvokeOnMainThread<T>(Func<T> task);
|
||||
|
||||
/// <summary>
|
||||
/// Invoke a task on the main thread and wait for completion
|
||||
/// </summary>
|
||||
/// <param name="task">Task to run without return value</param>
|
||||
/// <example>InvokeOnMainThread(methodThatReturnsNothing);</example>
|
||||
/// <example>InvokeOnMainThread(() => methodThatReturnsNothing(argument));</example>
|
||||
/// <example>InvokeOnMainThread(() => { yourCode(); });</example>
|
||||
void InvokeOnMainThread(Action task);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a network packet received or sent
|
||||
/// </summary>
|
||||
|
|
@ -78,7 +58,7 @@ namespace MinecraftClient.Protocol
|
|||
/// <summary>
|
||||
/// Called when a server was successfully joined
|
||||
/// </summary>
|
||||
void OnGameJoined();
|
||||
Task OnGameJoined();
|
||||
|
||||
/// <summary>
|
||||
/// Received chat/system message from the server
|
||||
|
|
@ -184,21 +164,21 @@ namespace MinecraftClient.Protocol
|
|||
/// Called ~10 times per second (10 ticks per second)
|
||||
/// Useful for updating bots in other parts of the program
|
||||
/// </summary>
|
||||
void OnUpdate();
|
||||
Task OnUpdate();
|
||||
|
||||
/// <summary>
|
||||
/// Registers the given plugin channel for the given bot.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel to register.</param>
|
||||
/// <param name="bot">The bot to register the channel for.</param>
|
||||
void RegisterPluginChannel(string channel, ChatBot bot);
|
||||
Task RegisterPluginChannel(string channel, ChatBot bot);
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters the given plugin channel for the given bot.
|
||||
/// </summary>
|
||||
/// <param name="channel">The channel to unregister.</param>
|
||||
/// <param name="bot">The bot to unregister the channel for.</param>
|
||||
void UnregisterPluginChannel(string channel, ChatBot bot);
|
||||
Task UnregisterPluginChannel(string channel, ChatBot bot);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a plugin channel packet to the server.
|
||||
|
|
@ -208,7 +188,7 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="data">The payload for the packet.</param>
|
||||
/// <param name="sendEvenIfNotRegistered">Whether the packet should be sent even if the server or the client hasn't registered it yet.</param>
|
||||
/// <returns>Whether the packet was sent: true if it was sent, false if there was a connection error or it wasn't registered.</returns>
|
||||
bool SendPluginChannelMessage(string channel, byte[] data, bool sendEvenIfNotRegistered = false);
|
||||
Task<bool> SendPluginChannelMessage(string channel, byte[] data, bool sendEvenIfNotRegistered = false);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a plugin channel message was sent from the server.
|
||||
|
|
@ -348,7 +328,7 @@ namespace MinecraftClient.Protocol
|
|||
/// </summary>
|
||||
/// <param name="uuid">Affected player's UUID</param>
|
||||
/// <param name="gamemode">New game mode</param>
|
||||
void OnGamemodeUpdate(Guid uuid, int gamemode);
|
||||
Task OnGamemodeUpdate(Guid uuid, int gamemode);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a player's latency has changed
|
||||
|
|
@ -472,6 +452,6 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="buttonId">Id of the clicked button</param>
|
||||
/// <returns>True if packet was successfully sent</returns>
|
||||
|
||||
bool ClickContainerButton(int windowId, int buttonId);
|
||||
public Task<bool> ClickContainerButton(int windowId, int buttonId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace MinecraftClient.Protocol
|
||||
|
|
@ -6,11 +7,10 @@ namespace MinecraftClient.Protocol
|
|||
// Thanks to https://stackoverflow.com/questions/60404612/parse-jwt-token-to-get-the-payload-content-only-without-external-library-in-c-sh
|
||||
public static class JwtPayloadDecode
|
||||
{
|
||||
public static string GetPayload(string token)
|
||||
public static MemoryStream GetPayload(string token)
|
||||
{
|
||||
var content = token.Split('.')[1];
|
||||
var jsonPayload = Encoding.UTF8.GetString(Decode(content));
|
||||
return jsonPayload;
|
||||
return new MemoryStream(Decode(content));
|
||||
}
|
||||
|
||||
private static byte[] Decode(string input)
|
||||
|
|
@ -23,7 +23,7 @@ namespace MinecraftClient.Protocol
|
|||
case 0: break; // No pad chars in this case
|
||||
case 2: output += "=="; break; // Two pad chars
|
||||
case 3: output += "="; break; // One pad char
|
||||
default: throw new System.ArgumentOutOfRangeException(nameof(input), "Illegal base64url string!");
|
||||
default: throw new ArgumentOutOfRangeException(nameof(input), "Illegal base64url string!");
|
||||
}
|
||||
var converted = Convert.FromBase64String(output); // Standard base64 decoder
|
||||
return converted;
|
||||
|
|
|
|||
|
|
@ -3,9 +3,18 @@ using System.Collections.Generic;
|
|||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using MinecraftClient.Protocol.ProfileKey;
|
||||
using static MinecraftClient.Settings;
|
||||
using static MinecraftClient.Settings.MainConfigHealper.MainConfig.GeneralConfig;
|
||||
|
||||
|
|
@ -13,9 +22,10 @@ namespace MinecraftClient.Protocol
|
|||
{
|
||||
static class Microsoft
|
||||
{
|
||||
private static readonly string clientId = "54473e32-df8f-42e9-a649-9419b0dab9d3";
|
||||
private static readonly string signinUrl = string.Format("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id={0}&response_type=code&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&scope=XboxLive.signin%20offline_access%20openid%20email&prompt=select_account&response_mode=fragment", clientId);
|
||||
private static readonly string tokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
|
||||
private const string clientId = "54473e32-df8f-42e9-a649-9419b0dab9d3";
|
||||
private const string tokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token";
|
||||
private const string signinUrl = $"https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id={clientId}&response_type=code&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&scope=XboxLive.signin%20offline_access%20openid%20email&prompt=select_account&response_mode=fragment";
|
||||
private const string certificates = "https://api.minecraftservices.com/player/certificates";
|
||||
|
||||
public static string SignInUrl { get { return signinUrl; } }
|
||||
|
||||
|
|
@ -26,7 +36,7 @@ namespace MinecraftClient.Protocol
|
|||
/// <returns>Sign-in URL with email pre-filled</returns>
|
||||
public static string GetSignInUrlWithHint(string loginHint)
|
||||
{
|
||||
return SignInUrl + "&login_hint=" + Uri.EscapeDataString(loginHint);
|
||||
return $"{SignInUrl}&login_hint={Uri.EscapeDataString(loginHint)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -34,11 +44,16 @@ namespace MinecraftClient.Protocol
|
|||
/// </summary>
|
||||
/// <param name="code">Auth code obtained after user signing in</param>
|
||||
/// <returns>Access token and refresh token</returns>
|
||||
public static LoginResponse RequestAccessToken(string code)
|
||||
public static async Task<LoginResponse> RequestAccessTokenAsync(HttpClient httpClient, string code)
|
||||
{
|
||||
string postData = "client_id={0}&grant_type=authorization_code&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&code={1}";
|
||||
postData = string.Format(postData, clientId, code);
|
||||
return RequestToken(postData);
|
||||
FormUrlEncodedContent postData = new(new KeyValuePair<string, string>[]
|
||||
{
|
||||
new("client_id", clientId),
|
||||
new("grant_type", "authorization_code"),
|
||||
new("redirect_uri", "https://mccteam.github.io/redirect.html"),
|
||||
new("code", code),
|
||||
});
|
||||
return await RequestTokenAsync(httpClient, postData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -46,11 +61,43 @@ namespace MinecraftClient.Protocol
|
|||
/// </summary>
|
||||
/// <param name="refreshToken">Refresh token</param>
|
||||
/// <returns>Access token and new refresh token</returns>
|
||||
public static LoginResponse RefreshAccessToken(string refreshToken)
|
||||
public static async Task<LoginResponse> RefreshAccessTokenAsync(HttpClient httpClient, string refreshToken)
|
||||
{
|
||||
string postData = "client_id={0}&grant_type=refresh_token&redirect_uri=https%3A%2F%2Fmccteam.github.io%2Fredirect.html&refresh_token={1}";
|
||||
postData = string.Format(postData, clientId, refreshToken);
|
||||
return RequestToken(postData);
|
||||
FormUrlEncodedContent postData = new(new KeyValuePair<string, string>[]
|
||||
{
|
||||
new("client_id", clientId),
|
||||
new("grant_type", "refresh_token"),
|
||||
new("redirect_uri", "https://mccteam.github.io/redirect.html"),
|
||||
new("refresh_token", refreshToken),
|
||||
});
|
||||
return await RequestTokenAsync(httpClient, postData);
|
||||
}
|
||||
|
||||
private record TokenInfo
|
||||
{
|
||||
public string? token_type { init; get; }
|
||||
public string? scope { init; get; }
|
||||
public int expires_in { init; get; }
|
||||
public int ext_expires_in { init; get; }
|
||||
public string? access_token { init; get; }
|
||||
public string? refresh_token { init; get; }
|
||||
public string? id_token { init; get; }
|
||||
public string? error { init; get; }
|
||||
public string? error_description { init; get; }
|
||||
}
|
||||
|
||||
private record JwtPayloadInIdToken
|
||||
{
|
||||
public string? ver { init; get; }
|
||||
public string? iss { init; get; }
|
||||
public string? sub { init; get; }
|
||||
public string? aud { init; get; }
|
||||
public long exp { init; get; }
|
||||
public long iat { init; get; }
|
||||
public long nbf { init; get; }
|
||||
public string? email { init; get; }
|
||||
public string? tid { init; get; }
|
||||
public string? aio { init; get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -58,45 +105,88 @@ namespace MinecraftClient.Protocol
|
|||
/// </summary>
|
||||
/// <param name="postData">Complete POST data for the request</param>
|
||||
/// <returns></returns>
|
||||
private static LoginResponse RequestToken(string postData)
|
||||
private static async Task<LoginResponse> RequestTokenAsync(HttpClient httpClient, FormUrlEncodedContent postData)
|
||||
{
|
||||
var request = new ProxiedWebRequest(tokenUrl)
|
||||
{
|
||||
UserAgent = "MCC/" + Program.Version
|
||||
};
|
||||
var response = request.Post("application/x-www-form-urlencoded", postData);
|
||||
var jsonData = Json.ParseJson(response.Body);
|
||||
using HttpResponseMessage response = await httpClient.PostAsync(tokenUrl, postData);
|
||||
|
||||
TokenInfo jsonData = (await response.Content.ReadFromJsonAsync<TokenInfo>())!;
|
||||
|
||||
// Error handling
|
||||
if (jsonData.Properties.ContainsKey("error"))
|
||||
if (!string.IsNullOrEmpty(jsonData.error))
|
||||
{
|
||||
throw new Exception(jsonData.Properties["error_description"].StringValue);
|
||||
throw new Exception(jsonData.error_description);
|
||||
}
|
||||
else
|
||||
{
|
||||
string accessToken = jsonData.Properties["access_token"].StringValue;
|
||||
string refreshToken = jsonData.Properties["refresh_token"].StringValue;
|
||||
int expiresIn = int.Parse(jsonData.Properties["expires_in"].StringValue, NumberStyles.Any, CultureInfo.CurrentCulture);
|
||||
|
||||
// Extract email from JWT
|
||||
string payload = JwtPayloadDecode.GetPayload(jsonData.Properties["id_token"].StringValue);
|
||||
var jsonPayload = Json.ParseJson(payload);
|
||||
string email = jsonPayload.Properties["email"].StringValue;
|
||||
Stream payload = JwtPayloadDecode.GetPayload(jsonData.id_token!);
|
||||
JwtPayloadInIdToken jsonPayload = (await JsonSerializer.DeserializeAsync<JwtPayloadInIdToken>(payload))!;
|
||||
|
||||
return new LoginResponse()
|
||||
{
|
||||
Email = email,
|
||||
AccessToken = accessToken,
|
||||
RefreshToken = refreshToken,
|
||||
ExpiresIn = expiresIn
|
||||
Email = jsonPayload.email!,
|
||||
AccessToken = jsonData.access_token!,
|
||||
RefreshToken = jsonData.refresh_token!,
|
||||
ExpiresIn = jsonData.expires_in,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private record ProfileKeyResult
|
||||
{
|
||||
public KeyPair? keyPair { init; get; }
|
||||
public string? publicKeySignature { init; get; }
|
||||
public string? publicKeySignatureV2 { init; get; }
|
||||
public DateTime expiresAt { init; get; }
|
||||
public DateTime refreshedAfter { init; get; }
|
||||
|
||||
public record KeyPair
|
||||
{
|
||||
public string? privateKey { init; get; }
|
||||
public string? publicKey { init; get; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request the key to be used for message signing.
|
||||
/// </summary>
|
||||
/// <param name="accessToken">Access token in session</param>
|
||||
/// <returns>Profile key</returns>
|
||||
public static async Task<PlayerKeyPair?> RequestProfileKeyAsync(HttpClient httpClient, string accessToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
using HttpRequestMessage request = new(HttpMethod.Post, certificates);
|
||||
|
||||
request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken));
|
||||
|
||||
using HttpResponseMessage response = await httpClient.SendAsync(request);
|
||||
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLine(response.ToString());
|
||||
|
||||
ProfileKeyResult jsonData = (await response.Content.ReadFromJsonAsync<ProfileKeyResult>())!;
|
||||
|
||||
PublicKey publicKey = new(jsonData.keyPair!.publicKey!, jsonData.publicKeySignature, jsonData.publicKeySignatureV2);
|
||||
|
||||
PrivateKey privateKey = new(jsonData.keyPair!.privateKey!);
|
||||
|
||||
return new PlayerKeyPair(publicKey, privateKey, jsonData.expiresAt, jsonData.refreshedAfter);
|
||||
}
|
||||
catch (HttpRequestException e)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§cFetch profile key failed: " + e.Message);
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted("§c" + e.StackTrace);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void OpenBrowser(string link)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var ps = new ProcessStartInfo(link)
|
||||
{
|
||||
|
|
@ -106,11 +196,11 @@ namespace MinecraftClient.Protocol
|
|||
|
||||
Process.Start(ps);
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
else if (OperatingSystem.IsLinux())
|
||||
{
|
||||
Process.Start("xdg-open", link);
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
Process.Start("open", link);
|
||||
}
|
||||
|
|
@ -134,53 +224,86 @@ namespace MinecraftClient.Protocol
|
|||
}
|
||||
}
|
||||
|
||||
static class XboxLive
|
||||
static partial class XboxLive
|
||||
{
|
||||
private static readonly string authorize = "https://login.live.com/oauth20_authorize.srf?client_id=000000004C12AE6F&redirect_uri=https://login.live.com/oauth20_desktop.srf&scope=service::user.auth.xboxlive.com::MBI_SSL&display=touch&response_type=token&locale=en";
|
||||
private static readonly string xbl = "https://user.auth.xboxlive.com/user/authenticate";
|
||||
private static readonly string xsts = "https://xsts.auth.xboxlive.com/xsts/authorize";
|
||||
internal const string UserAgent = "Mozilla/5.0 (XboxReplay; XboxLiveAuth/3.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36";
|
||||
|
||||
private static readonly string userAgent = "Mozilla/5.0 (XboxReplay; XboxLiveAuth/3.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36";
|
||||
private const string xsts = "https://xsts.auth.xboxlive.com/xsts/authorize";
|
||||
private const string xbl = "https://user.auth.xboxlive.com/user/authenticate";
|
||||
private const string authorize = "https://login.live.com/oauth20_authorize.srf?client_id=000000004C12AE6F&redirect_uri=https://login.live.com/oauth20_desktop.srf&scope=service::user.auth.xboxlive.com::MBI_SSL&display=touch&response_type=token&locale=en";
|
||||
|
||||
private static readonly Regex ppft = new("sFTTag:'.*value=\"(.*)\"\\/>'");
|
||||
private static readonly Regex urlPost = new("urlPost:'(.+?(?=\'))");
|
||||
private static readonly Regex confirm = new("identity\\/confirm");
|
||||
private static readonly Regex invalidAccount = new("Sign in to", RegexOptions.IgnoreCase);
|
||||
private static readonly Regex twoFA = new("Help us protect your account", RegexOptions.IgnoreCase);
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.General)
|
||||
{
|
||||
AllowTrailingCommas = true,
|
||||
PropertyNameCaseInsensitive = false,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
};
|
||||
|
||||
public static string SignInUrl { get { return authorize; } }
|
||||
|
||||
private record AuthPayload
|
||||
{
|
||||
public Propertie? Properties { init; get; }
|
||||
public string? RelyingParty { init; get; }
|
||||
public string? TokenType { init; get; }
|
||||
|
||||
public record Propertie
|
||||
{
|
||||
public string? AuthMethod { init; get; }
|
||||
public string? SiteName { init; get; }
|
||||
public string? RpsTicket { init; get; }
|
||||
public string? SandboxId { init; get; }
|
||||
public string[]? UserTokens { init; get; }
|
||||
}
|
||||
}
|
||||
|
||||
private record AuthResult
|
||||
{
|
||||
public DateTime IssueInstant { init; get; }
|
||||
public DateTime NotAfter { init; get; }
|
||||
public string? Token { init; get; }
|
||||
public DisplayClaim? DisplayClaims { init; get; }
|
||||
|
||||
public record DisplayClaim
|
||||
{
|
||||
public Dictionary<string, string>[]? xui { init; get; }
|
||||
}
|
||||
}
|
||||
|
||||
private record AuthError
|
||||
{
|
||||
public string? Identity { init; get; }
|
||||
public long XErr { init; get; }
|
||||
public string? Message { init; get; }
|
||||
public string? Redirect { init; get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pre-authentication
|
||||
/// </summary>
|
||||
/// <remarks>This step is to get the login page for later use</remarks>
|
||||
/// <returns></returns>
|
||||
public static PreAuthResponse PreAuth()
|
||||
public static async Task<PreAuthResponse> PreAuthAsync(HttpClient httpClient)
|
||||
{
|
||||
var request = new ProxiedWebRequest(authorize)
|
||||
{
|
||||
UserAgent = userAgent
|
||||
};
|
||||
var response = request.Get();
|
||||
using HttpResponseMessage response = await httpClient.GetAsync(authorize);
|
||||
|
||||
string html = response.Body;
|
||||
string html = await response.Content.ReadAsStringAsync();
|
||||
|
||||
string PPFT = ppft.Match(html).Groups[1].Value;
|
||||
string urlPost = XboxLive.urlPost.Match(html).Groups[1].Value;
|
||||
string PPFT = GetPpftRegex().Match(html).Groups[1].Value;
|
||||
|
||||
string urlPost = GetUrlPostRegex().Match(html).Groups[1].Value;
|
||||
|
||||
if (string.IsNullOrEmpty(PPFT) || string.IsNullOrEmpty(urlPost))
|
||||
{
|
||||
throw new Exception("Fail to extract PPFT or urlPost");
|
||||
}
|
||||
//Console.WriteLine("PPFT: {0}", PPFT);
|
||||
//Console.WriteLine();
|
||||
//Console.WriteLine("urlPost: {0}", urlPost);
|
||||
|
||||
return new PreAuthResponse()
|
||||
{
|
||||
UrlPost = urlPost,
|
||||
PPFT = PPFT,
|
||||
Cookie = response.Cookies
|
||||
Cookie = new()// response.Cookies
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -192,69 +315,54 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="password">Account password</param>
|
||||
/// <param name="preAuth"></param>
|
||||
/// <returns></returns>
|
||||
public static Microsoft.LoginResponse UserLogin(string email, string password, PreAuthResponse preAuth)
|
||||
public static async Task<Microsoft.LoginResponse> UserLoginAsync(HttpClient httpClient, string email, string password, PreAuthResponse preAuth)
|
||||
{
|
||||
var request = new ProxiedWebRequest(preAuth.UrlPost, preAuth.Cookie)
|
||||
FormUrlEncodedContent postData = new(new KeyValuePair<string, string>[]
|
||||
{
|
||||
UserAgent = userAgent
|
||||
};
|
||||
new("login", email),
|
||||
new("loginfmt", email),
|
||||
new("passwd", password),
|
||||
new("PPFT", preAuth.PPFT),
|
||||
});
|
||||
|
||||
string postData = "login=" + Uri.EscapeDataString(email)
|
||||
+ "&loginfmt=" + Uri.EscapeDataString(email)
|
||||
+ "&passwd=" + Uri.EscapeDataString(password)
|
||||
+ "&PPFT=" + Uri.EscapeDataString(preAuth.PPFT);
|
||||
using HttpResponseMessage response = await httpClient.PostAsync(preAuth.UrlPost, postData);
|
||||
|
||||
var response = request.Post("application/x-www-form-urlencoded", postData);
|
||||
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
{
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLine(response.ToString());
|
||||
}
|
||||
|
||||
if (response.StatusCode >= 300 && response.StatusCode <= 399)
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
string url = response.Headers.Get("Location")!;
|
||||
string hash = url.Split('#')[1];
|
||||
|
||||
var request2 = new ProxiedWebRequest(url);
|
||||
var response2 = request2.Get();
|
||||
|
||||
if (response2.StatusCode != 200)
|
||||
{
|
||||
throw new Exception("Authentication failed");
|
||||
}
|
||||
string hash = response.RequestMessage!.RequestUri!.Fragment[1..];
|
||||
|
||||
if (string.IsNullOrEmpty(hash))
|
||||
{
|
||||
throw new Exception("Cannot extract access token");
|
||||
}
|
||||
var dict = Request.ParseQueryString(hash);
|
||||
|
||||
//foreach (var pair in dict)
|
||||
//{
|
||||
// Console.WriteLine("{0}: {1}", pair.Key, pair.Value);
|
||||
//}
|
||||
var dict = Request.ParseQueryString(hash);
|
||||
|
||||
return new Microsoft.LoginResponse()
|
||||
{
|
||||
Email = email,
|
||||
AccessToken = dict["access_token"],
|
||||
RefreshToken = dict["refresh_token"],
|
||||
ExpiresIn = int.Parse(dict["expires_in"], NumberStyles.Any, CultureInfo.CurrentCulture)
|
||||
ExpiresIn = int.Parse(dict["expires_in"])
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
if (twoFA.IsMatch(response.Body))
|
||||
string body = await response.Content.ReadAsStringAsync();
|
||||
if (GetTwoFARegex().IsMatch(body))
|
||||
{
|
||||
// TODO: Handle 2FA
|
||||
throw new Exception("2FA enabled but not supported yet. Use browser sign-in method or try to disable 2FA in Microsoft account settings");
|
||||
}
|
||||
else if (invalidAccount.IsMatch(response.Body))
|
||||
else if (GetInvalidAccountRegex().IsMatch(body))
|
||||
{
|
||||
throw new Exception("Invalid credentials. Check your credentials");
|
||||
}
|
||||
else throw new Exception("Unexpected response. Check your credentials. Response code: " + response.StatusCode);
|
||||
else
|
||||
{
|
||||
throw new Exception("Unexpected response. Check your credentials. Response code: " + response.StatusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -263,54 +371,54 @@ namespace MinecraftClient.Protocol
|
|||
/// </summary>
|
||||
/// <param name="loginResponse"></param>
|
||||
/// <returns></returns>
|
||||
public static XblAuthenticateResponse XblAuthenticate(Microsoft.LoginResponse loginResponse)
|
||||
public static async Task<XblAuthenticateResponse> XblAuthenticateAsync(HttpClient httpClient, Microsoft.LoginResponse loginResponse)
|
||||
{
|
||||
var request = new ProxiedWebRequest(xbl)
|
||||
{
|
||||
UserAgent = userAgent,
|
||||
Accept = "application/json"
|
||||
};
|
||||
request.Headers.Add("x-xbl-contract-version", "0");
|
||||
|
||||
var accessToken = loginResponse.AccessToken;
|
||||
string accessToken;
|
||||
if (Config.Main.General.Method == LoginMethod.browser)
|
||||
{
|
||||
// Our own client ID must have d= in front of the token or HTTP status 400
|
||||
// "Stolen" client ID must not have d= in front of the token or HTTP status 400
|
||||
accessToken = "d=" + accessToken;
|
||||
accessToken = "d=" + loginResponse.AccessToken;
|
||||
}
|
||||
else
|
||||
{
|
||||
accessToken = loginResponse.AccessToken;
|
||||
}
|
||||
|
||||
string payload = "{"
|
||||
+ "\"Properties\": {"
|
||||
+ "\"AuthMethod\": \"RPS\","
|
||||
+ "\"SiteName\": \"user.auth.xboxlive.com\","
|
||||
+ "\"RpsTicket\": \"" + accessToken + "\""
|
||||
+ "},"
|
||||
+ "\"RelyingParty\": \"http://auth.xboxlive.com\","
|
||||
+ "\"TokenType\": \"JWT\""
|
||||
+ "}";
|
||||
var response = request.Post("application/json", payload);
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
AuthPayload payload = new()
|
||||
{
|
||||
Properties = new AuthPayload.Propertie()
|
||||
{
|
||||
AuthMethod = "RPS",
|
||||
SiteName = "user.auth.xboxlive.com",
|
||||
RpsTicket = accessToken,
|
||||
},
|
||||
RelyingParty = "http://auth.xboxlive.com",
|
||||
TokenType = "JWT",
|
||||
};
|
||||
|
||||
using StringContent httpContent = new(JsonSerializer.Serialize(payload, JsonOptions), Encoding.UTF8, "application/json");
|
||||
|
||||
httpContent.Headers.Add("x-xbl-contract-version", "0");
|
||||
|
||||
using HttpResponseMessage response = await httpClient.PostAsync(xbl, httpContent);
|
||||
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLine(response.ToString());
|
||||
}
|
||||
if (response.StatusCode == 200)
|
||||
{
|
||||
string jsonString = response.Body;
|
||||
//Console.WriteLine(jsonString);
|
||||
|
||||
Json.JSONData json = Json.ParseJson(jsonString);
|
||||
string token = json.Properties["Token"].StringValue;
|
||||
string userHash = json.Properties["DisplayClaims"].Properties["xui"].DataArray[0].Properties["uhs"].StringValue;
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
AuthResult jsonData = (await response.Content.ReadFromJsonAsync<AuthResult>())!;
|
||||
|
||||
return new XblAuthenticateResponse()
|
||||
{
|
||||
Token = token,
|
||||
UserHash = userHash
|
||||
Token = jsonData.Token!,
|
||||
UserHash = jsonData.DisplayClaims!.xui![0]["uhs"],
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("XBL Authentication failed");
|
||||
throw new Exception("XBL Authentication failed, code = " + response.StatusCode.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -320,56 +428,53 @@ namespace MinecraftClient.Protocol
|
|||
/// <remarks>(Don't ask me what is XSTS, I DONT KNOW)</remarks>
|
||||
/// <param name="xblResponse"></param>
|
||||
/// <returns></returns>
|
||||
public static XSTSAuthenticateResponse XSTSAuthenticate(XblAuthenticateResponse xblResponse)
|
||||
public static async Task<XSTSAuthenticateResponse> XSTSAuthenticateAsync(HttpClient httpClient, XblAuthenticateResponse xblResponse)
|
||||
{
|
||||
var request = new ProxiedWebRequest(xsts)
|
||||
AuthPayload payload = new()
|
||||
{
|
||||
UserAgent = userAgent,
|
||||
Accept = "application/json"
|
||||
Properties = new AuthPayload.Propertie()
|
||||
{
|
||||
SandboxId = "RETAIL",
|
||||
UserTokens = new string[] { xblResponse.Token },
|
||||
},
|
||||
RelyingParty = "rp://api.minecraftservices.com/",
|
||||
TokenType = "JWT",
|
||||
};
|
||||
request.Headers.Add("x-xbl-contract-version", "1");
|
||||
|
||||
string payload = "{"
|
||||
+ "\"Properties\": {"
|
||||
+ "\"SandboxId\": \"RETAIL\","
|
||||
+ "\"UserTokens\": ["
|
||||
+ "\"" + xblResponse.Token + "\""
|
||||
+ "]"
|
||||
+ "},"
|
||||
+ "\"RelyingParty\": \"rp://api.minecraftservices.com/\","
|
||||
+ "\"TokenType\": \"JWT\""
|
||||
+ "}";
|
||||
var response = request.Post("application/json", payload);
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
{
|
||||
using StringContent httpContent = new(JsonSerializer.Serialize(payload, JsonOptions), Encoding.UTF8, "application/json");
|
||||
|
||||
httpContent.Headers.Add("x-xbl-contract-version", "1");
|
||||
|
||||
using HttpResponseMessage response = await httpClient.PostAsync(xsts, httpContent);
|
||||
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLine(response.ToString());
|
||||
}
|
||||
if (response.StatusCode == 200)
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
string jsonString = response.Body;
|
||||
Json.JSONData json = Json.ParseJson(jsonString);
|
||||
string token = json.Properties["Token"].StringValue;
|
||||
string userHash = json.Properties["DisplayClaims"].Properties["xui"].DataArray[0].Properties["uhs"].StringValue;
|
||||
AuthResult jsonData = (await response.Content.ReadFromJsonAsync<AuthResult>())!;
|
||||
|
||||
return new XSTSAuthenticateResponse()
|
||||
{
|
||||
Token = token,
|
||||
UserHash = userHash
|
||||
Token = jsonData.Token!,
|
||||
UserHash = jsonData.DisplayClaims!.xui![0]["uhs"],
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
if (response.StatusCode == 401)
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
Json.JSONData json = Json.ParseJson(response.Body);
|
||||
if (json.Properties["XErr"].StringValue == "2148916233")
|
||||
{
|
||||
AuthError jsonData = (await response.Content.ReadFromJsonAsync<AuthError>())!;
|
||||
if (jsonData.XErr == 2148916233)
|
||||
throw new Exception("The account doesn't have an Xbox account");
|
||||
}
|
||||
else if (json.Properties["XErr"].StringValue == "2148916238")
|
||||
{
|
||||
else if (jsonData.XErr == 2148916235)
|
||||
throw new Exception("The account is from a country where Xbox Live is not available/banned");
|
||||
else if (jsonData.XErr == 2148916236 || jsonData.XErr == 2148916237)
|
||||
throw new Exception("The account needs adult verification on Xbox page. (South Korea)");
|
||||
else if (jsonData.XErr == 2148916238)
|
||||
throw new Exception("The account is a child (under 18) and cannot proceed unless the account is added to a Family by an adult");
|
||||
}
|
||||
else throw new Exception("Unknown XSTS error code: " + json.Properties["XErr"].StringValue);
|
||||
else
|
||||
throw new Exception("Unknown XSTS error code: " + jsonData.XErr.ToString() + ", Check " + jsonData.Redirect);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -396,13 +501,63 @@ namespace MinecraftClient.Protocol
|
|||
public string Token;
|
||||
public string UserHash;
|
||||
}
|
||||
|
||||
[GeneratedRegex("sFTTag:'.*value=\"(.*)\"\\/>'")]
|
||||
private static partial Regex GetPpftRegex();
|
||||
|
||||
[GeneratedRegex("urlPost:'(.+?(?='))")]
|
||||
private static partial Regex GetUrlPostRegex();
|
||||
|
||||
[GeneratedRegex("identity\\/confirm")]
|
||||
private static partial Regex GetConfirmRegex();
|
||||
|
||||
[GeneratedRegex("Sign in to", RegexOptions.IgnoreCase, "zh-CN")]
|
||||
private static partial Regex GetInvalidAccountRegex();
|
||||
|
||||
[GeneratedRegex("Help us protect your account", RegexOptions.IgnoreCase, "zh-CN")]
|
||||
private static partial Regex GetTwoFARegex();
|
||||
}
|
||||
|
||||
static class MinecraftWithXbox
|
||||
{
|
||||
private static readonly string loginWithXbox = "https://api.minecraftservices.com/authentication/login_with_xbox";
|
||||
private static readonly string ownership = "https://api.minecraftservices.com/entitlements/mcstore";
|
||||
private static readonly string profile = "https://api.minecraftservices.com/minecraft/profile";
|
||||
private const string profile = "https://api.minecraftservices.com/minecraft/profile";
|
||||
private const string ownership = "https://api.minecraftservices.com/entitlements/mcstore";
|
||||
private const string loginWithXbox = "https://api.minecraftservices.com/authentication/login_with_xbox";
|
||||
|
||||
private record LoginPayload
|
||||
{
|
||||
public string? identityToken { init; get; }
|
||||
}
|
||||
|
||||
private record LoginResult
|
||||
{
|
||||
public string? username { init; get; }
|
||||
public string[]? roles { init; get; }
|
||||
public string? access_token { init; get; }
|
||||
public string? token_type { init; get; }
|
||||
public int expires_in { init; get; }
|
||||
}
|
||||
|
||||
private record GameOwnershipResult
|
||||
{
|
||||
public Dictionary<string, string>[]? items { init; get; }
|
||||
public string? signature { init; get; }
|
||||
public string? keyId { init; get; }
|
||||
}
|
||||
|
||||
private record GameProfileResult
|
||||
{
|
||||
public string? id { init; get; }
|
||||
public string? name { init; get; }
|
||||
public Dictionary<string, string>[]? skins { init; get; }
|
||||
public Dictionary<string, string>[]? capes { init; get; }
|
||||
/* Error */
|
||||
public string? path { init; get; }
|
||||
public string? errorType { init; get; }
|
||||
public string? error { init; get; }
|
||||
public string? errorMessage { init; get; }
|
||||
public string? developerMessage { init; get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Login to Minecraft using the XSTS token and user hash obtained before
|
||||
|
|
@ -410,25 +565,23 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="userHash"></param>
|
||||
/// <param name="xstsToken"></param>
|
||||
/// <returns></returns>
|
||||
public static string LoginWithXbox(string userHash, string xstsToken)
|
||||
public static async Task<string> LoginWithXboxAsync(HttpClient httpClient, string userHash, string xstsToken)
|
||||
{
|
||||
var request = new ProxiedWebRequest(loginWithXbox)
|
||||
LoginPayload payload = new()
|
||||
{
|
||||
Accept = "application/json"
|
||||
identityToken = $"XBL3.0 x={userHash};{xstsToken}",
|
||||
};
|
||||
|
||||
string payload = "{\"identityToken\": \"XBL3.0 x=" + userHash + ";" + xstsToken + "\"}";
|
||||
var response = request.Post("application/json", payload);
|
||||
using StringContent httpContent = new(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
{
|
||||
using HttpResponseMessage response = await httpClient.PostAsync(loginWithXbox, httpContent);
|
||||
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLine(response.ToString());
|
||||
}
|
||||
|
||||
string jsonString = response.Body;
|
||||
Json.JSONData json = Json.ParseJson(jsonString);
|
||||
LoginResult jsonData = (await response.Content.ReadFromJsonAsync<LoginResult>())!;
|
||||
|
||||
return json.Properties["access_token"].StringValue;
|
||||
return jsonData.access_token!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -436,39 +589,40 @@ namespace MinecraftClient.Protocol
|
|||
/// </summary>
|
||||
/// <param name="accessToken"></param>
|
||||
/// <returns>True if the user own the game</returns>
|
||||
public static bool UserHasGame(string accessToken)
|
||||
public static async Task<bool> CheckUserHasGameAsync(HttpClient httpClient, string accessToken)
|
||||
{
|
||||
var request = new ProxiedWebRequest(ownership);
|
||||
using HttpRequestMessage request = new(HttpMethod.Get, ownership);
|
||||
request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken));
|
||||
var response = request.Get();
|
||||
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
{
|
||||
using HttpResponseMessage response = await httpClient.SendAsync(request);
|
||||
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLine(response.ToString());
|
||||
}
|
||||
|
||||
string jsonString = response.Body;
|
||||
Json.JSONData json = Json.ParseJson(jsonString);
|
||||
return json.Properties["items"].DataArray.Count > 0;
|
||||
GameOwnershipResult jsonData = (await response.Content.ReadFromJsonAsync<GameOwnershipResult>())!;
|
||||
|
||||
return jsonData.items!.Length > 0;
|
||||
}
|
||||
|
||||
public static UserProfile GetUserProfile(string accessToken)
|
||||
public static async Task<UserProfile> GetUserProfileAsync(HttpClient httpClient, string accessToken)
|
||||
{
|
||||
var request = new ProxiedWebRequest(profile);
|
||||
using HttpRequestMessage request = new(HttpMethod.Get, profile);
|
||||
request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken));
|
||||
var response = request.Get();
|
||||
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
{
|
||||
using HttpResponseMessage response = await httpClient.SendAsync(request);
|
||||
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLine(response.ToString());
|
||||
}
|
||||
|
||||
string jsonString = response.Body;
|
||||
Json.JSONData json = Json.ParseJson(jsonString);
|
||||
GameProfileResult jsonData = (await response.Content.ReadFromJsonAsync<GameProfileResult>())!;
|
||||
|
||||
if (!string.IsNullOrEmpty(jsonData.error))
|
||||
throw new Exception($"{jsonData.errorType}: {jsonData.error}. {jsonData.errorMessage}");
|
||||
|
||||
return new UserProfile()
|
||||
{
|
||||
UUID = json.Properties["id"].StringValue,
|
||||
UserName = json.Properties["name"].StringValue
|
||||
UUID = jsonData.id!,
|
||||
UserName = jsonData.name!,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
203
MinecraftClient/Protocol/PacketPipeline/AesStream.cs
Normal file
203
MinecraftClient/Protocol/PacketPipeline/AesStream.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
189
MinecraftClient/Protocol/PacketPipeline/PacketStream.cs
Normal file
189
MinecraftClient/Protocol/PacketPipeline/PacketStream.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
157
MinecraftClient/Protocol/PacketPipeline/SocketWrapper.cs
Normal file
157
MinecraftClient/Protocol/PacketPipeline/SocketWrapper.cs
Normal 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) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
119
MinecraftClient/Protocol/PacketPipeline/ZlibBaseStream.cs
Normal file
119
MinecraftClient/Protocol/PacketPipeline/ZlibBaseStream.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using MinecraftClient.Protocol.Message;
|
||||
|
||||
namespace MinecraftClient.Protocol.ProfileKey
|
||||
|
|
@ -10,51 +11,6 @@ namespace MinecraftClient.Protocol.ProfileKey
|
|||
{
|
||||
private static readonly SHA256 sha256Hash = SHA256.Create();
|
||||
|
||||
private static readonly string certificates = "https://api.minecraftservices.com/player/certificates";
|
||||
|
||||
public static PlayerKeyPair? GetNewProfileKeys(string accessToken)
|
||||
{
|
||||
ProxiedWebRequest.Response? response = null;
|
||||
try
|
||||
{
|
||||
var request = new ProxiedWebRequest(certificates)
|
||||
{
|
||||
Accept = "application/json"
|
||||
};
|
||||
request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken));
|
||||
|
||||
response = request.Post("application/json", "");
|
||||
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
{
|
||||
ConsoleIO.WriteLine(response.Body.ToString());
|
||||
}
|
||||
|
||||
string jsonString = response.Body;
|
||||
Json.JSONData json = Json.ParseJson(jsonString);
|
||||
|
||||
PublicKey publicKey = new(pemKey: json.Properties["keyPair"].Properties["publicKey"].StringValue,
|
||||
sig: json.Properties["publicKeySignature"].StringValue,
|
||||
sigV2: json.Properties["publicKeySignatureV2"].StringValue);
|
||||
|
||||
PrivateKey privateKey = new(pemKey: json.Properties["keyPair"].Properties["privateKey"].StringValue);
|
||||
|
||||
return new PlayerKeyPair(publicKey, privateKey,
|
||||
expiresAt: json.Properties["expiresAt"].StringValue,
|
||||
refreshedAfter: json.Properties["refreshedAfter"].StringValue);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
int code = response == null ? 0 : response.StatusCode;
|
||||
ConsoleIO.WriteLineFormatted("§cFetch profile key failed: HttpCode = " + code + ", Error = " + e.Message);
|
||||
if (Settings.Config.Logging.DebugMessages)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§c" + e.StackTrace);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] DecodePemKey(string key, string prefix, string suffix)
|
||||
{
|
||||
int i = key.IndexOf(prefix);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +1,38 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MinecraftClient.Protocol.ProfileKey
|
||||
{
|
||||
public class PlayerKeyPair
|
||||
{
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("PublicKey")]
|
||||
public PublicKey PublicKey;
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("PrivateKey")]
|
||||
public PrivateKey PrivateKey;
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("ExpiresAt")]
|
||||
public DateTime ExpiresAt;
|
||||
|
||||
public DateTime RefreshedAfter; // Todo: add a timer
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("RefreshedAfter")]
|
||||
public DateTime RefreshedAfter;
|
||||
|
||||
[JsonIgnore]
|
||||
private const string DataTimeFormat = "yyyy-MM-ddTHH:mm:ss.ffffffZ";
|
||||
|
||||
public PlayerKeyPair(PublicKey keyPublic, PrivateKey keyPrivate, string expiresAt, string refreshedAfter)
|
||||
[JsonConstructor]
|
||||
public PlayerKeyPair(PublicKey PublicKey, PrivateKey PrivateKey, DateTime ExpiresAt, DateTime RefreshedAfter)
|
||||
{
|
||||
PublicKey = keyPublic;
|
||||
PrivateKey = keyPrivate;
|
||||
try
|
||||
{
|
||||
ExpiresAt = DateTime.ParseExact(expiresAt, DataTimeFormat, System.Globalization.CultureInfo.InvariantCulture).ToUniversalTime();
|
||||
RefreshedAfter = DateTime.ParseExact(refreshedAfter, DataTimeFormat, System.Globalization.CultureInfo.InvariantCulture).ToUniversalTime();
|
||||
}
|
||||
catch
|
||||
{
|
||||
ExpiresAt = DateTime.Parse(expiresAt).ToUniversalTime();
|
||||
RefreshedAfter = DateTime.Parse(refreshedAfter).ToUniversalTime();
|
||||
}
|
||||
this.PublicKey = PublicKey;
|
||||
this.PrivateKey = PrivateKey;
|
||||
this.ExpiresAt = ExpiresAt;
|
||||
this.RefreshedAfter = RefreshedAfter;
|
||||
}
|
||||
|
||||
public bool NeedRefresh()
|
||||
|
|
@ -54,21 +57,6 @@ namespace MinecraftClient.Protocol.ProfileKey
|
|||
return timeOffset.ToUnixTimeSeconds();
|
||||
}
|
||||
|
||||
public static PlayerKeyPair FromString(string tokenString)
|
||||
{
|
||||
string[] fields = tokenString.Split(',');
|
||||
|
||||
if (fields.Length < 6)
|
||||
throw new InvalidDataException("Invalid string format");
|
||||
|
||||
PublicKey publicKey = new(pemKey: fields[0].Trim(),
|
||||
sig: fields[1].Trim(), sigV2: fields[2].Trim());
|
||||
|
||||
PrivateKey privateKey = new(pemKey: fields[3].Trim());
|
||||
|
||||
return new PlayerKeyPair(publicKey, privateKey, fields[4].Trim(), fields[5].Trim());
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
List<string> datas = new();
|
||||
|
|
|
|||
|
|
@ -1,15 +1,20 @@
|
|||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json.Serialization;
|
||||
using MinecraftClient.Protocol.Message;
|
||||
|
||||
namespace MinecraftClient.Protocol.ProfileKey
|
||||
{
|
||||
public class PrivateKey
|
||||
{
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("Key")]
|
||||
public byte[] Key { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
private readonly RSA rsa;
|
||||
|
||||
[JsonIgnore]
|
||||
private byte[]? precedingSignature = null;
|
||||
|
||||
public PrivateKey(string pemKey)
|
||||
|
|
@ -20,6 +25,14 @@ namespace MinecraftClient.Protocol.ProfileKey
|
|||
rsa.ImportPkcs8PrivateKey(Key, out _);
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public PrivateKey(byte[] Key)
|
||||
{
|
||||
this.Key = Key;
|
||||
rsa = RSA.Create();
|
||||
rsa.ImportPkcs8PrivateKey(Key, out _);
|
||||
}
|
||||
|
||||
public byte[] SignData(byte[] data)
|
||||
{
|
||||
return rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,26 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json.Serialization;
|
||||
using MinecraftClient.Protocol.Message;
|
||||
|
||||
namespace MinecraftClient.Protocol.ProfileKey
|
||||
{
|
||||
public class PublicKey
|
||||
{
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("Key")]
|
||||
public byte[] Key { get; set; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("Signature")]
|
||||
public byte[]? Signature { get; set; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("SignatureV2")]
|
||||
public byte[]? SignatureV2 { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
private readonly RSA rsa;
|
||||
|
||||
public PublicKey(string pemKey, string? sig = null, string? sigV2 = null)
|
||||
|
|
@ -36,6 +47,12 @@ namespace MinecraftClient.Protocol.ProfileKey
|
|||
Signature = signature;
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public PublicKey(byte[] Key, byte[]? Signature, byte[]? SignatureV2) : this(Key, Signature!)
|
||||
{
|
||||
this.SignatureV2 = SignatureV2;
|
||||
}
|
||||
|
||||
public bool VerifyData(byte[] data, byte[] signature)
|
||||
{
|
||||
return rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -9,6 +9,7 @@ using System.Net.Sockets;
|
|||
using System.Security.Authentication;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MinecraftClient.Proxy;
|
||||
|
||||
namespace MinecraftClient.Protocol
|
||||
|
|
@ -25,7 +26,7 @@ namespace MinecraftClient.Protocol
|
|||
|
||||
private readonly string httpVersion = "HTTP/1.1";
|
||||
|
||||
private ITcpFactory? tcpFactory;
|
||||
private readonly ITcpFactory? tcpFactory;
|
||||
private bool isProxied = false; // Send absolute Url in request if true
|
||||
|
||||
private readonly Uri uri;
|
||||
|
|
@ -45,7 +46,7 @@ namespace MinecraftClient.Protocol
|
|||
/// Set to true to tell the http client proxy is enabled
|
||||
/// </summary>
|
||||
public bool IsProxy { get { return isProxied; } set { isProxied = value; } }
|
||||
public bool Debug { get { return Settings.Config.Logging.DebugMessages; } }
|
||||
public static bool Debug { get { return Settings.Config.Logging.DebugMessages; } }
|
||||
|
||||
/// <summary>
|
||||
/// Create a new http request
|
||||
|
|
@ -105,9 +106,9 @@ namespace MinecraftClient.Protocol
|
|||
/// Perform GET request and get the response. Proxy is handled automatically
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Response Get()
|
||||
public async Task<Response> Get()
|
||||
{
|
||||
return Send("GET");
|
||||
return await Send("GET");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -116,12 +117,12 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="contentType">The content type of request body</param>
|
||||
/// <param name="body">Request body</param>
|
||||
/// <returns></returns>
|
||||
public Response Post(string contentType, string body)
|
||||
public async Task<Response> Post(string contentType, string body)
|
||||
{
|
||||
Headers.Add("Content-Type", contentType);
|
||||
// Calculate length
|
||||
Headers.Add("Content-Length", Encoding.UTF8.GetBytes(body).Length.ToString());
|
||||
return Send("POST", body);
|
||||
return await Send("POST", body);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -130,35 +131,35 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="method">Method in string representation</param>
|
||||
/// <param name="body">Optional request body</param>
|
||||
/// <returns></returns>
|
||||
private Response Send(string method, string body = "")
|
||||
private async Task<Response> Send(string method, string body = "")
|
||||
{
|
||||
List<string> requestMessage = new()
|
||||
{
|
||||
string.Format("{0} {1} {2}", method.ToUpper(), isProxied ? AbsoluteUrl : Path, httpVersion) // Request line
|
||||
};
|
||||
|
||||
foreach (string key in Headers) // Headers
|
||||
{
|
||||
var value = Headers[key];
|
||||
requestMessage.Add(string.Format("{0}: {1}", key, value));
|
||||
}
|
||||
|
||||
requestMessage.Add(""); // <CR><LF>
|
||||
|
||||
if (body != "")
|
||||
{
|
||||
requestMessage.Add(body);
|
||||
}
|
||||
else requestMessage.Add(""); // <CR><LF>
|
||||
else
|
||||
requestMessage.Add(""); // <CR><LF>
|
||||
|
||||
if (Debug)
|
||||
{
|
||||
foreach (string l in requestMessage)
|
||||
{
|
||||
ConsoleIO.WriteLine("< " + l);
|
||||
}
|
||||
}
|
||||
|
||||
Response response = Response.Empty();
|
||||
|
||||
// FIXME: Use TcpFactory interface to avoid direct usage of the ProxyHandler class
|
||||
// TcpClient client = tcpFactory.CreateTcpClient(Host, Port);
|
||||
TcpClient client = ProxyHandler.NewTcpClient(Host, Port, true);
|
||||
TcpClient client = ProxyHandler.NewTcpClient(Host, Port, ProxyHandler.ClientType.Login);
|
||||
Stream stream;
|
||||
if (IsSecure)
|
||||
{
|
||||
|
|
@ -171,35 +172,25 @@ namespace MinecraftClient.Protocol
|
|||
}
|
||||
string h = string.Join("\r\n", requestMessage.ToArray());
|
||||
byte[] data = Encoding.ASCII.GetBytes(h);
|
||||
stream.Write(data, 0, data.Length);
|
||||
stream.Flush();
|
||||
await stream.WriteAsync(data);
|
||||
await stream.FlushAsync();
|
||||
|
||||
// Read response
|
||||
int statusCode = ReadHttpStatus(stream);
|
||||
var headers = ReadHeader(stream);
|
||||
string? rbody;
|
||||
if (headers.Get("transfer-encoding") == "chunked")
|
||||
{
|
||||
rbody = ReadBodyChunked(stream);
|
||||
}
|
||||
else
|
||||
{
|
||||
rbody = ReadBody(stream, int.Parse(headers.Get("content-length") ?? "0"));
|
||||
}
|
||||
int statusCode = await ReadHttpStatus(stream);
|
||||
var headers = await ReadHeader(stream);
|
||||
|
||||
Task<string> rbody = (headers.Get("transfer-encoding") == "chunked") ?
|
||||
ReadBodyChunked(stream) : ReadBody(stream, int.Parse(headers.Get("content-length") ?? "0"));
|
||||
|
||||
if (headers.Get("set-cookie") != null)
|
||||
{
|
||||
response.Cookies = ParseSetCookie(headers.GetValues("set-cookie") ?? Array.Empty<string>());
|
||||
}
|
||||
response.Body = rbody ?? "";
|
||||
|
||||
response.StatusCode = statusCode;
|
||||
response.Headers = headers;
|
||||
response.Body = await rbody;
|
||||
|
||||
try
|
||||
{
|
||||
stream.Close();
|
||||
client.Close();
|
||||
}
|
||||
catch { }
|
||||
try { stream.Close(); } catch { }
|
||||
try { client.Close(); } catch { }
|
||||
|
||||
return response;
|
||||
}
|
||||
|
|
@ -210,9 +201,9 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="s">Stream to read</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="InvalidDataException">If server return unknown data</exception>
|
||||
private static int ReadHttpStatus(Stream s)
|
||||
private static async Task<int> ReadHttpStatus(Stream s)
|
||||
{
|
||||
var httpHeader = ReadLine(s); // http header line
|
||||
var httpHeader = await ReadLine(s); // http header line
|
||||
if (httpHeader.StartsWith("HTTP/1.1") || httpHeader.StartsWith("HTTP/1.0"))
|
||||
{
|
||||
return int.Parse(httpHeader.Split(' ')[1], NumberStyles.Any, CultureInfo.CurrentCulture);
|
||||
|
|
@ -228,15 +219,15 @@ namespace MinecraftClient.Protocol
|
|||
/// </summary>
|
||||
/// <param name="s">Stream to read</param>
|
||||
/// <returns>Headers in lower-case</returns>
|
||||
private static NameValueCollection ReadHeader(Stream s)
|
||||
private static async Task<NameValueCollection> ReadHeader(Stream s)
|
||||
{
|
||||
var headers = new NameValueCollection();
|
||||
// Read headers
|
||||
string header;
|
||||
do
|
||||
{
|
||||
header = ReadLine(s);
|
||||
if (!String.IsNullOrEmpty(header))
|
||||
header = await ReadLine(s);
|
||||
if (!string.IsNullOrEmpty(header))
|
||||
{
|
||||
var tmp = header.Split(new char[] { ':' }, 2);
|
||||
var name = tmp[0].ToLower();
|
||||
|
|
@ -244,7 +235,7 @@ namespace MinecraftClient.Protocol
|
|||
headers.Add(name, value);
|
||||
}
|
||||
}
|
||||
while (!String.IsNullOrEmpty(header));
|
||||
while (!string.IsNullOrEmpty(header));
|
||||
return headers;
|
||||
}
|
||||
|
||||
|
|
@ -254,23 +245,19 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="s">Stream to read</param>
|
||||
/// <param name="length">Length of the body (the Content-Length header)</param>
|
||||
/// <returns>Body or null if length is zero</returns>
|
||||
private static string? ReadBody(Stream s, int length)
|
||||
private static async Task<string> ReadBody(Stream s, int length)
|
||||
{
|
||||
if (length > 0)
|
||||
{
|
||||
byte[] buffer = new byte[length];
|
||||
int r = 0;
|
||||
while (r < length)
|
||||
{
|
||||
var read = s.Read(buffer, r, length - r);
|
||||
r += read;
|
||||
Thread.Sleep(50);
|
||||
}
|
||||
int readed = 0;
|
||||
while (readed < length)
|
||||
readed += await s.ReadAsync(buffer.AsMemory(readed, length - readed));
|
||||
return Encoding.UTF8.GetString(buffer);
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -279,13 +266,13 @@ namespace MinecraftClient.Protocol
|
|||
/// </summary>
|
||||
/// <param name="s">Stream to read</param>
|
||||
/// <returns>Body or empty string if nothing is received</returns>
|
||||
private static string ReadBodyChunked(Stream s)
|
||||
private static async Task<string> ReadBodyChunked(Stream s)
|
||||
{
|
||||
List<byte> buffer1 = new();
|
||||
while (true)
|
||||
{
|
||||
string l = ReadLine(s);
|
||||
int size = Int32.Parse(l, NumberStyles.HexNumber);
|
||||
string l = await ReadLine(s);
|
||||
int size = int.Parse(l, NumberStyles.HexNumber);
|
||||
if (size == 0)
|
||||
break;
|
||||
byte[] buffer2 = new byte[size];
|
||||
|
|
@ -296,7 +283,7 @@ namespace MinecraftClient.Protocol
|
|||
r += read;
|
||||
Thread.Sleep(50);
|
||||
}
|
||||
ReadLine(s);
|
||||
await ReadLine(s);
|
||||
buffer1.AddRange(buffer2);
|
||||
}
|
||||
return Encoding.UTF8.GetString(buffer1.ToArray());
|
||||
|
|
@ -366,17 +353,15 @@ namespace MinecraftClient.Protocol
|
|||
/// </remarks>
|
||||
/// <param name="s">Stream to read</param>
|
||||
/// <returns>String</returns>
|
||||
private static string ReadLine(Stream s)
|
||||
private static async Task<string> ReadLine(Stream s)
|
||||
{
|
||||
List<byte> buffer = new();
|
||||
byte c;
|
||||
byte[] c = new byte[1];
|
||||
while (true)
|
||||
{
|
||||
int b = s.ReadByte();
|
||||
if (b == -1)
|
||||
break;
|
||||
c = (byte)b;
|
||||
if (c == '\n')
|
||||
try { await s.ReadExactlyAsync(c, 0, 1); }
|
||||
catch { break; }
|
||||
if (c[0] == '\n')
|
||||
{
|
||||
if (buffer.Last() == '\r')
|
||||
{
|
||||
|
|
@ -384,7 +369,7 @@ namespace MinecraftClient.Protocol
|
|||
break;
|
||||
}
|
||||
}
|
||||
buffer.Add(c);
|
||||
buffer.Add(c[0]);
|
||||
}
|
||||
return Encoding.UTF8.GetString(buffer.ToArray());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using Ionic.Zip;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Protocol.Handlers;
|
||||
using MinecraftClient.Protocol.Handlers.PacketPalettes;
|
||||
|
|
@ -135,15 +135,19 @@ namespace MinecraftClient.Protocol
|
|||
MetaData.duration = Convert.ToInt32((lastPacketTime - recordStartTime).TotalMilliseconds);
|
||||
MetaData.SaveToFile();
|
||||
|
||||
using (Stream recordingFile = new FileStream(Path.Combine(temporaryCache, recordingTmpFileName), FileMode.Open))
|
||||
using (FileStream zipToOpen = new(Path.Combine(ReplayFileDirectory, replayFileName), FileMode.Open))
|
||||
{
|
||||
using ZipArchive archive = new(zipToOpen, ZipArchiveMode.Create);
|
||||
|
||||
using (Stream recordingFile = new FileStream(Path.Combine(temporaryCache, recordingTmpFileName), FileMode.Open))
|
||||
{
|
||||
ZipArchiveEntry recordingTmpFileEntry = archive.CreateEntry(recordingTmpFileName);
|
||||
recordingFile.CopyTo(recordingTmpFileEntry.Open());
|
||||
}
|
||||
|
||||
using Stream metaDataFile = new FileStream(Path.Combine(temporaryCache, MetaData.MetaDataFileName), FileMode.Open);
|
||||
using ZipOutputStream zs = new(Path.Combine(ReplayFileDirectory, replayFileName));
|
||||
zs.PutNextEntry(recordingTmpFileName);
|
||||
recordingFile.CopyTo(zs);
|
||||
zs.PutNextEntry(MetaData.MetaDataFileName);
|
||||
metaDataFile.CopyTo(zs);
|
||||
zs.Close();
|
||||
ZipArchiveEntry metaDataFileEntry = archive.CreateEntry(MetaData.MetaDataFileName);
|
||||
metaDataFile.CopyTo(metaDataFileEntry.Open());
|
||||
}
|
||||
|
||||
File.Delete(Path.Combine(temporaryCache, recordingTmpFileName));
|
||||
|
|
@ -165,20 +169,21 @@ namespace MinecraftClient.Protocol
|
|||
MetaData.duration = Convert.ToInt32((lastPacketTime - recordStartTime).TotalMilliseconds);
|
||||
MetaData.SaveToFile();
|
||||
|
||||
using (Stream metaDataFile = new FileStream(Path.Combine(temporaryCache, MetaData.MetaDataFileName), FileMode.Open))
|
||||
using (FileStream zipToOpen = new(replayFileName, FileMode.OpenOrCreate))
|
||||
{
|
||||
using ZipOutputStream zs = new(replayFileName);
|
||||
zs.PutNextEntry(recordingTmpFileName);
|
||||
using ZipArchive archive = new(zipToOpen, ZipArchiveMode.Create);
|
||||
|
||||
ZipArchiveEntry recordingTmpFileEntry = archive.CreateEntry(recordingTmpFileName);
|
||||
// .CopyTo() method start from stream current position
|
||||
// We need to reset position in order to get full content
|
||||
var lastPosition = recordStream!.BaseStream.Position;
|
||||
recordStream.BaseStream.Position = 0;
|
||||
recordStream.BaseStream.CopyTo(zs);
|
||||
recordStream.BaseStream.CopyTo(recordingTmpFileEntry.Open());
|
||||
recordStream.BaseStream.Position = lastPosition;
|
||||
|
||||
zs.PutNextEntry(MetaData.MetaDataFileName);
|
||||
metaDataFile.CopyTo(zs);
|
||||
zs.Close();
|
||||
using Stream metaDataFile = new FileStream(Path.Combine(temporaryCache, MetaData.MetaDataFileName), FileMode.Open);
|
||||
ZipArchiveEntry metaDataFileEntry = archive.CreateEntry(MetaData.MetaDataFileName);
|
||||
metaDataFile.CopyTo(metaDataFileEntry.Open());
|
||||
}
|
||||
|
||||
WriteDebugLog("Backup replay file created.");
|
||||
|
|
|
|||
|
|
@ -1,9 +1,19 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Runtime.Serialization.Formatters.Binary;
|
||||
using System.ServiceModel.Channels;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
using MinecraftClient.Protocol.ProfileKey;
|
||||
using MinecraftClient.Scripting;
|
||||
using static MinecraftClient.Settings;
|
||||
using static MinecraftClient.Settings.MainConfigHealper.MainConfig.AdvancedConfig;
|
||||
|
||||
|
|
@ -12,58 +22,88 @@ namespace MinecraftClient.Protocol.Session
|
|||
/// <summary>
|
||||
/// Handle sessions caching and storage.
|
||||
/// </summary>
|
||||
public static class SessionCache
|
||||
public static partial class SessionCache
|
||||
{
|
||||
private const string SessionCacheFilePlaintext = "SessionCache.ini";
|
||||
private const string SessionCacheFileSerialized = "SessionCache.db";
|
||||
private static readonly string SessionCacheFileMinecraft = String.Concat(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
Path.DirectorySeparatorChar,
|
||||
".minecraft",
|
||||
Path.DirectorySeparatorChar,
|
||||
"launcher_profiles.json"
|
||||
);
|
||||
public class Cache
|
||||
{
|
||||
[JsonInclude]
|
||||
public Dictionary<string, SessionToken> SessionTokens = new();
|
||||
|
||||
private static FileMonitor? cachemonitor;
|
||||
private static readonly Dictionary<string, SessionToken> sessions = new();
|
||||
private static readonly Timer updatetimer = new(100);
|
||||
private static readonly List<KeyValuePair<string, SessionToken>> pendingadds = new();
|
||||
private static readonly BinaryFormatter formatter = new();
|
||||
[JsonInclude]
|
||||
public Dictionary<string, PlayerKeyPair> ProfileKeys = new();
|
||||
|
||||
[JsonInclude]
|
||||
public Dictionary<string, ServerInfo> ServerKeys = new();
|
||||
|
||||
public record ServerInfo
|
||||
{
|
||||
public ServerInfo(string serverIDhash, byte[] serverPublicKey)
|
||||
{
|
||||
ServerIDhash = serverIDhash;
|
||||
ServerPublicKey = serverPublicKey;
|
||||
}
|
||||
|
||||
public string? ServerIDhash { init; get; }
|
||||
public byte[]? ServerPublicKey { init; get; }
|
||||
}
|
||||
}
|
||||
|
||||
private static Cache cache = new();
|
||||
|
||||
private const string SessionCacheFileJson = "SessionCache.json";
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.General)
|
||||
{
|
||||
WriteIndented = true,
|
||||
AllowTrailingCommas = true,
|
||||
PropertyNameCaseInsensitive = false,
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||
};
|
||||
|
||||
|
||||
public static async Task ReadCacheSessionAsync()
|
||||
{
|
||||
if (File.Exists(SessionCacheFileJson))
|
||||
{
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading_session, SessionCacheFileJson));
|
||||
|
||||
FileStream fileStream = File.OpenRead(SessionCacheFileJson);
|
||||
|
||||
try
|
||||
{
|
||||
Cache? diskCache = (Cache?)await JsonSerializer.DeserializeAsync(fileStream, typeof(Cache), JsonOptions);
|
||||
|
||||
if (diskCache != null)
|
||||
{
|
||||
cache = diskCache;
|
||||
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, cache.SessionTokens.Count, cache.ProfileKeys.Count));
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail_plain, e.Message));
|
||||
}
|
||||
catch (JsonException e)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail_plain, e.Message));
|
||||
}
|
||||
|
||||
await fileStream.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve whether SessionCache contains a session for the given login.
|
||||
/// </summary>
|
||||
/// <param name="login">User login used with Minecraft.net</param>
|
||||
/// <returns>TRUE if session is available</returns>
|
||||
public static bool Contains(string login)
|
||||
public static bool ContainsSession(string login)
|
||||
{
|
||||
return sessions.ContainsKey(login);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Store a session and save it to disk if required.
|
||||
/// </summary>
|
||||
/// <param name="login">User login used with Minecraft.net</param>
|
||||
/// <param name="session">User session token used with Minecraft.net</param>
|
||||
public static void Store(string login, SessionToken session)
|
||||
{
|
||||
if (Contains(login))
|
||||
{
|
||||
sessions[login] = session;
|
||||
}
|
||||
else
|
||||
{
|
||||
sessions.Add(login, session);
|
||||
}
|
||||
|
||||
if (Config.Main.Advanced.SessionCache == CacheType.disk && updatetimer.Enabled == true)
|
||||
{
|
||||
pendingadds.Add(new KeyValuePair<string, SessionToken>(login, session));
|
||||
}
|
||||
else if (Config.Main.Advanced.SessionCache == CacheType.disk)
|
||||
{
|
||||
SaveToDisk();
|
||||
}
|
||||
return cache.SessionTokens.ContainsKey(login);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -71,202 +111,92 @@ namespace MinecraftClient.Protocol.Session
|
|||
/// </summary>
|
||||
/// <param name="login">User login used with Minecraft.net</param>
|
||||
/// <returns>SessionToken for given login</returns>
|
||||
public static SessionToken Get(string login)
|
||||
public static Tuple<SessionToken?, PlayerKeyPair?> GetSession(string login)
|
||||
{
|
||||
return sessions[login];
|
||||
cache.SessionTokens.TryGetValue(login, out SessionToken? sessionToken);
|
||||
cache.ProfileKeys.TryGetValue(login, out PlayerKeyPair? playerKeyPair);
|
||||
return new(sessionToken, playerKeyPair);
|
||||
}
|
||||
|
||||
public static Cache.ServerInfo? GetServerInfo(string server)
|
||||
{
|
||||
if (cache.ServerKeys.TryGetValue(server, out Cache.ServerInfo? info))
|
||||
return info;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize cache monitoring to keep cache updated with external changes.
|
||||
/// Store a session and save it to disk if required.
|
||||
/// </summary>
|
||||
/// <returns>TRUE if session tokens are seeded from file</returns>
|
||||
public static bool InitializeDiskCache()
|
||||
/// <param name="login">User login used with Minecraft.net</param>
|
||||
/// <param name="newSession">User session token used with Minecraft.net</param>
|
||||
public static async Task StoreSessionAsync(string login, SessionToken? sessionToken, PlayerKeyPair? profileKey)
|
||||
{
|
||||
cachemonitor = new FileMonitor(AppDomain.CurrentDomain.BaseDirectory, SessionCacheFilePlaintext, new FileSystemEventHandler(OnChanged));
|
||||
updatetimer.Elapsed += HandlePending;
|
||||
return LoadFromDisk();
|
||||
if (sessionToken != null)
|
||||
cache.SessionTokens[login] = sessionToken;
|
||||
if (profileKey != null)
|
||||
cache.ProfileKeys[login] = profileKey;
|
||||
|
||||
if (Config.Main.Advanced.SessionCache == CacheType.disk)
|
||||
await SaveToDisk();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads cache on external cache file change.
|
||||
/// </summary>
|
||||
/// <param name="sender">Sender</param>
|
||||
/// <param name="e">Event data</param>
|
||||
private static void OnChanged(object sender, FileSystemEventArgs e)
|
||||
public static void StoreServerInfo(string server, string ServerIDhash, byte[] ServerPublicKey)
|
||||
{
|
||||
updatetimer.Stop();
|
||||
updatetimer.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after timer elapsed. Reads disk cache and adds new/modified sessions back.
|
||||
/// </summary>
|
||||
/// <param name="sender">Sender</param>
|
||||
/// <param name="e">Event data</param>
|
||||
private static void HandlePending(object? sender, ElapsedEventArgs e)
|
||||
{
|
||||
updatetimer.Stop();
|
||||
LoadFromDisk();
|
||||
|
||||
foreach (KeyValuePair<string, SessionToken> pending in pendingadds.ToArray())
|
||||
{
|
||||
Store(pending.Key, pending.Value);
|
||||
pendingadds.Remove(pending);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads cache file and loads SessionTokens into SessionCache.
|
||||
/// </summary>
|
||||
/// <returns>True if data is successfully loaded</returns>
|
||||
private static bool LoadFromDisk()
|
||||
{
|
||||
//Grab sessions in the Minecraft directory
|
||||
if (File.Exists(SessionCacheFileMinecraft))
|
||||
{
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading, Path.GetFileName(SessionCacheFileMinecraft)));
|
||||
Json.JSONData mcSession = new(Json.JSONData.DataType.String);
|
||||
try
|
||||
{
|
||||
mcSession = Json.ParseJson(File.ReadAllText(SessionCacheFileMinecraft));
|
||||
}
|
||||
catch (IOException) { /* Failed to read file from disk -- ignoring */ }
|
||||
if (mcSession.Type == Json.JSONData.DataType.Object
|
||||
&& mcSession.Properties.ContainsKey("clientToken")
|
||||
&& mcSession.Properties.ContainsKey("authenticationDatabase"))
|
||||
{
|
||||
string clientID = mcSession.Properties["clientToken"].StringValue.Replace("-", "");
|
||||
Dictionary<string, Json.JSONData> sessionItems = mcSession.Properties["authenticationDatabase"].Properties;
|
||||
foreach (string key in sessionItems.Keys)
|
||||
{
|
||||
if (Guid.TryParseExact(key, "N", out Guid temp))
|
||||
{
|
||||
Dictionary<string, Json.JSONData> sessionItem = sessionItems[key].Properties;
|
||||
if (sessionItem.ContainsKey("displayName")
|
||||
&& sessionItem.ContainsKey("accessToken")
|
||||
&& sessionItem.ContainsKey("username")
|
||||
&& sessionItem.ContainsKey("uuid"))
|
||||
{
|
||||
string login = Settings.ToLowerIfNeed(sessionItem["username"].StringValue);
|
||||
try
|
||||
{
|
||||
SessionToken session = SessionToken.FromString(String.Join(",",
|
||||
sessionItem["accessToken"].StringValue,
|
||||
sessionItem["displayName"].StringValue,
|
||||
sessionItem["uuid"].StringValue.Replace("-", ""),
|
||||
clientID
|
||||
));
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, login, session.ID));
|
||||
sessions[login] = session;
|
||||
}
|
||||
catch (InvalidDataException) { /* Not a valid session */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Serialized session cache file in binary format
|
||||
if (File.Exists(SessionCacheFileSerialized))
|
||||
{
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_converting, SessionCacheFileSerialized));
|
||||
|
||||
try
|
||||
{
|
||||
using FileStream fs = new(SessionCacheFileSerialized, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
#pragma warning disable SYSLIB0011 // BinaryFormatter.Deserialize() is obsolete
|
||||
// Possible risk of information disclosure or remote code execution. The impact of this vulnerability is limited to the user side only.
|
||||
Dictionary<string, SessionToken> sessionsTemp = (Dictionary<string, SessionToken>)formatter.Deserialize(fs);
|
||||
#pragma warning restore SYSLIB0011 // BinaryFormatter.Deserialize() is obsolete
|
||||
foreach (KeyValuePair<string, SessionToken> item in sessionsTemp)
|
||||
{
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, item.Key, item.Value.ID));
|
||||
sessions[item.Key] = item.Value;
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail, ex.Message));
|
||||
}
|
||||
catch (SerializationException ex2)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_malformed, ex2.Message));
|
||||
}
|
||||
}
|
||||
|
||||
//User-editable session cache file in text format
|
||||
if (File.Exists(SessionCacheFilePlaintext))
|
||||
{
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loading_session, SessionCacheFilePlaintext));
|
||||
|
||||
try
|
||||
{
|
||||
foreach (string line in FileMonitor.ReadAllLinesWithRetries(SessionCacheFilePlaintext))
|
||||
{
|
||||
if (!line.Trim().StartsWith("#"))
|
||||
{
|
||||
string[] keyValue = line.Split('=');
|
||||
if (keyValue.Length == 2)
|
||||
{
|
||||
try
|
||||
{
|
||||
string login = Settings.ToLowerIfNeed(keyValue[0]);
|
||||
SessionToken session = SessionToken.FromString(keyValue[1]);
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_loaded, login, session.ID));
|
||||
sessions[login] = session;
|
||||
}
|
||||
catch (InvalidDataException e)
|
||||
{
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_string, keyValue[1], e.Message));
|
||||
}
|
||||
}
|
||||
else if (Config.Logging.DebugMessages)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted(string.Format(Translations.cache_ignore_line, line));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_read_fail_plain, e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
return sessions.Count > 0;
|
||||
cache.ServerKeys[server] = new(ServerIDhash, ServerPublicKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves SessionToken's from SessionCache into cache file.
|
||||
/// </summary>
|
||||
private static void SaveToDisk()
|
||||
private static async Task SaveToDisk()
|
||||
{
|
||||
if (Config.Logging.DebugMessages)
|
||||
ConsoleIO.WriteLineFormatted("§8" + Translations.cache_saving, acceptnewlines: true);
|
||||
|
||||
List<string> sessionCacheLines = new()
|
||||
foreach ((string login, SessionToken session) in cache.SessionTokens)
|
||||
{
|
||||
"# Generated by MCC v" + Program.Version + " - Keep it secret & Edit at own risk!",
|
||||
"# Login=SessionID,PlayerName,UUID,ClientID,RefreshToken,ServerIDhash,ServerPublicKey"
|
||||
};
|
||||
foreach (KeyValuePair<string, SessionToken> entry in sessions)
|
||||
sessionCacheLines.Add(entry.Key + '=' + entry.Value.ToString());
|
||||
if (!GetJwtRegex().IsMatch(session.ID))
|
||||
cache.SessionTokens.Remove(login);
|
||||
else if (!ChatBot.IsValidName(session.PlayerName))
|
||||
cache.SessionTokens.Remove(login);
|
||||
else if (!Guid.TryParseExact(session.PlayerID, "N", out _))
|
||||
cache.SessionTokens.Remove(login);
|
||||
else if (!Guid.TryParseExact(session.ClientID, "N", out _))
|
||||
cache.SessionTokens.Remove(login);
|
||||
// No validation on refresh token because it is custom format token (not Jwt)
|
||||
}
|
||||
|
||||
foreach ((string login, PlayerKeyPair profileKey) in cache.ProfileKeys)
|
||||
{
|
||||
if (profileKey.NeedRefresh())
|
||||
cache.ProfileKeys.Remove(login);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
FileMonitor.WriteAllLinesWithRetries(SessionCacheFilePlaintext, sessionCacheLines);
|
||||
FileStream fileStream = File.Open(SessionCacheFileJson, FileMode.Create);
|
||||
|
||||
await fileStream.WriteAsync(Encoding.UTF8.GetBytes($"/* Generated by MCC v{Program.Version} - Keep it secret & Edit at own risk! */{Environment.NewLine}"));
|
||||
|
||||
await JsonSerializer.SerializeAsync(fileStream, cache, typeof(Cache), JsonOptions);
|
||||
|
||||
await fileStream.FlushAsync();
|
||||
fileStream.Close();
|
||||
await fileStream.DisposeAsync();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_save_fail, e.Message));
|
||||
}
|
||||
catch (JsonException e)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.cache_save_fail, e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
[GeneratedRegex("^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+$", RegexOptions.Compiled)]
|
||||
private static partial Regex GetJwtRegex();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using MinecraftClient.Scripting;
|
||||
|
|
@ -9,92 +11,39 @@ namespace MinecraftClient.Protocol.Session
|
|||
[Serializable]
|
||||
public class SessionToken
|
||||
{
|
||||
private static readonly Regex JwtRegex = new("^[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+\\.[A-Za-z0-9-_]+$");
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("SessionID")]
|
||||
public string ID { get; set; }
|
||||
public string PlayerName { get; set; }
|
||||
public string PlayerID { get; set; }
|
||||
public string ClientID { get; set; }
|
||||
public string RefreshToken { get; set; }
|
||||
public string ServerIDhash { get; set; }
|
||||
public byte[]? ServerPublicKey { get; set; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("PlayerName")]
|
||||
public string PlayerName { get; set; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("PlayerID")]
|
||||
public string PlayerID { get; set; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("ClientID")]
|
||||
public string ClientID { get; set; }
|
||||
|
||||
[JsonInclude]
|
||||
[JsonPropertyName("RefreshToken")]
|
||||
public string RefreshToken { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string? ServerInfoHash = null;
|
||||
|
||||
[JsonIgnore]
|
||||
public Task<bool>? SessionPreCheckTask = null;
|
||||
|
||||
public SessionToken()
|
||||
{
|
||||
ID = String.Empty;
|
||||
PlayerName = String.Empty;
|
||||
PlayerID = String.Empty;
|
||||
ClientID = String.Empty;
|
||||
RefreshToken = String.Empty;
|
||||
ServerIDhash = String.Empty;
|
||||
ServerPublicKey = null;
|
||||
}
|
||||
|
||||
public bool SessionPreCheck()
|
||||
{
|
||||
if (ID == string.Empty || PlayerID == String.Empty || ServerPublicKey == null)
|
||||
return false;
|
||||
Crypto.CryptoHandler.ClientAESPrivateKey ??= Crypto.CryptoHandler.GenerateAESPrivateKey();
|
||||
string serverHash = Crypto.CryptoHandler.GetServerHash(ServerIDhash, ServerPublicKey, Crypto.CryptoHandler.ClientAESPrivateKey);
|
||||
if (ProtocolHandler.SessionCheck(PlayerID, ID, serverHash))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return String.Join(",", ID, PlayerName, PlayerID, ClientID, RefreshToken, ServerIDhash,
|
||||
(ServerPublicKey == null) ? String.Empty : Convert.ToBase64String(ServerPublicKey));
|
||||
}
|
||||
|
||||
public static SessionToken FromString(string tokenString)
|
||||
{
|
||||
string[] fields = tokenString.Split(',');
|
||||
if (fields.Length < 4)
|
||||
throw new InvalidDataException("Invalid string format");
|
||||
|
||||
SessionToken session = new()
|
||||
{
|
||||
ID = fields[0],
|
||||
PlayerName = fields[1],
|
||||
PlayerID = fields[2],
|
||||
ClientID = fields[3]
|
||||
};
|
||||
// Backward compatible with old session file without refresh token field
|
||||
if (fields.Length > 4)
|
||||
session.RefreshToken = fields[4];
|
||||
else
|
||||
session.RefreshToken = String.Empty;
|
||||
if (fields.Length > 5)
|
||||
session.ServerIDhash = fields[5];
|
||||
else
|
||||
session.ServerIDhash = String.Empty;
|
||||
if (fields.Length > 6)
|
||||
{
|
||||
try
|
||||
{
|
||||
session.ServerPublicKey = Convert.FromBase64String(fields[6]);
|
||||
}
|
||||
catch
|
||||
{
|
||||
session.ServerPublicKey = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
session.ServerPublicKey = null;
|
||||
if (!JwtRegex.IsMatch(session.ID))
|
||||
throw new InvalidDataException("Invalid session ID");
|
||||
if (!ChatBot.IsValidName(session.PlayerName))
|
||||
throw new InvalidDataException("Invalid player name");
|
||||
if (!Guid.TryParseExact(session.PlayerID, "N", out _))
|
||||
throw new InvalidDataException("Invalid player ID");
|
||||
if (!Guid.TryParseExact(session.ClientID, "N", out _))
|
||||
throw new InvalidDataException("Invalid client ID");
|
||||
// No validation on refresh token because it is custom format token (not Jwt)
|
||||
|
||||
return session;
|
||||
ID = string.Empty;
|
||||
PlayerName = string.Empty;
|
||||
PlayerID = string.Empty;
|
||||
ClientID = string.Empty;
|
||||
RefreshToken = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
using System.Net.Sockets;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using Starksoft.Aspen.Proxy;
|
||||
using Tomlet.Attributes;
|
||||
|
||||
|
|
@ -17,14 +20,17 @@ namespace MinecraftClient.Proxy
|
|||
[TomlDoNotInlineObject]
|
||||
public class Configs
|
||||
{
|
||||
[TomlInlineComment("$Proxy.Enabled_Update$")]
|
||||
public bool Enabled_Update = false;
|
||||
[NonSerialized] // Compatible with old settings.
|
||||
public bool? Enabled_Login = false, Enabled_Ingame = false, Enabled_Update = false;
|
||||
|
||||
[TomlInlineComment("$Proxy.Enabled_Login$")]
|
||||
public bool Enabled_Login = false;
|
||||
[TomlInlineComment("$Proxy.Ingame_Proxy$")]
|
||||
public ProxyPreferenceType Ingame_Proxy = ProxyPreferenceType.disable;
|
||||
|
||||
[TomlInlineComment("$Proxy.Enabled_Ingame$")]
|
||||
public bool Enabled_Ingame = false;
|
||||
[TomlInlineComment("$Proxy.Login_Proxy$")]
|
||||
public ProxyPreferenceType Login_Proxy = ProxyPreferenceType.follow_system;
|
||||
|
||||
[TomlInlineComment("$Proxy.MCC_Update_Proxy$")]
|
||||
public ProxyPreferenceType MCC_Update_Proxy = ProxyPreferenceType.follow_system;
|
||||
|
||||
[TomlInlineComment("$Proxy.Server$")]
|
||||
public ProxyInfoConfig Server = new("0.0.0.0", 8080);
|
||||
|
|
@ -33,12 +39,22 @@ namespace MinecraftClient.Proxy
|
|||
public ProxyType Proxy_Type = ProxyType.HTTP;
|
||||
|
||||
[TomlInlineComment("$Proxy.Username$")]
|
||||
public string Username = "";
|
||||
public string Username = string.Empty;
|
||||
|
||||
[TomlInlineComment("$Proxy.Password$")]
|
||||
public string Password = "";
|
||||
public string Password = string.Empty;
|
||||
|
||||
public void OnSettingUpdate() { }
|
||||
public void OnSettingUpdate()
|
||||
{
|
||||
{ // Compatible with old settings.
|
||||
if (Enabled_Login.HasValue && Enabled_Login.Value)
|
||||
Login_Proxy = ProxyPreferenceType.custom;
|
||||
if (Enabled_Ingame.HasValue && Enabled_Ingame.Value)
|
||||
Ingame_Proxy = ProxyPreferenceType.custom;
|
||||
if (Enabled_Update.HasValue && Enabled_Update.Value)
|
||||
MCC_Update_Proxy = ProxyPreferenceType.custom;
|
||||
}
|
||||
}
|
||||
|
||||
public struct ProxyInfoConfig
|
||||
{
|
||||
|
|
@ -53,8 +69,12 @@ namespace MinecraftClient.Proxy
|
|||
}
|
||||
|
||||
public enum ProxyType { HTTP, SOCKS4, SOCKS4a, SOCKS5 };
|
||||
|
||||
public enum ProxyPreferenceType { custom, follow_system, disable };
|
||||
}
|
||||
|
||||
public enum ClientType { Ingame, Login, Update };
|
||||
|
||||
private static readonly ProxyClientFactory factory = new();
|
||||
private static IProxyClient? proxy;
|
||||
private static bool proxy_ok = false;
|
||||
|
|
@ -66,11 +86,14 @@ namespace MinecraftClient.Proxy
|
|||
/// <param name="port">Target port</param>
|
||||
/// <param name="login">True if the purpose is logging in to a Minecraft account</param>
|
||||
|
||||
public static TcpClient NewTcpClient(string host, int port, bool login = false)
|
||||
public static TcpClient NewTcpClient(string host, int port, ClientType clientType)
|
||||
{
|
||||
if (clientType == ClientType.Update)
|
||||
throw new NotSupportedException();
|
||||
try
|
||||
{
|
||||
if (login ? Config.Enabled_Login : Config.Enabled_Ingame)
|
||||
Configs.ProxyPreferenceType proxyPreference = clientType == ClientType.Ingame ? Config.Ingame_Proxy : Config.Login_Proxy;
|
||||
if (proxyPreference == Configs.ProxyPreferenceType.custom)
|
||||
{
|
||||
ProxyType innerProxytype = ProxyType.Http;
|
||||
|
||||
|
|
@ -95,7 +118,30 @@ namespace MinecraftClient.Proxy
|
|||
|
||||
return proxy.CreateConnection(host, port);
|
||||
}
|
||||
else return new TcpClient(host, port);
|
||||
else if (proxyPreference == Configs.ProxyPreferenceType.follow_system)
|
||||
{
|
||||
Uri? webProxy = WebRequest.GetSystemWebProxy().GetProxy(new("http://" + host));
|
||||
if (webProxy != null)
|
||||
{
|
||||
proxy = factory.CreateProxyClient(ProxyType.Http, webProxy.Host, webProxy.Port);
|
||||
|
||||
if (!proxy_ok)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.proxy_connected, webProxy.Host, webProxy.Port));
|
||||
proxy_ok = true;
|
||||
}
|
||||
|
||||
return proxy.CreateConnection(host, port);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new TcpClient(host, port);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return new TcpClient(host, port);
|
||||
}
|
||||
}
|
||||
catch (ProxyException e)
|
||||
{
|
||||
|
|
@ -104,5 +150,58 @@ namespace MinecraftClient.Proxy
|
|||
throw new SocketException((int)SocketError.HostUnreachable);
|
||||
}
|
||||
}
|
||||
|
||||
public static HttpClient NewHttpClient(ClientType clientType, HttpClientHandler? httpClientHandler = null)
|
||||
{
|
||||
if (clientType == ClientType.Ingame)
|
||||
throw new NotSupportedException();
|
||||
|
||||
httpClientHandler ??= new();
|
||||
AddProxySettings(clientType, ref httpClientHandler);
|
||||
return new HttpClient(httpClientHandler);
|
||||
}
|
||||
|
||||
public static void AddProxySettings(ClientType clientType, ref HttpClientHandler httpClientHandler)
|
||||
{
|
||||
if (clientType == ClientType.Ingame)
|
||||
throw new NotSupportedException();
|
||||
|
||||
Configs.ProxyPreferenceType proxyPreference = clientType == ClientType.Login ? Config.Login_Proxy : Config.MCC_Update_Proxy;
|
||||
|
||||
if (proxyPreference == Configs.ProxyPreferenceType.custom)
|
||||
{
|
||||
httpClientHandler ??= new();
|
||||
|
||||
string proxyAddress;
|
||||
if (!string.IsNullOrWhiteSpace(Settings.Config.Proxy.Username) && !string.IsNullOrWhiteSpace(Settings.Config.Proxy.Password))
|
||||
proxyAddress = string.Format("{0}://{3}:{4}@{1}:{2}",
|
||||
Settings.Config.Proxy.Proxy_Type.ToString().ToLower(),
|
||||
Settings.Config.Proxy.Server.Host,
|
||||
Settings.Config.Proxy.Server.Port,
|
||||
Settings.Config.Proxy.Username,
|
||||
Settings.Config.Proxy.Password);
|
||||
else
|
||||
proxyAddress = string.Format("{0}://{1}:{2}",
|
||||
Settings.Config.Proxy.Proxy_Type.ToString().ToLower(),
|
||||
Settings.Config.Proxy.Server.Host, Settings.Config.Proxy.Server.Port);
|
||||
|
||||
httpClientHandler.Proxy = new WebProxy(proxyAddress, true);
|
||||
httpClientHandler.UseProxy = true;
|
||||
}
|
||||
else if (proxyPreference == Configs.ProxyPreferenceType.follow_system)
|
||||
{
|
||||
httpClientHandler.Proxy = WebRequest.GetSystemWebProxy();
|
||||
httpClientHandler.UseProxy = true;
|
||||
}
|
||||
else if (proxyPreference == Configs.ProxyPreferenceType.disable)
|
||||
{
|
||||
httpClientHandler ??= new();
|
||||
httpClientHandler.UseProxy = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1699,7 +1699,8 @@ namespace MinecraftClient {
|
|||
/// Looks up a localized string similar to Connect to a server via a proxy instead of connecting directly
|
||||
///If Mojang session services are blocked on your network, set Enabled_Login=true to login using proxy.
|
||||
///If the connection to the Minecraft game server is blocked by the firewall, set Enabled_Ingame=true to use a proxy to connect to the game server.
|
||||
////!\ Make sure your server rules allow Proxies or VPNs before setting enabled=true, or you may face consequences!.
|
||||
////!\ Make sure your server rules allow Proxies or VPNs before setting enabled=true, or you may face consequences!
|
||||
///Use "custom", "follow_system" or "disable"..
|
||||
/// </summary>
|
||||
internal static string Proxy {
|
||||
get {
|
||||
|
|
@ -1710,27 +1711,27 @@ namespace MinecraftClient {
|
|||
/// <summary>
|
||||
/// Looks up a localized string similar to Whether to connect to the game server through a proxy..
|
||||
/// </summary>
|
||||
internal static string Proxy_Enabled_Ingame {
|
||||
internal static string Proxy_Ingame_Proxy {
|
||||
get {
|
||||
return ResourceManager.GetString("Proxy.Enabled_Ingame", resourceCulture);
|
||||
return ResourceManager.GetString("Proxy.Ingame_Proxy", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Whether to connect to the login server through a proxy..
|
||||
/// </summary>
|
||||
internal static string Proxy_Enabled_Login {
|
||||
internal static string Proxy_Login_Proxy {
|
||||
get {
|
||||
return ResourceManager.GetString("Proxy.Enabled_Login", resourceCulture);
|
||||
return ResourceManager.GetString("Proxy.Login_Proxy", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Whether to download MCC updates via proxy..
|
||||
/// </summary>
|
||||
internal static string Proxy_Enabled_Update {
|
||||
internal static string Proxy_MCC_Update_Proxy {
|
||||
get {
|
||||
return ResourceManager.GetString("Proxy.Enabled_Update", resourceCulture);
|
||||
return ResourceManager.GetString("Proxy.MCC_Update_Proxy", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -751,15 +751,16 @@ Usage examples: "/tell <mybot> connect Server1", "/connect Server2"</value
|
|||
<value>Connect to a server via a proxy instead of connecting directly
|
||||
If Mojang session services are blocked on your network, set Enabled_Login=true to login using proxy.
|
||||
If the connection to the Minecraft game server is blocked by the firewall, set Enabled_Ingame=true to use a proxy to connect to the game server.
|
||||
/!\ Make sure your server rules allow Proxies or VPNs before setting enabled=true, or you may face consequences!</value>
|
||||
/!\ Make sure your server rules allow Proxies or VPNs before setting enabled=true, or you may face consequences!
|
||||
Use "custom", "follow_system" or "disable".</value>
|
||||
</data>
|
||||
<data name="Proxy.Enabled_Ingame" xml:space="preserve">
|
||||
<data name="Proxy.Ingame_Proxy" xml:space="preserve">
|
||||
<value>Whether to connect to the game server through a proxy.</value>
|
||||
</data>
|
||||
<data name="Proxy.Enabled_Login" xml:space="preserve">
|
||||
<data name="Proxy.Login_Proxy" xml:space="preserve">
|
||||
<value>Whether to connect to the login server through a proxy.</value>
|
||||
</data>
|
||||
<data name="Proxy.Enabled_Update" xml:space="preserve">
|
||||
<data name="Proxy.MCC_Update_Proxy" xml:space="preserve">
|
||||
<value>Whether to download MCC updates via proxy.</value>
|
||||
</data>
|
||||
<data name="Proxy.Password" xml:space="preserve">
|
||||
|
|
|
|||
|
|
@ -2127,7 +2127,7 @@ namespace MinecraftClient {
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Loaded session: {0}:{1}.
|
||||
/// Looks up a localized string similar to Reads {0} session cache and {1} signature key caches from the disk cache..
|
||||
/// </summary>
|
||||
internal static string cache_loaded {
|
||||
get {
|
||||
|
|
@ -4130,6 +4130,24 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to [Settings] The language code is invalid!.
|
||||
/// </summary>
|
||||
internal static string config_invaild_language {
|
||||
get {
|
||||
return ResourceManager.GetString("config.invaild_language", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to [Settings] Only Microsoft accounts support logging in using the browser method..
|
||||
/// </summary>
|
||||
internal static string config_invaild_login_method {
|
||||
get {
|
||||
return ResourceManager.GetString("config.invaild_login_method", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Settings have been loaded from {0}.
|
||||
/// </summary>
|
||||
|
|
@ -4148,15 +4166,6 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The language code is invalid!.
|
||||
/// </summary>
|
||||
internal static string config_Main_Advanced_language_invaild {
|
||||
get {
|
||||
return ResourceManager.GetString("config.Main.Advanced.language.invaild", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The current setting is saved as {0}.
|
||||
/// </summary>
|
||||
|
|
@ -5295,7 +5304,16 @@ namespace MinecraftClient {
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Login :.
|
||||
/// Looks up a localized string similar to Exceptions occur when applying settings from command line parameters ({0}), and some entries may be ignored..
|
||||
/// </summary>
|
||||
internal static string mcc_load_from_args_fail {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.load_from_args_fail", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Login:.
|
||||
/// </summary>
|
||||
internal static string mcc_login {
|
||||
get {
|
||||
|
|
@ -5312,6 +5330,15 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Timed out when requesting {0}..
|
||||
/// </summary>
|
||||
internal static string mcc_network_timeout {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.network_timeout", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Failed to perform SRV lookup for {0}
|
||||
///{1}: {2}.
|
||||
|
|
@ -5531,7 +5558,7 @@ namespace MinecraftClient {
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Settings file MinecraftClient.ini has been generated..
|
||||
/// Looks up a localized string similar to The configuration file is saved as {0}.
|
||||
/// </summary>
|
||||
internal static string mcc_settings_generated {
|
||||
get {
|
||||
|
|
@ -5539,6 +5566,15 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Error: Unhandled exception:.
|
||||
/// </summary>
|
||||
internal static string mcc_unhandled_exception {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.unhandled_exception", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Unknown or not supported MC version {0}.
|
||||
///Switching to autodetection mode..
|
||||
|
|
|
|||
|
|
@ -822,7 +822,7 @@ Add the ID of this chat to "Authorized_Chat_Ids" field in the configuration file
|
|||
<value>Ignoring profile key token string '{0}': {1}</value>
|
||||
</data>
|
||||
<data name="cache.loaded" xml:space="preserve">
|
||||
<value>Loaded session: {0}:{1}</value>
|
||||
<value>Reads {0} session cache and {1} signature key caches from the disk cache.</value>
|
||||
</data>
|
||||
<data name="cache.loaded_keys" xml:space="preserve">
|
||||
<value>Loaded profile key, it will be refresh at {0}</value>
|
||||
|
|
@ -1499,8 +1499,8 @@ You can use "/chunk status {0:0.0} {1:0.0} {2:0.0}" to check the chunk loading s
|
|||
<data name="config.load.fail" xml:space="preserve">
|
||||
<value>Failed to load settings:</value>
|
||||
</data>
|
||||
<data name="config.Main.Advanced.language.invaild" xml:space="preserve">
|
||||
<value>The language code is invalid!</value>
|
||||
<data name="config.invaild_language" xml:space="preserve">
|
||||
<value>[Settings] The language code is invalid!</value>
|
||||
</data>
|
||||
<data name="config.saving" xml:space="preserve">
|
||||
<value>The current setting is saved as {0}</value>
|
||||
|
|
@ -1884,7 +1884,7 @@ Type '{0}quit' to leave the server.</value>
|
|||
<value>Link: {0}</value>
|
||||
</data>
|
||||
<data name="mcc.login" xml:space="preserve">
|
||||
<value>Login :</value>
|
||||
<value>Login:</value>
|
||||
</data>
|
||||
<data name="mcc.login_basic_io" xml:space="preserve">
|
||||
<value>Please type the username or email of your choice.</value>
|
||||
|
|
@ -1964,7 +1964,7 @@ MCC is running with default settings.</value>
|
|||
<value>Cached session is still valid for {0}.</value>
|
||||
</data>
|
||||
<data name="mcc.settings_generated" xml:space="preserve">
|
||||
<value>Settings file MinecraftClient.ini has been generated.</value>
|
||||
<value>The configuration file is saved as {0}</value>
|
||||
</data>
|
||||
<data name="mcc.unknown_version" xml:space="preserve">
|
||||
<value>Unknown or not supported MC version {0}.
|
||||
|
|
@ -2028,4 +2028,16 @@ Logging in...</value>
|
|||
<data name="proxy.connected" xml:space="preserve">
|
||||
<value>Connected to proxy {0}:{1}</value>
|
||||
</data>
|
||||
<data name="mcc.load_from_args_fail" xml:space="preserve">
|
||||
<value>Exceptions occur when applying settings from command line parameters ({0}), and some entries may be ignored.</value>
|
||||
</data>
|
||||
<data name="mcc.unhandled_exception" xml:space="preserve">
|
||||
<value>Error: Unhandled exception:</value>
|
||||
</data>
|
||||
<data name="mcc.network_timeout" xml:space="preserve">
|
||||
<value>Timed out when requesting {0}.</value>
|
||||
</data>
|
||||
<data name="config.invaild_login_method" xml:space="preserve">
|
||||
<value>[Settings] Only Microsoft accounts support logging in using the browser method.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
@ -4,6 +4,7 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Brigadier.NET;
|
||||
using MinecraftClient.CommandHandler;
|
||||
using MinecraftClient.Inventory;
|
||||
|
|
@ -36,14 +37,16 @@ namespace MinecraftClient.Scripting
|
|||
//Handler will be automatically set on bot loading, don't worry about this
|
||||
public void SetHandler(McClient handler) { _handler = handler; }
|
||||
protected void SetMaster(ChatBot master) { this.master = master; }
|
||||
protected void LoadBot(ChatBot bot) { Handler.BotUnLoad(bot); Handler.BotLoad(bot); }
|
||||
protected void LoadBot(ChatBot bot) { Handler.BotUnLoad(bot).Wait(); Handler.BotLoad(bot); }
|
||||
protected List<ChatBot> GetLoadedChatBots() { return Handler.GetLoadedChatBots(); }
|
||||
protected void UnLoadBot(ChatBot bot) { Handler.BotUnLoad(bot); }
|
||||
protected void UnLoadBot(ChatBot bot) { Handler.BotUnLoad(bot).Wait(); }
|
||||
private McClient? _handler = null;
|
||||
private ChatBot? master = null;
|
||||
private readonly List<string> registeredPluginChannels = new();
|
||||
|
||||
private readonly object delayTasksLock = new();
|
||||
private readonly List<TaskWithDelay> delayedTasks = new();
|
||||
|
||||
protected McClient Handler
|
||||
{
|
||||
get
|
||||
|
|
@ -117,10 +120,15 @@ namespace MinecraftClient.Scripting
|
|||
public virtual void AfterGameJoined() { }
|
||||
|
||||
/// <summary>
|
||||
/// Will be called every ~100ms (10fps) if loaded in MinecraftCom
|
||||
/// Will be called every ~100ms (10tps) if loaded in MinecraftCom
|
||||
/// </summary>
|
||||
public virtual void Update() { }
|
||||
|
||||
/// <summary>
|
||||
/// Will be called every ~50ms (20tps) if loaded in MinecraftCom
|
||||
/// </summary>
|
||||
public virtual async Task OnClientTickAsync() { await Task.CompletedTask; }
|
||||
|
||||
/// <summary>
|
||||
/// Will be called every player break block in gamemode 0
|
||||
/// </summary>
|
||||
|
|
@ -157,8 +165,8 @@ namespace MinecraftClient.Scripting
|
|||
/// </summary>
|
||||
/// <param name="reason">Disconnect Reason</param>
|
||||
/// <param name="message">Kick message, if any</param>
|
||||
/// <returns>Return TRUE if the client is about to restart</returns>
|
||||
public virtual bool OnDisconnect(DisconnectReason reason, string message) { return false; }
|
||||
/// <returns>A return value less than zero indicates no reconnection, otherwise it is the number of milliseconds to wait before reconnecting.</returns>
|
||||
public virtual int OnDisconnect(DisconnectReason reason, string message) { return -1; }
|
||||
|
||||
/// <summary>
|
||||
/// Called when a plugin channel message is received.
|
||||
|
|
@ -252,6 +260,14 @@ namespace MinecraftClient.Scripting
|
|||
/// <param name="gamemode">New Game Mode (0: Survival, 1: Creative, 2: Adventure, 3: Spectator).</param>
|
||||
public virtual void OnGamemodeUpdate(string playername, Guid uuid, int gamemode) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Game Mode has been updated for a player
|
||||
/// </summary>
|
||||
/// <param name="playername">Player Name</param>
|
||||
/// <param name="uuid">Player UUID</param>
|
||||
/// <param name="gamemode">New Game Mode (0: Survival, 1: Creative, 2: Adventure, 3: Spectator).</param>
|
||||
public virtual async Task OnGamemodeUpdateAsync(string playername, Guid uuid, int gamemode) { await Task.CompletedTask; }
|
||||
|
||||
/// <summary>
|
||||
/// Called when the Latency has been updated for a player
|
||||
/// </summary>
|
||||
|
|
@ -502,7 +518,7 @@ namespace MinecraftClient.Scripting
|
|||
protected bool SendText(string text, bool sendImmediately = false)
|
||||
{
|
||||
LogToConsole("Sending '" + text + "'");
|
||||
Handler.SendText(text);
|
||||
Handler.SendText(text).Wait();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -929,7 +945,7 @@ namespace MinecraftClient.Scripting
|
|||
ConsoleIO.WriteLogLine(string.Format(Translations.chatbot_reconnect, botName));
|
||||
}
|
||||
McClient.ReconnectionAttemptsLeft = ExtraAttempts;
|
||||
Program.Restart(delaySeconds, keepAccountAndServerSettings);
|
||||
Program.Restart(delaySeconds * 10, keepAccountAndServerSettings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -945,7 +961,7 @@ namespace MinecraftClient.Scripting
|
|||
/// </summary>
|
||||
protected void UnloadBot()
|
||||
{
|
||||
Handler.BotUnLoad(this);
|
||||
Handler.BotUnLoad(this).Wait();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1021,7 +1037,15 @@ namespace MinecraftClient.Scripting
|
|||
/// </summary>
|
||||
private bool SendEntityAction(Protocol.EntityActionType entityAction)
|
||||
{
|
||||
return Handler.SendEntityAction(entityAction);
|
||||
return Handler.SendEntityAction(entityAction).Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send Entity Action
|
||||
/// </summary>
|
||||
private async Task<bool> SendEntityActionAsync(Protocol.EntityActionType entityAction)
|
||||
{
|
||||
return await Handler.SendEntityAction(entityAction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1032,15 +1056,15 @@ namespace MinecraftClient.Scripting
|
|||
/// <param name="lookAtBlock">Also look at the block before digging</param>
|
||||
protected bool DigBlock(Location location, bool swingArms = true, bool lookAtBlock = true)
|
||||
{
|
||||
return Handler.DigBlock(location, swingArms, lookAtBlock);
|
||||
return Handler.DigBlock(location, swingArms, lookAtBlock).Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SetSlot
|
||||
/// </summary>
|
||||
protected void SetSlot(int slotNum)
|
||||
protected bool SetSlot(int slotNum)
|
||||
{
|
||||
Handler.ChangeSlot((short)slotNum);
|
||||
return Handler.ChangeSlot((short)slotNum).Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1273,7 +1297,7 @@ namespace MinecraftClient.Scripting
|
|||
protected void RegisterPluginChannel(string channel)
|
||||
{
|
||||
registeredPluginChannels.Add(channel);
|
||||
Handler.RegisterPluginChannel(channel, this);
|
||||
Handler.RegisterPluginChannel(channel, this).Wait();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1283,7 +1307,7 @@ namespace MinecraftClient.Scripting
|
|||
protected void UnregisterPluginChannel(string channel)
|
||||
{
|
||||
registeredPluginChannels.RemoveAll(chan => chan == channel);
|
||||
Handler.UnregisterPluginChannel(channel, this);
|
||||
Handler.UnregisterPluginChannel(channel, this).Wait();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1303,7 +1327,7 @@ namespace MinecraftClient.Scripting
|
|||
return false;
|
||||
}
|
||||
}
|
||||
return Handler.SendPluginChannelMessage(channel, data, sendEvenIfNotRegistered);
|
||||
return Handler.SendPluginChannelMessage(channel, data, sendEvenIfNotRegistered).Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1325,7 +1349,7 @@ namespace MinecraftClient.Scripting
|
|||
[Obsolete("Prefer using InteractType enum instead of int for interaction type")]
|
||||
protected bool InteractEntity(int EntityID, int type, Hand hand = Hand.MainHand)
|
||||
{
|
||||
return Handler.InteractEntity(EntityID, (InteractType)type, hand);
|
||||
return Handler.InteractEntity(EntityID, (InteractType)type, hand).Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1337,7 +1361,7 @@ namespace MinecraftClient.Scripting
|
|||
/// <returns>TRUE in case of success</returns>
|
||||
protected bool InteractEntity(int EntityID, InteractType type, Hand hand = Hand.MainHand)
|
||||
{
|
||||
return Handler.InteractEntity(EntityID, type, hand);
|
||||
return Handler.InteractEntity(EntityID, type, hand).Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1351,7 +1375,7 @@ namespace MinecraftClient.Scripting
|
|||
/// <returns>TRUE if item given successfully</returns>
|
||||
protected bool CreativeGive(int slot, ItemType itemType, int count, Dictionary<string, object>? nbt = null)
|
||||
{
|
||||
return Handler.DoCreativeGive(slot, itemType, count, nbt);
|
||||
return Handler.DoCreativeGive(slot, itemType, count, nbt).Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1373,7 +1397,7 @@ namespace MinecraftClient.Scripting
|
|||
/// <returns>TRUE if animation successfully done</returns>
|
||||
public bool SendAnimation(Hand hand = Hand.MainHand)
|
||||
{
|
||||
return Handler.DoAnimation((int)hand);
|
||||
return Handler.DoAnimation((int)hand).Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1382,7 +1406,7 @@ namespace MinecraftClient.Scripting
|
|||
/// <returns>TRUE if successful</returns>
|
||||
protected bool UseItemInHand()
|
||||
{
|
||||
return Handler.UseItemOnHand();
|
||||
return Handler.UseItemOnHand().Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1391,7 +1415,7 @@ namespace MinecraftClient.Scripting
|
|||
/// <returns>TRUE if successful</returns>
|
||||
protected bool UseItemInLeftHand()
|
||||
{
|
||||
return Handler.UseItemOnLeftHand();
|
||||
return Handler.UseItemOnLeftHand().Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1412,7 +1436,7 @@ namespace MinecraftClient.Scripting
|
|||
/// <returns>TRUE if successfully placed</returns>
|
||||
public bool SendPlaceBlock(Location location, Direction blockFace, Hand hand = Hand.MainHand)
|
||||
{
|
||||
return Handler.PlaceBlock(location, blockFace, hand);
|
||||
return Handler.PlaceBlock(location, blockFace, hand).Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1443,7 +1467,7 @@ namespace MinecraftClient.Scripting
|
|||
/// <returns>TRUE in case of success</returns>
|
||||
protected bool WindowAction(int inventoryId, int slot, WindowActionType actionType)
|
||||
{
|
||||
return Handler.DoWindowAction(inventoryId, slot, actionType);
|
||||
return Handler.DoWindowAction(inventoryId, slot, actionType).Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1463,7 +1487,7 @@ namespace MinecraftClient.Scripting
|
|||
/// <returns>True if success</returns>
|
||||
protected bool ChangeSlot(short slot)
|
||||
{
|
||||
return Handler.ChangeSlot(slot);
|
||||
return Handler.ChangeSlot(slot).Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1494,7 +1518,7 @@ namespace MinecraftClient.Scripting
|
|||
/// <param name="line4"> text1 four</param>
|
||||
protected bool UpdateSign(Location location, string line1, string line2, string line3, string line4)
|
||||
{
|
||||
return Handler.UpdateSign(location, line1, line2, line3, line4);
|
||||
return Handler.UpdateSign(location, line1, line2, line3, line4).Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1503,7 +1527,7 @@ namespace MinecraftClient.Scripting
|
|||
/// <param name="selectedSlot">Trade slot to select, starts at 0.</param>
|
||||
protected bool SelectTrade(int selectedSlot)
|
||||
{
|
||||
return Handler.SelectTrade(selectedSlot);
|
||||
return Handler.SelectTrade(selectedSlot).Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1512,7 +1536,7 @@ namespace MinecraftClient.Scripting
|
|||
/// <param name="entity">player to teleport to</param>
|
||||
protected bool SpectatorTeleport(Entity entity)
|
||||
{
|
||||
return Handler.Spectate(entity);
|
||||
return Handler.Spectate(entity).Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1521,7 +1545,7 @@ namespace MinecraftClient.Scripting
|
|||
/// <param name="uuid">uuid of entity to teleport to</param>
|
||||
protected bool SpectatorTeleport(Guid UUID)
|
||||
{
|
||||
return Handler.SpectateByUUID(UUID);
|
||||
return Handler.SpectateByUUID(UUID).Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1533,7 +1557,7 @@ namespace MinecraftClient.Scripting
|
|||
/// <param name="flags">command block flags</param>
|
||||
protected bool UpdateCommandBlock(Location location, string command, CommandBlockMode mode, CommandBlockFlags flags)
|
||||
{
|
||||
return Handler.UpdateCommandBlock(location, command, mode, flags);
|
||||
return Handler.UpdateCommandBlock(location, command, mode, flags).Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1543,7 +1567,7 @@ namespace MinecraftClient.Scripting
|
|||
/// <returns>True if success</returns>
|
||||
protected bool CloseInventory(int inventoryID)
|
||||
{
|
||||
return Handler.CloseInventory(inventoryID);
|
||||
return Handler.CloseInventory(inventoryID).Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1561,7 +1585,7 @@ namespace MinecraftClient.Scripting
|
|||
protected bool Respawn()
|
||||
{
|
||||
if (Handler.GetHealth() <= 0)
|
||||
return Handler.SendRespawnPacket();
|
||||
return Handler.SendRespawnPacket().Result;
|
||||
else return false;
|
||||
}
|
||||
|
||||
|
|
@ -1586,32 +1610,6 @@ namespace MinecraftClient.Scripting
|
|||
return Handler.GetProtocolVersion();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke a task on the main thread, wait for completion and retrieve return value.
|
||||
/// </summary>
|
||||
/// <param name="task">Task to run with any type or return value</param>
|
||||
/// <returns>Any result returned from task, result type is inferred from the task</returns>
|
||||
/// <example>bool result = InvokeOnMainThread(methodThatReturnsAbool);</example>
|
||||
/// <example>bool result = InvokeOnMainThread(() => methodThatReturnsAbool(argument));</example>
|
||||
/// <example>int result = InvokeOnMainThread(() => { yourCode(); return 42; });</example>
|
||||
/// <typeparam name="T">Type of the return value</typeparam>
|
||||
protected T InvokeOnMainThread<T>(Func<T> task)
|
||||
{
|
||||
return Handler.InvokeOnMainThread(task);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoke a task on the main thread and wait for completion
|
||||
/// </summary>
|
||||
/// <param name="task">Task to run without return value</param>
|
||||
/// <example>InvokeOnMainThread(methodThatReturnsNothing);</example>
|
||||
/// <example>InvokeOnMainThread(() => methodThatReturnsNothing(argument));</example>
|
||||
/// <example>InvokeOnMainThread(() => { yourCode(); });</example>
|
||||
protected void InvokeOnMainThread(Action task)
|
||||
{
|
||||
Handler.InvokeOnMainThread(task);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schedule a task to run on the main thread, and do not wait for completion
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -8,11 +8,13 @@ using System.Runtime.CompilerServices;
|
|||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MinecraftClient.Protocol;
|
||||
using MinecraftClient.Proxy;
|
||||
using Tomlet;
|
||||
using Tomlet.Attributes;
|
||||
using Tomlet.Models;
|
||||
using static MinecraftClient.Protocol.ProtocolHandler;
|
||||
using static MinecraftClient.Settings.AppVarConfigHelper;
|
||||
using static MinecraftClient.Settings.ChatBotConfigHealper;
|
||||
using static MinecraftClient.Settings.ChatFormatConfigHelper;
|
||||
|
|
@ -27,10 +29,9 @@ using static MinecraftClient.Settings.SignatureConfigHelper;
|
|||
|
||||
namespace MinecraftClient
|
||||
{
|
||||
public static class Settings
|
||||
public static partial class Settings
|
||||
{
|
||||
private const int CommentsAlignPosition = 45;
|
||||
private readonly static Regex CommentRegex = new(@"^(.*)\s?#\s\$([\w\.]+)\$\s*$$", RegexOptions.Compiled);
|
||||
|
||||
// Other Settings
|
||||
public const string TranslationsFile_Version = "1.19.3";
|
||||
|
|
@ -43,7 +44,7 @@ namespace MinecraftClient
|
|||
|
||||
public static class InternalConfig
|
||||
{
|
||||
public static string ServerIP = String.Empty;
|
||||
public static string ServerIP = string.Empty;
|
||||
|
||||
public static ushort ServerPort = 25565;
|
||||
|
||||
|
|
@ -180,7 +181,7 @@ namespace MinecraftClient
|
|||
return new(true, false);
|
||||
}
|
||||
|
||||
public static void WriteToFile(string filepath, bool backupOldFile)
|
||||
public static async Task WriteToFileAsync(string filepath, bool backupOldFile)
|
||||
{
|
||||
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
|
||||
string tomlString = TomletMain.TomlStringFrom(Config);
|
||||
|
|
@ -190,7 +191,7 @@ namespace MinecraftClient
|
|||
StringBuilder newConfig = new();
|
||||
foreach (string line in tomlList)
|
||||
{
|
||||
Match matchComment = CommentRegex.Match(line);
|
||||
Match matchComment = GetCommentRegex().Match(line);
|
||||
if (matchComment.Success && matchComment.Groups.Count == 3)
|
||||
{
|
||||
string config = matchComment.Groups[1].Value, comment = matchComment.Groups[2].Value;
|
||||
|
|
@ -215,7 +216,7 @@ namespace MinecraftClient
|
|||
try
|
||||
{
|
||||
if (new FileInfo(filepath).Length == newConfigByte.Length)
|
||||
if (File.ReadAllBytes(filepath).SequenceEqual(newConfigByte))
|
||||
if ((await File.ReadAllBytesAsync(filepath)).SequenceEqual(newConfigByte))
|
||||
needUpdate = false;
|
||||
}
|
||||
catch { }
|
||||
|
|
@ -238,7 +239,7 @@ namespace MinecraftClient
|
|||
|
||||
if (backupSuccessed)
|
||||
{
|
||||
try { File.WriteAllBytes(filepath, newConfigByte); }
|
||||
try { await File.WriteAllBytesAsync(filepath, newConfigByte); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§c" + string.Format(Translations.config_write_fail, filepath));
|
||||
|
|
@ -252,7 +253,7 @@ namespace MinecraftClient
|
|||
/// Load settings from the command line
|
||||
/// </summary>
|
||||
/// <param name="args">Command-line arguments</param>
|
||||
/// <exception cref="System.ArgumentException">Thrown on invalid arguments</exception>
|
||||
/// <exception cref="ArgumentException">Thrown on invalid arguments</exception>
|
||||
public static void LoadArguments(string[] args)
|
||||
{
|
||||
int positionalIndex = 0;
|
||||
|
|
@ -293,7 +294,7 @@ namespace MinecraftClient
|
|||
InternalConfig.Account.Password = argument;
|
||||
break;
|
||||
case 2:
|
||||
Config.Main.SetServerIP(new MainConfig.ServerInfoConfig(argument), true);
|
||||
Config.Main.SetServerIP(new ServerInfoConfig(argument), true);
|
||||
InternalConfig.KeepServerSettings = true;
|
||||
break;
|
||||
case 3:
|
||||
|
|
@ -326,12 +327,12 @@ namespace MinecraftClient
|
|||
}
|
||||
}
|
||||
|
||||
public static class MainConfigHealper
|
||||
public static partial class MainConfigHealper
|
||||
{
|
||||
public static MainConfig Config = new();
|
||||
|
||||
[TomlDoNotInlineObject]
|
||||
public class MainConfig
|
||||
public partial class MainConfig
|
||||
{
|
||||
public GeneralConfig General = new();
|
||||
|
||||
|
|
@ -385,8 +386,12 @@ namespace MinecraftClient
|
|||
//Server IP (IP or domain names contains at least a dot)
|
||||
if (sip.Length == 1 && !serverInfo.Port.HasValue && host.Contains('.') && host.Any(c => char.IsLetter(c)) &&
|
||||
Settings.Config.Main.Advanced.ResolveSrvRecords != ResolveSrvRecordType.no)
|
||||
{
|
||||
//Domain name without port may need Minecraft SRV Record lookup
|
||||
ProtocolHandler.MinecraftServiceLookup(ref host, ref port);
|
||||
var lookup = MinecraftServiceLookupAsync(host);
|
||||
if (lookup.Result.Item1)
|
||||
(host, port) = (lookup.Result.Item2, lookup.Result.Item3);
|
||||
}
|
||||
InternalConfig.ServerIP = host;
|
||||
InternalConfig.ServerPort = port;
|
||||
return true;
|
||||
|
|
@ -417,6 +422,12 @@ namespace MinecraftClient
|
|||
|
||||
General.Server.Host ??= string.Empty;
|
||||
|
||||
if (General.AccountType == GeneralConfig.LoginType.mojang && General.Method == GeneralConfig.LoginMethod.browser)
|
||||
{
|
||||
General.Method = GeneralConfig.LoginMethod.mcc;
|
||||
ConsoleIO.WriteLogLine(Translations.config_invaild_login_method);
|
||||
}
|
||||
|
||||
if (Advanced.MessageCooldown < 0)
|
||||
Advanced.MessageCooldown = 0;
|
||||
|
||||
|
|
@ -436,12 +447,12 @@ namespace MinecraftClient
|
|||
Thread.CurrentThread.CurrentUICulture = culture;
|
||||
}
|
||||
|
||||
Advanced.Language = Regex.Replace(Advanced.Language, @"[^-^_^\w^*\d]", string.Empty).Replace('-', '_');
|
||||
Advanced.Language = GetLanguageCodeRegex().Replace(Advanced.Language, string.Empty).Replace('-', '_');
|
||||
Advanced.Language = ToLowerIfNeed(Advanced.Language);
|
||||
if (!AvailableLang.Contains(Advanced.Language))
|
||||
{
|
||||
Advanced.Language = GetDefaultGameLanguage();
|
||||
ConsoleIO.WriteLogLine("[Settings] " + Translations.config_Main_Advanced_language_invaild);
|
||||
ConsoleIO.WriteLogLine(Translations.config_invaild_language);
|
||||
}
|
||||
|
||||
if (!InternalConfig.KeepServerSettings)
|
||||
|
|
@ -510,7 +521,7 @@ namespace MinecraftClient
|
|||
public InternalCmdCharType InternalCmdChar = InternalCmdCharType.slash;
|
||||
|
||||
[TomlInlineComment("$Main.Advanced.message_cooldown$")]
|
||||
public double MessageCooldown = 1.0;
|
||||
public double MessageCooldown = 0.4;
|
||||
|
||||
[TomlInlineComment("$Main.Advanced.bot_owners$")]
|
||||
public List<string> BotOwners = new() { "Player1", "Player2" };
|
||||
|
|
@ -519,7 +530,7 @@ namespace MinecraftClient
|
|||
public string MinecraftVersion = "auto";
|
||||
|
||||
[TomlInlineComment("$Main.Advanced.mc_forge$")]
|
||||
public ForgeConfigType EnableForge = ForgeConfigType.auto;
|
||||
public ForgeConfigType EnableForge = ForgeConfigType.no;
|
||||
|
||||
[TomlInlineComment("$Main.Advanced.brand_info$")]
|
||||
public BrandInfoType BrandInfo = BrandInfoType.mcc;
|
||||
|
|
@ -646,7 +657,7 @@ namespace MinecraftClient
|
|||
public AccountInfoConfig(string Login)
|
||||
{
|
||||
this.Login = Login;
|
||||
this.Password = "-";
|
||||
Password = "-";
|
||||
}
|
||||
|
||||
public AccountInfoConfig(string Login, string Password)
|
||||
|
|
@ -668,7 +679,7 @@ namespace MinecraftClient
|
|||
|
||||
if (sip.Length > 1)
|
||||
{
|
||||
try { this.Port = Convert.ToUInt16(sip[1]); }
|
||||
try { Port = Convert.ToUInt16(sip[1]); }
|
||||
catch (FormatException) { }
|
||||
}
|
||||
}
|
||||
|
|
@ -679,6 +690,9 @@ namespace MinecraftClient
|
|||
this.Port = Port;
|
||||
}
|
||||
}
|
||||
|
||||
[GeneratedRegex("[^-^_^\\w^*\\d]")]
|
||||
private static partial Regex GetLanguageCodeRegex();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -955,7 +969,7 @@ namespace MinecraftClient
|
|||
/// <returns>True if the parameters were valid</returns>
|
||||
public bool SetVar(string varName, object varData)
|
||||
{
|
||||
varName = Settings.ToLowerIfNeed(new string(varName.TakeWhile(char.IsLetterOrDigit).ToArray()));
|
||||
varName = ToLowerIfNeed(new string(varName.TakeWhile(char.IsLetterOrDigit).ToArray()));
|
||||
if (varName.Length > 0)
|
||||
{
|
||||
bool isString = varData.GetType() == typeof(string);
|
||||
|
|
@ -1063,7 +1077,7 @@ namespace MinecraftClient
|
|||
if (varname_ok)
|
||||
{
|
||||
string varname = var_name.ToString();
|
||||
string varname_lower = Settings.ToLowerIfNeed(varname);
|
||||
string varname_lower = ToLowerIfNeed(varname);
|
||||
i = i + varname.Length + 1;
|
||||
|
||||
switch (varname_lower)
|
||||
|
|
@ -1074,7 +1088,7 @@ namespace MinecraftClient
|
|||
case "serverport": result.Append(InternalConfig.ServerPort); break;
|
||||
case "datetime":
|
||||
DateTime time = DateTime.Now;
|
||||
result.Append(String.Format("{0}-{1}-{2} {3}:{4}:{5}",
|
||||
result.Append(string.Format("{0}-{1}-{2} {3}:{4}:{5}",
|
||||
time.Year.ToString("0000"),
|
||||
time.Month.ToString("00"),
|
||||
time.Day.ToString("00"),
|
||||
|
|
@ -1162,13 +1176,13 @@ namespace MinecraftClient
|
|||
public byte GetByte()
|
||||
{
|
||||
return (byte)(
|
||||
((Cape ? 1 : 0) << 0)
|
||||
| ((Jacket ? 1 : 0) << 1)
|
||||
| ((Sleeve_Left ? 1 : 0) << 2)
|
||||
| ((Sleeve_Right ? 1 : 0) << 3)
|
||||
| ((Pants_Left ? 1 : 0) << 4)
|
||||
| ((Pants_Right ? 1 : 0) << 5)
|
||||
| ((Hat ? 1 : 0) << 6)
|
||||
(Cape ? 1 : 0) << 0
|
||||
| (Jacket ? 1 : 0) << 1
|
||||
| (Sleeve_Left ? 1 : 0) << 2
|
||||
| (Sleeve_Right ? 1 : 0) << 3
|
||||
| (Pants_Left ? 1 : 0) << 4
|
||||
| (Pants_Right ? 1 : 0) << 5
|
||||
| (Hat ? 1 : 0) << 6
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1821,9 +1835,9 @@ namespace MinecraftClient
|
|||
const string lookupStringL = "---------------------------------!-#$%&-()*+,-./0123456789:;<=>?@abcdefghijklmnopqrstuvwxyz[-]^_`abcdefghijklmnopqrstuvwxyz{|}~-";
|
||||
|
||||
bool needLower = false;
|
||||
foreach (Char c in str)
|
||||
foreach (char c in str)
|
||||
{
|
||||
if (Char.IsUpper(c))
|
||||
if (char.IsUpper(c))
|
||||
{
|
||||
needLower = true;
|
||||
break;
|
||||
|
|
@ -1849,6 +1863,10 @@ namespace MinecraftClient
|
|||
time = Math.Min(int.MaxValue / 10, time);
|
||||
return (int)Math.Round(time * 10);
|
||||
}
|
||||
|
||||
|
||||
[GeneratedRegex("^(.*)\\s?#\\s\\$([\\w\\.]+)\\$\\s*$$", RegexOptions.Compiled)]
|
||||
private static partial Regex GetCommentRegex();
|
||||
}
|
||||
|
||||
public static class InternalCmdCharTypeExtensions
|
||||
|
|
|
|||
|
|
@ -8,12 +8,15 @@ using System.Runtime.InteropServices;
|
|||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MinecraftClient.Proxy;
|
||||
|
||||
namespace MinecraftClient
|
||||
{
|
||||
internal static class UpgradeHelper
|
||||
internal static partial class UpgradeHelper
|
||||
{
|
||||
private const string GithubReleaseUrl = "https://github.com/MCCTeam/Minecraft-Console-Client/releases";
|
||||
internal const string GithubReleaseUrl = "https://github.com/MCCTeam/Minecraft-Console-Client/releases";
|
||||
|
||||
// private static HttpClient? httpClient = null;
|
||||
|
||||
private static int running = 0; // Type: bool; 1 for running; 0 for stopped;
|
||||
private static CancellationTokenSource cancellationTokenSource = new();
|
||||
|
|
@ -23,29 +26,17 @@ namespace MinecraftClient
|
|||
private static DateTime downloadStartTime = DateTime.Now, lastNotifyTime = DateTime.Now;
|
||||
private static TimeSpan minNotifyInterval = TimeSpan.FromMilliseconds(3000);
|
||||
|
||||
public static void CheckUpdate(bool forceUpdate = false)
|
||||
public static async Task CheckUpdate(bool forceUpdate = false)
|
||||
{
|
||||
bool needPromptUpdate = true;
|
||||
if (!forceUpdate && CompareVersionInfo(Settings.Config.Head.CurrentVersion, Settings.Config.Head.LatestVersion))
|
||||
await DoCheckUpdate(CancellationToken.None);
|
||||
if (CompareVersionInfo(Settings.Config.Head.CurrentVersion, Settings.Config.Head.LatestVersion))
|
||||
{
|
||||
needPromptUpdate = false;
|
||||
ConsoleIO.WriteLineFormatted("§e" + string.Format(Translations.mcc_has_update, GithubReleaseUrl), true);
|
||||
}
|
||||
Task.Run(() =>
|
||||
else if (forceUpdate)
|
||||
{
|
||||
DoCheckUpdate(CancellationToken.None);
|
||||
if (needPromptUpdate)
|
||||
{
|
||||
if (CompareVersionInfo(Settings.Config.Head.CurrentVersion, Settings.Config.Head.LatestVersion))
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§e" + string.Format(Translations.mcc_has_update, GithubReleaseUrl), true);
|
||||
}
|
||||
else if (forceUpdate)
|
||||
{
|
||||
ConsoleIO.WriteLine(Translations.mcc_update_already_latest + ' ' + Translations.mcc_update_promote_force_cmd);
|
||||
}
|
||||
}
|
||||
});
|
||||
ConsoleIO.WriteLine(Translations.mcc_update_already_latest + ' ' + Translations.mcc_update_promote_force_cmd);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool DownloadLatestBuild(bool forceUpdate, bool isCommandLine = false)
|
||||
|
|
@ -71,7 +62,7 @@ namespace MinecraftClient
|
|||
}
|
||||
else
|
||||
{
|
||||
string latestVersion = DoCheckUpdate(cancellationToken);
|
||||
string latestVersion = await DoCheckUpdate(cancellationToken);
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
}
|
||||
|
|
@ -89,7 +80,7 @@ namespace MinecraftClient
|
|||
ConsoleIO.WriteLine(string.Format(Translations.mcc_update_exist_update, latestVersion, OSInfo));
|
||||
|
||||
HttpClientHandler httpClientHandler = new() { AllowAutoRedirect = true };
|
||||
AddProxySettings(httpClientHandler);
|
||||
ProxyHandler.AddProxySettings(ProxyHandler.ClientType.Update, ref httpClientHandler);
|
||||
|
||||
ProgressMessageHandler progressMessageHandler = new(httpClientHandler);
|
||||
progressMessageHandler.HttpReceiveProgress += (_, info) =>
|
||||
|
|
@ -183,43 +174,33 @@ namespace MinecraftClient
|
|||
Thread.Sleep(500);
|
||||
}
|
||||
|
||||
private static string DoCheckUpdate(CancellationToken cancellationToken)
|
||||
internal static async Task<string> DoCheckUpdate(CancellationToken cancellationToken)
|
||||
{
|
||||
string latestBuildInfo = string.Empty;
|
||||
|
||||
HttpClientHandler httpClientHandler = new() { AllowAutoRedirect = false };
|
||||
AddProxySettings(httpClientHandler);
|
||||
HttpClient httpClient = new(httpClientHandler);
|
||||
Task<HttpResponseMessage>? httpWebRequest = null;
|
||||
try
|
||||
ProxyHandler.AddProxySettings(ProxyHandler.ClientType.Update, ref httpClientHandler);
|
||||
using HttpClient httpClient = new(httpClientHandler);
|
||||
using HttpRequestMessage request = new(HttpMethod.Head, GithubReleaseUrl + "/latest");
|
||||
using HttpResponseMessage response = await httpClient.SendAsync(request, cancellationToken);
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
httpWebRequest = httpClient.GetAsync(GithubReleaseUrl + "/latest", HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
httpWebRequest.Wait();
|
||||
if (!cancellationToken.IsCancellationRequested)
|
||||
if (response.Headers.Location != null)
|
||||
{
|
||||
HttpResponseMessage res = httpWebRequest.Result;
|
||||
if (res.Headers.Location != null)
|
||||
Match match = GetReleaseUrlRegex().Match(response.Headers.Location.ToString());
|
||||
if (match.Success && match.Groups.Count == 5)
|
||||
{
|
||||
Match match = Regex.Match(res.Headers.Location.ToString(), GithubReleaseUrl + @"/tag/(\d{4})(\d{2})(\d{2})-(\d+)");
|
||||
if (match.Success && match.Groups.Count == 5)
|
||||
string year = match.Groups[1].Value, month = match.Groups[2].Value, day = match.Groups[3].Value, run = match.Groups[4].Value;
|
||||
string latestVersion = string.Format("GitHub build {0}, built on {1}-{2}-{3}", run, year, month, day);
|
||||
latestBuildInfo = string.Format("{0}{1}{2}-{3}", year, month, day, run);
|
||||
if (latestVersion != Settings.Config.Head.LatestVersion)
|
||||
{
|
||||
string year = match.Groups[1].Value, month = match.Groups[2].Value, day = match.Groups[3].Value, run = match.Groups[4].Value;
|
||||
string latestVersion = string.Format("GitHub build {0}, built on {1}-{2}-{3}", run, year, month, day);
|
||||
latestBuildInfo = string.Format("{0}{1}{2}-{3}", year, month, day, run);
|
||||
if (latestVersion != Settings.Config.Head.LatestVersion)
|
||||
{
|
||||
Settings.Config.Head.LatestVersion = latestVersion;
|
||||
Program.WriteBackSettings(false);
|
||||
}
|
||||
Settings.Config.Head.LatestVersion = latestVersion;
|
||||
_ = Program.WriteBackSettings(false);
|
||||
}
|
||||
}
|
||||
res.Dispose();
|
||||
}
|
||||
httpWebRequest.Dispose();
|
||||
}
|
||||
catch (Exception) { }
|
||||
finally { httpWebRequest?.Dispose(); }
|
||||
httpClient.Dispose();
|
||||
httpClientHandler.Dispose();
|
||||
return latestBuildInfo;
|
||||
}
|
||||
|
||||
|
|
@ -247,12 +228,12 @@ namespace MinecraftClient
|
|||
return string.Empty;
|
||||
}
|
||||
|
||||
private static bool CompareVersionInfo(string? current, string? latest)
|
||||
internal static bool CompareVersionInfo(string? current, string? latest)
|
||||
{
|
||||
if (current == null || latest == null)
|
||||
return false;
|
||||
Regex reg = new(@"\w+\sbuild\s(\d+),\sbuilt\son\s(\d{4})[-\/\.\s]?(\d{2})[-\/\.\s]?(\d{2}).*");
|
||||
Regex reg2 = new(@"\w+\sbuild\s(\d+),\sbuilt\son\s\w+\s(\d{2})[-\/\.\s]?(\d{2})[-\/\.\s]?(\d{4}).*");
|
||||
Regex reg = GetVersionRegex1();
|
||||
Regex reg2 = GetVersionRegex2();
|
||||
|
||||
DateTime? curTime = null, latestTime = null;
|
||||
|
||||
|
|
@ -302,24 +283,13 @@ namespace MinecraftClient
|
|||
return false;
|
||||
}
|
||||
|
||||
private static void AddProxySettings(HttpClientHandler httpClientHandler)
|
||||
{
|
||||
if (Settings.Config.Proxy.Enabled_Update)
|
||||
{
|
||||
string proxyAddress;
|
||||
if (!string.IsNullOrWhiteSpace(Settings.Config.Proxy.Username) && !string.IsNullOrWhiteSpace(Settings.Config.Proxy.Password))
|
||||
proxyAddress = string.Format("{0}://{3}:{4}@{1}:{2}",
|
||||
Settings.Config.Proxy.Proxy_Type.ToString().ToLower(),
|
||||
Settings.Config.Proxy.Server.Host,
|
||||
Settings.Config.Proxy.Server.Port,
|
||||
Settings.Config.Proxy.Username,
|
||||
Settings.Config.Proxy.Password);
|
||||
else
|
||||
proxyAddress = string.Format("{0}://{1}:{2}",
|
||||
Settings.Config.Proxy.Proxy_Type.ToString().ToLower(),
|
||||
Settings.Config.Proxy.Server.Host, Settings.Config.Proxy.Server.Port);
|
||||
httpClientHandler.Proxy = new WebProxy(proxyAddress, true);
|
||||
}
|
||||
}
|
||||
[GeneratedRegex("https://github.com/MCCTeam/Minecraft-Console-Client/releases/tag/(\\d{4})(\\d{2})(\\d{2})-(\\d+)")]
|
||||
private static partial Regex GetReleaseUrlRegex();
|
||||
|
||||
[GeneratedRegex("\\w+\\sbuild\\s(\\d+),\\sbuilt\\son\\s(\\d{4})[-\\/\\.\\s]?(\\d{2})[-\\/\\.\\s]?(\\d{2}).*")]
|
||||
private static partial Regex GetVersionRegex1();
|
||||
|
||||
[GeneratedRegex("\\w+\\sbuild\\s(\\d+),\\sbuilt\\son\\s\\w+\\s(\\d{2})[-\\/\\.\\s]?(\\d{2})[-\\/\\.\\s]?(\\d{4}).*")]
|
||||
private static partial Regex GetVersionRegex2();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ namespace MinecraftClient.WinAPI
|
|||
SETICON = 0x0080,
|
||||
}
|
||||
|
||||
private static void SetWindowIcon(System.Drawing.Icon icon)
|
||||
private static void SetWindowIcon(Icon icon)
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
|
|
@ -43,55 +43,40 @@ namespace MinecraftClient.WinAPI
|
|||
/// <summary>
|
||||
/// Asynchronously download the player's skin and set the head as console icon
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public static void SetPlayerIconAsync(string playerName)
|
||||
public static async Task SetPlayerIconAsync(HttpClient httpClient, string playerName)
|
||||
{
|
||||
Thread t = new(new ThreadStart(delegate
|
||||
if (!OperatingSystem.IsWindows())
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
HttpClient httpClient = new();
|
||||
using Stream imageStream = await httpClient.GetStreamAsync($"https://minotar.net/helm/{playerName}/100.png");
|
||||
try
|
||||
{
|
||||
Task<Stream> httpWebRequest = httpClient.GetStreamAsync("https://minotar.net/helm/" + playerName + "/100.png");
|
||||
httpWebRequest.Wait();
|
||||
Stream imageStream = httpWebRequest.Result;
|
||||
try
|
||||
{
|
||||
Bitmap skin = new(Image.FromStream(imageStream)); //Read skin from network
|
||||
SetWindowIcon(Icon.FromHandle(skin.GetHicon())); // Windows 10+ (New console)
|
||||
SetConsoleIcon(skin.GetHicon()); // Windows 8 and lower (Older console)
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
/* Invalid image in HTTP response */
|
||||
}
|
||||
imageStream.Dispose();
|
||||
httpWebRequest.Dispose();
|
||||
Bitmap skin = new(Image.FromStream(imageStream)); //Read skin from network
|
||||
SetWindowIcon(Icon.FromHandle(skin.GetHicon())); // Windows 10+ (New console)
|
||||
SetConsoleIcon(skin.GetHicon()); // Windows 8 and lower (Older console)
|
||||
}
|
||||
catch (AggregateException ae)
|
||||
catch (ArgumentException)
|
||||
{
|
||||
bool needRevert = false;
|
||||
foreach (var ex in ae.InnerExceptions)
|
||||
{
|
||||
if (ex is HttpRequestException || ex is TaskCanceledException) //Skin not found? Reset to default icon
|
||||
needRevert = true;
|
||||
}
|
||||
if (needRevert)
|
||||
RevertToMCCIcon();
|
||||
}
|
||||
catch (HttpRequestException) //Skin not found? Reset to default icon
|
||||
{
|
||||
RevertToMCCIcon();
|
||||
}
|
||||
finally
|
||||
{
|
||||
httpClient.Dispose();
|
||||
/* Invalid image in HTTP response */
|
||||
}
|
||||
}
|
||||
))
|
||||
catch (AggregateException ae)
|
||||
{
|
||||
Name = "Player skin icon setter"
|
||||
};
|
||||
t.Start();
|
||||
bool needRevert = false;
|
||||
foreach (var ex in ae.InnerExceptions)
|
||||
{
|
||||
if (ex is HttpRequestException || ex is TaskCanceledException) //Skin not found? Reset to default icon
|
||||
needRevert = true;
|
||||
}
|
||||
if (needRevert)
|
||||
RevertToMCCIcon();
|
||||
}
|
||||
catch (HttpRequestException) //Skin not found? Reset to default icon
|
||||
{
|
||||
RevertToMCCIcon();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue