mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Merge origin/master into feat/mcp-server
This commit is contained in:
commit
f4c160979c
86 changed files with 16765 additions and 2758 deletions
36
MinecraftClient/Achievement.cs
Normal file
36
MinecraftClient/Achievement.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace MinecraftClient
|
||||
{
|
||||
/// <summary>
|
||||
/// The type of an achievement or advancement.
|
||||
/// </summary>
|
||||
public enum AchievementType
|
||||
{
|
||||
Task,
|
||||
Challenge,
|
||||
Goal,
|
||||
Legacy
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a Minecraft achievement (pre-1.12) or advancement (1.12+).
|
||||
/// </summary>
|
||||
/// <param name="Id">Resource identifier, e.g. "minecraft:story/root" or "achievement.openInventory"</param>
|
||||
/// <param name="Title">Display title (null for legacy achievements without display info)</param>
|
||||
/// <param name="Description">Display description (null for legacy achievements without display info)</param>
|
||||
/// <param name="Type">The frame type / achievement category</param>
|
||||
/// <param name="IsHidden">Whether this advancement is hidden in the UI</param>
|
||||
/// <param name="IsCompleted">Whether all requirements have been met</param>
|
||||
/// <param name="Requirements">OR-groups of criterion names; all groups must be satisfied</param>
|
||||
/// <param name="CriteriaProgress">Per-criterion completion status</param>
|
||||
public record Achievement(
|
||||
string Id,
|
||||
string? Title,
|
||||
string? Description,
|
||||
AchievementType Type,
|
||||
bool IsHidden,
|
||||
bool IsCompleted,
|
||||
IReadOnlyList<IReadOnlyList<string>> Requirements,
|
||||
IReadOnlyDictionary<string, bool> CriteriaProgress);
|
||||
}
|
||||
|
|
@ -5,7 +5,9 @@ using System.Threading;
|
|||
using Brigadier.NET.Builder;
|
||||
using MinecraftClient.CommandHandler;
|
||||
using MinecraftClient.CommandHandler.Patch;
|
||||
using MinecraftClient.Inventory;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_20_6;
|
||||
using MinecraftClient.Scripting;
|
||||
using Tomlet.Attributes;
|
||||
|
||||
|
|
@ -25,15 +27,12 @@ namespace MinecraftClient.ChatBots
|
|||
|
||||
public bool Enabled = false;
|
||||
|
||||
[NonSerialized]
|
||||
[TomlInlineComment("$ChatBot.AutoDig.Auto_Tool_Switch$")]
|
||||
public bool Auto_Tool_Switch = false;
|
||||
|
||||
[NonSerialized]
|
||||
[TomlInlineComment("$ChatBot.AutoDig.Durability_Limit$")]
|
||||
public int Durability_Limit = 2;
|
||||
|
||||
[NonSerialized]
|
||||
[TomlInlineComment("$ChatBot.AutoDig.Drop_Low_Durability_Tools$")]
|
||||
public bool Drop_Low_Durability_Tools = false;
|
||||
|
||||
|
|
@ -65,6 +64,8 @@ namespace MinecraftClient.ChatBots
|
|||
|
||||
public void OnSettingUpdate()
|
||||
{
|
||||
Durability_Limit = Math.Max(0, Durability_Limit);
|
||||
|
||||
if (Auto_Start_Delay >= 0)
|
||||
Auto_Start_Delay = Math.Max(0.1, Auto_Start_Delay);
|
||||
|
||||
|
|
@ -225,6 +226,102 @@ namespace MinecraftClient.ChatBots
|
|||
}
|
||||
}
|
||||
|
||||
private static int GetLegacyMaxDamage(ItemType itemType)
|
||||
{
|
||||
return itemType switch
|
||||
{
|
||||
ItemType.WoodenPickaxe or ItemType.WoodenAxe or ItemType.WoodenShovel or ItemType.WoodenSword or ItemType.WoodenHoe => 59,
|
||||
ItemType.StonePickaxe or ItemType.StoneAxe or ItemType.StoneShovel or ItemType.StoneSword or ItemType.StoneHoe => 131,
|
||||
ItemType.IronPickaxe or ItemType.IronAxe or ItemType.IronShovel or ItemType.IronSword or ItemType.IronHoe => 250,
|
||||
ItemType.GoldenPickaxe or ItemType.GoldenAxe or ItemType.GoldenShovel or ItemType.GoldenSword or ItemType.GoldenHoe => 32,
|
||||
ItemType.DiamondPickaxe or ItemType.DiamondAxe or ItemType.DiamondShovel or ItemType.DiamondSword or ItemType.DiamondHoe => 1561,
|
||||
ItemType.NetheritePickaxe or ItemType.NetheriteAxe or ItemType.NetheriteShovel or ItemType.NetheriteSword or ItemType.NetheriteHoe => 2031,
|
||||
ItemType.Shears => 238,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
|
||||
private static int GetMaxDamage(Item item)
|
||||
{
|
||||
if (item.Components is not null)
|
||||
{
|
||||
var maxDamageComponent = item.Components.OfType<MaxDamageComponent>().FirstOrDefault();
|
||||
if (maxDamageComponent is not null)
|
||||
return maxDamageComponent.MaxDamage;
|
||||
}
|
||||
|
||||
return GetLegacyMaxDamage(item.Type);
|
||||
}
|
||||
|
||||
private static int GetRemainingDurability(Item item)
|
||||
{
|
||||
int maxDamage = GetMaxDamage(item);
|
||||
return maxDamage > 0 ? maxDamage - item.Damage : int.MaxValue;
|
||||
}
|
||||
|
||||
private bool HasEnoughDurability(Item item)
|
||||
{
|
||||
return Config.Durability_Limit <= 0 || GetRemainingDurability(item) >= Config.Durability_Limit;
|
||||
}
|
||||
|
||||
private bool IsBelowDurabilityLimit(Item? item)
|
||||
{
|
||||
return item is not null && Config.Durability_Limit > 0 && GetRemainingDurability(item) < Config.Durability_Limit;
|
||||
}
|
||||
|
||||
private static bool IsRecommendedTool(Item? item, ItemType[] recommendedTools)
|
||||
{
|
||||
return item is not null && recommendedTools.Contains(item.Type);
|
||||
}
|
||||
|
||||
private bool SwapToolIntoHand(int sourceSlot, int handSlot)
|
||||
{
|
||||
return WindowAction(0, sourceSlot, WindowActionType.LeftClick)
|
||||
&& WindowAction(0, handSlot, WindowActionType.LeftClick)
|
||||
&& WindowAction(0, sourceSlot, WindowActionType.LeftClick);
|
||||
}
|
||||
|
||||
private bool EnsureSuitableTool(Material blockType)
|
||||
{
|
||||
if (!inventoryEnabled || !Config.Auto_Tool_Switch)
|
||||
return true;
|
||||
|
||||
ItemType[] recommendedTools = Material2Tool.GetCorrectToolForBlock(blockType);
|
||||
if (recommendedTools.Length == 0)
|
||||
return true;
|
||||
|
||||
Container container = GetPlayerInventory();
|
||||
int handSlot = 36 + GetCurrentSlot();
|
||||
container.Items.TryGetValue(handSlot, out Item? currentTool);
|
||||
|
||||
if (currentTool is not null && IsRecommendedTool(currentTool, recommendedTools) && HasEnoughDurability(currentTool))
|
||||
return true;
|
||||
|
||||
foreach (ItemType recommendedTool in recommendedTools)
|
||||
{
|
||||
foreach ((int slot, Item item) in container.Items)
|
||||
{
|
||||
if (slot == handSlot || item.Type != recommendedTool || !HasEnoughDurability(item))
|
||||
continue;
|
||||
|
||||
if (!SwapToolIntoHand(slot, handSlot))
|
||||
return false;
|
||||
|
||||
LogToConsole(GetTimestamp() + ": " + string.Format(Translations.bot_autodig_switch, item.GetTypeString(), slot));
|
||||
|
||||
if (Config.Drop_Low_Durability_Tools && IsBelowDurabilityLimit(currentTool) &&
|
||||
WindowAction(0, slot, WindowActionType.DropItemStack))
|
||||
{
|
||||
LogToConsole(GetTimestamp() + ": " + string.Format(Translations.bot_autodig_drop_low_durability, currentTool!.GetTypeString(), slot));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return !IsBelowDurabilityLimit(currentTool);
|
||||
}
|
||||
|
||||
public override void Update()
|
||||
{
|
||||
lock (stateLock)
|
||||
|
|
@ -293,6 +390,9 @@ namespace MinecraftClient.ChatBots
|
|||
if (Config.Mode == Configs.ModeType.lookat ||
|
||||
(Config.Mode == Configs.ModeType.both && Config._Locations.Contains(blockLoc)))
|
||||
{
|
||||
if (!EnsureSuitableTool(block.Type))
|
||||
return false;
|
||||
|
||||
if (DigBlock(blockLoc, Direction.Down, lookAtBlock: false))
|
||||
{
|
||||
currentDig = blockLoc;
|
||||
|
|
@ -354,6 +454,9 @@ namespace MinecraftClient.ChatBots
|
|||
|
||||
if (minDistance <= 6.0)
|
||||
{
|
||||
if (!EnsureSuitableTool(targetBlock.Type))
|
||||
return false;
|
||||
|
||||
if (DigBlock(target, Direction.Down, lookAtBlock: true))
|
||||
{
|
||||
currentDig = target;
|
||||
|
|
@ -388,6 +491,9 @@ namespace MinecraftClient.ChatBots
|
|||
((Config.List_Type == Configs.ListType.whitelist && Config.Blocks.Contains(block.Type)) ||
|
||||
(Config.List_Type == Configs.ListType.blacklist && !Config.Blocks.Contains(block.Type))))
|
||||
{
|
||||
if (!EnsureSuitableTool(block.Type))
|
||||
return false;
|
||||
|
||||
if (DigBlock(blockLoc, Direction.Down, lookAtBlock: true))
|
||||
{
|
||||
currentDig = blockLoc;
|
||||
|
|
|
|||
|
|
@ -62,6 +62,21 @@ namespace MinecraftClient.ChatBots
|
|||
[TomlInlineComment("$ChatBot.AutoFishing.Hook_Threshold$")]
|
||||
public double Hook_Threshold = 0.2;
|
||||
|
||||
[TomlInlineComment("$ChatBot.AutoFishing.Enable_Velocity_Detection$")]
|
||||
public bool Enable_Velocity_Detection = true;
|
||||
|
||||
[TomlInlineComment("$ChatBot.AutoFishing.Velocity_Hook_Threshold$")]
|
||||
public double Velocity_Hook_Threshold = -0.2;
|
||||
|
||||
[TomlInlineComment("$ChatBot.AutoFishing.Enable_Sound_Detection$")]
|
||||
public bool Enable_Sound_Detection = true;
|
||||
|
||||
[TomlInlineComment("$ChatBot.AutoFishing.Sound_Distance$")]
|
||||
public double Sound_Distance = 5.0;
|
||||
|
||||
[TomlInlineComment("$ChatBot.AutoFishing.Detection_Warmup$")]
|
||||
public double Detection_Warmup = 1.0;
|
||||
|
||||
[TomlInlineComment("$ChatBot.AutoFishing.Log_Fish_Bobber$")]
|
||||
public bool Log_Fish_Bobber = false;
|
||||
|
||||
|
|
@ -97,6 +112,15 @@ namespace MinecraftClient.ChatBots
|
|||
|
||||
if (Hook_Threshold < 0)
|
||||
Hook_Threshold = -Hook_Threshold;
|
||||
|
||||
if (Velocity_Hook_Threshold > 0)
|
||||
Velocity_Hook_Threshold = -Velocity_Hook_Threshold;
|
||||
|
||||
if (Sound_Distance < 0)
|
||||
Sound_Distance = -Sound_Distance;
|
||||
|
||||
if (Detection_Warmup < 0)
|
||||
Detection_Warmup = 0;
|
||||
}
|
||||
|
||||
public struct LocationConfig
|
||||
|
|
@ -171,6 +195,7 @@ namespace MinecraftClient.ChatBots
|
|||
private Entity? fishingBobber;
|
||||
private Location LastPos = Location.Zero;
|
||||
private DateTime CaughtTime = DateTime.Now;
|
||||
private DateTime BobberSpawnTime = DateTime.MinValue;
|
||||
private int fishItemCounter = 15;
|
||||
private Dictionary<ItemType, uint> fishItemCnt = new();
|
||||
private Entity fishItem = new(-1, EntityType.Item, Location.Zero);
|
||||
|
|
@ -464,6 +489,7 @@ namespace MinecraftClient.ChatBots
|
|||
fishingBobber = entity;
|
||||
LastPos = entity.Location;
|
||||
isFishing = true;
|
||||
BobberSpawnTime = DateTime.Now;
|
||||
|
||||
castTimeout = 24;
|
||||
counter = 0;
|
||||
|
|
@ -500,7 +526,7 @@ namespace MinecraftClient.ChatBots
|
|||
public override void OnEntityMove(Entity entity)
|
||||
{
|
||||
if (isFishing && entity is not null && fishingBobber!.ID == entity.ID &&
|
||||
(state == FishingState.WaitingFishToBite || state == FishingState.WaitingFishingBobber))
|
||||
state == FishingState.WaitingFishToBite)
|
||||
{
|
||||
Location Pos = entity.Location;
|
||||
double Dx = LastPos.X - Pos.X;
|
||||
|
|
@ -515,13 +541,7 @@ namespace MinecraftClient.ChatBots
|
|||
Math.Abs(Dz) < Math.Abs(Config.Stationary_Threshold) &&
|
||||
Math.Abs(Dy) > Math.Abs(Config.Hook_Threshold))
|
||||
{
|
||||
// prevent triggering multiple time
|
||||
if ((DateTime.Now - CaughtTime).TotalSeconds > 1)
|
||||
{
|
||||
isFishing = false;
|
||||
CaughtTime = DateTime.Now;
|
||||
OnCaughtFish();
|
||||
}
|
||||
TryCatchFish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -540,6 +560,38 @@ namespace MinecraftClient.ChatBots
|
|||
}
|
||||
}
|
||||
|
||||
public override void OnEntityVelocity(Entity entity, double velocityX, double velocityY, double velocityZ)
|
||||
{
|
||||
if (!Config.Enable_Velocity_Detection || !CanUseAdvancedDetection())
|
||||
return;
|
||||
|
||||
if (fishingBobber is null || entity.ID != fishingBobber.ID)
|
||||
return;
|
||||
|
||||
if (velocityY <= Config.Velocity_Hook_Threshold)
|
||||
TryCatchFish();
|
||||
}
|
||||
|
||||
public override void OnSoundEffect(string? soundName, Location? location, int category, float volume, float pitch,
|
||||
Entity? sourceEntity)
|
||||
{
|
||||
if (!Config.Enable_Sound_Detection || !CanUseAdvancedDetection())
|
||||
return;
|
||||
|
||||
if (!IsFishingBobberSplashSound(soundName))
|
||||
return;
|
||||
|
||||
Location? soundLocation = location;
|
||||
if (soundLocation is null && sourceEntity is not null)
|
||||
soundLocation = sourceEntity.Location;
|
||||
|
||||
if (soundLocation is null || fishingBobber is null)
|
||||
return;
|
||||
|
||||
if (soundLocation.Value.Distance(fishingBobber.Location) <= Config.Sound_Distance)
|
||||
TryCatchFish();
|
||||
}
|
||||
|
||||
public override void AfterGameJoined()
|
||||
{
|
||||
StartFishing();
|
||||
|
|
@ -562,10 +614,42 @@ namespace MinecraftClient.ChatBots
|
|||
fishingBobber = null;
|
||||
LastPos = Location.Zero;
|
||||
CaughtTime = DateTime.Now;
|
||||
BobberSpawnTime = DateTime.MinValue;
|
||||
|
||||
return base.OnDisconnect(reason, message);
|
||||
}
|
||||
|
||||
private bool CanUseAdvancedDetection()
|
||||
{
|
||||
if (!isFishing || fishingBobber is null || state != FishingState.WaitingFishToBite)
|
||||
return false;
|
||||
|
||||
return (DateTime.Now - BobberSpawnTime).TotalSeconds >= Config.Detection_Warmup;
|
||||
}
|
||||
|
||||
private void TryCatchFish()
|
||||
{
|
||||
if (!CanUseAdvancedDetection())
|
||||
return;
|
||||
|
||||
// Prevent repeated catches from multiple packets of the same bite.
|
||||
if ((DateTime.Now - CaughtTime).TotalSeconds <= 1)
|
||||
return;
|
||||
|
||||
isFishing = false;
|
||||
CaughtTime = DateTime.Now;
|
||||
OnCaughtFish();
|
||||
}
|
||||
|
||||
private static bool IsFishingBobberSplashSound(string? soundName)
|
||||
{
|
||||
return string.Equals(soundName, "minecraft:entity.fishing_bobber.splash",
|
||||
StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(soundName, "entity.fishing_bobber.splash", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(soundName, "minecraft:entity.bobber.splash", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(soundName, "entity.bobber.splash", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when detected a fish is caught
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using MinecraftClient.Scripting;
|
||||
using Tomlet.Attributes;
|
||||
|
||||
|
|
@ -95,6 +95,11 @@ namespace MinecraftClient.ChatBots
|
|||
_Initialize();
|
||||
}
|
||||
|
||||
public override void AfterGameJoined()
|
||||
{
|
||||
Configs._BotRecoAttempts = 0;
|
||||
}
|
||||
|
||||
private void _Initialize()
|
||||
{
|
||||
McClient.ReconnectionAttemptsLeft = Config.Retries;
|
||||
|
|
@ -144,10 +149,17 @@ namespace MinecraftClient.ChatBots
|
|||
{
|
||||
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));
|
||||
|
||||
// TODO: Change this translation string to add the retries left text
|
||||
LogToConsole(string.Format(Translations.bot_autoRelog_wait, delay) + $" ({Config.Retries - Configs._BotRecoAttempts} retries left)");
|
||||
ReconnectToTheServer(Config.Retries - Configs._BotRecoAttempts, (int)Math.Floor(delay), true);
|
||||
|
||||
int retriesLeft = Config.Retries - Configs._BotRecoAttempts;
|
||||
if (retriesLeft < 0)
|
||||
retriesLeft = 0;
|
||||
|
||||
string retriesDisplay = Config.Retries == int.MaxValue
|
||||
? Translations.bot_autoRelog_retries_unlimited
|
||||
: retriesLeft.ToString();
|
||||
|
||||
LogToConsole(string.Format(Translations.bot_autoRelog_wait_with_retries, delay, retriesDisplay));
|
||||
ReconnectToTheServer(retriesLeft, (int)Math.Floor(delay), true);
|
||||
}
|
||||
|
||||
public static bool OnDisconnectStatic(DisconnectReason reason, string message)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Brigadier.NET.Builder;
|
||||
using DSharpPlus;
|
||||
|
|
@ -34,6 +37,9 @@ namespace MinecraftClient.ChatBots
|
|||
private DiscordChannel? discordChannel;
|
||||
private BridgeDirection bridgeDirection = BridgeDirection.Both;
|
||||
|
||||
private readonly ConcurrentQueue<string> aggregationBuffer = new();
|
||||
private Timer? aggregationTimer;
|
||||
|
||||
public static Configs Config = new();
|
||||
|
||||
[TomlDoNotInlineObject]
|
||||
|
|
@ -62,6 +68,12 @@ namespace MinecraftClient.ChatBots
|
|||
[TomlInlineComment("$ChatBot.DiscordBridge.AllowOtherBotMessages$")]
|
||||
public bool Allow_Other_Bot_Messages = false;
|
||||
|
||||
[TomlInlineComment("$ChatBot.DiscordBridge.RelayAllMessages$")]
|
||||
public bool Relay_All_Messages = false;
|
||||
|
||||
[TomlInlineComment("$ChatBot.DiscordBridge.MessageAggregationInterval$")]
|
||||
public double Message_Aggregation_Interval = 3.0;
|
||||
|
||||
[TomlPrecedingComment("$ChatBot.DiscordBridge.Formats$")]
|
||||
public string PrivateMessageFormat = "**[Private Message]** {username}: {message}";
|
||||
public string PublicMessageFormat = "{username}: {message}";
|
||||
|
|
@ -70,6 +82,8 @@ namespace MinecraftClient.ChatBots
|
|||
public void OnSettingUpdate()
|
||||
{
|
||||
Message_Send_Timeout = Message_Send_Timeout <= 0 ? 3 : Message_Send_Timeout;
|
||||
if (Message_Aggregation_Interval < 0)
|
||||
Message_Aggregation_Interval = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -100,6 +114,12 @@ namespace MinecraftClient.ChatBots
|
|||
.Redirect(McClient.dispatcher.GetRoot().GetChild("help").GetChild(CommandName)))
|
||||
);
|
||||
|
||||
if (Config.Message_Aggregation_Interval > 0)
|
||||
{
|
||||
var intervalMs = (int)(Config.Message_Aggregation_Interval * 1000);
|
||||
aggregationTimer = new Timer(_ => FlushAggregationBuffer(), null, intervalMs, intervalMs);
|
||||
}
|
||||
|
||||
Task.Run(async () => await MainAsync());
|
||||
}
|
||||
|
||||
|
|
@ -107,6 +127,7 @@ namespace MinecraftClient.ChatBots
|
|||
{
|
||||
McClient.dispatcher.Unregister(CommandName);
|
||||
McClient.dispatcher.GetRoot().GetChild("help").RemoveChild(CommandName);
|
||||
StopAggregation();
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
|
|
@ -147,6 +168,40 @@ namespace MinecraftClient.ChatBots
|
|||
return r.SetAndReturn(CmdResult.Status.Done, string.Format(Translations.bot_DiscordBridge_direction, bridgeName));
|
||||
}
|
||||
|
||||
private void FlushAggregationBuffer()
|
||||
{
|
||||
if (aggregationBuffer.IsEmpty || !CanSendMessages())
|
||||
return;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
while (aggregationBuffer.TryDequeue(out var line))
|
||||
{
|
||||
if (sb.Length + line.Length + 1 > 1900)
|
||||
{
|
||||
SendMessage(sb.ToString());
|
||||
sb.Clear();
|
||||
}
|
||||
|
||||
if (sb.Length > 0)
|
||||
sb.AppendLine();
|
||||
sb.Append(line);
|
||||
}
|
||||
|
||||
if (sb.Length > 0)
|
||||
SendMessage(sb.ToString());
|
||||
}
|
||||
|
||||
private void StopAggregation()
|
||||
{
|
||||
if (aggregationTimer is not null)
|
||||
{
|
||||
aggregationTimer.Dispose();
|
||||
aggregationTimer = null;
|
||||
}
|
||||
|
||||
FlushAggregationBuffer();
|
||||
}
|
||||
|
||||
~DiscordBridge()
|
||||
{
|
||||
Disconnect();
|
||||
|
|
@ -188,7 +243,6 @@ namespace MinecraftClient.ChatBots
|
|||
|
||||
text = GetVerbatim(text).Trim();
|
||||
|
||||
// Stop the crash when an empty text is recived somehow
|
||||
if (string.IsNullOrEmpty(text))
|
||||
return;
|
||||
|
||||
|
|
@ -205,7 +259,10 @@ namespace MinecraftClient.ChatBots
|
|||
message = Config.TeleportRequestMessageFormat.Replace("{username}", username).Replace("{timestamp}", GetTimestamp()).Trim();
|
||||
teleportRequest = true;
|
||||
}
|
||||
else message = text;
|
||||
else if (Config.Relay_All_Messages)
|
||||
message = text;
|
||||
else
|
||||
return;
|
||||
|
||||
if (teleportRequest)
|
||||
{
|
||||
|
|
@ -223,7 +280,13 @@ namespace MinecraftClient.ChatBots
|
|||
SendMessage(messageBuilder);
|
||||
return;
|
||||
}
|
||||
else SendMessage(GetDiscordText(message));
|
||||
|
||||
string discordText = GetDiscordText(message);
|
||||
|
||||
if (Config.Message_Aggregation_Interval > 0)
|
||||
aggregationBuffer.Enqueue(discordText);
|
||||
else
|
||||
SendMessage(discordText);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
106
MinecraftClient/Commands/AchievementCommand.cs
Normal file
106
MinecraftClient/Commands/AchievementCommand.cs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
using System.Linq;
|
||||
using System.Text;
|
||||
using Brigadier.NET;
|
||||
using Brigadier.NET.Builder;
|
||||
using MinecraftClient.CommandHandler;
|
||||
|
||||
namespace MinecraftClient.Commands
|
||||
{
|
||||
public class AchievementCommand : Command
|
||||
{
|
||||
public override string CmdName => "achievement";
|
||||
public override string CmdUsage => "achievement <list|locked|unlocked>";
|
||||
public override string CmdDesc => Translations.cmd_achievement_desc;
|
||||
|
||||
public override void RegisterCommand(CommandDispatcher<CmdResult> dispatcher)
|
||||
{
|
||||
dispatcher.Register(l => l.Literal("help")
|
||||
.Then(l => l.Literal(CmdName)
|
||||
.Executes(r => GetUsage(r.Source, string.Empty))
|
||||
.Then(l => l.Literal("list")
|
||||
.Executes(r => GetUsage(r.Source, "list")))
|
||||
.Then(l => l.Literal("locked")
|
||||
.Executes(r => GetUsage(r.Source, "locked")))
|
||||
.Then(l => l.Literal("unlocked")
|
||||
.Executes(r => GetUsage(r.Source, "unlocked")))
|
||||
)
|
||||
);
|
||||
|
||||
dispatcher.Register(l => l.Literal(CmdName)
|
||||
.Executes(r => ListAchievements(r.Source, null))
|
||||
.Then(l => l.Literal("list")
|
||||
.Executes(r => ListAchievements(r.Source, null)))
|
||||
.Then(l => l.Literal("locked")
|
||||
.Executes(r => ListAchievements(r.Source, false)))
|
||||
.Then(l => l.Literal("unlocked")
|
||||
.Executes(r => ListAchievements(r.Source, true)))
|
||||
.Then(l => l.Literal("_help")
|
||||
.Executes(r => GetUsage(r.Source, string.Empty))
|
||||
.Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName)))
|
||||
);
|
||||
}
|
||||
|
||||
private int GetUsage(CmdResult r, string? cmd)
|
||||
{
|
||||
return r.SetAndReturn(cmd switch
|
||||
{
|
||||
#pragma warning disable format
|
||||
"list" => GetCmdDescTranslated(),
|
||||
"locked" => GetCmdDescTranslated(),
|
||||
"unlocked" => GetCmdDescTranslated(),
|
||||
_ => GetCmdDescTranslated(),
|
||||
#pragma warning restore format
|
||||
});
|
||||
}
|
||||
|
||||
/// <param name="completed">null = all, true = unlocked only, false = locked only</param>
|
||||
private static int ListAchievements(CmdResult r, bool? completed)
|
||||
{
|
||||
McClient handler = CmdResult.currentHandler!;
|
||||
|
||||
Achievement[] items = completed switch
|
||||
{
|
||||
true => handler.GetUnlockedAchievements(),
|
||||
false => handler.GetLockedAchievements(),
|
||||
null => handler.GetAchievements()
|
||||
};
|
||||
|
||||
if (items.Length == 0)
|
||||
{
|
||||
string msg = completed switch
|
||||
{
|
||||
true => Translations.cmd_achievement_none_unlocked,
|
||||
false => Translations.cmd_achievement_none_locked,
|
||||
_ => Translations.cmd_achievement_none
|
||||
};
|
||||
return r.SetAndReturn(CmdResult.Status.Done, msg);
|
||||
}
|
||||
|
||||
string header = completed switch
|
||||
{
|
||||
true => Translations.cmd_achievement_header_unlocked,
|
||||
false => Translations.cmd_achievement_header_locked,
|
||||
_ => Translations.cmd_achievement_header
|
||||
};
|
||||
|
||||
StringBuilder sb = new();
|
||||
sb.AppendLine(header);
|
||||
|
||||
foreach (Achievement a in items.OrderBy(static a => a.Id))
|
||||
{
|
||||
string status = a.IsCompleted
|
||||
? Translations.cmd_achievement_done
|
||||
: Translations.cmd_achievement_todo;
|
||||
|
||||
string display = a.Title is not null
|
||||
? string.Format(Translations.cmd_achievement_entry_titled, status, a.Title, a.Id, a.Type)
|
||||
: string.Format(Translations.cmd_achievement_entry, status, a.Id, a.Type);
|
||||
|
||||
sb.AppendLine(display);
|
||||
}
|
||||
|
||||
handler.Log.Info(sb.ToString().TrimEnd());
|
||||
return r.SetAndReturn(CmdResult.Status.Done);
|
||||
}
|
||||
}
|
||||
}
|
||||
284
MinecraftClient/Commands/Minimap.cs
Normal file
284
MinecraftClient/Commands/Minimap.cs
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
using System;
|
||||
using Brigadier.NET;
|
||||
using Brigadier.NET.Builder;
|
||||
using MinecraftClient.CommandHandler;
|
||||
using MinecraftClient.Tui;
|
||||
using Avalonia.Threading;
|
||||
using static MinecraftClient.CommandHandler.CmdResult;
|
||||
|
||||
namespace MinecraftClient.Commands
|
||||
{
|
||||
class Minimap : Command
|
||||
{
|
||||
public override string CmdName => "minimap";
|
||||
public override string CmdUsage => "minimap [on|off] | minimap zoom [in|out|<1-16>] | minimap names [players|hostile|neutral|passive] [on|off] | minimap names [all_on|all_off] | minimap position [top_left|top_right|center|bottom_left|bottom_right] | minimap cave [auto|on|off]";
|
||||
public override string CmdDesc => Translations.cmd_minimap_desc;
|
||||
|
||||
public override void RegisterCommand(CommandDispatcher<CmdResult> dispatcher)
|
||||
{
|
||||
dispatcher.Register(l => l.Literal("help")
|
||||
.Then(l => l.Literal(CmdName)
|
||||
.Executes(r => GetUsage(r.Source, string.Empty))
|
||||
)
|
||||
);
|
||||
|
||||
dispatcher.Register(l => l.Literal(CmdName)
|
||||
.Executes(r => DoToggle(r.Source))
|
||||
.Then(l => l.Literal("on")
|
||||
.Executes(r => DoOn(r.Source)))
|
||||
.Then(l => l.Literal("off")
|
||||
.Executes(r => DoOff(r.Source)))
|
||||
.Then(l => l.Literal("zoom")
|
||||
.Executes(r => DoZoomInfo(r.Source))
|
||||
.Then(l => l.Literal("in")
|
||||
.Executes(r => DoZoomIn(r.Source)))
|
||||
.Then(l => l.Literal("out")
|
||||
.Executes(r => DoZoomOut(r.Source)))
|
||||
.Then(l => l.Argument("level", Arguments.Integer(MinimapControl.MinZoom, MinimapControl.MaxZoom))
|
||||
.Executes(r => DoZoomSet(r.Source, Arguments.GetInteger(r, "level")))))
|
||||
.Then(l => l.Literal("names")
|
||||
.Executes(r => DoNamesInfo(r.Source))
|
||||
.Then(l => l.Literal("all_on")
|
||||
.Executes(r => DoNamesAll(r.Source, true)))
|
||||
.Then(l => l.Literal("all_off")
|
||||
.Executes(r => DoNamesAll(r.Source, false)))
|
||||
.Then(l => l.Literal("players")
|
||||
.Executes(r => DoNamesCatInfo(r.Source, MobCategory.Player))
|
||||
.Then(l => l.Literal("on")
|
||||
.Executes(r => DoNamesCatSet(r.Source, MobCategory.Player, true)))
|
||||
.Then(l => l.Literal("off")
|
||||
.Executes(r => DoNamesCatSet(r.Source, MobCategory.Player, false))))
|
||||
.Then(l => l.Literal("hostile")
|
||||
.Executes(r => DoNamesCatInfo(r.Source, MobCategory.Hostile))
|
||||
.Then(l => l.Literal("on")
|
||||
.Executes(r => DoNamesCatSet(r.Source, MobCategory.Hostile, true)))
|
||||
.Then(l => l.Literal("off")
|
||||
.Executes(r => DoNamesCatSet(r.Source, MobCategory.Hostile, false))))
|
||||
.Then(l => l.Literal("neutral")
|
||||
.Executes(r => DoNamesCatInfo(r.Source, MobCategory.Neutral))
|
||||
.Then(l => l.Literal("on")
|
||||
.Executes(r => DoNamesCatSet(r.Source, MobCategory.Neutral, true)))
|
||||
.Then(l => l.Literal("off")
|
||||
.Executes(r => DoNamesCatSet(r.Source, MobCategory.Neutral, false))))
|
||||
.Then(l => l.Literal("passive")
|
||||
.Executes(r => DoNamesCatInfo(r.Source, MobCategory.Passive))
|
||||
.Then(l => l.Literal("on")
|
||||
.Executes(r => DoNamesCatSet(r.Source, MobCategory.Passive, true)))
|
||||
.Then(l => l.Literal("off")
|
||||
.Executes(r => DoNamesCatSet(r.Source, MobCategory.Passive, false)))))
|
||||
.Then(l => l.Literal("position")
|
||||
.Executes(r => DoPositionInfo(r.Source))
|
||||
.Then(l => l.Literal("top_left")
|
||||
.Executes(r => DoPositionSet(r.Source, MinimapPosition.top_left)))
|
||||
.Then(l => l.Literal("top_right")
|
||||
.Executes(r => DoPositionSet(r.Source, MinimapPosition.top_right)))
|
||||
.Then(l => l.Literal("center")
|
||||
.Executes(r => DoPositionSet(r.Source, MinimapPosition.center)))
|
||||
.Then(l => l.Literal("bottom_left")
|
||||
.Executes(r => DoPositionSet(r.Source, MinimapPosition.bottom_left)))
|
||||
.Then(l => l.Literal("bottom_right")
|
||||
.Executes(r => DoPositionSet(r.Source, MinimapPosition.bottom_right))))
|
||||
.Then(l => l.Literal("cave")
|
||||
.Executes(r => DoCaveInfo(r.Source))
|
||||
.Then(l => l.Literal("auto")
|
||||
.Executes(r => DoCaveSet(r.Source, CaveModeOption.auto)))
|
||||
.Then(l => l.Literal("on")
|
||||
.Executes(r => DoCaveSet(r.Source, CaveModeOption.on)))
|
||||
.Then(l => l.Literal("off")
|
||||
.Executes(r => DoCaveSet(r.Source, CaveModeOption.off))))
|
||||
.Then(l => l.Literal("_help")
|
||||
.Executes(r => GetUsage(r.Source, string.Empty))
|
||||
.Redirect(dispatcher.GetRoot().GetChild("help")?.GetChild(CmdName)))
|
||||
);
|
||||
}
|
||||
|
||||
private int GetUsage(CmdResult r, string _) =>
|
||||
r.SetAndReturn(GetCmdDescTranslated());
|
||||
|
||||
private static MainTuiView? GetTuiView(CmdResult r)
|
||||
{
|
||||
if (ConsoleIO.Backend is not TuiConsoleBackend)
|
||||
{
|
||||
r.SetAndReturn(Status.Fail, Translations.cmd_minimap_tui_only);
|
||||
return null;
|
||||
}
|
||||
return TuiConsoleBackend.Instance?.GetView();
|
||||
}
|
||||
|
||||
private static int DoToggle(CmdResult r)
|
||||
{
|
||||
var view = GetTuiView(r);
|
||||
if (view is null) return (int)r.status;
|
||||
|
||||
bool wasVisible = view.IsMinimapVisible;
|
||||
Dispatcher.UIThread.Post(() => view.ToggleMinimap());
|
||||
string msg = wasVisible
|
||||
? Translations.cmd_minimap_disabled
|
||||
: Translations.cmd_minimap_enabled;
|
||||
return r.SetAndReturn(Status.Done, msg);
|
||||
}
|
||||
|
||||
private static int DoOn(CmdResult r)
|
||||
{
|
||||
var view = GetTuiView(r);
|
||||
if (view is null) return (int)r.status;
|
||||
|
||||
Dispatcher.UIThread.Post(() => view.ShowMinimap());
|
||||
return r.SetAndReturn(Status.Done, Translations.cmd_minimap_enabled);
|
||||
}
|
||||
|
||||
private static int DoOff(CmdResult r)
|
||||
{
|
||||
var view = GetTuiView(r);
|
||||
if (view is null) return (int)r.status;
|
||||
|
||||
Dispatcher.UIThread.Post(() => view.HideMinimap());
|
||||
return r.SetAndReturn(Status.Done, Translations.cmd_minimap_disabled);
|
||||
}
|
||||
|
||||
private static int DoZoomInfo(CmdResult r)
|
||||
{
|
||||
var view = GetTuiView(r);
|
||||
if (view is null) return (int)r.status;
|
||||
|
||||
int current = view.GetMinimapZoom();
|
||||
return r.SetAndReturn(Status.Done,
|
||||
string.Format(Translations.cmd_minimap_zoom_current, current, MinimapControl.MaxZoom));
|
||||
}
|
||||
|
||||
private static int DoZoomIn(CmdResult r)
|
||||
{
|
||||
var view = GetTuiView(r);
|
||||
if (view is null) return (int)r.status;
|
||||
|
||||
int newLevel = Math.Max(view.GetMinimapZoom() - 1, MinimapControl.MinZoom);
|
||||
Dispatcher.UIThread.Post(() => view.SetMinimapZoom(newLevel));
|
||||
return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_minimap_zoom_set, newLevel));
|
||||
}
|
||||
|
||||
private static int DoZoomOut(CmdResult r)
|
||||
{
|
||||
var view = GetTuiView(r);
|
||||
if (view is null) return (int)r.status;
|
||||
|
||||
int newLevel = Math.Min(view.GetMinimapZoom() + 1, MinimapControl.MaxZoom);
|
||||
Dispatcher.UIThread.Post(() => view.SetMinimapZoom(newLevel));
|
||||
return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_minimap_zoom_set, newLevel));
|
||||
}
|
||||
|
||||
private static int DoZoomSet(CmdResult r, int level)
|
||||
{
|
||||
var view = GetTuiView(r);
|
||||
if (view is null) return (int)r.status;
|
||||
|
||||
Dispatcher.UIThread.Post(() => view.SetMinimapZoom(level));
|
||||
return r.SetAndReturn(Status.Done, string.Format(Translations.cmd_minimap_zoom_set, level));
|
||||
}
|
||||
|
||||
private static int DoNamesInfo(CmdResult r)
|
||||
{
|
||||
var view = GetTuiView(r);
|
||||
if (view is null) return (int)r.status;
|
||||
|
||||
var nc = view.GetMinimapNameConfig();
|
||||
string status = string.Format(Translations.cmd_minimap_names_status,
|
||||
BoolStr(nc.Players), BoolStr(nc.Hostile), BoolStr(nc.Neutral), BoolStr(nc.Passive));
|
||||
return r.SetAndReturn(Status.Done, status);
|
||||
}
|
||||
|
||||
private static int DoNamesAll(CmdResult r, bool on)
|
||||
{
|
||||
var view = GetTuiView(r);
|
||||
if (view is null) return (int)r.status;
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
view.GetMinimapNameConfig().SetAll(on);
|
||||
view.SyncMinimapNameConfig();
|
||||
});
|
||||
string msg = on ? Translations.cmd_minimap_names_all_on : Translations.cmd_minimap_names_all_off;
|
||||
return r.SetAndReturn(Status.Done, msg);
|
||||
}
|
||||
|
||||
private static int DoNamesCatInfo(CmdResult r, MobCategory cat)
|
||||
{
|
||||
var view = GetTuiView(r);
|
||||
if (view is null) return (int)r.status;
|
||||
|
||||
var nc = view.GetMinimapNameConfig();
|
||||
bool val = cat switch
|
||||
{
|
||||
MobCategory.Player => nc.Players,
|
||||
MobCategory.Hostile => nc.Hostile,
|
||||
MobCategory.Neutral => nc.Neutral,
|
||||
MobCategory.Passive => nc.Passive,
|
||||
_ => false,
|
||||
};
|
||||
return r.SetAndReturn(Status.Done,
|
||||
string.Format(Translations.cmd_minimap_names_cat, cat, BoolStr(val)));
|
||||
}
|
||||
|
||||
private static int DoNamesCatSet(CmdResult r, MobCategory cat, bool on)
|
||||
{
|
||||
var view = GetTuiView(r);
|
||||
if (view is null) return (int)r.status;
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var nc = view.GetMinimapNameConfig();
|
||||
switch (cat)
|
||||
{
|
||||
case MobCategory.Player: nc.Players = on; break;
|
||||
case MobCategory.Hostile: nc.Hostile = on; break;
|
||||
case MobCategory.Neutral: nc.Neutral = on; break;
|
||||
case MobCategory.Passive: nc.Passive = on; break;
|
||||
}
|
||||
view.SyncMinimapNameConfig();
|
||||
});
|
||||
return r.SetAndReturn(Status.Done,
|
||||
string.Format(Translations.cmd_minimap_names_cat_set, cat, BoolStr(on)));
|
||||
}
|
||||
|
||||
private static int DoPositionInfo(CmdResult r)
|
||||
{
|
||||
var view = GetTuiView(r);
|
||||
if (view is null) return (int)r.status;
|
||||
|
||||
var pos = view.GetMinimapPosition();
|
||||
return r.SetAndReturn(Status.Done,
|
||||
string.Format(Translations.cmd_minimap_position_current, pos));
|
||||
}
|
||||
|
||||
private static int DoPositionSet(CmdResult r, MinimapPosition pos)
|
||||
{
|
||||
var view = GetTuiView(r);
|
||||
if (view is null) return (int)r.status;
|
||||
|
||||
Dispatcher.UIThread.Post(() => view.SetMinimapPosition(pos));
|
||||
return r.SetAndReturn(Status.Done,
|
||||
string.Format(Translations.cmd_minimap_position_set, pos));
|
||||
}
|
||||
|
||||
private static int DoCaveInfo(CmdResult r)
|
||||
{
|
||||
var view = GetTuiView(r);
|
||||
if (view is null) return (int)r.status;
|
||||
|
||||
var mode = view.GetMinimapCaveMode();
|
||||
return r.SetAndReturn(Status.Done,
|
||||
string.Format(Translations.cmd_minimap_cave_current, mode));
|
||||
}
|
||||
|
||||
private static int DoCaveSet(CmdResult r, CaveModeOption mode)
|
||||
{
|
||||
var view = GetTuiView(r);
|
||||
if (view is null) return (int)r.status;
|
||||
|
||||
Dispatcher.UIThread.Post(() => view.SetMinimapCaveMode(mode));
|
||||
return r.SetAndReturn(Status.Done,
|
||||
string.Format(Translations.cmd_minimap_cave_set, mode));
|
||||
}
|
||||
|
||||
private static string BoolStr(bool v) => v ? "ON" : "OFF";
|
||||
}
|
||||
}
|
||||
98
MinecraftClient/Commands/RecipeBook.cs
Normal file
98
MinecraftClient/Commands/RecipeBook.cs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
using System.Text;
|
||||
using Brigadier.NET;
|
||||
using Brigadier.NET.Builder;
|
||||
using MinecraftClient.CommandHandler;
|
||||
|
||||
namespace MinecraftClient.Commands
|
||||
{
|
||||
public class RecipeBook : Command
|
||||
{
|
||||
public override string CmdName => "recipebook";
|
||||
public override string CmdUsage => "recipebook <list|craft|craftall> [recipe id]";
|
||||
public override string CmdDesc => Translations.cmd_recipebook_desc;
|
||||
|
||||
public override void RegisterCommand(CommandDispatcher<CmdResult> dispatcher)
|
||||
{
|
||||
dispatcher.Register(l => l.Literal("help")
|
||||
.Then(l => l.Literal(CmdName)
|
||||
.Executes(r => GetUsage(r.Source, string.Empty))
|
||||
.Then(l => l.Literal("list")
|
||||
.Executes(r => GetUsage(r.Source, "list")))
|
||||
.Then(l => l.Literal("craft")
|
||||
.Executes(r => GetUsage(r.Source, "craft")))
|
||||
.Then(l => l.Literal("craftall")
|
||||
.Executes(r => GetUsage(r.Source, "craftall")))
|
||||
)
|
||||
);
|
||||
|
||||
dispatcher.Register(l => l.Literal(CmdName)
|
||||
.Then(l => l.Literal("list")
|
||||
.Executes(r => ListRecipes(r.Source)))
|
||||
.Then(l => l.Literal("craft")
|
||||
.Then(l => l.Argument("RecipeId", Arguments.String())
|
||||
.Executes(r => CraftRecipe(r.Source, Arguments.GetString(r, "RecipeId"), makeAll: false))))
|
||||
.Then(l => l.Literal("craftall")
|
||||
.Then(l => l.Argument("RecipeId", Arguments.String())
|
||||
.Executes(r => CraftRecipe(r.Source, Arguments.GetString(r, "RecipeId"), makeAll: true))))
|
||||
.Then(l => l.Literal("_help")
|
||||
.Executes(r => GetUsage(r.Source, string.Empty))
|
||||
.Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName)))
|
||||
);
|
||||
}
|
||||
|
||||
private int GetUsage(CmdResult r, string? cmd)
|
||||
{
|
||||
return r.SetAndReturn(cmd switch
|
||||
{
|
||||
#pragma warning disable format // @formatter:off
|
||||
"list" => GetCmdDescTranslated(),
|
||||
"craft" => GetCmdDescTranslated(),
|
||||
"craftall" => GetCmdDescTranslated(),
|
||||
_ => GetCmdDescTranslated(),
|
||||
#pragma warning restore format // @formatter:on
|
||||
});
|
||||
}
|
||||
|
||||
private int ListRecipes(CmdResult r)
|
||||
{
|
||||
McClient handler = CmdResult.currentHandler!;
|
||||
if (!handler.GetInventoryEnabled())
|
||||
return r.SetAndReturn(CmdResult.Status.FailNeedInventory);
|
||||
|
||||
RecipeBookRecipeEntry[] recipes = handler.GetUnlockedRecipes();
|
||||
if (recipes.Length == 0)
|
||||
return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_recipebook_no_recipes);
|
||||
|
||||
StringBuilder response = new();
|
||||
response.AppendLine(Translations.cmd_recipebook_list);
|
||||
foreach (RecipeBookRecipeEntry recipe in recipes)
|
||||
response.AppendLine("- " + recipe.DisplayText);
|
||||
|
||||
handler.Log.Info(response.ToString().TrimEnd());
|
||||
return r.SetAndReturn(CmdResult.Status.Done);
|
||||
}
|
||||
|
||||
private int CraftRecipe(CmdResult r, string recipeId, bool makeAll)
|
||||
{
|
||||
McClient handler = CmdResult.currentHandler!;
|
||||
if (!handler.GetInventoryEnabled())
|
||||
return r.SetAndReturn(CmdResult.Status.FailNeedInventory);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(recipeId))
|
||||
return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_recipebook_recipe_id_empty);
|
||||
|
||||
if (handler.GetProtocolVersion() < Protocol.Handlers.Protocol18Handler.MC_1_13_Version)
|
||||
return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_recipebook_unsupported);
|
||||
|
||||
if (handler.GetActiveRecipeBookInventory() is null)
|
||||
return r.SetAndReturn(CmdResult.Status.Fail, Translations.cmd_recipebook_no_active_inventory);
|
||||
|
||||
string normalizedRecipeId = McClient.NormalizeRecipeArgument(recipeId, handler.GetProtocolVersion());
|
||||
string successMessage = string.Format(makeAll ? Translations.cmd_recipebook_craftall_sent : Translations.cmd_recipebook_craft_sent, normalizedRecipeId);
|
||||
|
||||
return handler.SendPlaceRecipe(recipeId, makeAll)
|
||||
? r.SetAndReturn(CmdResult.Status.Done, successMessage)
|
||||
: r.SetAndReturn(CmdResult.Status.Fail, string.Format(Translations.cmd_recipebook_craft_failed, normalizedRecipeId));
|
||||
}
|
||||
}
|
||||
}
|
||||
77
MinecraftClient/Commands/Teams.cs
Normal file
77
MinecraftClient/Commands/Teams.cs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Brigadier.NET;
|
||||
using Brigadier.NET.Builder;
|
||||
using MinecraftClient.CommandHandler;
|
||||
using MinecraftClient.Mapping;
|
||||
|
||||
namespace MinecraftClient.Commands
|
||||
{
|
||||
public class Teams : Command
|
||||
{
|
||||
public override string CmdName => "teams";
|
||||
public override string CmdUsage => "teams";
|
||||
public override string CmdDesc => Translations.cmd_teams_desc;
|
||||
|
||||
public override void RegisterCommand(CommandDispatcher<CmdResult> dispatcher)
|
||||
{
|
||||
dispatcher.Register(l => l.Literal("help")
|
||||
.Then(l => l.Literal(CmdName)
|
||||
.Executes(r => GetUsage(r.Source, string.Empty))
|
||||
)
|
||||
);
|
||||
|
||||
dispatcher.Register(l => l.Literal(CmdName)
|
||||
.Executes(r => DoListTeams(r.Source))
|
||||
.Then(l => l.Literal("_help")
|
||||
.Executes(r => GetUsage(r.Source, string.Empty))
|
||||
.Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName)))
|
||||
);
|
||||
}
|
||||
|
||||
private int GetUsage(CmdResult r, string? cmd)
|
||||
{
|
||||
return r.SetAndReturn(cmd switch
|
||||
{
|
||||
#pragma warning disable format // @formatter:off
|
||||
_ => GetCmdDescTranslated(),
|
||||
#pragma warning restore format // @formatter:on
|
||||
});
|
||||
}
|
||||
|
||||
private static int DoListTeams(CmdResult r)
|
||||
{
|
||||
McClient handler = CmdResult.currentHandler!;
|
||||
Dictionary<string, PlayerTeam> snapshot = handler.GetTeams();
|
||||
|
||||
if (snapshot.Count == 0)
|
||||
return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_teams_no_teams);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
foreach (var team in snapshot.Values.OrderBy(static t => t.Name, StringComparer.Ordinal))
|
||||
{
|
||||
sb.AppendLine(string.Format(Translations.cmd_teams_team_header,
|
||||
team.Name,
|
||||
team.DisplayName,
|
||||
team.Color,
|
||||
team.Prefix,
|
||||
team.Suffix,
|
||||
team.NameTagVisibility,
|
||||
team.CollisionRule,
|
||||
team.AllowFriendlyFire,
|
||||
team.SeeFriendlyInvisibles));
|
||||
|
||||
if (team.Members.Count == 0)
|
||||
sb.AppendLine(Translations.cmd_teams_team_no_members);
|
||||
else
|
||||
sb.AppendLine(string.Format(Translations.cmd_teams_team_members,
|
||||
team.Members.Count,
|
||||
string.Join(", ", team.Members.OrderBy(static m => m, StringComparer.OrdinalIgnoreCase))));
|
||||
}
|
||||
|
||||
return r.SetAndReturn(CmdResult.Status.Done, sb.ToString().TrimEnd());
|
||||
}
|
||||
}
|
||||
}
|
||||
63
MinecraftClient/Commands/Tryout.cs
Normal file
63
MinecraftClient/Commands/Tryout.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
using Brigadier.NET;
|
||||
using Brigadier.NET.Builder;
|
||||
using MinecraftClient.CommandHandler;
|
||||
using static MinecraftClient.Settings.ConsoleConfigHealper.ConsoleConfig;
|
||||
|
||||
namespace MinecraftClient.Commands
|
||||
{
|
||||
public class Tryout : Command
|
||||
{
|
||||
public override string CmdName => "tryout";
|
||||
public override string CmdUsage => "tryout [list|tui]";
|
||||
public override string CmdDesc => Translations.cmd_tryout_desc;
|
||||
|
||||
public override void RegisterCommand(CommandDispatcher<CmdResult> dispatcher)
|
||||
{
|
||||
dispatcher.Register(l => l.Literal("help")
|
||||
.Then(l => l.Literal(CmdName)
|
||||
.Executes(r => GetUsage(r.Source))
|
||||
)
|
||||
);
|
||||
|
||||
dispatcher.Register(l => l.Literal(CmdName)
|
||||
.Executes(r => ListTryouts(r.Source))
|
||||
.Then(l => l.Literal("list")
|
||||
.Executes(r => ListTryouts(r.Source)))
|
||||
.Then(l => l.Literal("tui")
|
||||
.Executes(r => EnableTuiMode(r.Source)))
|
||||
.Then(l => l.Literal("_help")
|
||||
.Executes(r => GetUsage(r.Source))
|
||||
.Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName)))
|
||||
);
|
||||
}
|
||||
|
||||
private int GetUsage(CmdResult r)
|
||||
{
|
||||
return r.SetAndReturn(GetCmdDescTranslated());
|
||||
}
|
||||
|
||||
private int ListTryouts(CmdResult r)
|
||||
{
|
||||
return r.SetAndReturn(string.Join('\n',
|
||||
GetCmdDescTranslated(),
|
||||
string.Empty,
|
||||
Translations.cmd_tryout_list_header,
|
||||
$" - {Translations.cmd_tryout_list_tui}"));
|
||||
}
|
||||
|
||||
private int EnableTuiMode(CmdResult r)
|
||||
{
|
||||
var previousMode = Settings.Config.Console.General.ConsoleMode;
|
||||
if (previousMode == ConsoleModeType.tui)
|
||||
{
|
||||
return r.SetAndReturn(CmdResult.Status.Done, Translations.cmd_tryout_tui_already_enabled);
|
||||
}
|
||||
|
||||
Settings.Config.Console.General.ConsoleMode = ConsoleModeType.tui;
|
||||
Program.WriteBackSettings();
|
||||
|
||||
return r.SetAndReturn(CmdResult.Status.Done,
|
||||
string.Format(Translations.cmd_tryout_tui_enabled, previousMode, ConsoleModeType.tui));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
using Brigadier.NET;
|
||||
using Brigadier.NET.Builder;
|
||||
using MinecraftClient.CommandHandler;
|
||||
using MinecraftClient.Inventory;
|
||||
using MinecraftClient.Mapping;
|
||||
using static MinecraftClient.CommandHandler.CmdResult;
|
||||
|
||||
|
|
@ -9,7 +10,7 @@ namespace MinecraftClient.Commands
|
|||
class Useblock : Command
|
||||
{
|
||||
public override string CmdName { get { return "useblock"; } }
|
||||
public override string CmdUsage { get { return "useblock <x> <y> <z>"; } }
|
||||
public override string CmdUsage { get { return "useblock <x> <y> <z> [mainhand|offhand]"; } }
|
||||
public override string CmdDesc { get { return Translations.cmd_useblock_desc; } }
|
||||
|
||||
public override void RegisterCommand(CommandDispatcher<CmdResult> dispatcher)
|
||||
|
|
@ -22,7 +23,11 @@ namespace MinecraftClient.Commands
|
|||
|
||||
dispatcher.Register(l => l.Literal(CmdName)
|
||||
.Then(l => l.Argument("Location", MccArguments.Location())
|
||||
.Executes(r => UseBlockAtLocation(r.Source, MccArguments.GetLocation(r, "Location"))))
|
||||
.Executes(r => UseBlockAtLocation(r.Source, MccArguments.GetLocation(r, "Location"), Hand.MainHand))
|
||||
.Then(l => l.Literal("mainhand")
|
||||
.Executes(r => UseBlockAtLocation(r.Source, MccArguments.GetLocation(r, "Location"), Hand.MainHand)))
|
||||
.Then(l => l.Literal("offhand")
|
||||
.Executes(r => UseBlockAtLocation(r.Source, MccArguments.GetLocation(r, "Location"), Hand.OffHand))))
|
||||
.Then(l => l.Literal("_help")
|
||||
.Executes(r => GetUsage(r.Source, string.Empty))
|
||||
.Redirect(dispatcher.GetRoot().GetChild("help").GetChild(CmdName)))
|
||||
|
|
@ -39,7 +44,7 @@ namespace MinecraftClient.Commands
|
|||
});
|
||||
}
|
||||
|
||||
private int UseBlockAtLocation(CmdResult r, Location block)
|
||||
private int UseBlockAtLocation(CmdResult r, Location block, Hand hand)
|
||||
{
|
||||
McClient handler = CmdResult.currentHandler!;
|
||||
if (!handler.GetTerrainEnabled())
|
||||
|
|
@ -48,7 +53,7 @@ namespace MinecraftClient.Commands
|
|||
Location current = handler.GetCurrentLocation();
|
||||
block = block.ToAbsolute(current).ToFloor();
|
||||
Location blockCenter = block.ToCenter();
|
||||
bool res = handler.PlaceBlock(block, Direction.Down, lookAtBlock: true);
|
||||
bool res = handler.PlaceBlock(block, Direction.Down, hand, lookAtBlock: true);
|
||||
return r.SetAndReturn(string.Format(Translations.cmd_useblock_use, blockCenter.X, blockCenter.Y, blockCenter.Z, res ? "succeeded" : "failed"), res);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ namespace MinecraftClient
|
|||
/// </summary>
|
||||
public static void WriteLine(string line)
|
||||
{
|
||||
if (BasicIO)
|
||||
if (BasicIO || Backend is null)
|
||||
Console.WriteLine(line);
|
||||
else
|
||||
Backend.WriteLine(line);
|
||||
|
|
@ -137,7 +137,7 @@ namespace MinecraftClient
|
|||
{
|
||||
str = str.Replace('\n', ' ');
|
||||
}
|
||||
if (BasicIO)
|
||||
if (BasicIO || Backend is null)
|
||||
{
|
||||
if (BasicIO_NoColor)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
|
|
@ -23,10 +24,46 @@ public static class Json
|
|||
public static JsonNode? ParseJson(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return null;
|
||||
ReadOnlySpan<char> text = json.AsSpan().TrimStart();
|
||||
if (!LooksLikeJson(text))
|
||||
return JsonValue.Create(json);
|
||||
|
||||
try { return JsonNode.Parse(json); }
|
||||
catch (JsonException) { return JsonValue.Create(json); }
|
||||
}
|
||||
|
||||
private static bool LooksLikeJson(ReadOnlySpan<char> text)
|
||||
{
|
||||
if (text.IsEmpty)
|
||||
return false;
|
||||
|
||||
return text[0] switch
|
||||
{
|
||||
'{' or '"' => true,
|
||||
'[' => LooksLikeJsonArray(text[1..]),
|
||||
'-' => text.Length > 1 && char.IsAsciiDigit(text[1]),
|
||||
>= '0' and <= '9' => true,
|
||||
't' or 'f' or 'n' => true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private static bool LooksLikeJsonArray(ReadOnlySpan<char> text)
|
||||
{
|
||||
text = text.TrimStart();
|
||||
if (text.IsEmpty)
|
||||
return false;
|
||||
|
||||
return text[0] switch
|
||||
{
|
||||
']' or '{' or '[' or '"' => true,
|
||||
'-' => text.Length > 1 && char.IsAsciiDigit(text[1]),
|
||||
>= '0' and <= '9' => true,
|
||||
't' or 'f' or 'n' => true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escape a string for embedding inside a JSON string literal.
|
||||
/// Uses System.Text.Json serialization and strips the surrounding quotes.
|
||||
|
|
@ -52,4 +89,4 @@ public static class JsonNodeExtensions
|
|||
JsonValue val when val.TryGetValue<string>(out var s) => s,
|
||||
_ => node.ToJsonString()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
53
MinecraftClient/LegacyAchievementCatalog.cs
Normal file
53
MinecraftClient/LegacyAchievementCatalog.cs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MinecraftClient
|
||||
{
|
||||
internal static class LegacyAchievementCatalog
|
||||
{
|
||||
public static IReadOnlyList<string> Ids { get; } =
|
||||
[
|
||||
"achievement.openInventory",
|
||||
"achievement.mineWood",
|
||||
"achievement.buildWorkBench",
|
||||
"achievement.buildPickaxe",
|
||||
"achievement.buildFurnace",
|
||||
"achievement.acquireIron",
|
||||
"achievement.buildHoe",
|
||||
"achievement.makeBread",
|
||||
"achievement.bakeCake",
|
||||
"achievement.buildBetterPickaxe",
|
||||
"achievement.cookFish",
|
||||
"achievement.onARail",
|
||||
"achievement.buildSword",
|
||||
"achievement.killEnemy",
|
||||
"achievement.killCow",
|
||||
"achievement.flyPig",
|
||||
"achievement.snipeSkeleton",
|
||||
"achievement.diamonds",
|
||||
"achievement.diamondsToYou",
|
||||
"achievement.portal",
|
||||
"achievement.ghast",
|
||||
"achievement.blazeRod",
|
||||
"achievement.potion",
|
||||
"achievement.theEnd",
|
||||
"achievement.theEnd2",
|
||||
"achievement.enchantments",
|
||||
"achievement.overkill",
|
||||
"achievement.bookcase",
|
||||
"achievement.breedCow",
|
||||
"achievement.spawnWither",
|
||||
"achievement.killWither",
|
||||
"achievement.fullBeacon",
|
||||
"achievement.exploreAllBiomes",
|
||||
"achievement.overpowered"
|
||||
];
|
||||
|
||||
private static readonly HashSet<string> s_idSet = new(Ids, StringComparer.Ordinal);
|
||||
|
||||
public static bool Contains(string id)
|
||||
{
|
||||
return s_idSet.Contains(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
1326
MinecraftClient/Mapping/BlockHardness.cs
Normal file
1326
MinecraftClient/Mapping/BlockHardness.cs
Normal file
File diff suppressed because it is too large
Load diff
572
MinecraftClient/Mapping/MiningCalculator.cs
Normal file
572
MinecraftClient/Mapping/MiningCalculator.cs
Normal file
|
|
@ -0,0 +1,572 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using MinecraftClient.Inventory;
|
||||
using MinecraftClient.Protocol.Handlers;
|
||||
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_20_6;
|
||||
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components._1_21_5;
|
||||
using MinecraftClient.Protocol.Handlers.StructuredComponents.Components.Subcomponents._1_20_6;
|
||||
|
||||
namespace MinecraftClient.Mapping
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes dig duration in ticks for survival-style block breaking.
|
||||
/// Version-aware across 1.8-1.21.11+, using tool speed, enchantments, effects, and attributes.
|
||||
/// </summary>
|
||||
public static class MiningCalculator
|
||||
{
|
||||
/// <summary>
|
||||
/// Compute the number of ticks required to break a block in survival mode.
|
||||
/// Returns 0 for instant-break blocks, -1 for unbreakable blocks.
|
||||
/// </summary>
|
||||
/// <param name="blockMaterial">The block material to break</param>
|
||||
/// <param name="heldItem">The item in the player's main hand (null for empty hand)</param>
|
||||
/// <param name="helmetItem">The item in the player's helmet slot (null if empty, used for Aqua Affinity)</param>
|
||||
/// <param name="effects">Currently active player effects</param>
|
||||
/// <param name="playerAttributes">Cached player attribute values (from OnEntityProperties)</param>
|
||||
/// <param name="isUnderwater">Whether the player's eyes are submerged in water</param>
|
||||
/// <param name="isOnGround">Whether the player is on the ground</param>
|
||||
/// <param name="protocolVersion">The Minecraft protocol version</param>
|
||||
/// <returns>Ticks to break the block, 0 for instant, -1 for unbreakable</returns>
|
||||
public static int ComputeDigTicks(
|
||||
Material blockMaterial,
|
||||
Item? heldItem,
|
||||
Item? helmetItem,
|
||||
Dictionary<Effects, EffectData> effects,
|
||||
Dictionary<string, double> playerAttributes,
|
||||
bool isUnderwater,
|
||||
bool isOnGround,
|
||||
int protocolVersion)
|
||||
{
|
||||
float hardness = BlockHardness.GetHardness(blockMaterial);
|
||||
|
||||
if (hardness < 0)
|
||||
return -1; // Unbreakable
|
||||
|
||||
if (hardness == 0)
|
||||
return 0; // Instant break
|
||||
|
||||
float destroySpeed = GetDestroySpeed(
|
||||
blockMaterial, heldItem, helmetItem, effects, playerAttributes,
|
||||
isUnderwater, isOnGround, protocolVersion);
|
||||
|
||||
bool correctTool = HasCorrectToolForDrops(blockMaterial, heldItem, protocolVersion);
|
||||
int divisor = correctTool ? 30 : 100;
|
||||
|
||||
float destroyProgress = destroySpeed / hardness / divisor;
|
||||
|
||||
if (destroyProgress >= 1.0f)
|
||||
return 0; // Instant break
|
||||
|
||||
return (int)MathF.Ceiling(1.0f / destroyProgress);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute the player's destroy speed for a given block, following vanilla formulas.
|
||||
/// </summary>
|
||||
private static float GetDestroySpeed(
|
||||
Material blockMaterial,
|
||||
Item? heldItem,
|
||||
Item? helmetItem,
|
||||
Dictionary<Effects, EffectData> effects,
|
||||
Dictionary<string, double> playerAttributes,
|
||||
bool isUnderwater,
|
||||
bool isOnGround,
|
||||
int protocolVersion)
|
||||
{
|
||||
float speed = GetToolSpeed(blockMaterial, heldItem, protocolVersion);
|
||||
|
||||
if (speed > 1.0f)
|
||||
{
|
||||
speed += GetEfficiencyBonus(heldItem, playerAttributes, protocolVersion);
|
||||
}
|
||||
|
||||
int digSpeedAmplifier = GetDigSpeedAmplifier(effects);
|
||||
if (digSpeedAmplifier >= 0)
|
||||
speed *= 1.0f + (digSpeedAmplifier + 1) * 0.2f;
|
||||
|
||||
// Mining Fatigue
|
||||
if (effects.TryGetValue(Effects.MiningFatigue, out var fatigueData))
|
||||
{
|
||||
float multiplier = fatigueData.Amplifier switch
|
||||
{
|
||||
0 => 0.3f,
|
||||
1 => 0.09f,
|
||||
2 => 0.0027f,
|
||||
_ => 8.1E-4f
|
||||
};
|
||||
speed *= multiplier;
|
||||
}
|
||||
|
||||
// Attribute multipliers for modern versions
|
||||
if (protocolVersion >= Protocol18Handler.MC_1_20_6_Version)
|
||||
{
|
||||
// BLOCK_BREAK_SPEED attribute (default 1.0)
|
||||
if (playerAttributes.TryGetValue("player.block_break_speed", out double bbs))
|
||||
speed *= (float)bbs;
|
||||
}
|
||||
|
||||
// Underwater penalty
|
||||
if (isUnderwater)
|
||||
{
|
||||
if (protocolVersion >= Protocol18Handler.MC_1_21_11_Version)
|
||||
{
|
||||
// 1.21.11+: Uses SUBMERGED_MINING_SPEED attribute (default 0.2)
|
||||
double submergedSpeed = 0.2;
|
||||
if (playerAttributes.TryGetValue("player.submerged_mining_speed", out double sms))
|
||||
submergedSpeed = sms;
|
||||
speed *= (float)submergedSpeed;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Pre-1.21.11: /5 unless Aqua Affinity
|
||||
bool hasAquaAffinity = GetEnchantmentLevel(helmetItem, Enchantments.AquaAffinity, protocolVersion) > 0;
|
||||
if (!hasAquaAffinity)
|
||||
speed /= 5.0f;
|
||||
}
|
||||
}
|
||||
|
||||
// Airborne penalty
|
||||
if (!isOnGround)
|
||||
speed /= 5.0f;
|
||||
|
||||
return speed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the base tool mining speed for a block.
|
||||
/// For 1.20.6+ with ToolComponent, uses structured component data.
|
||||
/// For older versions, uses hardcoded tool speed tables.
|
||||
/// </summary>
|
||||
private static float GetToolSpeed(Material blockMaterial, Item? heldItem, int protocolVersion)
|
||||
{
|
||||
if (heldItem is null)
|
||||
return 1.0f;
|
||||
|
||||
// Modern path: use ToolComponent from structured components
|
||||
if (protocolVersion >= Protocol18Handler.MC_1_20_6_Version
|
||||
&& TryGetToolRules(heldItem, out List<RuleSubComponent>? rules, out float defaultMiningSpeed))
|
||||
{
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
if (rule.HasSpeed && MatchesBlockSet(rule.Blocks, blockMaterial))
|
||||
return rule.Speed;
|
||||
}
|
||||
|
||||
// Structured tool data covers modern mining rules, but keep the legacy fallback for
|
||||
// explicit block holder-sets that MCC cannot resolve yet (for example cobweb).
|
||||
if (defaultMiningSpeed > 1.0f)
|
||||
return defaultMiningSpeed;
|
||||
}
|
||||
|
||||
// Legacy path: hardcoded tool speed tables
|
||||
return GetLegacyToolSpeed(heldItem.Type, blockMaterial);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the tool provides correct drops for a block.
|
||||
/// </summary>
|
||||
private static bool HasCorrectToolForDrops(Material blockMaterial, Item? heldItem, int protocolVersion)
|
||||
{
|
||||
if (!BlockHardness.RequiresCorrectTool(blockMaterial))
|
||||
return true;
|
||||
|
||||
if (heldItem is null)
|
||||
return false;
|
||||
|
||||
// Modern path: check ToolComponent rules
|
||||
if (protocolVersion >= Protocol18Handler.MC_1_20_6_Version
|
||||
&& TryGetToolRules(heldItem, out List<RuleSubComponent>? rules, out _))
|
||||
{
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
if (rule.HasCorrectDropForBlocks && MatchesBlockSet(rule.Blocks, blockMaterial))
|
||||
return rule.CorrectDropForBlocks;
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy path, plus a modern fallback for direct block holder-sets MCC cannot resolve yet.
|
||||
return IsCorrectToolLegacy(heldItem.Type, blockMaterial);
|
||||
}
|
||||
|
||||
private static bool TryGetToolRules(
|
||||
Item heldItem,
|
||||
[NotNullWhen(true)] out List<RuleSubComponent>? rules,
|
||||
out float defaultMiningSpeed)
|
||||
{
|
||||
rules = null;
|
||||
defaultMiningSpeed = 1.0f;
|
||||
|
||||
if (heldItem.Components is null)
|
||||
return false;
|
||||
|
||||
if (heldItem.Components.OfType<ToolComponent>().FirstOrDefault() is ToolComponent toolComponent)
|
||||
{
|
||||
rules = toolComponent.Rules;
|
||||
defaultMiningSpeed = toolComponent.DefaultMiningSpeed;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (heldItem.Components.OfType<ToolComponent1215>().FirstOrDefault() is ToolComponent1215 toolComponent1215)
|
||||
{
|
||||
rules = toolComponent1215.Rules;
|
||||
defaultMiningSpeed = toolComponent1215.DefaultMiningSpeed;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match a block material against a ToolComponent BlockSetSubcomponent.
|
||||
/// </summary>
|
||||
private static bool MatchesBlockSet(
|
||||
Protocol.Handlers.StructuredComponents.Components.Subcomponents._1_20_6.BlockSetSubcomponent blockSet,
|
||||
Material blockMaterial)
|
||||
{
|
||||
if (blockSet.BlockIds is not null)
|
||||
{
|
||||
// Check against explicit block state IDs
|
||||
foreach (int blockId in blockSet.BlockIds)
|
||||
{
|
||||
if (Block.Palette.FromId(blockId) == blockMaterial)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (blockSet.TagName is not null)
|
||||
{
|
||||
// Match against tag name (e.g., "minecraft:mineable/pickaxe")
|
||||
return MatchesBlockTag(blockSet.TagName, blockMaterial);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Approximate block tag matching using Material2Tool categories.
|
||||
/// Tags like "minecraft:mineable/pickaxe" map to the appropriate tool categories.
|
||||
/// </summary>
|
||||
private static bool MatchesBlockTag(string tagName, Material blockMaterial)
|
||||
{
|
||||
// Normalize tag name
|
||||
string tag = tagName.Replace("minecraft:", "");
|
||||
|
||||
ItemType[] tools = Material2Tool.GetCorrectToolForBlock(blockMaterial);
|
||||
return tag switch
|
||||
{
|
||||
"mineable/pickaxe" => tools.Length > 0 && IsPickaxe(tools[0]),
|
||||
"mineable/axe" => tools.Length > 0 && IsAxe(tools[0]),
|
||||
"mineable/shovel" => tools.Length > 0 && IsShovel(tools[0]),
|
||||
"mineable/hoe" => tools.Length > 0 && IsHoe(tools[0]),
|
||||
"leaves" => IsLeaf(blockMaterial),
|
||||
"wool" => IsWool(blockMaterial),
|
||||
"incorrect_for_wooden_tool" => RequiresHigherTier(blockMaterial, 0),
|
||||
"incorrect_for_gold_tool" => RequiresHigherTier(blockMaterial, 0),
|
||||
"incorrect_for_stone_tool" => RequiresHigherTier(blockMaterial, 1),
|
||||
"incorrect_for_copper_tool" => RequiresHigherTier(blockMaterial, 1),
|
||||
"incorrect_for_iron_tool" => RequiresHigherTier(blockMaterial, 2),
|
||||
"incorrect_for_diamond_tool" => RequiresHigherTier(blockMaterial, 3),
|
||||
"incorrect_for_netherite_tool" => RequiresHigherTier(blockMaterial, 4),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private static bool RequiresHigherTier(Material blockMaterial, int tier)
|
||||
{
|
||||
ItemType[] recommended = Material2Tool.GetCorrectToolForBlock(blockMaterial);
|
||||
if (recommended.Length == 0)
|
||||
return false;
|
||||
|
||||
return GetRequiredTier(blockMaterial, recommended) > tier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the enchantment level from an item, supporting both legacy NBT and modern structured components.
|
||||
/// </summary>
|
||||
public static int GetEnchantmentLevel(Item? item, Enchantments enchantment, int protocolVersion)
|
||||
{
|
||||
if (item is null)
|
||||
return 0;
|
||||
|
||||
// Modern path: structured components (1.20.6+)
|
||||
var enchList = item.EnchantmentList;
|
||||
if (enchList is not null)
|
||||
{
|
||||
var ench = enchList.FirstOrDefault(e => e.Type == enchantment);
|
||||
if (ench is not null)
|
||||
return ench.Level;
|
||||
}
|
||||
|
||||
// Legacy path: NBT data
|
||||
if (item.NBT is not null &&
|
||||
item.NBT.TryGetValue("Enchantments", out object? enchantments))
|
||||
{
|
||||
try
|
||||
{
|
||||
string enchNameLower = GetEnchantmentResourceName(enchantment);
|
||||
foreach (Dictionary<string, object> enchEntry in (object[])enchantments)
|
||||
{
|
||||
string id = ((string)enchEntry["id"]).ToLowerInvariant();
|
||||
if (id == enchNameLower || id == "minecraft:" + enchNameLower)
|
||||
return (short)enchEntry["lvl"];
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// NBT parsing failure - return 0
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map Enchantments enum to Minecraft resource name (e.g., "efficiency").
|
||||
/// </summary>
|
||||
private static string GetEnchantmentResourceName(Enchantments enchantment)
|
||||
{
|
||||
return enchantment switch
|
||||
{
|
||||
Enchantments.AquaAffinity => "aqua_affinity",
|
||||
Enchantments.BaneOfArthropods => "bane_of_arthropods",
|
||||
Enchantments.BlastProtection => "blast_protection",
|
||||
Enchantments.Efficiency => "efficiency",
|
||||
Enchantments.FeatherFalling => "feather_falling",
|
||||
Enchantments.FireAspect => "fire_aspect",
|
||||
Enchantments.FireProtection => "fire_protection",
|
||||
Enchantments.FrostWalker => "frost_walker",
|
||||
Enchantments.LuckOfTheSea => "luck_of_the_sea",
|
||||
Enchantments.ProjectileProtection => "projectile_protection",
|
||||
Enchantments.QuickCharge => "quick_charge",
|
||||
Enchantments.SilkTouch => "silk_touch",
|
||||
Enchantments.SoulSpeed => "soul_speed",
|
||||
Enchantments.SwiftSneak => "swift_sneak",
|
||||
Enchantments.VanishingCurse => "vanishing_curse",
|
||||
Enchantments.BindingCurse => "binding_curse",
|
||||
Enchantments.WindBurst => "wind_burst",
|
||||
_ => enchantment.ToString().ToUnderscoreCase()
|
||||
};
|
||||
}
|
||||
|
||||
#region Legacy Tool Speed Tables
|
||||
|
||||
/// <summary>
|
||||
/// Legacy tool speed for pre-1.20.6 versions using hardcoded values.
|
||||
/// </summary>
|
||||
private static float GetLegacyToolSpeed(ItemType toolType, Material blockMaterial)
|
||||
{
|
||||
float specialToolSpeed = toolType switch
|
||||
{
|
||||
ItemType.Shears => GetShearsSpeed(blockMaterial),
|
||||
_ when IsSword(toolType) && blockMaterial == Material.Cobweb => 15.0f,
|
||||
_ => 1.0f
|
||||
};
|
||||
|
||||
if (specialToolSpeed > 1.0f)
|
||||
return specialToolSpeed;
|
||||
|
||||
ItemType[] recommended = Material2Tool.GetCorrectToolForBlock(blockMaterial);
|
||||
if (recommended.Length == 0)
|
||||
return 1.0f;
|
||||
|
||||
// Check if the held tool matches the recommended tool category
|
||||
ToolCategory heldCategory = GetToolCategory(toolType);
|
||||
ToolCategory neededCategory = GetToolCategory(recommended[0]);
|
||||
|
||||
if (heldCategory == ToolCategory.None || heldCategory != neededCategory)
|
||||
return 1.0f;
|
||||
|
||||
return GetBaseToolSpeed(toolType);
|
||||
}
|
||||
|
||||
private static float GetBaseToolSpeed(ItemType toolType)
|
||||
{
|
||||
return toolType switch
|
||||
{
|
||||
// Wooden tools
|
||||
ItemType.WoodenPickaxe or ItemType.WoodenAxe or ItemType.WoodenShovel or
|
||||
ItemType.WoodenSword or ItemType.WoodenHoe => 2.0f,
|
||||
|
||||
// Stone tools
|
||||
ItemType.StonePickaxe or ItemType.StoneAxe or ItemType.StoneShovel or
|
||||
ItemType.StoneSword or ItemType.StoneHoe => 4.0f,
|
||||
|
||||
// Iron tools
|
||||
ItemType.IronPickaxe or ItemType.IronAxe or ItemType.IronShovel or
|
||||
ItemType.IronSword or ItemType.IronHoe => 6.0f,
|
||||
|
||||
// Diamond tools
|
||||
ItemType.DiamondPickaxe or ItemType.DiamondAxe or ItemType.DiamondShovel or
|
||||
ItemType.DiamondSword or ItemType.DiamondHoe => 8.0f,
|
||||
|
||||
// Netherite tools
|
||||
ItemType.NetheritePickaxe or ItemType.NetheriteAxe or ItemType.NetheriteShovel or
|
||||
ItemType.NetheriteSword or ItemType.NetheriteHoe => 9.0f,
|
||||
|
||||
// Golden tools
|
||||
ItemType.GoldenPickaxe or ItemType.GoldenAxe or ItemType.GoldenShovel or
|
||||
ItemType.GoldenSword or ItemType.GoldenHoe => 12.0f,
|
||||
|
||||
// Shears
|
||||
ItemType.Shears => 2.0f,
|
||||
|
||||
_ => 1.0f
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the held tool is the correct tool for drops in legacy versions.
|
||||
/// Uses Material2Tool's recommendations to determine correctness.
|
||||
/// </summary>
|
||||
private static bool IsCorrectToolLegacy(ItemType toolType, Material blockMaterial)
|
||||
{
|
||||
ItemType[] recommended = Material2Tool.GetCorrectToolForBlock(blockMaterial);
|
||||
if (recommended.Length == 0)
|
||||
return false;
|
||||
|
||||
ToolCategory heldCategory = GetToolCategory(toolType);
|
||||
ToolCategory neededCategory = GetToolCategory(recommended[0]);
|
||||
|
||||
if (heldCategory == ToolCategory.None || heldCategory != neededCategory)
|
||||
{
|
||||
if (toolType == ItemType.Shears && blockMaterial == Material.Cobweb)
|
||||
return true;
|
||||
if (IsSword(toolType) && blockMaterial == Material.Cobweb)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check tool tier requirement
|
||||
int heldTier = GetToolTier(toolType);
|
||||
int requiredTier = GetRequiredTier(blockMaterial, recommended);
|
||||
|
||||
return heldTier >= requiredTier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the minimum tool tier required for a block based on Material2Tool's recommendation ordering.
|
||||
/// </summary>
|
||||
private static int GetRequiredTier(Material blockMaterial, ItemType[] recommended)
|
||||
{
|
||||
if (recommended.Length == 0)
|
||||
return 0;
|
||||
|
||||
// Material2Tool lists tools from highest to lowest tier.
|
||||
// The last tool in the array is the minimum required tier.
|
||||
return GetToolTier(recommended[^1]);
|
||||
}
|
||||
|
||||
private enum ToolCategory
|
||||
{
|
||||
None,
|
||||
Pickaxe,
|
||||
Axe,
|
||||
Shovel,
|
||||
Hoe,
|
||||
Sword,
|
||||
Shears
|
||||
}
|
||||
|
||||
private static ToolCategory GetToolCategory(ItemType item)
|
||||
{
|
||||
if (IsPickaxe(item)) return ToolCategory.Pickaxe;
|
||||
if (IsAxe(item)) return ToolCategory.Axe;
|
||||
if (IsShovel(item)) return ToolCategory.Shovel;
|
||||
if (IsHoe(item)) return ToolCategory.Hoe;
|
||||
if (IsSword(item)) return ToolCategory.Sword;
|
||||
if (item == ItemType.Shears) return ToolCategory.Shears;
|
||||
return ToolCategory.None;
|
||||
}
|
||||
|
||||
private static int GetToolTier(ItemType item)
|
||||
{
|
||||
string name = item.ToString();
|
||||
if (name.StartsWith("Wooden")) return 0;
|
||||
if (name.StartsWith("Golden")) return 0;
|
||||
if (name.StartsWith("Stone")) return 1;
|
||||
if (name.StartsWith("Iron")) return 2;
|
||||
if (name.StartsWith("Diamond")) return 3;
|
||||
if (name.StartsWith("Netherite")) return 4;
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static bool IsPickaxe(ItemType item) =>
|
||||
item is ItemType.WoodenPickaxe or ItemType.StonePickaxe or ItemType.IronPickaxe
|
||||
or ItemType.GoldenPickaxe or ItemType.DiamondPickaxe or ItemType.NetheritePickaxe;
|
||||
|
||||
private static bool IsAxe(ItemType item) =>
|
||||
item is ItemType.WoodenAxe or ItemType.StoneAxe or ItemType.IronAxe
|
||||
or ItemType.GoldenAxe or ItemType.DiamondAxe or ItemType.NetheriteAxe;
|
||||
|
||||
private static bool IsShovel(ItemType item) =>
|
||||
item is ItemType.WoodenShovel or ItemType.StoneShovel or ItemType.IronShovel
|
||||
or ItemType.GoldenShovel or ItemType.DiamondShovel or ItemType.NetheriteShovel;
|
||||
|
||||
private static bool IsHoe(ItemType item) =>
|
||||
item is ItemType.WoodenHoe or ItemType.StoneHoe or ItemType.IronHoe
|
||||
or ItemType.GoldenHoe or ItemType.DiamondHoe or ItemType.NetheriteHoe;
|
||||
|
||||
private static bool IsSword(ItemType item) =>
|
||||
item is ItemType.WoodenSword or ItemType.StoneSword or ItemType.IronSword
|
||||
or ItemType.GoldenSword or ItemType.DiamondSword or ItemType.NetheriteSword;
|
||||
|
||||
private static float GetShearsSpeed(Material block)
|
||||
{
|
||||
return block switch
|
||||
{
|
||||
Material.Cobweb => 15.0f,
|
||||
Material.Vine or Material.GlowLichen => 2.0f,
|
||||
_ when IsLeaf(block) => 15.0f,
|
||||
_ when IsWool(block) => 5.0f,
|
||||
_ => 1.0f
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsShearable(Material block) =>
|
||||
block == Material.Cobweb || IsLeaf(block) || IsWool(block) || block is Material.Vine or Material.GlowLichen;
|
||||
|
||||
private static bool IsLeaf(Material block) =>
|
||||
block is Material.OakLeaves or Material.SpruceLeaves or Material.BirchLeaves
|
||||
or Material.JungleLeaves or Material.AcaciaLeaves or Material.DarkOakLeaves
|
||||
or Material.CherryLeaves or Material.MangroveLeaves or Material.AzaleaLeaves
|
||||
or Material.FloweringAzaleaLeaves or Material.PaleOakLeaves;
|
||||
|
||||
private static bool IsWool(Material block) =>
|
||||
block is Material.WhiteWool or Material.OrangeWool or Material.MagentaWool
|
||||
or Material.LightBlueWool or Material.YellowWool or Material.LimeWool
|
||||
or Material.PinkWool or Material.GrayWool or Material.LightGrayWool
|
||||
or Material.CyanWool or Material.PurpleWool or Material.BlueWool
|
||||
or Material.BrownWool or Material.GreenWool or Material.RedWool
|
||||
or Material.BlackWool;
|
||||
|
||||
private static float GetEfficiencyBonus(Item? heldItem, Dictionary<string, double> playerAttributes, int protocolVersion)
|
||||
{
|
||||
if (protocolVersion >= Protocol18Handler.MC_1_21_11_Version
|
||||
&& playerAttributes.TryGetValue("player.mining_efficiency", out double miningEfficiency)
|
||||
&& miningEfficiency > 0.0)
|
||||
{
|
||||
return (float)miningEfficiency;
|
||||
}
|
||||
|
||||
int efficiencyLevel = GetEnchantmentLevel(heldItem, Enchantments.Efficiency, protocolVersion);
|
||||
return efficiencyLevel > 0 ? efficiencyLevel * efficiencyLevel + 1 : 0.0f;
|
||||
}
|
||||
|
||||
private static int GetDigSpeedAmplifier(Dictionary<Effects, EffectData> effects)
|
||||
{
|
||||
int amplifier = -1;
|
||||
|
||||
if (effects.TryGetValue(Effects.Haste, out var hasteData))
|
||||
amplifier = Math.Max(amplifier, hasteData.Amplifier);
|
||||
|
||||
if (effects.TryGetValue(Effects.ConduitPower, out var conduitData))
|
||||
amplifier = Math.Max(amplifier, conduitData.Amplifier);
|
||||
|
||||
return amplifier;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
@ -232,8 +232,8 @@ namespace MinecraftClient.Mapping
|
|||
int tentativeGScore = current.GScore + (int)current.Location.DistanceSquared(neighbor);
|
||||
|
||||
// If the neighbor is not in the GScoreDict OR its current tentativeGScore is lower than the previously saved one:
|
||||
if (!gScoreDict.ContainsKey(neighbor) ||
|
||||
(gScoreDict.ContainsKey(neighbor) && tentativeGScore < gScoreDict[neighbor]))
|
||||
if (!gScoreDict.TryGetValue(neighbor, out int existingGScore) ||
|
||||
tentativeGScore < existingGScore)
|
||||
{
|
||||
// Save the new relation between the neighbored block and the current one
|
||||
cameFrom[neighbor] = current.Location;
|
||||
|
|
|
|||
49
MinecraftClient/Mapping/PlayerTeam.cs
Normal file
49
MinecraftClient/Mapping/PlayerTeam.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace MinecraftClient.Mapping
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a Minecraft scoreboard team and its current state.
|
||||
/// </summary>
|
||||
public class PlayerTeam
|
||||
{
|
||||
/// <summary>Team internal name (up to 16 chars)</summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Display name component (formatted text)</summary>
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Friendly fire is allowed between team members</summary>
|
||||
public bool AllowFriendlyFire { get; set; }
|
||||
|
||||
/// <summary>Team members can see invisible teammates</summary>
|
||||
public bool SeeFriendlyInvisibles { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Nametag visibility rule.
|
||||
/// Values: "always", "hideForOtherTeams", "hideForOwnTeam", "never"
|
||||
/// </summary>
|
||||
public string NameTagVisibility { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Collision rule.
|
||||
/// Values: "always", "pushOtherTeams", "pushOwnTeam", "never"
|
||||
/// </summary>
|
||||
public string CollisionRule { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Team color as ChatFormatting enum ordinal (-1 = RESET/none,
|
||||
/// 0–15 = BLACK … WHITE).
|
||||
/// </summary>
|
||||
public int Color { get; set; } = -1;
|
||||
|
||||
/// <summary>Prefix displayed before member names (formatted text)</summary>
|
||||
public string Prefix { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Suffix displayed after member names (formatted text)</summary>
|
||||
public string Suffix { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Current set of player / entity names on this team</summary>
|
||||
public HashSet<string> Members { get; } = new(System.StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
|
@ -12,9 +12,9 @@ namespace MinecraftClient.Mapping
|
|||
{
|
||||
/// <summary>
|
||||
/// The chunks contained into the Minecraft world
|
||||
/// Tuple<int, int>: Tuple<chunkX, chunkZ>
|
||||
/// (int ChunkX, int ChunkZ): chunkX, chunkZ
|
||||
/// </summary>
|
||||
private ConcurrentDictionary<Tuple<int, int>, ChunkColumn> chunks = new();
|
||||
private ConcurrentDictionary<(int ChunkX, int ChunkZ), ChunkColumn> chunks = new();
|
||||
|
||||
/// <summary>
|
||||
/// The dimension info of the world
|
||||
|
|
@ -49,12 +49,12 @@ namespace MinecraftClient.Mapping
|
|||
{
|
||||
get
|
||||
{
|
||||
chunks.TryGetValue(new(chunkX, chunkZ), out ChunkColumn? chunkColumn);
|
||||
chunks.TryGetValue((chunkX, chunkZ), out ChunkColumn? chunkColumn);
|
||||
return chunkColumn;
|
||||
}
|
||||
set
|
||||
{
|
||||
Tuple<int, int> chunkCoord = new(chunkX, chunkZ);
|
||||
var chunkCoord = (chunkX, chunkZ);
|
||||
if (value is null)
|
||||
chunks.TryRemove(chunkCoord, out _);
|
||||
else
|
||||
|
|
@ -361,7 +361,7 @@ namespace MinecraftClient.Mapping
|
|||
/// <param name="loadCompleted">Whether the ChunkColumn has been fully loaded</param>
|
||||
public void StoreChunk(int chunkX, int chunkY, int chunkZ, int chunkColumnSize, Chunk? chunk, bool loadCompleted)
|
||||
{
|
||||
ChunkColumn chunkColumn = chunks.GetOrAdd(new(chunkX, chunkZ), (_) => new(chunkColumnSize));
|
||||
ChunkColumn chunkColumn = chunks.GetOrAdd((chunkX, chunkZ), (_) => new(chunkColumnSize));
|
||||
chunkColumn[chunkY] = chunk;
|
||||
if (loadCompleted)
|
||||
chunkColumn.FullyLoaded = true;
|
||||
|
|
|
|||
|
|
@ -44,10 +44,15 @@ namespace MinecraftClient
|
|||
|
||||
private readonly Queue<Action> threadTasks = new();
|
||||
private readonly Lock threadTasksLock = new();
|
||||
private readonly Lock recipeBookLock = new();
|
||||
private readonly Lock achievementsLock = new();
|
||||
|
||||
private readonly List<ChatBot> bots = new();
|
||||
private static readonly List<ChatBot> botsOnHold = new();
|
||||
private static readonly Dictionary<int, Container> inventories = new();
|
||||
private readonly Dictionary<string, RecipeBookRecipeEntry> unlockedRecipes = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, Achievement> achievements = new(StringComparer.Ordinal);
|
||||
private string? activeAdvancementTab;
|
||||
|
||||
private readonly Dictionary<string, List<ChatBot>> registeredBotPluginChannels = new();
|
||||
private readonly List<string> registeredServerPluginChannels = new();
|
||||
|
|
@ -105,6 +110,12 @@ namespace MinecraftClient
|
|||
|
||||
// player effects
|
||||
private readonly Dictionary<Effects, EffectData> playerEffects = new();
|
||||
|
||||
// player attributes (e.g., block_break_speed, mining_efficiency, submerged_mining_speed)
|
||||
private readonly Dictionary<string, double> playerAttributes = new();
|
||||
|
||||
// scoreboard teams (key = team name)
|
||||
private readonly Dictionary<string, PlayerTeam> teams = new(StringComparer.Ordinal);
|
||||
|
||||
// Sneaking
|
||||
public bool IsSneaking { get; set; } = false;
|
||||
|
|
@ -156,6 +167,30 @@ namespace MinecraftClient
|
|||
return new Dictionary<Effects, EffectData>(playerEffects);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a snapshot of all known scoreboard teams.
|
||||
/// </summary>
|
||||
/// <returns>Dictionary mapping team name to <see cref="PlayerTeam"/></returns>
|
||||
public Dictionary<string, PlayerTeam> GetTeams()
|
||||
{
|
||||
lock (teams)
|
||||
return new Dictionary<string, PlayerTeam>(teams, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the team that contains the given player/entity name, or <c>null</c> if not found.
|
||||
/// </summary>
|
||||
public PlayerTeam? GetPlayerTeam(string playerName)
|
||||
{
|
||||
lock (teams)
|
||||
{
|
||||
foreach (var team in teams.Values)
|
||||
if (team.Members.Contains(playerName))
|
||||
return team;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public int GetLevel() { return playerLevel; }
|
||||
public int GetTotalExperience() { return playerTotalExperience; }
|
||||
public byte GetCurrentSlot() { return CurrentSlot; }
|
||||
|
|
@ -1257,6 +1292,7 @@ namespace MinecraftClient
|
|||
inventoryHandlingEnabled = false;
|
||||
inventoryHandlingRequested = false;
|
||||
inventories.Clear();
|
||||
ClearUnlockedRecipes();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
|
@ -1358,6 +1394,54 @@ namespace MinecraftClient
|
|||
return lastEnchantment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all unlocked recipe book recipe identifiers.
|
||||
/// </summary>
|
||||
/// <returns>Unlocked recipe identifiers sorted alphabetically</returns>
|
||||
public RecipeBookRecipeEntry[] GetUnlockedRecipes()
|
||||
{
|
||||
lock (recipeBookLock)
|
||||
{
|
||||
return unlockedRecipes.Values.OrderBy(static recipe => recipe.CommandId, StringComparer.Ordinal).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all achievements/advancements known to the client.
|
||||
/// </summary>
|
||||
/// <returns>Snapshot of all achievements</returns>
|
||||
public Achievement[] GetAchievements()
|
||||
{
|
||||
lock (achievementsLock)
|
||||
{
|
||||
return [.. achievements.Values];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get only completed achievements/advancements.
|
||||
/// </summary>
|
||||
/// <returns>Snapshot of completed achievements</returns>
|
||||
public Achievement[] GetUnlockedAchievements()
|
||||
{
|
||||
lock (achievementsLock)
|
||||
{
|
||||
return achievements.Values.Where(static a => a.IsCompleted).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get only incomplete achievements/advancements.
|
||||
/// </summary>
|
||||
/// <returns>Snapshot of locked achievements</returns>
|
||||
public Achievement[] GetLockedAchievements()
|
||||
{
|
||||
lock (achievementsLock)
|
||||
{
|
||||
return achievements.Values.Where(static a => !a.IsCompleted).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all Entities
|
||||
/// </summary>
|
||||
|
|
@ -1404,6 +1488,22 @@ namespace MinecraftClient
|
|||
return GetInventory(0)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the currently active inventory if it supports recipe book crafting.
|
||||
/// </summary>
|
||||
/// <returns>Active recipe book inventory, or null if the active inventory does not support recipe book crafting</returns>
|
||||
public Container? GetActiveRecipeBookInventory()
|
||||
{
|
||||
if (InvokeRequired)
|
||||
return InvokeOnMainThread(() => GetActiveRecipeBookInventory());
|
||||
|
||||
if (inventories.Count == 0)
|
||||
return null;
|
||||
|
||||
Container activeInventory = inventories.MaxBy(static pair => pair.Key).Value;
|
||||
return SupportsRecipeBook(activeInventory.Type) ? activeInventory : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a set of online player names
|
||||
/// </summary>
|
||||
|
|
@ -1499,6 +1599,12 @@ namespace MinecraftClient
|
|||
if (String.IsNullOrEmpty(text))
|
||||
return;
|
||||
|
||||
if (!CanSendMessage)
|
||||
{
|
||||
Log.Warn(Translations.mcc_send_text_not_connected);
|
||||
return;
|
||||
}
|
||||
|
||||
int maxLength = handler.GetMaxChatMessageLength();
|
||||
|
||||
lock (chatQueue)
|
||||
|
|
@ -2516,6 +2622,7 @@ namespace MinecraftClient
|
|||
|
||||
inventories.Clear();
|
||||
inventories[0] = new Container(0, ContainerType.PlayerInventory, "Player Inventory");
|
||||
ClearUnlockedRecipes();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -2599,6 +2706,13 @@ namespace MinecraftClient
|
|||
if (lookAtBlock)
|
||||
UpdateLocation(GetCurrentLocation(), location);
|
||||
|
||||
// Auto-compute dig duration for survival/adventure mode when not explicitly supplied
|
||||
if (duration <= 0 && protocolversion >= Protocol18Handler.MC_1_8_Version
|
||||
&& gamemode is 0 or 2) // Survival or Adventure
|
||||
{
|
||||
duration = ComputeAutoDigDuration(location);
|
||||
}
|
||||
|
||||
// Send dig start and dig end, will need to wait for server response to know dig result
|
||||
// See https://wiki.vg/How_to_Write_a_Client#Digging for more details
|
||||
bool result = handler.SendPlayerDigging(0, location, blockFace, sequenceId++)
|
||||
|
|
@ -2616,6 +2730,52 @@ namespace MinecraftClient
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute the automatic dig duration in seconds for a block, based on held tool,
|
||||
/// enchantments, effects, attributes, and player state.
|
||||
/// Returns 0 for instant-break blocks.
|
||||
/// </summary>
|
||||
private double ComputeAutoDigDuration(Location location)
|
||||
{
|
||||
try
|
||||
{
|
||||
Block block = world.GetBlock(location);
|
||||
Material blockMaterial = block.Type;
|
||||
|
||||
if (blockMaterial == Material.Air)
|
||||
return 0;
|
||||
|
||||
// Get held item from player inventory
|
||||
Item? heldItem = null;
|
||||
Item? helmetItem = null;
|
||||
if (inventories.TryGetValue(0, out var playerInv))
|
||||
{
|
||||
int hotbarSlot = 36 + CurrentSlot; // Hotbar slots are 36-44
|
||||
playerInv.Items.TryGetValue(hotbarSlot, out heldItem);
|
||||
playerInv.Items.TryGetValue(5, out helmetItem); // Slot 5 = helmet
|
||||
}
|
||||
|
||||
int ticks = MiningCalculator.ComputeDigTicks(
|
||||
blockMaterial,
|
||||
heldItem,
|
||||
helmetItem,
|
||||
playerEffects,
|
||||
playerAttributes,
|
||||
playerPhysics.InWater,
|
||||
playerPhysics.OnGround,
|
||||
protocolversion);
|
||||
|
||||
if (ticks <= 0)
|
||||
return 0;
|
||||
|
||||
return (double)ticks / Settings.ClientTicksPerSecond;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Change active slot in the player inventory
|
||||
/// </summary>
|
||||
|
|
@ -2752,6 +2912,31 @@ namespace MinecraftClient
|
|||
|
||||
return handler.SendRenameItem(itemName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a recipe book craft request for the currently active crafting inventory.
|
||||
/// </summary>
|
||||
/// <param name="recipeId">Recipe identifier to craft</param>
|
||||
/// <param name="makeAll">True to craft as many items as possible</param>
|
||||
/// <returns>True if the packet was sent</returns>
|
||||
public bool SendPlaceRecipe(string recipeId, bool makeAll)
|
||||
{
|
||||
if (InvokeRequired)
|
||||
return InvokeOnMainThread(() => SendPlaceRecipe(recipeId, makeAll));
|
||||
|
||||
if (protocolversion < Protocol18Handler.MC_1_13_Version)
|
||||
return false;
|
||||
|
||||
Container? activeInventory = GetActiveRecipeBookInventory();
|
||||
if (activeInventory is null)
|
||||
return false;
|
||||
|
||||
string normalizedRecipeId = NormalizeRecipeArgument(recipeId, protocolversion);
|
||||
if (normalizedRecipeId.Length == 0)
|
||||
return false;
|
||||
|
||||
return handler.SendPlaceRecipe(activeInventory.ID, normalizedRecipeId, makeAll);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Event handlers: An event occurs on the Server
|
||||
|
|
@ -3737,6 +3922,44 @@ namespace MinecraftClient
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when an entity velocity update is received.
|
||||
/// </summary>
|
||||
/// <param name="entityID">Entity ID</param>
|
||||
/// <param name="velocityX">Velocity on X axis (blocks/tick)</param>
|
||||
/// <param name="velocityY">Velocity on Y axis (blocks/tick)</param>
|
||||
/// <param name="velocityZ">Velocity on Z axis (blocks/tick)</param>
|
||||
public void OnEntityVelocity(int entityID, double velocityX, double velocityY, double velocityZ)
|
||||
{
|
||||
if (entities.TryGetValue(entityID, out Entity? entity))
|
||||
DispatchBotEvent(bot => bot.OnEntityVelocity(entity, velocityX, velocityY, velocityZ));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a sound packet is received.
|
||||
/// </summary>
|
||||
/// <param name="soundName">Sound key when available, otherwise null</param>
|
||||
/// <param name="location">Sound location when available</param>
|
||||
/// <param name="category">Sound category id from packet</param>
|
||||
/// <param name="volume">Sound volume</param>
|
||||
/// <param name="pitch">Sound pitch</param>
|
||||
/// <param name="entityID">Source entity id for entity sound packets, if any</param>
|
||||
public void OnSoundEffect(string? soundName, Location? location, int category, float volume, float pitch,
|
||||
int? entityID)
|
||||
{
|
||||
Entity? sourceEntity = null;
|
||||
Location? resolvedLocation = location;
|
||||
|
||||
if (entityID is int id && entities.TryGetValue(id, out Entity? entity))
|
||||
{
|
||||
sourceEntity = entity;
|
||||
resolvedLocation ??= entity.Location;
|
||||
}
|
||||
|
||||
DispatchBotEvent(bot => bot.OnSoundEffect(soundName, resolvedLocation, category, volume, pitch,
|
||||
sourceEntity));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when received entity properties from server.
|
||||
/// </summary>
|
||||
|
|
@ -3746,6 +3969,9 @@ namespace MinecraftClient
|
|||
{
|
||||
if (EntityID == playerEntityID)
|
||||
{
|
||||
foreach (var kvp in prop)
|
||||
playerAttributes[kvp.Key] = kvp.Value;
|
||||
|
||||
DispatchBotEvent(bot => bot.OnPlayerProperty(prop));
|
||||
}
|
||||
}
|
||||
|
|
@ -3777,11 +4003,15 @@ namespace MinecraftClient
|
|||
{
|
||||
DateTime currentTime = DateTime.Now;
|
||||
long tickDiff = WorldAge - lastAge;
|
||||
Double tps = tickDiff / (currentTime - lastTime).TotalSeconds;
|
||||
double tps = tickDiff / (currentTime - lastTime).TotalSeconds;
|
||||
lastAge = WorldAge;
|
||||
lastTime = currentTime;
|
||||
if (tps <= 20 && tps > 0)
|
||||
if (tps > 0)
|
||||
{
|
||||
// A Minecraft server cannot genuinely exceed 20 TPS; values above 20 are
|
||||
// caused by packet-timing jitter. Clamp instead of discarding so that a
|
||||
// healthy server averages to 20 rather than being biased downward.
|
||||
tps = Math.Min(tps, 20.0);
|
||||
// calculate average tps
|
||||
if (tpsSamples.Count >= maxSamples)
|
||||
{
|
||||
|
|
@ -3951,7 +4181,78 @@ namespace MinecraftClient
|
|||
{
|
||||
DispatchBotEvent(bot => bot.OnUpdateScore(entityName, action, objectiveName, objectiveDisplayName, objectiveValue, numberFormat));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Called when a Teams packet is received. Updates the internal team state and notifies bots.
|
||||
/// </summary>
|
||||
public void OnTeam(string teamName, byte method, string displayName, byte friendlyFlags,
|
||||
string nameTagVisibility, string collisionRule, int color,
|
||||
string prefix, string suffix, List<string> players)
|
||||
{
|
||||
lock (teams)
|
||||
{
|
||||
switch (method)
|
||||
{
|
||||
case 0: // create
|
||||
var newTeam = new PlayerTeam
|
||||
{
|
||||
Name = teamName,
|
||||
DisplayName = displayName,
|
||||
AllowFriendlyFire = (friendlyFlags & 0x01) != 0,
|
||||
SeeFriendlyInvisibles = (friendlyFlags & 0x02) != 0,
|
||||
NameTagVisibility = nameTagVisibility,
|
||||
CollisionRule = collisionRule,
|
||||
Color = color,
|
||||
Prefix = prefix,
|
||||
Suffix = suffix
|
||||
};
|
||||
foreach (var p in players)
|
||||
newTeam.Members.Add(p);
|
||||
teams[teamName] = newTeam;
|
||||
break;
|
||||
|
||||
case 1: // remove
|
||||
teams.Remove(teamName);
|
||||
break;
|
||||
|
||||
case 2: // update parameters
|
||||
if (!teams.TryGetValue(teamName, out var updateTeam))
|
||||
{
|
||||
updateTeam = new PlayerTeam { Name = teamName };
|
||||
teams[teamName] = updateTeam;
|
||||
}
|
||||
updateTeam.DisplayName = displayName;
|
||||
updateTeam.AllowFriendlyFire = (friendlyFlags & 0x01) != 0;
|
||||
updateTeam.SeeFriendlyInvisibles = (friendlyFlags & 0x02) != 0;
|
||||
updateTeam.NameTagVisibility = nameTagVisibility;
|
||||
updateTeam.CollisionRule = collisionRule;
|
||||
updateTeam.Color = color;
|
||||
updateTeam.Prefix = prefix;
|
||||
updateTeam.Suffix = suffix;
|
||||
break;
|
||||
|
||||
case 3: // add players
|
||||
if (!teams.TryGetValue(teamName, out var addTeam))
|
||||
{
|
||||
addTeam = new PlayerTeam { Name = teamName };
|
||||
teams[teamName] = addTeam;
|
||||
}
|
||||
foreach (var p in players)
|
||||
addTeam.Members.Add(p);
|
||||
break;
|
||||
|
||||
case 4: // remove players
|
||||
if (teams.TryGetValue(teamName, out var removeTeam))
|
||||
foreach (var p in players)
|
||||
removeTeam.Members.Remove(p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
DispatchBotEvent(bot => bot.OnTeam(teamName, method, displayName, friendlyFlags,
|
||||
nameTagVisibility, collisionRule, color, prefix, suffix, players));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Called when the client received the Tab Header and Footer
|
||||
/// </summary>
|
||||
|
|
@ -4152,6 +4453,95 @@ namespace MinecraftClient
|
|||
Log.Debug("CanSendMessage = " + canSendMessage);
|
||||
}
|
||||
|
||||
public void OnRecipeBookAdd(RecipeBookRecipeEntry[] recipes, bool replace)
|
||||
{
|
||||
lock (recipeBookLock)
|
||||
{
|
||||
if (replace)
|
||||
unlockedRecipes.Clear();
|
||||
|
||||
foreach (RecipeBookRecipeEntry recipe in recipes)
|
||||
{
|
||||
// Guard against malformed server packets that send empty display IDs.
|
||||
if (!string.IsNullOrWhiteSpace(recipe.CommandId))
|
||||
unlockedRecipes[recipe.CommandId] = recipe;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnRecipeBookRemove(string[] recipeIds)
|
||||
{
|
||||
lock (recipeBookLock)
|
||||
{
|
||||
foreach (string recipeId in recipeIds)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(recipeId))
|
||||
unlockedRecipes.Remove(recipeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnAchievementsUpdate(IReadOnlyList<Achievement> added, IReadOnlyList<string> removedIds, bool reset)
|
||||
{
|
||||
lock (achievementsLock)
|
||||
{
|
||||
if (reset)
|
||||
achievements.Clear();
|
||||
|
||||
// Remove entries
|
||||
foreach (string id in removedIds)
|
||||
achievements.Remove(id);
|
||||
|
||||
// Add/update entries. For progress-only updates (no definition),
|
||||
// merge with existing definition if available.
|
||||
foreach (Achievement entry in added)
|
||||
{
|
||||
if (entry.Title is null && achievements.TryGetValue(entry.Id, out Achievement? existing))
|
||||
{
|
||||
// Progress-only update - merge with existing definition
|
||||
bool isCompleted = ComputeAchievementCompleted(existing.Requirements, entry.CriteriaProgress);
|
||||
achievements[entry.Id] = existing with { IsCompleted = isCompleted, CriteriaProgress = entry.CriteriaProgress };
|
||||
}
|
||||
else
|
||||
{
|
||||
achievements[entry.Id] = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DispatchBotEvent(bot => bot.OnAchievementUpdate(added, removedIds, reset));
|
||||
}
|
||||
|
||||
public void OnSelectAdvancementTab(string? tabId)
|
||||
{
|
||||
activeAdvancementTab = tabId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compute whether an achievement is completed based on AND-of-ORs requirements.
|
||||
/// </summary>
|
||||
private static bool ComputeAchievementCompleted(IReadOnlyList<IReadOnlyList<string>> requirements, IReadOnlyDictionary<string, bool> criteria)
|
||||
{
|
||||
if (requirements.Count == 0)
|
||||
return true;
|
||||
|
||||
foreach (IReadOnlyList<string> group in requirements)
|
||||
{
|
||||
bool groupSatisfied = false;
|
||||
foreach (string criterion in group)
|
||||
{
|
||||
if (criteria.TryGetValue(criterion, out bool done) && done)
|
||||
{
|
||||
groupSatisfied = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!groupSatisfied)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a click container button packet to the server.
|
||||
/// Used for Enchanting table, Lectern, stone cutter and loom
|
||||
|
|
@ -4296,6 +4686,51 @@ namespace MinecraftClient
|
|||
return ((int)blockLocation.X, (int)blockLocation.Y, (int)blockLocation.Z);
|
||||
}
|
||||
|
||||
private static bool SupportsRecipeBook(ContainerType containerType)
|
||||
{
|
||||
return containerType switch
|
||||
{
|
||||
ContainerType.PlayerInventory or
|
||||
ContainerType.Crafting or
|
||||
ContainerType.Furnace or
|
||||
ContainerType.BlastFurnace or
|
||||
ContainerType.Smoker or
|
||||
ContainerType.Stonecutter => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private void ClearUnlockedRecipes()
|
||||
{
|
||||
lock (recipeBookLock)
|
||||
{
|
||||
unlockedRecipes.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalize a recipe argument for the target protocol version.
|
||||
/// Legacy recipe-book packets use identifiers and default to the minecraft namespace.
|
||||
/// 1.21.2+ recipe-book packets use numeric recipe display ids and should be left trimmed-only.
|
||||
/// </summary>
|
||||
internal static string NormalizeRecipeArgument(string recipeId, int protocolVersion)
|
||||
{
|
||||
return protocolVersion >= Protocol18Handler.MC_1_21_2_Version
|
||||
? recipeId.Trim()
|
||||
: NormalizeRecipeId(recipeId);
|
||||
}
|
||||
|
||||
private static string NormalizeRecipeId(string recipeId)
|
||||
{
|
||||
string trimmedRecipeId = recipeId.Trim();
|
||||
if (trimmedRecipeId.Length == 0)
|
||||
return string.Empty;
|
||||
|
||||
return trimmedRecipeId.Contains(':', StringComparison.Ordinal)
|
||||
? trimmedRecipeId
|
||||
: "minecraft:" + trimmedRecipeId;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@
|
|||
<ItemGroup>
|
||||
<EmbeddedResource Include="Physics\BlockShapeData.json" LogicalName="BlockShapeData.json" />
|
||||
<EmbeddedResource Include="Mcp\Prompts\MccMcpOperatorPrompt.md" LogicalName="MccMcpOperatorPrompt.md" />
|
||||
<EmbeddedResource Include="Tui\MinimapBlockColors.json" LogicalName="MinimapBlockColors.json" />
|
||||
<EmbeddedResource Include="Tui\MinimapEntityCategories.json" LogicalName="MinimapEntityCategories.json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Protocol\Handlers\Compression\**" />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
|
@ -19,7 +20,7 @@ namespace MinecraftClient.Physics
|
|||
private static readonly Aabb[] FullBlockArray = { FullBlock };
|
||||
private static readonly Aabb[] EmptyArray = Array.Empty<Aabb>();
|
||||
|
||||
private static Dictionary<int, Aabb[]>? stateToShape;
|
||||
private static FrozenDictionary<int, Aabb[]>? stateToShape;
|
||||
private static Dictionary<string, object>? prismarineBlocks;
|
||||
private static Dictionary<int, Aabb[]>? prismarineShapes;
|
||||
|
||||
|
|
@ -136,14 +137,21 @@ namespace MinecraftClient.Physics
|
|||
|
||||
private static void BuildStateMap()
|
||||
{
|
||||
stateToShape = new Dictionary<int, Aabb[]>();
|
||||
var builder = new Dictionary<int, Aabb[]>();
|
||||
|
||||
if (prismarineBlocks is null || prismarineShapes is null)
|
||||
{
|
||||
stateToShape = builder.ToFrozenDictionary();
|
||||
return;
|
||||
}
|
||||
|
||||
var palette = Block.Palette;
|
||||
var dict = GetPaletteDict(palette);
|
||||
if (dict is null) return;
|
||||
if (dict is null)
|
||||
{
|
||||
stateToShape = builder.ToFrozenDictionary();
|
||||
return;
|
||||
}
|
||||
|
||||
// Group consecutive state IDs by Material to find state ranges per block
|
||||
var materialRanges = new Dictionary<Material, List<(int start, int end)>>();
|
||||
|
|
@ -182,20 +190,20 @@ namespace MinecraftClient.Physics
|
|||
{
|
||||
var shapes = prismarineShapes.GetValueOrDefault(singleShapeId, EmptyArray);
|
||||
for (int sid = start; sid <= end; sid++)
|
||||
stateToShape[sid] = shapes;
|
||||
builder[sid] = shapes;
|
||||
}
|
||||
else if (blockShapeData is List<int> shapeIdList)
|
||||
{
|
||||
for (int i = 0; i < stateCount && (globalStateOffset + i) < shapeIdList.Count; i++)
|
||||
{
|
||||
int shapeId = shapeIdList[globalStateOffset + i];
|
||||
stateToShape[start + i] = prismarineShapes.GetValueOrDefault(shapeId, EmptyArray);
|
||||
builder[start + i] = prismarineShapes.GetValueOrDefault(shapeId, EmptyArray);
|
||||
}
|
||||
}
|
||||
globalStateOffset += stateCount;
|
||||
}
|
||||
}
|
||||
|
||||
stateToShape = builder.ToFrozenDictionary();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ using System.Globalization;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Loader;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -161,14 +163,7 @@ namespace MinecraftClient
|
|||
}
|
||||
|
||||
if (configResult.NeedWriteDefault)
|
||||
{
|
||||
Config.Main.Advanced.Language = Settings.GetDefaultGameLanguage();
|
||||
WriteBackSettings(false);
|
||||
}
|
||||
else if (configResult.Success)
|
||||
{
|
||||
WriteBackSettings(true);
|
||||
}
|
||||
|
||||
if (!Config.Main.Advanced.EnableSentry)
|
||||
_sentrySdk?.Dispose();
|
||||
|
|
@ -181,12 +176,22 @@ namespace MinecraftClient
|
|||
};
|
||||
|
||||
// --- Determine console mode and initialize backend ---
|
||||
if (!OperatingSystem.IsWindows())
|
||||
InstallCursesNativeResolver();
|
||||
|
||||
if (!ConsoleIO.BasicIO && Config.Console.General.ConsoleMode == ConsoleModeType.tui)
|
||||
{
|
||||
ConsoleIO.Backend?.Shutdown();
|
||||
var tuiBackend = new Tui.TuiConsoleBackend();
|
||||
ConsoleIO.Backend = tuiBackend;
|
||||
tuiBackend.RunTuiMainLoop(args, startupState);
|
||||
try
|
||||
{
|
||||
var tuiBackend = new Tui.TuiConsoleBackend();
|
||||
ConsoleIO.Backend = tuiBackend;
|
||||
tuiBackend.RunTuiMainLoop(args, startupState);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
HandleTuiStartupFailure(ex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -200,9 +205,46 @@ namespace MinecraftClient
|
|||
if (!ProcessStartupState(startupState))
|
||||
return;
|
||||
|
||||
// Wait for this issue to be fixed before enabling it: https://github.com/Consolonia/Consolonia/issues/602
|
||||
// MaybePrintClassicModeTuiRecommendation();
|
||||
|
||||
RunStartupSequence(args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Consolonia's Unix.Terminal uses <c>[DllImport("libcoreclr.so")]</c> to reach
|
||||
/// <c>dlopen</c>/<c>dlsym</c> on .NET Core. The library ships a
|
||||
/// <c>SetDllImportResolver</c> that maps <c>libcoreclr.so</c> to the current
|
||||
/// process, but it is compiled under <c>#if NET6_0</c> (exact TFM match) instead
|
||||
/// of <c>NET6_0_OR_GREATER</c>, so it is dead code when the consuming project
|
||||
/// targets net8.0+. On a self-contained single-file publish the physical
|
||||
/// <c>libcoreclr.so</c> does not exist on the search path, causing a
|
||||
/// <c>DllNotFoundException</c> that crashes the TUI.
|
||||
///
|
||||
/// We work around this by registering our own resolver before any Consolonia
|
||||
/// code runs: if any assembly asks for <c>libcoreclr.so</c> we return
|
||||
/// <c>(IntPtr)(-1)</c> which the runtime interprets as "the current process".
|
||||
/// </summary>
|
||||
private static void InstallCursesNativeResolver()
|
||||
{
|
||||
AssemblyLoadContext.Default.ResolvingUnmanagedDll += (assembly, libraryName) =>
|
||||
libraryName == "libcoreclr.so" ? (IntPtr)(-1) : IntPtr.Zero;
|
||||
}
|
||||
|
||||
private static void HandleTuiStartupFailure(Exception exception)
|
||||
{
|
||||
Config.Console.General.ConsoleMode = ConsoleModeType.classic;
|
||||
WriteBackSettings(enableBackup: false);
|
||||
|
||||
ConsoleIO.Backend = new ClassicConsoleBackend();
|
||||
ConsoleIO.Backend.Init();
|
||||
|
||||
ConsoleIO.WriteLineFormatted("§c" + Translations.mcc_tui_startup_failed);
|
||||
ConsoleIO.WriteLine(exception.ToString());
|
||||
ConsoleIO.WriteLineFormatted("§e" + Translations.mcc_report_issue);
|
||||
ConsoleIO.WriteLineFormatted("§e" + Translations.mcc_tui_startup_fallback_classic);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints the application banner and processes the startup state collected before
|
||||
/// the console backend was ready. Called once from classic mode or from TUI after
|
||||
|
|
@ -211,14 +253,33 @@ namespace MinecraftClient
|
|||
/// <returns>True if startup can continue; false if config load failed and user chose to exit.</returns>
|
||||
internal static bool ProcessStartupState(StartupState state)
|
||||
{
|
||||
ConsoleIO.WriteLine($"Minecraft Console Client v{Version} - for MC {MCLowestVersion} to {MCHighestVersion} - Github.com/MCCTeam");
|
||||
if (BuildInfo is not null)
|
||||
ConsoleIO.WriteLineFormatted("§8" + BuildInfo);
|
||||
if (Config.Console.General.Display_Icon_Banner && ConsoleIO.Backend is Tui.TuiConsoleBackend tuiBanner)
|
||||
{
|
||||
var view = tuiBanner.GetView();
|
||||
if (view is not null)
|
||||
{
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var panel = Tui.MccBannerPanelBuilder.Build(BuildInfo);
|
||||
view.AppendControlToLog(panel);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowClassicBanner();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowClassicBanner();
|
||||
}
|
||||
|
||||
var cfg = state.ConfigResult;
|
||||
|
||||
if (cfg.NeedWriteDefault)
|
||||
{
|
||||
WriteBackSettings(false);
|
||||
|
||||
if (cfg.IsLegacyUpgrade)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§c" + Translations.mcc_use_new_config);
|
||||
|
|
@ -243,6 +304,8 @@ namespace MinecraftClient
|
|||
}
|
||||
else
|
||||
{
|
||||
WriteBackSettings(true);
|
||||
|
||||
if (!Config.Main.Advanced.Language.StartsWith("en"))
|
||||
ConsoleIO.WriteLine(string.Format(Translations.mcc_help_us_translate, Settings.TranslationProjectUrl));
|
||||
}
|
||||
|
|
@ -250,6 +313,26 @@ namespace MinecraftClient
|
|||
return true;
|
||||
}
|
||||
|
||||
private static void ShowClassicBanner()
|
||||
{
|
||||
ConsoleIO.WriteLine(string.Format(Translations.mcc_banner_classic, Version, MCLowestVersion, MCHighestVersion, "Github.com/MCCTeam"));
|
||||
if (BuildInfo is not null)
|
||||
ConsoleIO.WriteLineFormatted("§8" + BuildInfo);
|
||||
}
|
||||
|
||||
private static void MaybePrintClassicModeTuiRecommendation()
|
||||
{
|
||||
if (ConsoleIO.BasicIO
|
||||
|| Config.Console.General.ConsoleMode != ConsoleModeType.classic
|
||||
|| Console.IsInputRedirected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
char cmdChar = Config.Main.Advanced.InternalCmdChar.ToChar();
|
||||
ConsoleIO.WriteLineFormatted("§8" + string.Format(Translations.mcc_console_mode_tui_recommendation, cmdChar));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a failed config load by prompting the user to fix or regenerate the config file.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -411,6 +411,37 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
return ReadNextNbt(cache, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an ItemStackTemplate (26.1+) from a cache of bytes.
|
||||
/// Unlike ItemStack, this uses item-first encoding: item_id, count, DataComponentPatch.
|
||||
/// ItemStackTemplate is always non-empty (no count=0 sentinel).
|
||||
/// </summary>
|
||||
public Item ReadNextItemStackTemplate(Queue<byte> cache, ItemPalette itemPalette)
|
||||
{
|
||||
var itemId = ReadNextVarInt(cache);
|
||||
var itemCount = ReadNextVarInt(cache);
|
||||
var item = new Item(itemPalette.FromId(itemId), itemCount, null);
|
||||
|
||||
var numberOfComponentsToAdd = ReadNextVarInt(cache);
|
||||
var numberofComponentsToRemove = ReadNextVarInt(cache);
|
||||
var structuredComponentHandler = new StructuredComponentsHandler(protocolversion, this, itemPalette);
|
||||
var strcturedComponentsToAdd = new List<StructuredComponent>(numberOfComponentsToAdd);
|
||||
|
||||
for (var i = 0; i < numberOfComponentsToAdd; i++)
|
||||
{
|
||||
var componentTypeId = ReadNextVarInt(cache);
|
||||
strcturedComponentsToAdd.Add(structuredComponentHandler.Parse(componentTypeId, cache));
|
||||
}
|
||||
|
||||
for (var i = 0; i < numberofComponentsToRemove; i++)
|
||||
ReadNextVarInt(cache);
|
||||
|
||||
if (strcturedComponentsToAdd.Count > 0)
|
||||
item.Components = strcturedComponentsToAdd;
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read a single item slot from a cache of bytes and remove it from the cache
|
||||
/// </summary>
|
||||
|
|
@ -664,8 +695,10 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
}
|
||||
}
|
||||
|
||||
return new Entity(entityID, entityType, new Location(entityX, entityY, entityZ), entityYaw, entityPitch,
|
||||
var entity = new Entity(entityID, entityType, new Location(entityX, entityY, entityZ), entityYaw, entityPitch,
|
||||
data);
|
||||
entity.UUID = entityUUID;
|
||||
return entity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1021,20 +1054,44 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
}
|
||||
}
|
||||
|
||||
private static bool HasLpVec3Continuation(int firstByte) => (firstByte & 4) == 4;
|
||||
|
||||
private static double UnpackLpVec3(long packedAxis)
|
||||
{
|
||||
return Math.Min((double)(packedAxis & 32767L), 32766.0) * 2.0 / 32766.0 - 1.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an LpVec3 (low-precision vec3) from the cache (1.21.9+).
|
||||
/// Variable-length encoding: first byte 0 = zero vector; otherwise
|
||||
/// 2 bytes + 4 bytes (6 total), plus an optional VarInt continuation.
|
||||
/// Read and decode an LpVec3 (low-precision vec3) from the cache (1.21.9+).
|
||||
/// Returned vector is expressed in blocks per tick.
|
||||
/// </summary>
|
||||
public void ReadNextLpVec3(Queue<byte> cache)
|
||||
public (double X, double Y, double Z) ReadNextLpVec3Values(Queue<byte> cache)
|
||||
{
|
||||
int first = ReadNextByte(cache);
|
||||
if (first == 0)
|
||||
return;
|
||||
ReadNextByte(cache); // second byte
|
||||
ReadData(4, cache); // uint32
|
||||
if ((first & 4) == 4) // continuation bit set
|
||||
ReadNextVarInt(cache);
|
||||
return (0.0, 0.0, 0.0);
|
||||
|
||||
int second = ReadNextByte(cache);
|
||||
uint high = (uint)ReadNextInt(cache);
|
||||
long packed = ((long)high << 16) | (long)(second << 8) | (uint)first;
|
||||
|
||||
long scale = first & 3;
|
||||
if (HasLpVec3Continuation(first))
|
||||
scale |= ((long)ReadNextVarInt(cache) & 0xFFFFFFFFL) << 2;
|
||||
|
||||
return (
|
||||
UnpackLpVec3(packed >> 3) * scale,
|
||||
UnpackLpVec3(packed >> 18) * scale,
|
||||
UnpackLpVec3(packed >> 33) * scale
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read an LpVec3 (low-precision vec3) from the cache (1.21.9+) and discard it.
|
||||
/// </summary>
|
||||
public void ReadNextLpVec3(Queue<byte> cache)
|
||||
{
|
||||
ReadNextLpVec3Values(cache);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -811,6 +811,11 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
return false; //Currently not implemented
|
||||
}
|
||||
|
||||
public bool SendPlaceRecipe(int windowId, string recipeId, bool makeAll)
|
||||
{
|
||||
return false; //MC 1.8-1.12.1 recipe book not supported
|
||||
}
|
||||
|
||||
public bool SendCloseWindow(int windowId)
|
||||
{
|
||||
return false; //Currently not implemented
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -190,6 +190,15 @@ namespace MinecraftClient.Protocol
|
|||
|
||||
bool ClickContainerButton(int windowId, int buttonId);
|
||||
|
||||
/// <summary>
|
||||
/// Send a place recipe packet to the server for the active recipe book container.
|
||||
/// </summary>
|
||||
/// <param name="windowId">Id of the window being clicked</param>
|
||||
/// <param name="recipeId">Recipe identifier to craft</param>
|
||||
/// <param name="makeAll">True to craft as many items as possible</param>
|
||||
/// <returns>True if packet was successfully sent</returns>
|
||||
bool SendPlaceRecipe(int windowId, string recipeId, bool makeAll);
|
||||
|
||||
/// <summary>
|
||||
/// Plays animation
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -295,6 +295,16 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="onGround">TRUE if on ground</param>
|
||||
void OnEntityTeleport(int entityID, Double x, Double y, Double z, bool onGround);
|
||||
|
||||
/// <summary>
|
||||
/// Called when an entity velocity update packet is received.
|
||||
/// Velocity values are in blocks per tick.
|
||||
/// </summary>
|
||||
/// <param name="entityID">Entity ID</param>
|
||||
/// <param name="velocityX">Velocity X</param>
|
||||
/// <param name="velocityY">Velocity Y</param>
|
||||
/// <param name="velocityZ">Velocity Z</param>
|
||||
void OnEntityVelocity(int entityID, double velocityX, double velocityY, double velocityZ);
|
||||
|
||||
/// <summary>
|
||||
/// Called when additional properties have been received for an entity
|
||||
/// </summary>
|
||||
|
|
@ -371,6 +381,17 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="affectedBlocks">Amount of affected blocks</param>
|
||||
void OnExplosion(Location location, float strength, int affectedBlocks);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a sound packet is received.
|
||||
/// </summary>
|
||||
/// <param name="soundName">Sound key if available, otherwise null</param>
|
||||
/// <param name="location">Sound location for world sounds, or null if unavailable</param>
|
||||
/// <param name="category">Sound category id</param>
|
||||
/// <param name="volume">Sound volume</param>
|
||||
/// <param name="pitch">Sound pitch</param>
|
||||
/// <param name="entityID">Source entity id for entity-sound packets, if any</param>
|
||||
void OnSoundEffect(string? soundName, Location? location, int category, float volume, float pitch, int? entityID);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a player's game mode has changed
|
||||
/// </summary>
|
||||
|
|
@ -468,6 +489,23 @@ namespace MinecraftClient.Protocol
|
|||
/// <param name="numberFormat">Number format: 0 - blank, 1 - styled, 2 - fixed</param>
|
||||
void OnUpdateScore(string entityName, int action, string objectiveName, string objectiveDisplayName, int objectiveValue, int numberFormat);
|
||||
|
||||
/// <summary>
|
||||
/// Called when a Teams packet is received from the server.
|
||||
/// </summary>
|
||||
/// <param name="teamName">Internal team name (up to 16 chars)</param>
|
||||
/// <param name="method">0=create, 1=remove, 2=update, 3=add players, 4=remove players</param>
|
||||
/// <param name="displayName">Display name (formatted). Present when method is 0 or 2.</param>
|
||||
/// <param name="friendlyFlags">Bit 0=allowFriendlyFire, bit 1=seeFriendlyInvisibles. Present when method is 0 or 2.</param>
|
||||
/// <param name="nameTagVisibility">Nametag visibility rule string. Present when method is 0 or 2.</param>
|
||||
/// <param name="collisionRule">Collision rule string. Present when method is 0 or 2.</param>
|
||||
/// <param name="color">ChatFormatting color value (-1=none). Present when method is 0 or 2.</param>
|
||||
/// <param name="prefix">Member name prefix (formatted). Present when method is 0 or 2.</param>
|
||||
/// <param name="suffix">Member name suffix (formatted). Present when method is 0 or 2.</param>
|
||||
/// <param name="players">Player/entity names. Present when method is 0, 3, or 4.</param>
|
||||
void OnTeam(string teamName, byte method, string displayName, byte friendlyFlags,
|
||||
string nameTagVisibility, string collisionRule, int color,
|
||||
string prefix, string suffix, List<string> players);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the client received the Tab Header and Footer
|
||||
/// </summary>
|
||||
|
|
@ -524,6 +562,33 @@ namespace MinecraftClient.Protocol
|
|||
|
||||
public void SetCanSendMessage(bool canSendMessage);
|
||||
|
||||
/// <summary>
|
||||
/// Called when recipe book recipes are added or replaced.
|
||||
/// </summary>
|
||||
/// <param name="recipes">Recipe entries to add</param>
|
||||
/// <param name="replace">True to replace the currently tracked recipe book entries</param>
|
||||
public void OnRecipeBookAdd(RecipeBookRecipeEntry[] recipes, bool replace);
|
||||
|
||||
/// <summary>
|
||||
/// Called when recipe book recipes are removed.
|
||||
/// </summary>
|
||||
/// <param name="recipeIds">Recipe identifiers to remove</param>
|
||||
public void OnRecipeBookRemove(string[] recipeIds);
|
||||
|
||||
/// <summary>
|
||||
/// Called when achievement/advancement data is received from the server.
|
||||
/// </summary>
|
||||
/// <param name="added">Achievements that were added or updated</param>
|
||||
/// <param name="removedIds">IDs of achievements that were removed</param>
|
||||
/// <param name="reset">True if all existing state should be cleared before applying</param>
|
||||
public void OnAchievementsUpdate(IReadOnlyList<Achievement> added, IReadOnlyList<string> removedIds, bool reset);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the server selects an advancement tab.
|
||||
/// </summary>
|
||||
/// <param name="tabId">The tab identifier, or null if no tab is selected</param>
|
||||
public void OnSelectAdvancementTab(string? tabId);
|
||||
|
||||
/// <summary>
|
||||
/// Send a click container button packet to the server.
|
||||
/// Used for Enchanting table, Lectern, stone cutter and loom
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using System.Linq;
|
|||
using System.Net.Http;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using DnsClient;
|
||||
using MinecraftClient.Protocol.Handlers;
|
||||
using MinecraftClient.Protocol.Handlers.Forge;
|
||||
|
|
@ -388,6 +389,61 @@ namespace MinecraftClient.Protocol
|
|||
}
|
||||
}
|
||||
|
||||
private static readonly Regex VersionTokenRegex = new(@"\d+\.\d+(?:\.\d+)?", RegexOptions.Compiled);
|
||||
|
||||
private static readonly int[] SupportedProtocols18 =
|
||||
[
|
||||
4, 5, 47, 107, 108, 109, 110, 210, 315, 316, 335, 338, 340, 393, 401, 404,
|
||||
477, 480, 485, 490, 498, 573, 575, 578, 735, 736, 751, 753, 754, 755, 756,
|
||||
757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771,
|
||||
772, 773, 774, 775
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// For multi-version servers (e.g. "Requires MC 1.8 / 1.21"), try to find the
|
||||
/// highest protocol version that both the server and MCC support.
|
||||
/// Returns true if the protocol was upgraded, with the new value in
|
||||
/// <paramref name="protocolVersion"/>.
|
||||
/// </summary>
|
||||
public static bool TryUpgradeProtocolVersion(string versionName, ref int protocolVersion)
|
||||
{
|
||||
if (string.IsNullOrEmpty(versionName))
|
||||
return false;
|
||||
|
||||
var matches = VersionTokenRegex.Matches(versionName);
|
||||
if (matches.Count < 2)
|
||||
return false;
|
||||
|
||||
int bestProtocol = protocolVersion;
|
||||
string bestVersion = "";
|
||||
|
||||
foreach (Match m in matches)
|
||||
{
|
||||
int proto = MCVer2ProtocolVersion(m.Value);
|
||||
if (proto <= 0)
|
||||
continue;
|
||||
if (Array.IndexOf(SupportedProtocols18, proto) < 0)
|
||||
continue;
|
||||
if (proto > bestProtocol)
|
||||
{
|
||||
bestProtocol = proto;
|
||||
bestVersion = m.Value;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestProtocol > protocolVersion && bestVersion.Length > 0)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted("§8" + string.Format(
|
||||
Translations.mcc_server_info_version_upgrade,
|
||||
ProtocolVersion2MCVer(protocolVersion), protocolVersion,
|
||||
"§a" + bestVersion + "§8", bestProtocol));
|
||||
protocolVersion = bestProtocol;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a network protocol version number to human-readable Minecraft version number
|
||||
/// </summary>
|
||||
|
|
|
|||
119
MinecraftClient/Protocol/ServerStatusDisplay.cs
Normal file
119
MinecraftClient/Protocol/ServerStatusDisplay.cs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
using System;
|
||||
using System.Text;
|
||||
using MinecraftClient.Protocol.Message;
|
||||
using MinecraftClient.Scripting;
|
||||
|
||||
namespace MinecraftClient.Protocol
|
||||
{
|
||||
internal static class ServerStatusDisplay
|
||||
{
|
||||
private const int MaxSamplePlayers = 10;
|
||||
|
||||
internal static void Show(ServerStatusInfo info)
|
||||
{
|
||||
if (ConsoleIO.Backend is Tui.TuiConsoleBackend tuiBackend)
|
||||
ShowTui(info, tuiBackend);
|
||||
else
|
||||
ShowClassic(info);
|
||||
}
|
||||
|
||||
private static void ShowClassic(ServerStatusInfo info)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine();
|
||||
sb.Append("§8§m");
|
||||
sb.Append(new string('-', 50));
|
||||
sb.AppendLine("§r");
|
||||
|
||||
if (!string.IsNullOrEmpty(info.MotdRaw))
|
||||
{
|
||||
try
|
||||
{
|
||||
sb.AppendLine(ChatParser.ParseText(info.MotdRaw));
|
||||
}
|
||||
catch
|
||||
{
|
||||
sb.AppendLine(info.MotdRaw);
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append("§f");
|
||||
sb.Append(Translations.mcc_server_info_label_server);
|
||||
sb.Append(" §b");
|
||||
sb.Append(info.Host);
|
||||
sb.Append("§7:§b");
|
||||
sb.AppendLine(info.Port.ToString());
|
||||
|
||||
sb.Append("§f");
|
||||
sb.Append(Translations.mcc_server_info_label_version);
|
||||
sb.Append(" §b");
|
||||
sb.Append(ChatBot.GetVerbatim(info.VersionName));
|
||||
sb.Append(" §7(");
|
||||
sb.Append(string.Format(Translations.mcc_server_info_label_protocol, "§e" + info.ProtocolVersion + "§7"));
|
||||
sb.AppendLine(")");
|
||||
|
||||
if (info.ResolvedProtocol != 0)
|
||||
{
|
||||
string resolvedMcVer = ProtocolHandler.ProtocolVersion2MCVer(info.ResolvedProtocol);
|
||||
sb.Append("§f");
|
||||
sb.Append(Translations.mcc_server_info_label_connecting_as);
|
||||
sb.Append(" §a");
|
||||
sb.Append(resolvedMcVer);
|
||||
sb.Append(" §7(");
|
||||
sb.Append(string.Format(Translations.mcc_server_info_label_protocol, "§a" + info.ResolvedProtocol + "§7"));
|
||||
sb.AppendLine(")");
|
||||
}
|
||||
|
||||
if (info.PingMs >= 0)
|
||||
{
|
||||
sb.Append("§f");
|
||||
sb.Append(Translations.mcc_server_info_label_ping);
|
||||
sb.Append(" §a");
|
||||
sb.AppendLine(string.Format(Translations.mcc_server_info_label_ping_ms, info.PingMs));
|
||||
}
|
||||
|
||||
sb.Append("§f");
|
||||
sb.Append(Translations.mcc_server_info_label_players);
|
||||
sb.Append(" §a");
|
||||
sb.Append(info.OnlinePlayers);
|
||||
sb.Append("§7/§c");
|
||||
sb.AppendLine(info.MaxPlayers.ToString());
|
||||
|
||||
if (info.SamplePlayers.Count > 0)
|
||||
{
|
||||
sb.Append("§f");
|
||||
sb.AppendLine(Translations.mcc_server_info_label_online);
|
||||
|
||||
int shown = Math.Min(info.SamplePlayers.Count, MaxSamplePlayers);
|
||||
for (int i = 0; i < shown; i++)
|
||||
sb.AppendLine($" §a{info.SamplePlayers[i].Name}");
|
||||
|
||||
if (info.SamplePlayers.Count > shown)
|
||||
sb.AppendLine($" §7{string.Format(Translations.mcc_server_info_sample_more, info.SamplePlayers.Count - shown)}");
|
||||
}
|
||||
|
||||
sb.Append("§8§m");
|
||||
sb.Append(new string('-', 50));
|
||||
sb.Append("§r");
|
||||
|
||||
ConsoleIO.WriteLineFormatted(sb.ToString(), acceptnewlines: true);
|
||||
}
|
||||
|
||||
private static void ShowTui(ServerStatusInfo info, Tui.TuiConsoleBackend backend)
|
||||
{
|
||||
var view = backend.GetView();
|
||||
if (view is null)
|
||||
{
|
||||
ShowClassic(info);
|
||||
return;
|
||||
}
|
||||
|
||||
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var panel = Tui.ServerStatusPanelBuilder.Build(info);
|
||||
view.AppendControlToLog(panel);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
30
MinecraftClient/Protocol/ServerStatusInfo.cs
Normal file
30
MinecraftClient/Protocol/ServerStatusInfo.cs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MinecraftClient.Protocol
|
||||
{
|
||||
/// <summary>
|
||||
/// Holds the structured result of a Minecraft server status (SLP) ping,
|
||||
/// including MOTD, player counts, sample player list, version, and favicon.
|
||||
/// </summary>
|
||||
public sealed class ServerStatusInfo
|
||||
{
|
||||
public string Host { get; init; } = string.Empty;
|
||||
public int Port { get; init; }
|
||||
public string VersionName { get; init; } = string.Empty;
|
||||
public int ProtocolVersion { get; init; }
|
||||
public int ResolvedProtocol { get; set; }
|
||||
public int OnlinePlayers { get; init; }
|
||||
public int MaxPlayers { get; init; }
|
||||
public List<SamplePlayer> SamplePlayers { get; init; } = [];
|
||||
public string MotdRaw { get; init; } = string.Empty;
|
||||
public string? FaviconBase64 { get; init; }
|
||||
public long PingMs { get; init; }
|
||||
|
||||
public sealed class SamplePlayer
|
||||
{
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public string Id { get; init; } = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
4
MinecraftClient/RecipeBookRecipeEntry.cs
Normal file
4
MinecraftClient/RecipeBookRecipeEntry.cs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
namespace MinecraftClient
|
||||
{
|
||||
public readonly record struct RecipeBookRecipeEntry(string CommandId, string DisplayText);
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -311,6 +311,21 @@ You can use "/fish" to control the bot manually.
|
|||
<data name="ChatBot.AutoFishing.Hook_Threshold" xml:space="preserve">
|
||||
<value>A "stationary" hook that moves above this threshold in the Y-axis will be considered to have caught a fish.</value>
|
||||
</data>
|
||||
<data name="ChatBot.AutoFishing.Enable_Velocity_Detection" xml:space="preserve">
|
||||
<value>Enable fish bite detection using fishing bobber velocity packets.</value>
|
||||
</data>
|
||||
<data name="ChatBot.AutoFishing.Velocity_Hook_Threshold" xml:space="preserve">
|
||||
<value>Velocity Y threshold (blocks/tick). Values below this are treated as a bite. Keep this value negative.</value>
|
||||
</data>
|
||||
<data name="ChatBot.AutoFishing.Enable_Sound_Detection" xml:space="preserve">
|
||||
<value>Enable fish bite detection using splash sounds near the fishing bobber.</value>
|
||||
</data>
|
||||
<data name="ChatBot.AutoFishing.Sound_Distance" xml:space="preserve">
|
||||
<value>Maximum distance (blocks) between splash sound and bobber to treat it as a bite.</value>
|
||||
</data>
|
||||
<data name="ChatBot.AutoFishing.Detection_Warmup" xml:space="preserve">
|
||||
<value>Delay (seconds) after bobber spawn before bite detection starts. Helps ignore cast-entry splash/motion.</value>
|
||||
</data>
|
||||
<data name="ChatBot.AutoFishing.Log_Fish_Bobber" xml:space="preserve">
|
||||
<value>Used to adjust the above two thresholds, which when enabled will print the change in the position of the fishhook entity upon receipt of its movement packet.</value>
|
||||
</data>
|
||||
|
|
@ -393,6 +408,12 @@ For Discord message formatting, check the following: https://mccteam.github.io/r
|
|||
<data name="ChatBot.DiscordBridge.AllowOtherBotMessages" xml:space="preserve">
|
||||
<value>When enabled, messages from other Discord bots in the channel will be relayed to Minecraft chat. The bridge always ignores its own messages to prevent loops.</value>
|
||||
</data>
|
||||
<data name="ChatBot.DiscordBridge.RelayAllMessages" xml:space="preserve">
|
||||
<value>When enabled, all text received from the Minecraft server (including system messages, join/leave notifications, etc.) will be relayed to Discord, not just player chat and private messages.</value>
|
||||
</data>
|
||||
<data name="ChatBot.DiscordBridge.MessageAggregationInterval" xml:space="preserve">
|
||||
<value>Interval in seconds to aggregate messages before sending them to Discord. When set to 0, messages are sent immediately one by one. When set to a value like 1.0, messages received within that interval are batched into a single Discord message. Useful for reducing Discord API rate limits.</value>
|
||||
</data>
|
||||
<data name="ChatBot.Farmer" xml:space="preserve">
|
||||
<value>Automatically farms crops for you (plants, breaks and bonemeals them).
|
||||
Crop types available: Beetroot, Carrot, Melon, Netherwart, Pumpkin, Potato, Wheat.
|
||||
|
|
@ -560,9 +581,18 @@ Custom colors are only available when using "vt100_24bit" color mode.</value>
|
|||
<data name="Console.General.ConsoleColorMode" xml:space="preserve">
|
||||
<value>Use "disable", "legacy_4bit", "vt100_4bit", "vt100_8bit" or "vt100_24bit". If a garbled code like "←[0m" appears on the terminal, you can try switching to "legacy_4bit" mode, or just disable it.</value>
|
||||
</data>
|
||||
<data name="Console.General.Display_Icon_Banner" xml:space="preserve">
|
||||
<value>Whether to display the MCC startup icon banner.</value>
|
||||
</data>
|
||||
<data name="Console.General.Display_Input" xml:space="preserve">
|
||||
<value>You can use "Ctrl+P" to print out the current input and cursor position.</value>
|
||||
</data>
|
||||
<data name="Console.General.History_Input_Records" xml:space="preserve">
|
||||
<value>Maximum number of input history records to keep.</value>
|
||||
</data>
|
||||
<data name="Console.General.TUI_Log_Scrollback" xml:space="preserve">
|
||||
<value>Maximum log lines kept in TUI mode scrollback. Set to 0 for automatic.</value>
|
||||
</data>
|
||||
<data name="Head" xml:space="preserve">
|
||||
<value>Startup Config File
|
||||
Please do not record extraneous data in this file as it will be overwritten by MCC.
|
||||
|
|
@ -933,6 +963,42 @@ Note: This does NOT require a Bot Token, only an Application ID. Discord must be
|
|||
<data name="Main.Advanced.enable_sentry" xml:space="preserve">
|
||||
<value>Set to false to opt-out of Sentry error logging.</value>
|
||||
</data>
|
||||
<data name="Console.Minimap" xml:space="preserve">
|
||||
<value>Settings for the TUI minimap overlay that shows terrain and entities.</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.Enabled" xml:space="preserve">
|
||||
<value>Whether the minimap is visible on startup in TUI mode.</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.Zoom" xml:space="preserve">
|
||||
<value>Blocks per pixel, 1-16. 1 = closest (1:1), 16 = farthest (16 blocks per pixel).</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.Width" xml:space="preserve">
|
||||
<value>Map width in pixels (characters). Range 10-120, default 40.</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.Height" xml:space="preserve">
|
||||
<value>Map height in pixels (must be even, uses half-block chars). Range 4-80, default 40.</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.Position" xml:space="preserve">
|
||||
<value>Minimap position: "top_left", "top_right", "center", "bottom_left", or "bottom_right".</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.ShowPlayerNames" xml:space="preserve">
|
||||
<value>Show player names on the minimap.</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.ShowHostileNames" xml:space="preserve">
|
||||
<value>Show hostile mob names on the minimap.</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.ShowNeutralNames" xml:space="preserve">
|
||||
<value>Show neutral mob names on the minimap.</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.ShowPassiveNames" xml:space="preserve">
|
||||
<value>Show passive mob names on the minimap.</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.RefreshInterval" xml:space="preserve">
|
||||
<value>Minimap refresh interval in milliseconds (100-5000).</value>
|
||||
</data>
|
||||
<data name="Console.Minimap.CaveMode" xml:space="preserve">
|
||||
<value>Cave rendering mode: "auto" (detect ceiling), "on" (always cave view), "off" (always surface view).</value>
|
||||
</data>
|
||||
<data name="Main.General.AuthlibUser" xml:space="preserve">
|
||||
<value>Yggdrasil authlib multi-user selection.</value>
|
||||
</data>
|
||||
|
|
|
|||
|
|
@ -437,6 +437,15 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Dropped low durability {0} from slot {1}..
|
||||
/// </summary>
|
||||
internal static string bot_autodig_drop_low_durability {
|
||||
get {
|
||||
return ResourceManager.GetString("bot.autodig.drop_low_durability", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The block currently pointed to is not in the allowed list..
|
||||
/// </summary>
|
||||
|
|
@ -473,6 +482,15 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Switch to {0} from slot {1}..
|
||||
/// </summary>
|
||||
internal static string bot_autodig_switch {
|
||||
get {
|
||||
return ResourceManager.GetString("bot.autodig.switch", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Added item {0}.
|
||||
/// </summary>
|
||||
|
|
@ -879,6 +897,24 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Waiting {0:0.000} seconds before reconnecting... ({1} retries left).
|
||||
/// </summary>
|
||||
internal static string bot_autoRelog_wait_with_retries {
|
||||
get {
|
||||
return ResourceManager.GetString("bot.autoRelog.wait_with_retries", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to unlimited.
|
||||
/// </summary>
|
||||
internal static string bot_autoRelog_retries_unlimited {
|
||||
get {
|
||||
return ResourceManager.GetString("bot.autoRelog.retries_unlimited", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to File not found: '{0}'.
|
||||
/// </summary>
|
||||
|
|
@ -2269,6 +2305,78 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
internal static string mcc_banner_classic {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.banner.classic", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string mcc_banner_label_mc_versions {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.banner.label_mc_versions", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string mcc_server_info_label_server {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.server_info.label_server", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string mcc_server_info_label_version {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.server_info.label_version", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string mcc_server_info_label_protocol {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.server_info.label_protocol", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string mcc_server_info_label_players {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.server_info.label_players", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string mcc_server_info_label_ping {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.server_info.label_ping", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string mcc_server_info_label_ping_ms {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.server_info.label_ping_ms", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string mcc_server_info_label_connecting_as {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.server_info.label_connecting_as", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string mcc_server_info_label_online {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.server_info.label_online", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string mcc_server_info_sample_more {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.server_info.sample_more", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
internal static string mcc_server_info_version_upgrade {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.server_info.version_upgrade", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Converting session cache from disk: {0}.
|
||||
/// </summary>
|
||||
|
|
@ -3558,6 +3666,51 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to quickly enable recommended features..
|
||||
/// </summary>
|
||||
internal static string cmd_tryout_desc {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.tryout.desc", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Available quick actions:.
|
||||
/// </summary>
|
||||
internal static string cmd_tryout_list_header {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.tryout.list.header", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to tui: set [Console.General] ConsoleMode = "tui" for the next restart..
|
||||
/// </summary>
|
||||
internal static string cmd_tryout_list_tui {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.tryout.list.tui", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to [Console.General] ConsoleMode is already "tui" in the config. To switch back, set [Console.General] ConsoleMode = "classic". Restart MCC after changing it for the new mode to take effect..
|
||||
/// </summary>
|
||||
internal static string cmd_tryout_tui_already_enabled {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.tryout.tui.already_enabled", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Updated [Console.General] ConsoleMode from "{0}" to "{1}" in the config. To switch back, set [Console.General] ConsoleMode = "classic". Restart MCC to apply the change..
|
||||
/// </summary>
|
||||
internal static string cmd_tryout_tui_enabled {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.tryout.tui.enabled", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Display Health and Food saturation..
|
||||
/// </summary>
|
||||
|
|
@ -4254,6 +4407,87 @@ namespace MinecraftClient {
|
|||
return ResourceManager.GetString("cmd.nameitem.successful", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Failed to send recipe book craft request for {0}..
|
||||
/// </summary>
|
||||
internal static string cmd_recipebook_craft_failed {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.recipebook.craft.failed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Requested recipe {0}..
|
||||
/// </summary>
|
||||
internal static string cmd_recipebook_craft_sent {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.recipebook.craft.sent", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Requested recipe {0} with craft-all..
|
||||
/// </summary>
|
||||
internal static string cmd_recipebook_craftall_sent {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.recipebook.craftall.sent", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to List unlocked recipe book recipes and craft them through the active recipe book inventory..
|
||||
/// </summary>
|
||||
internal static string cmd_recipebook_desc {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.recipebook.desc", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Unlocked recipe book recipes.
|
||||
/// </summary>
|
||||
internal static string cmd_recipebook_list {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.recipebook.list", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to You need an active player crafting grid, crafting table, furnace, blast furnace, smoker, or stonecutter inventory..
|
||||
/// </summary>
|
||||
internal static string cmd_recipebook_no_active_inventory {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.recipebook.no.active.inventory", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No unlocked recipe book recipes are currently tracked..
|
||||
/// </summary>
|
||||
internal static string cmd_recipebook_no_recipes {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.recipebook.no.recipes", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The recipe identifier cannot be empty..
|
||||
/// </summary>
|
||||
internal static string cmd_recipebook_recipe_id_empty {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.recipebook.recipe.id.empty", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Recipe book crafting is only supported on Minecraft 1.13 and newer..
|
||||
/// </summary>
|
||||
internal static string cmd_recipebook_unsupported {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.recipebook.unsupported", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to restart and reconnect to the server..
|
||||
|
|
@ -4462,6 +4696,51 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to List all scoreboard teams and their members.
|
||||
/// </summary>
|
||||
internal static string cmd_teams_desc {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.teams.desc", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No teams are currently tracked.
|
||||
/// </summary>
|
||||
internal static string cmd_teams_no_teams {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.teams.no_teams", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Team '{0}' (display: {1}, ...).
|
||||
/// </summary>
|
||||
internal static string cmd_teams_team_header {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.teams.team_header", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Members ({0}): {1}.
|
||||
/// </summary>
|
||||
internal static string cmd_teams_team_members {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.teams.team_members", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No members.
|
||||
/// </summary>
|
||||
internal static string cmd_teams_team_no_members {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.teams.team_no_members", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Place a block or open chest.
|
||||
/// </summary>
|
||||
|
|
@ -5534,6 +5813,42 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Tip: try TUI mode for a cleaner interface, mouse-friendly container actions, and a nicer layout. Run {0}feature tui§8 to switch [Console.General] ConsoleMode to "tui" for the next restart..
|
||||
/// </summary>
|
||||
internal static string mcc_console_mode_tui_recommendation {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.console_mode_tui_recommendation", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to MCC encountered a problem while starting TUI mode..
|
||||
/// </summary>
|
||||
internal static string mcc_tui_startup_failed {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.tui_startup_failed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to As a fallback, MCC has automatically switched [Console.General] ConsoleMode to "classic". This will take effect after you restart MCC..
|
||||
/// </summary>
|
||||
internal static string mcc_tui_startup_fallback_classic {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.tui_startup_fallback_classic", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Please report this issue to the MCC Team..
|
||||
/// </summary>
|
||||
internal static string mcc_report_issue {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.report_issue", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to To sign in, open {0} in your browser and enter the code: {1}.
|
||||
/// </summary>
|
||||
|
|
@ -5861,6 +6176,15 @@ namespace MinecraftClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Cannot send text: not connected to a server..
|
||||
/// </summary>
|
||||
internal static string mcc_send_text_not_connected {
|
||||
get {
|
||||
return ResourceManager.GetString("mcc.send_text_not_connected", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Waiting {0} seconds before restarting....
|
||||
/// </summary>
|
||||
|
|
@ -6799,7 +7123,7 @@ namespace MinecraftClient {
|
|||
return ResourceManager.GetString("tui.crafting.grid", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Starting embedded MCP server....
|
||||
/// </summary>
|
||||
|
|
@ -6808,7 +7132,7 @@ namespace MinecraftClient {
|
|||
return ResourceManager.GetString("bot.mcpserver.starting", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Embedded MCP server started on {0}.
|
||||
/// </summary>
|
||||
|
|
@ -6817,7 +7141,7 @@ namespace MinecraftClient {
|
|||
return ResourceManager.GetString("bot.mcpserver.started", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Failed to start embedded MCP server: {0}.
|
||||
/// </summary>
|
||||
|
|
@ -6826,7 +7150,7 @@ namespace MinecraftClient {
|
|||
return ResourceManager.GetString("bot.mcpserver.start_failed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Embedded MCP auth token is required but environment variable {0} is empty..
|
||||
/// </summary>
|
||||
|
|
@ -6835,7 +7159,7 @@ namespace MinecraftClient {
|
|||
return ResourceManager.GetString("bot.mcpserver.missing_auth_token", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Embedded MCP server stopped..
|
||||
/// </summary>
|
||||
|
|
@ -6844,7 +7168,7 @@ namespace MinecraftClient {
|
|||
return ResourceManager.GetString("bot.mcpserver.stopped", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Failed to stop embedded MCP server cleanly: {0}.
|
||||
/// </summary>
|
||||
|
|
@ -6853,5 +7177,275 @@ namespace MinecraftClient {
|
|||
return ResourceManager.GetString("bot.mcpserver.stop_failed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Toggle the TUI minimap overlay, or adjust its zoom level..
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_desc {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.desc", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Minimap enabled..
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_enabled {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.enabled", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Minimap disabled..
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_disabled {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.disabled", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Minimap zoom set to {0}:1 (blocks per pixel)..
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_zoom_set {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.zoom_set", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Current minimap zoom: {0}:1 blocks/px (range 1-{1})..
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_zoom_current {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.zoom_current", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The minimap command is only available in TUI mode..
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_tui_only {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.tui_only", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Hostile.
|
||||
/// </summary>
|
||||
internal static string tui_minimap_legend_hostile {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.minimap.legend.hostile", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Passive.
|
||||
/// </summary>
|
||||
internal static string tui_minimap_legend_passive {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.minimap.legend.passive", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Neutral.
|
||||
/// </summary>
|
||||
internal static string tui_minimap_legend_neutral {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.minimap.legend.neutral", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Player.
|
||||
/// </summary>
|
||||
internal static string tui_minimap_legend_player {
|
||||
get {
|
||||
return ResourceManager.GetString("tui.minimap.legend.player", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Name display -- Players: {0}, Hostile: {1}, Neutral: {2}, Passive: {3}.
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_names_status {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.names_status", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to All entity name labels enabled..
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_names_all_on {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.names_all_on", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to All entity name labels disabled..
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_names_all_off {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.names_all_off", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} name display: {1}.
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_names_cat {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.names_cat", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} name display set to {1}..
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_names_cat_set {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.names_cat_set", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Current minimap position: {0}.
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_position_current {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.position_current", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Minimap position set to: {0}.
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_position_set {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.position_set", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Current cave mode: {0}.
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_cave_current {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.cave_current", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Cave mode set to: {0}.
|
||||
/// </summary>
|
||||
internal static string cmd_minimap_cave_set {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.minimap.cave_set", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to list achievements/advancements from the server..
|
||||
/// </summary>
|
||||
internal static string cmd_achievement_desc {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.achievement.desc", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No achievements/advancements received yet..
|
||||
/// </summary>
|
||||
internal static string cmd_achievement_none {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.achievement.none", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No completed achievements/advancements..
|
||||
/// </summary>
|
||||
internal static string cmd_achievement_none_unlocked {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.achievement.none_unlocked", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No incomplete achievements/advancements..
|
||||
/// </summary>
|
||||
internal static string cmd_achievement_none_locked {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.achievement.none_locked", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Achievements/Advancements:.
|
||||
/// </summary>
|
||||
internal static string cmd_achievement_header {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.achievement.header", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Completed achievements/advancements:.
|
||||
/// </summary>
|
||||
internal static string cmd_achievement_header_unlocked {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.achievement.header_unlocked", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Incomplete achievements/advancements:.
|
||||
/// </summary>
|
||||
internal static string cmd_achievement_header_locked {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.achievement.header_locked", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to [DONE].
|
||||
/// </summary>
|
||||
internal static string cmd_achievement_done {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.achievement.done", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to [TODO].
|
||||
/// </summary>
|
||||
internal static string cmd_achievement_todo {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.achievement.todo", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} {1} ({2}) [{3}].
|
||||
/// </summary>
|
||||
internal static string cmd_achievement_entry_titled {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.achievement.entry_titled", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to {0} {1} [{2}].
|
||||
/// </summary>
|
||||
internal static string cmd_achievement_entry {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd.achievement.entry", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -243,6 +243,9 @@
|
|||
<data name="bot.autodig.no_inv_handle" xml:space="preserve">
|
||||
<value>Inventory handling is not enabled. Unable to switch tools automatically.</value>
|
||||
</data>
|
||||
<data name="bot.autodig.drop_low_durability" xml:space="preserve">
|
||||
<value>Dropped low durability {0} from slot {1}.</value>
|
||||
</data>
|
||||
<data name="bot.autodig.start" xml:space="preserve">
|
||||
<value>Automatic digging has started.</value>
|
||||
</data>
|
||||
|
|
@ -252,6 +255,9 @@
|
|||
<data name="bot.autodig.stop" xml:space="preserve">
|
||||
<value>Auto-digging has been stopped.</value>
|
||||
</data>
|
||||
<data name="bot.autodig.switch" xml:space="preserve">
|
||||
<value>Switch to {0} from slot {1}.</value>
|
||||
</data>
|
||||
<data name="bot.autoDrop.added" xml:space="preserve">
|
||||
<value>Added item {0}</value>
|
||||
</data>
|
||||
|
|
@ -388,6 +394,12 @@
|
|||
<data name="bot.autoRelog.wait" xml:space="preserve">
|
||||
<value>Waiting {0:0.000} seconds before reconnecting...</value>
|
||||
</data>
|
||||
<data name="bot.autoRelog.wait_with_retries" xml:space="preserve">
|
||||
<value>Waiting {0:0.000} seconds before reconnecting... ({1} retries left)</value>
|
||||
</data>
|
||||
<data name="bot.autoRelog.retries_unlimited" xml:space="preserve">
|
||||
<value>unlimited</value>
|
||||
</data>
|
||||
<data name="bot.autoRespond.file_not_found" xml:space="preserve">
|
||||
<value>File not found: '{0}'</value>
|
||||
</data>
|
||||
|
|
@ -830,6 +842,42 @@ Add the ID of this chat to "Authorized_Chat_Ids" field in the configuration file
|
|||
<data name="botname.TestBot" xml:space="preserve">
|
||||
<value>TestBot</value>
|
||||
</data>
|
||||
<data name="mcc.banner.classic" xml:space="preserve">
|
||||
<value>Minecraft Console Client v{0} - for MC {1} to {2} - {3}</value>
|
||||
</data>
|
||||
<data name="mcc.banner.label_mc_versions" xml:space="preserve">
|
||||
<value>Supported MC Versions:</value>
|
||||
</data>
|
||||
<data name="mcc.server_info.label_server" xml:space="preserve">
|
||||
<value>Server:</value>
|
||||
</data>
|
||||
<data name="mcc.server_info.label_version" xml:space="preserve">
|
||||
<value>Version:</value>
|
||||
</data>
|
||||
<data name="mcc.server_info.label_protocol" xml:space="preserve">
|
||||
<value>Protocol: {0}</value>
|
||||
</data>
|
||||
<data name="mcc.server_info.label_players" xml:space="preserve">
|
||||
<value>Players:</value>
|
||||
</data>
|
||||
<data name="mcc.server_info.label_ping" xml:space="preserve">
|
||||
<value>Ping:</value>
|
||||
</data>
|
||||
<data name="mcc.server_info.label_ping_ms" xml:space="preserve">
|
||||
<value>{0} ms</value>
|
||||
</data>
|
||||
<data name="mcc.server_info.label_connecting_as" xml:space="preserve">
|
||||
<value>Connecting as:</value>
|
||||
</data>
|
||||
<data name="mcc.server_info.label_online" xml:space="preserve">
|
||||
<value>Online Players:</value>
|
||||
</data>
|
||||
<data name="mcc.server_info.sample_more" xml:space="preserve">
|
||||
<value>... +{0}</value>
|
||||
</data>
|
||||
<data name="mcc.server_info.version_upgrade" xml:space="preserve">
|
||||
<value>Server reported protocol {0} ({1}), upgraded to {2} ({3}) for best compatibility</value>
|
||||
</data>
|
||||
<data name="cache.converting" xml:space="preserve">
|
||||
<value>Converting session cache from disk: {0}</value>
|
||||
</data>
|
||||
|
|
@ -1249,6 +1297,21 @@ Change EnableEmoji=false in the settings if the display is confusing.</value>
|
|||
<data name="cmd.effects.none" xml:space="preserve">
|
||||
<value>No active effects.</value>
|
||||
</data>
|
||||
<data name="cmd.tryout.desc" xml:space="preserve">
|
||||
<value>try a recommended feature.</value>
|
||||
</data>
|
||||
<data name="cmd.tryout.list.header" xml:space="preserve">
|
||||
<value>Available tryouts:</value>
|
||||
</data>
|
||||
<data name="cmd.tryout.list.tui" xml:space="preserve">
|
||||
<value>tui: set [Console.General] ConsoleMode = "tui" for the next restart.</value>
|
||||
</data>
|
||||
<data name="cmd.tryout.tui.already_enabled" xml:space="preserve">
|
||||
<value>[Console.General] ConsoleMode is already "tui" in the config. To switch back, set [Console.General] ConsoleMode = "classic". Restart MCC after changing it for the new mode to take effect.</value>
|
||||
</data>
|
||||
<data name="cmd.tryout.tui.enabled" xml:space="preserve">
|
||||
<value>Updated [Console.General] ConsoleMode from "{0}" to "{1}" in the config. To switch back, set [Console.General] ConsoleMode = "classic". Restart MCC to apply the change.</value>
|
||||
</data>
|
||||
<data name="cmd.health.desc" xml:space="preserve">
|
||||
<value>Display Health and Food saturation.</value>
|
||||
</data>
|
||||
|
|
@ -1511,6 +1574,21 @@ You can use "/chunk status {0:0.0} {1:0.0} {2:0.0}" to check the chunk loading s
|
|||
<data name="cmd.tps.desc" xml:space="preserve">
|
||||
<value>Display server current tps (tick per second). May not be accurate</value>
|
||||
</data>
|
||||
<data name="cmd.teams.desc" xml:space="preserve">
|
||||
<value>List all scoreboard teams and their members.</value>
|
||||
</data>
|
||||
<data name="cmd.teams.no_teams" xml:space="preserve">
|
||||
<value>No teams are currently tracked.</value>
|
||||
</data>
|
||||
<data name="cmd.teams.team_header" xml:space="preserve">
|
||||
<value>Team '{0}' (display: {1}, color: {2}, prefix: '{3}', suffix: '{4}', nameTagVisibility: {5}, collisionRule: {6}, friendlyFire: {7}, seeInvisibles: {8})</value>
|
||||
</data>
|
||||
<data name="cmd.teams.team_members" xml:space="preserve">
|
||||
<value> Members ({0}): {1}</value>
|
||||
</data>
|
||||
<data name="cmd.teams.team_no_members" xml:space="preserve">
|
||||
<value> No members.</value>
|
||||
</data>
|
||||
<data name="cmd.useblock.desc" xml:space="preserve">
|
||||
<value>Place a block or open chest</value>
|
||||
</data>
|
||||
|
|
@ -1863,6 +1941,18 @@ You can use "/chunk status {0:0.0} {1:0.0} {2:0.0}" to check the chunk loading s
|
|||
<data name="mcc.connecting" xml:space="preserve">
|
||||
<value>Connecting to {0}...</value>
|
||||
</data>
|
||||
<data name="mcc.console_mode_tui_recommendation" xml:space="preserve">
|
||||
<value>Tip: try TUI mode for a cleaner interface, mouse-friendly inventory actions, and a nicer layout. Run {0}tryout tui§8 to switch [Console.General] ConsoleMode to "tui" for the next restart.</value>
|
||||
</data>
|
||||
<data name="mcc.tui_startup_failed" xml:space="preserve">
|
||||
<value>MCC encountered a problem while starting TUI mode.</value>
|
||||
</data>
|
||||
<data name="mcc.tui_startup_fallback_classic" xml:space="preserve">
|
||||
<value>As a fallback, MCC has automatically switched [Console.General] ConsoleMode to "classic". This will take effect after you restart MCC.</value>
|
||||
</data>
|
||||
<data name="mcc.report_issue" xml:space="preserve">
|
||||
<value>Please report this issue to the MCC Team.</value>
|
||||
</data>
|
||||
<data name="mcc.device_code_prompt" xml:space="preserve">
|
||||
<value>To sign in, open {0} in your browser and enter the code: §e{1}</value>
|
||||
</data>
|
||||
|
|
@ -1974,6 +2064,9 @@ Type '{0}quit' to leave the server.</value>
|
|||
<data name="mcc.restart" xml:space="preserve">
|
||||
<value>Restarting Minecraft Console Client...</value>
|
||||
</data>
|
||||
<data name="mcc.send_text_not_connected" xml:space="preserve">
|
||||
<value>Cannot send text: not connected to a server.</value>
|
||||
</data>
|
||||
<data name="mcc.restart_delay" xml:space="preserve">
|
||||
<value>Waiting {0} seconds before restarting...</value>
|
||||
</data>
|
||||
|
|
@ -1988,10 +2081,10 @@ MCC is running with default settings.</value>
|
|||
<value>Server is in offline mode.</value>
|
||||
</data>
|
||||
<data name="mcc.server_protocol" xml:space="preserve">
|
||||
<value>Server version : {0} (protocol v{1})</value>
|
||||
<value>Server version: {0} (protocol v{1})</value>
|
||||
</data>
|
||||
<data name="mcc.server_version" xml:space="preserve">
|
||||
<value>Server version : </value>
|
||||
<value>Server version: </value>
|
||||
</data>
|
||||
<data name="mcc.session" xml:space="preserve">
|
||||
<value>Checking Session...</value>
|
||||
|
|
@ -2151,6 +2244,33 @@ Logging in...</value>
|
|||
<data name="cmd.nameitem.desc" xml:space="preserve">
|
||||
<value>Set an item name when an Anvil inventory is active and the item is in the first slot.</value>
|
||||
</data>
|
||||
<data name="cmd.recipebook.craft.failed" xml:space="preserve">
|
||||
<value>Failed to send recipe book craft request for {0}.</value>
|
||||
</data>
|
||||
<data name="cmd.recipebook.craft.sent" xml:space="preserve">
|
||||
<value>Requested recipe {0}.</value>
|
||||
</data>
|
||||
<data name="cmd.recipebook.craftall.sent" xml:space="preserve">
|
||||
<value>Requested recipe {0} with craft-all.</value>
|
||||
</data>
|
||||
<data name="cmd.recipebook.desc" xml:space="preserve">
|
||||
<value>List unlocked recipe book recipes and craft them through the active recipe book inventory.</value>
|
||||
</data>
|
||||
<data name="cmd.recipebook.list" xml:space="preserve">
|
||||
<value>Unlocked recipe book recipes</value>
|
||||
</data>
|
||||
<data name="cmd.recipebook.no.active.inventory" xml:space="preserve">
|
||||
<value>You need an active player crafting grid, crafting table, furnace, blast furnace, smoker, or stonecutter inventory.</value>
|
||||
</data>
|
||||
<data name="cmd.recipebook.no.recipes" xml:space="preserve">
|
||||
<value>No unlocked recipe book recipes are currently tracked.</value>
|
||||
</data>
|
||||
<data name="cmd.recipebook.recipe.id.empty" xml:space="preserve">
|
||||
<value>The recipe identifier cannot be empty.</value>
|
||||
</data>
|
||||
<data name="cmd.recipebook.unsupported" xml:space="preserve">
|
||||
<value>Recipe book crafting is only supported on Minecraft 1.13 and newer.</value>
|
||||
</data>
|
||||
<data name="bot.antiafk.may.not.move" xml:space="preserve">
|
||||
<value>Bot movement lock is held by bot {0}, so the Anti AFK bot might not move!</value>
|
||||
</data>
|
||||
|
|
@ -2413,4 +2533,94 @@ see item details.</value>
|
|||
<data name="bot.mcpserver.stop_failed" xml:space="preserve">
|
||||
<value>Failed to stop embedded MCP server cleanly: {0}</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.desc" xml:space="preserve">
|
||||
<value>Toggle the TUI minimap overlay, or adjust its zoom level.</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.enabled" xml:space="preserve">
|
||||
<value>Minimap enabled.</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.disabled" xml:space="preserve">
|
||||
<value>Minimap disabled.</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.zoom_set" xml:space="preserve">
|
||||
<value>Minimap zoom set to {0}:1 (blocks per pixel).</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.zoom_current" xml:space="preserve">
|
||||
<value>Current minimap zoom: {0}:1 blocks/px (range 1-{1}).</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.tui_only" xml:space="preserve">
|
||||
<value>The minimap command is only available in TUI mode.</value>
|
||||
</data>
|
||||
<data name="tui.minimap.legend.hostile" xml:space="preserve">
|
||||
<value>Hostile</value>
|
||||
</data>
|
||||
<data name="tui.minimap.legend.passive" xml:space="preserve">
|
||||
<value>Passive</value>
|
||||
</data>
|
||||
<data name="tui.minimap.legend.neutral" xml:space="preserve">
|
||||
<value>Neutral</value>
|
||||
</data>
|
||||
<data name="tui.minimap.legend.player" xml:space="preserve">
|
||||
<value>Player</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.names_status" xml:space="preserve">
|
||||
<value>Name display -- Players: {0}, Hostile: {1}, Neutral: {2}, Passive: {3}</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.names_all_on" xml:space="preserve">
|
||||
<value>All entity name labels enabled.</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.names_all_off" xml:space="preserve">
|
||||
<value>All entity name labels disabled.</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.names_cat" xml:space="preserve">
|
||||
<value>{0} name display: {1}</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.names_cat_set" xml:space="preserve">
|
||||
<value>{0} name display set to {1}.</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.position_current" xml:space="preserve">
|
||||
<value>Current minimap position: {0}</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.position_set" xml:space="preserve">
|
||||
<value>Minimap position set to: {0}</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.cave_current" xml:space="preserve">
|
||||
<value>Current cave mode: {0}</value>
|
||||
</data>
|
||||
<data name="cmd.minimap.cave_set" xml:space="preserve">
|
||||
<value>Cave mode set to: {0}</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.desc" xml:space="preserve">
|
||||
<value>list achievements/advancements from the server.</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.none" xml:space="preserve">
|
||||
<value>No achievements/advancements received yet.</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.none_unlocked" xml:space="preserve">
|
||||
<value>No completed achievements/advancements.</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.none_locked" xml:space="preserve">
|
||||
<value>No incomplete achievements/advancements.</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.header" xml:space="preserve">
|
||||
<value>Achievements/Advancements:</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.header_unlocked" xml:space="preserve">
|
||||
<value>Completed achievements/advancements:</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.header_locked" xml:space="preserve">
|
||||
<value>Incomplete achievements/advancements:</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.done" xml:space="preserve">
|
||||
<value>[DONE]</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.todo" xml:space="preserve">
|
||||
<value>[TODO]</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.entry_titled" xml:space="preserve">
|
||||
<value>{0} {1} ({2}) [{3}]</value>
|
||||
</data>
|
||||
<data name="cmd.achievement.entry" xml:space="preserve">
|
||||
<value>{0} {1} [{2}]</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
|
|||
|
|
@ -205,6 +205,29 @@ namespace MinecraftClient.Scripting
|
|||
/// <param name="entity">Entity with updated location</param>
|
||||
public virtual void OnEntityMove(Entity entity) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when a tracked entity receives a velocity update packet.
|
||||
/// Velocity is expressed in blocks per tick.
|
||||
/// </summary>
|
||||
/// <param name="entity">Entity with updated velocity</param>
|
||||
/// <param name="velocityX">Velocity on X axis (blocks/tick)</param>
|
||||
/// <param name="velocityY">Velocity on Y axis (blocks/tick)</param>
|
||||
/// <param name="velocityZ">Velocity on Z axis (blocks/tick)</param>
|
||||
public virtual void OnEntityVelocity(Entity entity, double velocityX, double velocityY, double velocityZ) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when a sound packet is received.
|
||||
/// The sound name is null when the protocol provides only a registry id.
|
||||
/// </summary>
|
||||
/// <param name="soundName">Sound key when available, otherwise null</param>
|
||||
/// <param name="location">Sound position when available</param>
|
||||
/// <param name="category">Sound category id from packet</param>
|
||||
/// <param name="volume">Sound volume</param>
|
||||
/// <param name="pitch">Sound pitch</param>
|
||||
/// <param name="sourceEntity">Source entity for entity-sound packets when tracked</param>
|
||||
public virtual void OnSoundEffect(string? soundName, Location? location, int category, float volume, float pitch,
|
||||
Entity? sourceEntity) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when an entity rotates
|
||||
/// </summary>
|
||||
|
|
@ -367,6 +390,23 @@ namespace MinecraftClient.Scripting
|
|||
/// <param name="numberFormat">Number format: 0 - blank, 1 - styled, 2 - fixed</param>
|
||||
public virtual void OnUpdateScore(string entityName, int action, string objectiveName, string objectiveDisplayName, int value, int numberFormat) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when a Teams packet is received from the server.
|
||||
/// </summary>
|
||||
/// <param name="teamName">Internal team name (up to 16 chars)</param>
|
||||
/// <param name="method">0=create, 1=remove, 2=update, 3=add players, 4=remove players</param>
|
||||
/// <param name="displayName">Display name (formatted). Present when method is 0 or 2.</param>
|
||||
/// <param name="friendlyFlags">Bit 0=allowFriendlyFire, bit 1=seeFriendlyInvisibles. Present when method is 0 or 2.</param>
|
||||
/// <param name="nameTagVisibility">Nametag visibility rule. Present when method is 0 or 2.</param>
|
||||
/// <param name="collisionRule">Collision rule. Present when method is 0 or 2.</param>
|
||||
/// <param name="color">ChatFormatting color value (-1=none). Present when method is 0 or 2.</param>
|
||||
/// <param name="prefix">Member name prefix (formatted). Present when method is 0 or 2.</param>
|
||||
/// <param name="suffix">Member name suffix (formatted). Present when method is 0 or 2.</param>
|
||||
/// <param name="players">Player/entity names. Present when method is 0, 3, or 4.</param>
|
||||
public virtual void OnTeam(string teamName, byte method, string displayName, byte friendlyFlags,
|
||||
string nameTagVisibility, string collisionRule, int color,
|
||||
string prefix, string suffix, List<string> players) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when the client received the Tab Header and Footer
|
||||
/// </summary>
|
||||
|
|
@ -520,6 +560,14 @@ namespace MinecraftClient.Scripting
|
|||
/// <param name="block">The block</param>
|
||||
public virtual void OnBlockChange(Location location, Block block) { }
|
||||
|
||||
/// <summary>
|
||||
/// Called when achievement/advancement data is updated.
|
||||
/// </summary>
|
||||
/// <param name="updated">Achievements that were added or updated</param>
|
||||
/// <param name="removedIds">IDs of achievements that were removed</param>
|
||||
/// <param name="reset">Whether the achievement state was fully reset before this update</param>
|
||||
public virtual void OnAchievementUpdate(IReadOnlyList<Achievement> updated, IReadOnlyList<string> removedIds, bool reset) { }
|
||||
|
||||
/* =================================================================== */
|
||||
/* ToolBox - Methods below might be useful while creating your bot. */
|
||||
/* You should not need to interact with other classes of the program. */
|
||||
|
|
@ -1095,9 +1143,10 @@ namespace MinecraftClient.Scripting
|
|||
/// <param name="direction">Example: if your player is under a block that is being destroyed, use Down</param>
|
||||
/// <param name="swingArms">Also perform the "arm swing" animation</param>
|
||||
/// <param name="lookAtBlock">Also look at the block before digging</param>
|
||||
protected bool DigBlock(Location location, Direction direction, bool swingArms = true, bool lookAtBlock = true)
|
||||
/// <param name="duration">Dig duration in seconds. 0 = auto-compute for survival, or instant for creative</param>
|
||||
protected bool DigBlock(Location location, Direction direction, bool swingArms = true, bool lookAtBlock = true, double duration = 0)
|
||||
{
|
||||
return Handler.DigBlock(location, direction, swingArms, lookAtBlock);
|
||||
return Handler.DigBlock(location, direction, swingArms, lookAtBlock, duration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -1126,6 +1175,33 @@ namespace MinecraftClient.Scripting
|
|||
return Handler.GetEntities();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all achievements/advancements.
|
||||
/// </summary>
|
||||
/// <returns>Snapshot of all achievements</returns>
|
||||
protected Achievement[] GetAchievements()
|
||||
{
|
||||
return Handler.GetAchievements();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get only completed achievements/advancements.
|
||||
/// </summary>
|
||||
/// <returns>Snapshot of unlocked achievements</returns>
|
||||
protected Achievement[] GetUnlockedAchievements()
|
||||
{
|
||||
return Handler.GetUnlockedAchievements();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get only incomplete achievements/advancements.
|
||||
/// </summary>
|
||||
/// <returns>Snapshot of locked achievements</returns>
|
||||
protected Achievement[] GetLockedAchievements()
|
||||
{
|
||||
return Handler.GetLockedAchievements();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all players Latency
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -1108,6 +1108,9 @@ namespace MinecraftClient
|
|||
[TomlPrecedingComment("$Console.CommandSuggestion$")]
|
||||
public CommandSuggestionConfig CommandSuggestion = new();
|
||||
|
||||
[TomlPrecedingComment("$Console.Minimap$")]
|
||||
public MinimapConfig Minimap = new();
|
||||
|
||||
public void OnSettingUpdate()
|
||||
{
|
||||
var backend = ConsoleIO.Backend;
|
||||
|
|
@ -1207,11 +1210,17 @@ namespace MinecraftClient
|
|||
[TomlInlineComment("$Console.General.ConsoleColorMode$")]
|
||||
public ConsoleColorModeType ConsoleColorMode = ConsoleColorModeType.vt100_24bit;
|
||||
|
||||
[TomlInlineComment("$Console.General.Display_Icon_Banner$")]
|
||||
public bool Display_Icon_Banner = true;
|
||||
|
||||
[TomlInlineComment("$Console.General.Display_Input$")]
|
||||
public bool Display_Input = true;
|
||||
|
||||
[TomlInlineComment("$Console.General.History_Input_Records$")]
|
||||
public int History_Input_Records = 32;
|
||||
|
||||
[TomlInlineComment("$Console.General.TUI_Log_Scrollback$")]
|
||||
public int TUI_Log_Scrollback = 0;
|
||||
}
|
||||
|
||||
[TomlDoNotInlineObject]
|
||||
|
|
@ -1246,6 +1255,53 @@ namespace MinecraftClient
|
|||
|
||||
public enum ConsoleModeType { classic, tui };
|
||||
public enum ConsoleColorModeType { disable, legacy_4bit, vt100_4bit, vt100_8bit, vt100_24bit };
|
||||
|
||||
[TomlDoNotInlineObject]
|
||||
public class MinimapConfig
|
||||
{
|
||||
[TomlInlineComment("$Console.Minimap.Enabled$")]
|
||||
public bool Enabled = false;
|
||||
|
||||
[TomlInlineComment("$Console.Minimap.Zoom$")]
|
||||
public int Zoom = Tui.MinimapControl.DefaultZoom;
|
||||
|
||||
[TomlInlineComment("$Console.Minimap.Width$")]
|
||||
public int Width = Tui.MinimapControl.DefaultWidth;
|
||||
|
||||
[TomlInlineComment("$Console.Minimap.Height$")]
|
||||
public int Height = Tui.MinimapControl.DefaultHeight;
|
||||
|
||||
[TomlInlineComment("$Console.Minimap.Position$")]
|
||||
public Tui.MinimapPosition Position = Tui.MinimapPosition.top_right;
|
||||
|
||||
[TomlInlineComment("$Console.Minimap.ShowPlayerNames$")]
|
||||
public bool ShowPlayerNames = false;
|
||||
|
||||
[TomlInlineComment("$Console.Minimap.ShowHostileNames$")]
|
||||
public bool ShowHostileNames = false;
|
||||
|
||||
[TomlInlineComment("$Console.Minimap.ShowNeutralNames$")]
|
||||
public bool ShowNeutralNames = false;
|
||||
|
||||
[TomlInlineComment("$Console.Minimap.ShowPassiveNames$")]
|
||||
public bool ShowPassiveNames = false;
|
||||
|
||||
[TomlInlineComment("$Console.Minimap.RefreshInterval$")]
|
||||
public int RefreshInterval = Tui.MinimapControl.DefaultRefreshMs;
|
||||
|
||||
[TomlInlineComment("$Console.Minimap.CaveMode$")]
|
||||
public Tui.CaveModeOption CaveMode = Tui.CaveModeOption.auto;
|
||||
|
||||
public void OnSettingUpdate()
|
||||
{
|
||||
Zoom = Math.Clamp(Zoom, Tui.MinimapControl.MinZoom, Tui.MinimapControl.MaxZoom);
|
||||
Width = Math.Clamp(Width, 10, 120);
|
||||
Height = Math.Clamp(Height, 4, 80);
|
||||
if (Height % 2 != 0) Height++;
|
||||
RefreshInterval = Math.Clamp(RefreshInterval,
|
||||
Tui.MinimapControl.MinRefreshMs, Tui.MinimapControl.MaxRefreshMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
125
MinecraftClient/Tui/IconGridBuilder.cs
Normal file
125
MinecraftClient/Tui/IconGridBuilder.cs
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
using System;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
internal static class IconGridBuilder
|
||||
{
|
||||
internal static Grid BuildFromRgba(byte[] rgba, int srcWidth, int srcHeight, int displaySize)
|
||||
{
|
||||
int cellCols = displaySize;
|
||||
int cellRows = displaySize / 2;
|
||||
|
||||
var grid = new Grid();
|
||||
for (int c = 0; c < cellCols; c++)
|
||||
grid.ColumnDefinitions.Add(new ColumnDefinition(1, GridUnitType.Auto));
|
||||
for (int r = 0; r < cellRows; r++)
|
||||
grid.RowDefinitions.Add(new RowDefinition(1, GridUnitType.Auto));
|
||||
|
||||
for (int row = 0; row < cellRows; row++)
|
||||
{
|
||||
for (int col = 0; col < cellCols; col++)
|
||||
{
|
||||
int topPixelY = row * 2;
|
||||
int bottomPixelY = row * 2 + 1;
|
||||
|
||||
var topColor = SamplePixel(rgba, srcWidth, srcHeight, col, topPixelY, cellCols, displaySize);
|
||||
var bottomColor = SamplePixel(rgba, srcWidth, srcHeight, col, bottomPixelY, cellCols, displaySize);
|
||||
|
||||
var cell = new TextBlock
|
||||
{
|
||||
Text = "\u2580",
|
||||
Foreground = new SolidColorBrush(topColor),
|
||||
Background = new SolidColorBrush(bottomColor),
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(0),
|
||||
};
|
||||
|
||||
Grid.SetRow(cell, row);
|
||||
Grid.SetColumn(cell, col);
|
||||
grid.Children.Add(cell);
|
||||
}
|
||||
}
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
internal static Grid BuildFromBase64(string base64Data, int displaySize)
|
||||
{
|
||||
byte[] imageBytes;
|
||||
try
|
||||
{
|
||||
imageBytes = Convert.FromBase64String(base64Data);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new Grid();
|
||||
}
|
||||
|
||||
return BuildFromImageBytes(imageBytes, displaySize) ?? new Grid();
|
||||
}
|
||||
|
||||
internal static Grid? BuildFromImageBytes(byte[] imageBytes, int displaySize)
|
||||
{
|
||||
int srcWidth, srcHeight;
|
||||
byte[] rgba;
|
||||
try
|
||||
{
|
||||
(srcWidth, srcHeight, rgba) = DecodeImageToRgba(imageBytes);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return BuildFromRgba(rgba, srcWidth, srcHeight, displaySize);
|
||||
}
|
||||
|
||||
internal static (int Width, int Height, byte[] Rgba) DecodeImageToRgba(byte[] imageData)
|
||||
{
|
||||
using var image = new ImageMagick.MagickImage(imageData);
|
||||
int w = (int)image.Width;
|
||||
int h = (int)image.Height;
|
||||
|
||||
using var pixels = image.GetPixelsUnsafe();
|
||||
var rgba = new byte[w * h * 4];
|
||||
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
var pixel = pixels.GetPixel(x, y)!;
|
||||
int idx = (y * w + x) * 4;
|
||||
var color = pixel.ToColor()!;
|
||||
rgba[idx] = (byte)(color.R >> 8);
|
||||
rgba[idx + 1] = (byte)(color.G >> 8);
|
||||
rgba[idx + 2] = (byte)(color.B >> 8);
|
||||
rgba[idx + 3] = (byte)(color.A >> 8);
|
||||
}
|
||||
}
|
||||
|
||||
return (w, h, rgba);
|
||||
}
|
||||
|
||||
private static Color SamplePixel(byte[] rgba, int srcW, int srcH, int dstX, int dstY, int dstW, int dstH)
|
||||
{
|
||||
int srcX = dstX * srcW / dstW;
|
||||
int srcY = dstY * srcH / dstH;
|
||||
srcX = Math.Clamp(srcX, 0, srcW - 1);
|
||||
srcY = Math.Clamp(srcY, 0, srcH - 1);
|
||||
|
||||
int idx = (srcY * srcW + srcX) * 4;
|
||||
if (idx + 3 >= rgba.Length)
|
||||
return Color.FromRgb(0, 0, 0);
|
||||
|
||||
byte r = rgba[idx];
|
||||
byte g = rgba[idx + 1];
|
||||
byte b = rgba[idx + 2];
|
||||
byte a = rgba[idx + 3];
|
||||
|
||||
return a < 128 ? Color.FromRgb(0, 0, 0) : Color.FromRgb(r, g, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
|
|
@ -16,9 +17,20 @@ namespace MinecraftClient.Tui
|
|||
{
|
||||
public class MainTuiView : UserControl
|
||||
{
|
||||
private const int MaxLogLines = 5000;
|
||||
private static readonly int MaxLogLines = ResolveMaxLogLines();
|
||||
private const int CtrlCDoublePressMsec = 1500;
|
||||
|
||||
private static int ResolveMaxLogLines()
|
||||
{
|
||||
int configured = Settings.Config.Console.General.TUI_Log_Scrollback;
|
||||
if (configured > 0)
|
||||
return configured;
|
||||
|
||||
bool isArm = RuntimeInformation.ProcessArchitecture
|
||||
is Architecture.Arm or Architecture.Arm64;
|
||||
return isArm ? 500 : 3000;
|
||||
}
|
||||
|
||||
private readonly ObservableCollection<string> _logLines = new();
|
||||
private readonly ObservableCollection<Control> _logControls = new();
|
||||
private readonly ItemsControl _logItemsControl;
|
||||
|
|
@ -41,6 +53,12 @@ namespace MinecraftClient.Tui
|
|||
private long _lastLogClickTicks;
|
||||
private const int DoubleClickMsec = 500;
|
||||
|
||||
private readonly Border _minimapBorder;
|
||||
private readonly MinimapControl _minimapControl;
|
||||
private volatile bool _minimapVisible;
|
||||
|
||||
private TuiTooltipService? _tooltipService;
|
||||
|
||||
private readonly Border _suggestionBorder;
|
||||
private readonly StackPanel _suggestionPanel;
|
||||
private CommandSuggestion[] _suggestions = Array.Empty<CommandSuggestion>();
|
||||
|
|
@ -53,6 +71,8 @@ namespace MinecraftClient.Tui
|
|||
private int MaxVisibleSuggestions =>
|
||||
Math.Max(1, Settings.Config.Console.CommandSuggestion.Max_Displayed_Suggestions);
|
||||
|
||||
public TuiTooltipService? TooltipService => _tooltipService;
|
||||
|
||||
public MainTuiView()
|
||||
{
|
||||
Background = Brushes.Black;
|
||||
|
|
@ -70,6 +90,7 @@ namespace MinecraftClient.Tui
|
|||
{
|
||||
ItemsSource = _logControls,
|
||||
Focusable = false,
|
||||
ItemsPanel = new FuncTemplate<Panel?>(() => new VirtualizingStackPanel()),
|
||||
};
|
||||
|
||||
_logScrollViewer = new ScrollViewer
|
||||
|
|
@ -150,6 +171,30 @@ namespace MinecraftClient.Tui
|
|||
Margin = new Thickness(0, 0, 0, 1),
|
||||
};
|
||||
|
||||
var mmCfg = Settings.Config.Console.Minimap;
|
||||
mmCfg.OnSettingUpdate();
|
||||
_minimapControl = new MinimapControl(mmCfg.Width, mmCfg.Height);
|
||||
_minimapControl.BlocksPerPixel = mmCfg.Zoom;
|
||||
_minimapControl.RefreshIntervalMs = mmCfg.RefreshInterval;
|
||||
_minimapControl.NameConfig.Players = mmCfg.ShowPlayerNames;
|
||||
_minimapControl.NameConfig.Hostile = mmCfg.ShowHostileNames;
|
||||
_minimapControl.NameConfig.Neutral = mmCfg.ShowNeutralNames;
|
||||
_minimapControl.NameConfig.Passive = mmCfg.ShowPassiveNames;
|
||||
_minimapControl.CaveMode = mmCfg.CaveMode;
|
||||
|
||||
var (hAlign, vAlign, margin) = GetMinimapAlignment(mmCfg.Position);
|
||||
_minimapBorder = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromArgb(220, 15, 15, 15)),
|
||||
BorderBrush = new SolidColorBrush(Color.FromRgb(80, 80, 80)),
|
||||
BorderThickness = new Thickness(1),
|
||||
Child = _minimapControl,
|
||||
IsVisible = false,
|
||||
HorizontalAlignment = hAlign,
|
||||
VerticalAlignment = vAlign,
|
||||
Margin = margin,
|
||||
};
|
||||
|
||||
_mainContent = new DockPanel
|
||||
{
|
||||
Background = Brushes.Black,
|
||||
|
|
@ -164,11 +209,22 @@ namespace MinecraftClient.Tui
|
|||
_rootPanel = new Panel
|
||||
{
|
||||
Background = Brushes.Black,
|
||||
Children = { _mainContent, _notificationBorder, _suggestionBorder }
|
||||
Children = { _mainContent, _minimapBorder, _notificationBorder, _suggestionBorder }
|
||||
};
|
||||
|
||||
_tooltipService = new TuiTooltipService(_rootPanel);
|
||||
_minimapControl.TooltipService = _tooltipService;
|
||||
_minimapControl.Position = mmCfg.Position;
|
||||
|
||||
Content = _rootPanel;
|
||||
|
||||
if (mmCfg.Enabled)
|
||||
{
|
||||
_minimapVisible = true;
|
||||
_minimapBorder.IsVisible = true;
|
||||
_minimapControl.Start();
|
||||
}
|
||||
|
||||
StartStatusBarTimer();
|
||||
}
|
||||
|
||||
|
|
@ -986,6 +1042,104 @@ namespace MinecraftClient.Tui
|
|||
|
||||
#endregion
|
||||
|
||||
#region Minimap
|
||||
|
||||
public void ShowMinimap()
|
||||
{
|
||||
if (_minimapVisible) return;
|
||||
_minimapVisible = true;
|
||||
_minimapBorder.IsVisible = true;
|
||||
_minimapControl.Start();
|
||||
Settings.Config.Console.Minimap.Enabled = true;
|
||||
}
|
||||
|
||||
public void HideMinimap()
|
||||
{
|
||||
if (!_minimapVisible) return;
|
||||
_minimapVisible = false;
|
||||
_minimapControl.Stop();
|
||||
_minimapBorder.IsVisible = false;
|
||||
Settings.Config.Console.Minimap.Enabled = false;
|
||||
}
|
||||
|
||||
public void ToggleMinimap()
|
||||
{
|
||||
if (_minimapVisible)
|
||||
HideMinimap();
|
||||
else
|
||||
ShowMinimap();
|
||||
}
|
||||
|
||||
public bool IsMinimapVisible => _minimapVisible;
|
||||
|
||||
public void SetMinimapZoom(int level)
|
||||
{
|
||||
_minimapControl.BlocksPerPixel = level;
|
||||
Settings.Config.Console.Minimap.Zoom = level;
|
||||
}
|
||||
|
||||
public int GetMinimapZoom() => _minimapControl.BlocksPerPixel;
|
||||
|
||||
public NameDisplayConfig GetMinimapNameConfig() => _minimapControl.NameConfig;
|
||||
|
||||
public void SyncMinimapNameConfig()
|
||||
{
|
||||
var nc = _minimapControl.NameConfig;
|
||||
var cfg = Settings.Config.Console.Minimap;
|
||||
cfg.ShowPlayerNames = nc.Players;
|
||||
cfg.ShowHostileNames = nc.Hostile;
|
||||
cfg.ShowNeutralNames = nc.Neutral;
|
||||
cfg.ShowPassiveNames = nc.Passive;
|
||||
}
|
||||
|
||||
public void ResizeMinimap(int width, int height)
|
||||
{
|
||||
_minimapControl.Resize(width, height);
|
||||
Settings.Config.Console.Minimap.Width = width;
|
||||
Settings.Config.Console.Minimap.Height = height;
|
||||
}
|
||||
|
||||
public void SetMinimapPosition(MinimapPosition pos)
|
||||
{
|
||||
var (hAlign, vAlign, margin) = GetMinimapAlignment(pos);
|
||||
_minimapBorder.HorizontalAlignment = hAlign;
|
||||
_minimapBorder.VerticalAlignment = vAlign;
|
||||
_minimapBorder.Margin = margin;
|
||||
_minimapControl.Position = pos;
|
||||
Settings.Config.Console.Minimap.Position = pos;
|
||||
}
|
||||
|
||||
public MinimapPosition GetMinimapPosition() => Settings.Config.Console.Minimap.Position;
|
||||
|
||||
public void SetMinimapCaveMode(CaveModeOption mode)
|
||||
{
|
||||
_minimapControl.CaveMode = mode;
|
||||
Settings.Config.Console.Minimap.CaveMode = mode;
|
||||
}
|
||||
|
||||
public CaveModeOption GetMinimapCaveMode() => _minimapControl.CaveMode;
|
||||
|
||||
private static (HorizontalAlignment h, VerticalAlignment v, Thickness margin) GetMinimapAlignment(MinimapPosition pos) => pos switch
|
||||
{
|
||||
MinimapPosition.top_left => (HorizontalAlignment.Left, VerticalAlignment.Top, new Thickness(1, 1, 0, 0)),
|
||||
MinimapPosition.top_right => (HorizontalAlignment.Right, VerticalAlignment.Top, new Thickness(0, 1, 1, 0)),
|
||||
MinimapPosition.center => (HorizontalAlignment.Center, VerticalAlignment.Center, new Thickness(0)),
|
||||
MinimapPosition.bottom_left => (HorizontalAlignment.Left, VerticalAlignment.Bottom, new Thickness(1, 0, 0, 2)),
|
||||
MinimapPosition.bottom_right => (HorizontalAlignment.Right, VerticalAlignment.Bottom, new Thickness(0, 0, 1, 2)),
|
||||
_ => (HorizontalAlignment.Right, VerticalAlignment.Top, new Thickness(0, 1, 1, 0)),
|
||||
};
|
||||
|
||||
public void ApplyMinimapConfig()
|
||||
{
|
||||
var cfg = Settings.Config.Console.Minimap;
|
||||
if (cfg.Enabled && !_minimapVisible)
|
||||
ShowMinimap();
|
||||
else if (!cfg.Enabled && _minimapVisible)
|
||||
HideMinimap();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Overlay
|
||||
|
||||
public void ShowOverlay(Control content, Action? onClose = null)
|
||||
|
|
@ -1039,5 +1193,18 @@ namespace MinecraftClient.Tui
|
|||
_commandInput.Focus();
|
||||
}, DispatcherPriority.Loaded);
|
||||
}
|
||||
|
||||
#region Custom Control Append
|
||||
|
||||
public void AppendControlToLog(Control control)
|
||||
{
|
||||
_logLines.Add(string.Empty);
|
||||
_logControls.Add(control);
|
||||
TrimLog();
|
||||
if (_autoScroll)
|
||||
ScheduleScrollToEnd();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ namespace MinecraftClient.Tui
|
|||
IBrush currentColor = Brushes.White;
|
||||
bool bold = false;
|
||||
bool italic = false;
|
||||
bool underline = false;
|
||||
bool strikethrough = false;
|
||||
int start = 0;
|
||||
|
||||
for (int i = 0; i < text.Length; i++)
|
||||
|
|
@ -59,7 +61,7 @@ namespace MinecraftClient.Tui
|
|||
if (text[i] == '§' && i + 1 < text.Length)
|
||||
{
|
||||
if (i > start)
|
||||
AddRun(tb, text[start..i], currentColor, bold, italic);
|
||||
AddRun(tb, text[start..i], currentColor, bold, italic, underline, strikethrough);
|
||||
|
||||
char code = char.ToLower(text[i + 1]);
|
||||
|
||||
|
|
@ -68,6 +70,8 @@ namespace MinecraftClient.Tui
|
|||
currentColor = brush;
|
||||
bold = false;
|
||||
italic = false;
|
||||
underline = false;
|
||||
strikethrough = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -75,10 +79,14 @@ namespace MinecraftClient.Tui
|
|||
{
|
||||
case 'l': bold = true; break;
|
||||
case 'o': italic = true; break;
|
||||
case 'n': underline = true; break;
|
||||
case 'm': strikethrough = true; break;
|
||||
case 'r':
|
||||
currentColor = Brushes.White;
|
||||
bold = false;
|
||||
italic = false;
|
||||
underline = false;
|
||||
strikethrough = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -89,7 +97,7 @@ namespace MinecraftClient.Tui
|
|||
}
|
||||
|
||||
if (start < text.Length)
|
||||
AddRun(tb, text[start..], currentColor, bold, italic);
|
||||
AddRun(tb, text[start..], currentColor, bold, italic, underline, strikethrough);
|
||||
|
||||
if (tb.Inlines?.Count == 0)
|
||||
{
|
||||
|
|
@ -100,16 +108,29 @@ namespace MinecraftClient.Tui
|
|||
return tb;
|
||||
}
|
||||
|
||||
private static void AddRun(TextBlock tb, string text, IBrush color, bool bold, bool italic)
|
||||
private static void AddRun(TextBlock tb, string text, IBrush color,
|
||||
bool bold, bool italic, bool underline, bool strikethrough)
|
||||
{
|
||||
if (text.Length == 0) return;
|
||||
|
||||
tb.Inlines ??= new InlineCollection();
|
||||
|
||||
TextDecorationCollection? decorations = null;
|
||||
if (underline || strikethrough)
|
||||
{
|
||||
decorations = [];
|
||||
if (underline)
|
||||
decorations.Add(new TextDecoration { Location = TextDecorationLocation.Underline });
|
||||
if (strikethrough)
|
||||
decorations.Add(new TextDecoration { Location = TextDecorationLocation.Strikethrough });
|
||||
}
|
||||
|
||||
tb.Inlines.Add(new Run(text)
|
||||
{
|
||||
Foreground = color,
|
||||
FontWeight = bold ? FontWeight.Bold : FontWeight.Normal,
|
||||
FontStyle = italic ? FontStyle.Italic : FontStyle.Normal,
|
||||
TextDecorations = decorations,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
178
MinecraftClient/Tui/MccBannerPanelBuilder.cs
Normal file
178
MinecraftClient/Tui/MccBannerPanelBuilder.cs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Documents;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
internal static class MccBannerPanelBuilder
|
||||
{
|
||||
internal static Border Build(string? buildInfo)
|
||||
{
|
||||
var contentPanel = new DockPanel { Background = Brushes.Black };
|
||||
|
||||
var icon = BuildIcon();
|
||||
icon.VerticalAlignment = VerticalAlignment.Center;
|
||||
DockPanel.SetDock(icon, Dock.Left);
|
||||
contentPanel.Children.Add(icon);
|
||||
|
||||
var infoPanel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Vertical,
|
||||
Margin = new Thickness(1, 0, 0, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
|
||||
AddTitle(infoPanel);
|
||||
AddVersionRange(infoPanel);
|
||||
AddGithub(infoPanel);
|
||||
|
||||
if (buildInfo is not null)
|
||||
AddBuildInfo(infoPanel, buildInfo);
|
||||
|
||||
contentPanel.Children.Add(infoPanel);
|
||||
|
||||
return new Border
|
||||
{
|
||||
BorderBrush = new SolidColorBrush(Color.FromRgb(80, 80, 80)),
|
||||
BorderThickness = new Thickness(1),
|
||||
Background = new SolidColorBrush(Color.FromArgb(240, 20, 20, 20)),
|
||||
Padding = new Thickness(1, 0),
|
||||
Child = contentPanel,
|
||||
Margin = new Thickness(0),
|
||||
};
|
||||
}
|
||||
|
||||
private static void AddTitle(StackPanel panel)
|
||||
{
|
||||
var row = new TextBlock();
|
||||
row.Inlines!.Add(new Run("Minecraft Console Client")
|
||||
{ Foreground = Pal.Gold, FontWeight = FontWeight.Bold });
|
||||
row.Inlines.Add(new Run($" v{Program.Version}") { Foreground = Pal.Aqua });
|
||||
panel.Children.Add(row);
|
||||
}
|
||||
|
||||
private static void AddVersionRange(StackPanel panel)
|
||||
{
|
||||
var row = new TextBlock();
|
||||
row.Inlines!.Add(Lbl(Translations.mcc_banner_label_mc_versions));
|
||||
row.Inlines.Add(Val(Program.MCLowestVersion, Pal.Green));
|
||||
row.Inlines.Add(new Run(" - ") { Foreground = Pal.Gray });
|
||||
row.Inlines.Add(Val(Program.MCHighestVersion, Pal.Green));
|
||||
panel.Children.Add(row);
|
||||
}
|
||||
|
||||
private static void AddGithub(StackPanel panel)
|
||||
{
|
||||
var row = new TextBlock();
|
||||
row.Inlines!.Add(Val("Github.com/MCCTeam", Pal.Gray));
|
||||
panel.Children.Add(row);
|
||||
}
|
||||
|
||||
private static void AddBuildInfo(StackPanel panel, string buildInfo)
|
||||
{
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = buildInfo,
|
||||
Foreground = Pal.DarkGray,
|
||||
});
|
||||
}
|
||||
|
||||
#region Icon
|
||||
|
||||
private static readonly Color B1 = Color.FromRgb(200, 200, 200); // bezel bright
|
||||
private static readonly Color B2 = Color.FromRgb(160, 160, 160); // bezel mid
|
||||
private static readonly Color B3 = Color.FromRgb(120, 120, 120); // bezel dark
|
||||
private static readonly Color S = Color.FromRgb(20, 20, 20); // screen bg
|
||||
private static readonly Color C = Color.FromRgb(55, 200, 55); // creeper green
|
||||
|
||||
// @formatter:off
|
||||
private static readonly Color[,] Pixels =
|
||||
{
|
||||
{ B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B1, B2 },
|
||||
{ B1, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, B3 },
|
||||
{ B1, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, B3 },
|
||||
{ B1, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, B3 },
|
||||
{ B1, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, B3 },
|
||||
{ B1, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, B3 },
|
||||
{ B1, S, S, S, S, S, S, S, S, C, C, S, S, C, C, S, B3 },
|
||||
{ B1, S, S, S, S, S, S, S, S, C, C, S, S, C, C, S, B3 },
|
||||
{ B1, S, S, S, S, S, S, S, S, S, S, C, C, S, S, S, B3 },
|
||||
{ B1, S, S, S, S, S, S, S, S, S, C, C, C, C, S, S, B3 },
|
||||
{ B1, S, S, S, S, S, S, S, S, S, C, C, C, C, S, S, B3 },
|
||||
{ B1, S, S, S, S, S, S, S, S, S, C, S, S, C, S, S, B3 },
|
||||
{ B1, S, S, S, S, S, S, S, S, S, S, S, S, S, S, S, B3 },
|
||||
{ B2, B3, B3, B3, B3, B3, B3, B3, B3, B3, B3, B3, B3, B3, B3, B3, B3 },
|
||||
};
|
||||
// @formatter:on
|
||||
|
||||
private static Control BuildIcon()
|
||||
{
|
||||
int cols = Pixels.GetLength(1);
|
||||
int textRows = Pixels.GetLength(0) / 2;
|
||||
|
||||
var pixelGrid = new Grid();
|
||||
for (int c = 0; c < cols; c++)
|
||||
pixelGrid.ColumnDefinitions.Add(new ColumnDefinition(1, GridUnitType.Auto));
|
||||
for (int r = 0; r < textRows; r++)
|
||||
pixelGrid.RowDefinitions.Add(new RowDefinition(1, GridUnitType.Auto));
|
||||
|
||||
for (int row = 0; row < textRows; row++)
|
||||
{
|
||||
for (int col = 0; col < cols; col++)
|
||||
{
|
||||
var topColor = Pixels[row * 2, col];
|
||||
var bottomColor = Pixels[row * 2 + 1, col];
|
||||
|
||||
var cell = new TextBlock
|
||||
{
|
||||
Text = "\u2580",
|
||||
Foreground = new SolidColorBrush(topColor),
|
||||
Background = new SolidColorBrush(bottomColor),
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(0),
|
||||
};
|
||||
|
||||
Grid.SetRow(cell, row);
|
||||
Grid.SetColumn(cell, col);
|
||||
pixelGrid.Children.Add(cell);
|
||||
}
|
||||
}
|
||||
|
||||
var prompt = new TextBlock
|
||||
{
|
||||
Text = " >_",
|
||||
Foreground = new SolidColorBrush(Color.FromRgb(255, 255, 255)),
|
||||
Background = new SolidColorBrush(S),
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(0),
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
VerticalAlignment = VerticalAlignment.Top,
|
||||
};
|
||||
Grid.SetRow(prompt, 1);
|
||||
Grid.SetColumn(prompt, 1);
|
||||
Grid.SetColumnSpan(prompt, 4);
|
||||
pixelGrid.Children.Add(prompt);
|
||||
|
||||
return pixelGrid;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static Run Lbl(string text) =>
|
||||
new(text + " ") { Foreground = Pal.Gray };
|
||||
|
||||
private static Run Val(string text, IBrush color) =>
|
||||
new(text) { Foreground = color };
|
||||
|
||||
private static class Pal
|
||||
{
|
||||
public static readonly IBrush Gray = new SolidColorBrush(Color.FromRgb(170, 170, 170));
|
||||
public static readonly IBrush DarkGray = new SolidColorBrush(Color.FromRgb(85, 85, 85));
|
||||
public static readonly IBrush Aqua = new SolidColorBrush(Color.FromRgb(85, 255, 255));
|
||||
public static readonly IBrush Green = new SolidColorBrush(Color.FromRgb(85, 255, 85));
|
||||
public static readonly IBrush Gold = new SolidColorBrush(Color.FromRgb(255, 170, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
3552
MinecraftClient/Tui/MinimapBlockColors.json
Normal file
3552
MinecraftClient/Tui/MinimapBlockColors.json
Normal file
File diff suppressed because it is too large
Load diff
191
MinecraftClient/Tui/MinimapColorMap.cs
Normal file
191
MinecraftClient/Tui/MinimapColorMap.cs
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Avalonia.Media;
|
||||
using MinecraftClient.Mapping;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps block Materials to minimap colors using data extracted from Minecraft's
|
||||
/// official MapColor table. Colors are loaded from the embedded MinimapBlockColors.json
|
||||
/// resource generated by tools/gen_block_color_map.py.
|
||||
/// </summary>
|
||||
public static class MinimapColorMap
|
||||
{
|
||||
public static readonly Color WaterColor = Color.FromRgb(64, 64, 255);
|
||||
public static readonly Color IceColor = Color.FromRgb(160, 160, 255);
|
||||
public static readonly Color LavaColor = Color.FromRgb(255, 100, 0);
|
||||
public static readonly Color DefaultColor = Color.FromRgb(60, 60, 60);
|
||||
public static readonly Color VoidColor = Color.FromRgb(0, 0, 0);
|
||||
public static readonly Color CaveBorderColor = Color.FromRgb(16, 16, 16);
|
||||
public static readonly Color CaveSolidColor = Color.FromRgb(24, 20, 18);
|
||||
|
||||
private static readonly FrozenDictionary<Material, Color> ColorTable;
|
||||
private static readonly FrozenSet<Material> FullyTransparentMats;
|
||||
private static readonly FrozenSet<Material> WaterMats;
|
||||
private static readonly FrozenSet<Material> IceMats;
|
||||
|
||||
static MinimapColorMap()
|
||||
{
|
||||
var colors = new Dictionary<Material, Color>();
|
||||
var transparent = new HashSet<Material>();
|
||||
var water = new HashSet<Material>();
|
||||
var ice = new HashSet<Material>();
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = Assembly.GetExecutingAssembly()
|
||||
.GetManifestResourceStream("MinimapBlockColors.json");
|
||||
if (stream is not null)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(stream);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (root.TryGetProperty("colors", out var colorsEl))
|
||||
{
|
||||
foreach (var prop in colorsEl.EnumerateObject())
|
||||
{
|
||||
if (!Enum.TryParse<Material>(prop.Name, out var mat))
|
||||
continue;
|
||||
var arr = prop.Value;
|
||||
if (arr.GetArrayLength() < 3) continue;
|
||||
byte r = (byte)arr[0].GetInt32();
|
||||
byte g = (byte)arr[1].GetInt32();
|
||||
byte b = (byte)arr[2].GetInt32();
|
||||
colors[mat] = Color.FromRgb(r, g, b);
|
||||
}
|
||||
}
|
||||
|
||||
if (root.TryGetProperty("transparent", out var transEl))
|
||||
{
|
||||
foreach (var item in transEl.EnumerateArray())
|
||||
{
|
||||
if (Enum.TryParse<Material>(item.GetString(), out var mat))
|
||||
transparent.Add(mat);
|
||||
}
|
||||
}
|
||||
|
||||
if (root.TryGetProperty("water", out var waterEl))
|
||||
{
|
||||
foreach (var item in waterEl.EnumerateArray())
|
||||
{
|
||||
if (Enum.TryParse<Material>(item.GetString(), out var mat))
|
||||
water.Add(mat);
|
||||
}
|
||||
}
|
||||
|
||||
if (root.TryGetProperty("ice", out var iceEl))
|
||||
{
|
||||
foreach (var item in iceEl.EnumerateArray())
|
||||
{
|
||||
if (Enum.TryParse<Material>(item.GetString(), out var mat))
|
||||
ice.Add(mat);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConsoleIO.WriteLineFormatted($"\u00a7e[Minimap] Failed to load color data: {ex.Message}");
|
||||
}
|
||||
|
||||
if (transparent.Count == 0)
|
||||
{
|
||||
transparent.Add(Material.Air);
|
||||
transparent.Add(Material.CaveAir);
|
||||
transparent.Add(Material.VoidAir);
|
||||
}
|
||||
if (water.Count == 0)
|
||||
water.Add(Material.Water);
|
||||
if (ice.Count == 0)
|
||||
{
|
||||
ice.Add(Material.Ice);
|
||||
ice.Add(Material.PackedIce);
|
||||
ice.Add(Material.BlueIce);
|
||||
ice.Add(Material.FrostedIce);
|
||||
}
|
||||
|
||||
ColorTable = colors.ToFrozenDictionary();
|
||||
FullyTransparentMats = transparent.ToFrozenSet();
|
||||
WaterMats = water.ToFrozenSet();
|
||||
IceMats = ice.ToFrozenSet();
|
||||
}
|
||||
|
||||
public static bool IsFullyTransparent(Material m) => FullyTransparentMats.Contains(m);
|
||||
|
||||
/// <summary>
|
||||
/// Returns true for materials that block light propagation (solid, liquids),
|
||||
/// used by cave mode to find the surface from the player's Y level.
|
||||
/// Mirrors VoxelMap's lightDampening > 0 check.
|
||||
/// </summary>
|
||||
public static bool IsLightBlocking(Material m)
|
||||
=> (m == Material.Lava) || (!FullyTransparentMats.Contains(m) && m.IsSolid());
|
||||
|
||||
public static bool IsWater(Material m) => WaterMats.Contains(m);
|
||||
|
||||
public static bool IsIce(Material m) => IceMats.Contains(m);
|
||||
|
||||
public static Color GetBaseColor(Material m)
|
||||
{
|
||||
if (m == Material.Lava)
|
||||
return LavaColor;
|
||||
return ColorTable.GetValueOrDefault(m, DefaultColor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply Minecraft-style height shading. The shade multiplier depends on
|
||||
/// the height difference between the current block and the block to its north.
|
||||
/// Vanilla maps use four brightness levels: LOW (180/255), NORMAL (220/255),
|
||||
/// HIGH (255/255), and LOWEST (135/255). We use NORMAL as baseline and shift
|
||||
/// up/down based on delta.
|
||||
/// </summary>
|
||||
public static Color ApplyHeightShade(Color baseColor, int heightDelta)
|
||||
{
|
||||
int multiplier = heightDelta switch
|
||||
{
|
||||
> 0 => 255, // higher than neighbor: brightest
|
||||
0 => 220, // same height: normal
|
||||
_ => 180, // lower than neighbor: darker
|
||||
};
|
||||
byte r = (byte)(baseColor.R * multiplier / 255);
|
||||
byte g = (byte)(baseColor.G * multiplier / 255);
|
||||
byte b = (byte)(baseColor.B * multiplier / 255);
|
||||
return Color.FromRgb(r, g, b);
|
||||
}
|
||||
|
||||
public static Color BlendWaterColor(Color bottomColor, int waterDepth)
|
||||
{
|
||||
double alpha = Math.Min(0.85, 0.35 + waterDepth * 0.08);
|
||||
return Blend(WaterColor, bottomColor, alpha);
|
||||
}
|
||||
|
||||
public static Color BlendIceColor(Color bottomColor)
|
||||
{
|
||||
return Blend(IceColor, bottomColor, 0.35);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Darken a color to simulate underground lighting. Cave floors receive
|
||||
/// a minimum brightness of ~32/255 for non-solid blocks (matching VoxelMap),
|
||||
/// while solid/unreachable columns render as near-black.
|
||||
/// </summary>
|
||||
public static Color ApplyCaveDarkening(Color baseColor, double factor = 0.55)
|
||||
{
|
||||
byte r = (byte)(baseColor.R * factor);
|
||||
byte g = (byte)(baseColor.G * factor);
|
||||
byte b = (byte)(baseColor.B * factor);
|
||||
return Color.FromRgb(r, g, b);
|
||||
}
|
||||
|
||||
private static Color Blend(Color top, Color bottom, double topAlpha)
|
||||
{
|
||||
byte r = (byte)(top.R * topAlpha + bottom.R * (1.0 - topAlpha));
|
||||
byte g = (byte)(top.G * topAlpha + bottom.G * (1.0 - topAlpha));
|
||||
byte b = (byte)(top.B * topAlpha + bottom.B * (1.0 - topAlpha));
|
||||
return Color.FromRgb(r, g, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
1267
MinecraftClient/Tui/MinimapControl.cs
Normal file
1267
MinecraftClient/Tui/MinimapControl.cs
Normal file
File diff suppressed because it is too large
Load diff
167
MinecraftClient/Tui/MinimapEntityCategories.json
Normal file
167
MinecraftClient/Tui/MinimapEntityCategories.json
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
{
|
||||
"version": "26.1-rc-2",
|
||||
"hostile": [
|
||||
"Blaze",
|
||||
"Bogged",
|
||||
"Breeze",
|
||||
"CamelHusk",
|
||||
"Creaking",
|
||||
"Creeper",
|
||||
"Drowned",
|
||||
"ElderGuardian",
|
||||
"EnderDragon",
|
||||
"Endermite",
|
||||
"Evoker",
|
||||
"Ghast",
|
||||
"Giant",
|
||||
"Guardian",
|
||||
"Hoglin",
|
||||
"Husk",
|
||||
"Illusioner",
|
||||
"MagmaCube",
|
||||
"Parched",
|
||||
"Phantom",
|
||||
"Piglin",
|
||||
"PiglinBrute",
|
||||
"Pillager",
|
||||
"Ravager",
|
||||
"Shulker",
|
||||
"Silverfish",
|
||||
"Skeleton",
|
||||
"Slime",
|
||||
"Stray",
|
||||
"Vex",
|
||||
"Vindicator",
|
||||
"Warden",
|
||||
"Witch",
|
||||
"Wither",
|
||||
"WitherSkeleton",
|
||||
"Zoglin",
|
||||
"Zombie",
|
||||
"ZombieNautilus",
|
||||
"ZombieVillager"
|
||||
],
|
||||
"passive": [
|
||||
"Allay",
|
||||
"Armadillo",
|
||||
"Axolotl",
|
||||
"Bat",
|
||||
"Camel",
|
||||
"Cat",
|
||||
"Chicken",
|
||||
"Cod",
|
||||
"Cow",
|
||||
"Donkey",
|
||||
"Fox",
|
||||
"Frog",
|
||||
"GlowSquid",
|
||||
"HappyGhast",
|
||||
"Horse",
|
||||
"Mooshroom",
|
||||
"Mule",
|
||||
"Nautilus",
|
||||
"Ocelot",
|
||||
"Parrot",
|
||||
"Pig",
|
||||
"Pufferfish",
|
||||
"Rabbit",
|
||||
"Salmon",
|
||||
"Sheep",
|
||||
"SkeletonHorse",
|
||||
"Sniffer",
|
||||
"Squid",
|
||||
"Strider",
|
||||
"Tadpole",
|
||||
"TropicalFish",
|
||||
"Turtle",
|
||||
"Villager",
|
||||
"WanderingTrader",
|
||||
"ZombieHorse"
|
||||
],
|
||||
"neutral": [
|
||||
"Bee",
|
||||
"CaveSpider",
|
||||
"CopperGolem",
|
||||
"Dolphin",
|
||||
"Enderman",
|
||||
"Goat",
|
||||
"IronGolem",
|
||||
"Llama",
|
||||
"Panda",
|
||||
"PolarBear",
|
||||
"SnowGolem",
|
||||
"Spider",
|
||||
"TraderLlama",
|
||||
"Wolf",
|
||||
"ZombifiedPiglin"
|
||||
],
|
||||
"non_living": [
|
||||
"AcaciaBoat",
|
||||
"AcaciaChestBoat",
|
||||
"AreaEffectCloud",
|
||||
"ArmorStand",
|
||||
"Arrow",
|
||||
"BambooChestRaft",
|
||||
"BambooRaft",
|
||||
"BirchBoat",
|
||||
"BirchChestBoat",
|
||||
"BlockDisplay",
|
||||
"BreezeWindCharge",
|
||||
"CherryBoat",
|
||||
"CherryChestBoat",
|
||||
"ChestMinecart",
|
||||
"CommandBlockMinecart",
|
||||
"DarkOakBoat",
|
||||
"DarkOakChestBoat",
|
||||
"DragonFireball",
|
||||
"Egg",
|
||||
"EndCrystal",
|
||||
"EnderPearl",
|
||||
"EvokerFangs",
|
||||
"ExperienceBottle",
|
||||
"ExperienceOrb",
|
||||
"EyeOfEnder",
|
||||
"FallingBlock",
|
||||
"Fireball",
|
||||
"FireworkRocket",
|
||||
"FishingBobber",
|
||||
"FurnaceMinecart",
|
||||
"GlowItemFrame",
|
||||
"HopperMinecart",
|
||||
"Interaction",
|
||||
"Item",
|
||||
"ItemDisplay",
|
||||
"ItemFrame",
|
||||
"JungleBoat",
|
||||
"JungleChestBoat",
|
||||
"LeashKnot",
|
||||
"LightningBolt",
|
||||
"LingeringPotion",
|
||||
"LlamaSpit",
|
||||
"MangroveBoat",
|
||||
"MangroveChestBoat",
|
||||
"Mannequin",
|
||||
"Marker",
|
||||
"Minecart",
|
||||
"OakBoat",
|
||||
"OakChestBoat",
|
||||
"OminousItemSpawner",
|
||||
"Painting",
|
||||
"PaleOakBoat",
|
||||
"PaleOakChestBoat",
|
||||
"ShulkerBullet",
|
||||
"SmallFireball",
|
||||
"Snowball",
|
||||
"SpawnerMinecart",
|
||||
"SpectralArrow",
|
||||
"SplashPotion",
|
||||
"SpruceBoat",
|
||||
"SpruceChestBoat",
|
||||
"TextDisplay",
|
||||
"Tnt",
|
||||
"TntMinecart",
|
||||
"Trident",
|
||||
"WindCharge",
|
||||
"WitherSkull"
|
||||
]
|
||||
}
|
||||
178
MinecraftClient/Tui/MinimapEntityClassifier.cs
Normal file
178
MinecraftClient/Tui/MinimapEntityClassifier.cs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
using System;
|
||||
using System.Collections.Frozen;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Avalonia.Media;
|
||||
using MinecraftClient.Mapping;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
public enum MobCategory
|
||||
{
|
||||
Hostile,
|
||||
Passive,
|
||||
Neutral,
|
||||
Player,
|
||||
NonLiving,
|
||||
}
|
||||
|
||||
public enum MinimapPosition
|
||||
{
|
||||
top_left,
|
||||
top_right,
|
||||
center,
|
||||
bottom_left,
|
||||
bottom_right,
|
||||
}
|
||||
|
||||
public sealed class NameDisplayConfig
|
||||
{
|
||||
public volatile bool Players = false;
|
||||
public volatile bool Hostile = false;
|
||||
public volatile bool Neutral = false;
|
||||
public volatile bool Passive = false;
|
||||
|
||||
public bool AnyEnabled => Players || Hostile || Neutral || Passive;
|
||||
|
||||
public void SetAll(bool value)
|
||||
{
|
||||
Players = value;
|
||||
Hostile = value;
|
||||
Neutral = value;
|
||||
Passive = value;
|
||||
}
|
||||
|
||||
public bool ShouldShowName(MobCategory category) => category switch
|
||||
{
|
||||
MobCategory.Player => Players,
|
||||
MobCategory.Hostile => Hostile,
|
||||
MobCategory.Neutral => Neutral,
|
||||
MobCategory.Passive => Passive,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Classifies entities into minimap categories using data extracted from
|
||||
/// Minecraft's MobCategory assignments. Categories are loaded from the
|
||||
/// embedded MinimapEntityCategories.json resource generated by
|
||||
/// tools/gen_entity_category_map.py.
|
||||
/// </summary>
|
||||
public static class MinimapEntityClassifier
|
||||
{
|
||||
public static readonly Color HostileColor = Color.FromRgb(255, 68, 68);
|
||||
public static readonly Color PassiveColor = Color.FromRgb(68, 255, 68);
|
||||
public static readonly Color NeutralColor = Color.FromRgb(255, 170, 0);
|
||||
public static readonly Color PlayerColor = Color.FromRgb(255, 255, 255);
|
||||
public static readonly Color FadedGray = Color.FromRgb(100, 100, 100);
|
||||
|
||||
private static readonly FrozenDictionary<EntityType, MobCategory> CategoryTable;
|
||||
|
||||
static MinimapEntityClassifier()
|
||||
{
|
||||
var table = new Dictionary<EntityType, MobCategory>();
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = Assembly.GetExecutingAssembly()
|
||||
.GetManifestResourceStream("MinimapEntityCategories.json");
|
||||
if (stream is not null)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(stream);
|
||||
var root = doc.RootElement;
|
||||
|
||||
LoadCategory(root, "hostile", MobCategory.Hostile, table);
|
||||
LoadCategory(root, "passive", MobCategory.Passive, table);
|
||||
LoadCategory(root, "neutral", MobCategory.Neutral, table);
|
||||
LoadCategory(root, "non_living", MobCategory.NonLiving, table);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConsoleIO.WriteLogLine($"[Minimap] Failed to load entity categories: {ex.Message}");
|
||||
}
|
||||
|
||||
CategoryTable = table.ToFrozenDictionary();
|
||||
}
|
||||
|
||||
private static void LoadCategory(JsonElement root, string key,
|
||||
MobCategory category, Dictionary<EntityType, MobCategory> table)
|
||||
{
|
||||
if (!root.TryGetProperty(key, out var arr))
|
||||
return;
|
||||
|
||||
foreach (var el in arr.EnumerateArray())
|
||||
{
|
||||
var name = el.GetString();
|
||||
if (name is not null && Enum.TryParse<EntityType>(name, out var et))
|
||||
table.TryAdd(et, category);
|
||||
}
|
||||
}
|
||||
|
||||
public static MobCategory Classify(EntityType type)
|
||||
{
|
||||
if (type == EntityType.Player)
|
||||
return MobCategory.Player;
|
||||
return CategoryTable.GetValueOrDefault(type, MobCategory.NonLiving);
|
||||
}
|
||||
|
||||
public static Color GetBaseColor(MobCategory category) => category switch
|
||||
{
|
||||
MobCategory.Hostile => HostileColor,
|
||||
MobCategory.Passive => PassiveColor,
|
||||
MobCategory.Neutral => NeutralColor,
|
||||
MobCategory.Player => PlayerColor,
|
||||
_ => FadedGray,
|
||||
};
|
||||
|
||||
public static Color ApplyDepthFade(Color baseColor, double playerY, double entityY)
|
||||
{
|
||||
double depth = playerY - entityY;
|
||||
|
||||
if (depth <= 5.0)
|
||||
return baseColor;
|
||||
|
||||
if (depth >= 15.0)
|
||||
return FadedGray;
|
||||
|
||||
double t = (depth - 5.0) / 10.0;
|
||||
return Lerp(baseColor, FadedGray, t);
|
||||
}
|
||||
|
||||
public static bool ShouldDisplay(MobCategory category, double playerY, double entityY)
|
||||
{
|
||||
if (category == MobCategory.Player)
|
||||
return true;
|
||||
if (entityY >= playerY)
|
||||
return true;
|
||||
return playerY - entityY <= 15.0;
|
||||
}
|
||||
|
||||
public static int GetPriority(MobCategory category) => category switch
|
||||
{
|
||||
MobCategory.Hostile => 4,
|
||||
MobCategory.Player => 3,
|
||||
MobCategory.Neutral => 2,
|
||||
MobCategory.Passive => 1,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
public static string GetCategoryLabel(MobCategory category) => category switch
|
||||
{
|
||||
MobCategory.Hostile => Translations.tui_minimap_legend_hostile,
|
||||
MobCategory.Passive => Translations.tui_minimap_legend_passive,
|
||||
MobCategory.Neutral => Translations.tui_minimap_legend_neutral,
|
||||
MobCategory.Player => Translations.tui_minimap_legend_player,
|
||||
_ => "?",
|
||||
};
|
||||
|
||||
private static Color Lerp(Color a, Color b, double t)
|
||||
{
|
||||
byte r = (byte)(a.R + (b.R - a.R) * t);
|
||||
byte g = (byte)(a.G + (b.G - a.G) * t);
|
||||
byte bl = (byte)(a.B + (b.B - a.B) * t);
|
||||
return Color.FromRgb(r, g, bl);
|
||||
}
|
||||
}
|
||||
}
|
||||
196
MinecraftClient/Tui/ServerStatusPanelBuilder.cs
Normal file
196
MinecraftClient/Tui/ServerStatusPanelBuilder.cs
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
using System;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Documents;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
internal static class ServerStatusPanelBuilder
|
||||
{
|
||||
private const int MaxSamplePlayers = 10;
|
||||
private const int FaviconDisplaySize = 16;
|
||||
|
||||
internal static Border Build(Protocol.ServerStatusInfo info)
|
||||
{
|
||||
var contentPanel = new DockPanel { Background = Brushes.Black };
|
||||
|
||||
if (info.FaviconBase64 is not null)
|
||||
{
|
||||
var iconGrid = BuildFaviconGrid(info.FaviconBase64, FaviconDisplaySize);
|
||||
iconGrid.VerticalAlignment = VerticalAlignment.Center;
|
||||
DockPanel.SetDock(iconGrid, Dock.Left);
|
||||
contentPanel.Children.Add(iconGrid);
|
||||
}
|
||||
|
||||
var infoPanel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Vertical,
|
||||
Margin = new Thickness(1, 0, 0, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
|
||||
AddMotd(infoPanel, info);
|
||||
AddAddress(infoPanel, info);
|
||||
AddVersion(infoPanel, info);
|
||||
AddConnectingAs(infoPanel, info);
|
||||
AddPing(infoPanel, info);
|
||||
AddPlayers(infoPanel, info);
|
||||
AddSamplePlayers(infoPanel, info);
|
||||
|
||||
contentPanel.Children.Add(infoPanel);
|
||||
|
||||
return new Border
|
||||
{
|
||||
BorderBrush = new SolidColorBrush(Color.FromRgb(80, 80, 80)),
|
||||
BorderThickness = new Thickness(1),
|
||||
Background = new SolidColorBrush(Color.FromArgb(240, 20, 20, 20)),
|
||||
Padding = new Thickness(1, 0),
|
||||
Child = contentPanel,
|
||||
Margin = new Thickness(0),
|
||||
};
|
||||
}
|
||||
|
||||
private static void AddMotd(StackPanel panel, Protocol.ServerStatusInfo info)
|
||||
{
|
||||
if (string.IsNullOrEmpty(info.MotdRaw))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
string motdFormatted = Protocol.Message.ChatParser.ParseText(info.MotdRaw);
|
||||
foreach (string line in motdFormatted.Split('\n'))
|
||||
panel.Children.Add(McColorParser.CreateColoredTextBlock(line, TextWrapping.NoWrap));
|
||||
}
|
||||
catch
|
||||
{
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = info.MotdRaw,
|
||||
Foreground = Brushes.White,
|
||||
TextWrapping = TextWrapping.NoWrap,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddAddress(StackPanel panel, Protocol.ServerStatusInfo info)
|
||||
{
|
||||
var row = new TextBlock();
|
||||
row.Inlines!.Add(Label(Translations.mcc_server_info_label_server));
|
||||
row.Inlines.Add(Value(info.Host, McColors.Aqua));
|
||||
row.Inlines.Add(new Run($":{info.Port}") { Foreground = McColors.Gray });
|
||||
panel.Children.Add(row);
|
||||
}
|
||||
|
||||
private static void AddVersion(StackPanel panel, Protocol.ServerStatusInfo info)
|
||||
{
|
||||
string versionClean = Scripting.ChatBot.GetVerbatim(info.VersionName);
|
||||
var row = new TextBlock();
|
||||
row.Inlines!.Add(Label(Translations.mcc_server_info_label_version));
|
||||
row.Inlines.Add(Value(versionClean, McColors.Aqua));
|
||||
row.Inlines.Add(new Run(" (") { Foreground = McColors.Gray });
|
||||
row.Inlines.Add(new Run(string.Format(Translations.mcc_server_info_label_protocol, info.ProtocolVersion))
|
||||
{ Foreground = McColors.Gray });
|
||||
row.Inlines.Add(new Run(")") { Foreground = McColors.Gray });
|
||||
panel.Children.Add(row);
|
||||
}
|
||||
|
||||
private static void AddConnectingAs(StackPanel panel, Protocol.ServerStatusInfo info)
|
||||
{
|
||||
if (info.ResolvedProtocol == 0)
|
||||
return;
|
||||
|
||||
string resolvedMcVer = Protocol.ProtocolHandler.ProtocolVersion2MCVer(info.ResolvedProtocol);
|
||||
var row = new TextBlock();
|
||||
row.Inlines!.Add(Label(Translations.mcc_server_info_label_connecting_as));
|
||||
row.Inlines.Add(Value(resolvedMcVer, McColors.Green));
|
||||
row.Inlines.Add(new Run(" (") { Foreground = McColors.Gray });
|
||||
row.Inlines.Add(new Run(string.Format(Translations.mcc_server_info_label_protocol, info.ResolvedProtocol))
|
||||
{ Foreground = McColors.Gray });
|
||||
row.Inlines.Add(new Run(")") { Foreground = McColors.Gray });
|
||||
panel.Children.Add(row);
|
||||
}
|
||||
|
||||
private static void AddPing(StackPanel panel, Protocol.ServerStatusInfo info)
|
||||
{
|
||||
if (info.PingMs < 0)
|
||||
return;
|
||||
|
||||
var pingColor = info.PingMs < 100
|
||||
? McColors.Green
|
||||
: info.PingMs < 300
|
||||
? McColors.Yellow
|
||||
: McColors.Red;
|
||||
|
||||
var row = new TextBlock();
|
||||
row.Inlines!.Add(Label(Translations.mcc_server_info_label_ping));
|
||||
row.Inlines.Add(new Run(string.Format(Translations.mcc_server_info_label_ping_ms, info.PingMs))
|
||||
{ Foreground = pingColor });
|
||||
panel.Children.Add(row);
|
||||
}
|
||||
|
||||
private static void AddPlayers(StackPanel panel, Protocol.ServerStatusInfo info)
|
||||
{
|
||||
var row = new TextBlock();
|
||||
row.Inlines!.Add(Label(Translations.mcc_server_info_label_players));
|
||||
row.Inlines.Add(Value($"{info.OnlinePlayers}", McColors.Green));
|
||||
row.Inlines.Add(new Run("/") { Foreground = McColors.Gray });
|
||||
row.Inlines.Add(Value($"{info.MaxPlayers}", McColors.Red));
|
||||
panel.Children.Add(row);
|
||||
}
|
||||
|
||||
private static void AddSamplePlayers(StackPanel panel, Protocol.ServerStatusInfo info)
|
||||
{
|
||||
if (info.SamplePlayers.Count == 0)
|
||||
return;
|
||||
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = Translations.mcc_server_info_label_online,
|
||||
Foreground = McColors.Gray,
|
||||
});
|
||||
|
||||
int shown = Math.Min(info.SamplePlayers.Count, MaxSamplePlayers);
|
||||
for (int i = 0; i < shown; i++)
|
||||
{
|
||||
string name = info.SamplePlayers[i].Name;
|
||||
if (name.Contains('\u00a7'))
|
||||
panel.Children.Add(McColorParser.CreateColoredTextBlock($" {name}", TextWrapping.NoWrap));
|
||||
else
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $" {name}",
|
||||
Foreground = McColors.Green,
|
||||
});
|
||||
}
|
||||
|
||||
if (info.SamplePlayers.Count > shown)
|
||||
{
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $" {string.Format(Translations.mcc_server_info_sample_more, info.SamplePlayers.Count - shown)}",
|
||||
Foreground = McColors.Gray,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static Run Label(string text) =>
|
||||
new(text + " ") { Foreground = McColors.Gray };
|
||||
|
||||
private static Run Value(string text, IBrush color) =>
|
||||
new(text) { Foreground = color };
|
||||
|
||||
private static Grid BuildFaviconGrid(string base64Png, int displaySize) =>
|
||||
IconGridBuilder.BuildFromBase64(base64Png, displaySize);
|
||||
|
||||
private static class McColors
|
||||
{
|
||||
public static readonly IBrush Gray = new SolidColorBrush(Color.FromRgb(170, 170, 170));
|
||||
public static readonly IBrush Aqua = new SolidColorBrush(Color.FromRgb(85, 255, 255));
|
||||
public static readonly IBrush Green = new SolidColorBrush(Color.FromRgb(85, 255, 85));
|
||||
public static readonly IBrush Red = new SolidColorBrush(Color.FromRgb(255, 85, 85));
|
||||
public static readonly IBrush Yellow = new SolidColorBrush(Color.FromRgb(255, 255, 85));
|
||||
}
|
||||
}
|
||||
}
|
||||
114
MinecraftClient/Tui/TuiTooltipService.cs
Normal file
114
MinecraftClient/Tui/TuiTooltipService.cs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace MinecraftClient.Tui
|
||||
{
|
||||
public sealed class TuiTooltipLine
|
||||
{
|
||||
public string Text { get; init; } = "";
|
||||
public IBrush Foreground { get; init; } = Brushes.White;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Global tooltip that floats above all TUI content.
|
||||
/// Owned by MainTuiView, used by minimap / chat / other components.
|
||||
/// </summary>
|
||||
public sealed class TuiTooltipService
|
||||
{
|
||||
private readonly Panel _rootPanel;
|
||||
private readonly Canvas _canvas;
|
||||
private readonly Border _border;
|
||||
private readonly StackPanel _content;
|
||||
|
||||
internal TuiTooltipService(Panel rootPanel)
|
||||
{
|
||||
_content = new StackPanel { Orientation = Avalonia.Layout.Orientation.Vertical };
|
||||
_border = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.FromArgb(230, 20, 20, 20)),
|
||||
BorderBrush = new SolidColorBrush(Color.FromRgb(120, 120, 120)),
|
||||
BorderThickness = new Thickness(1),
|
||||
Padding = new Thickness(1),
|
||||
Child = _content,
|
||||
IsVisible = false,
|
||||
};
|
||||
|
||||
_canvas = new Canvas
|
||||
{
|
||||
IsHitTestVisible = false,
|
||||
Children = { _border },
|
||||
};
|
||||
|
||||
_rootPanel = rootPanel;
|
||||
rootPanel.Children.Add(_canvas);
|
||||
}
|
||||
|
||||
/// <param name="mouseX">Global X of the mouse cursor.</param>
|
||||
/// <param name="mouseY">Global Y of the mouse cursor.</param>
|
||||
/// <param name="preferRight">
|
||||
/// If true, try placing tooltip to the right of mouseX;
|
||||
/// if false, try placing to the left.
|
||||
/// The service auto-flips when the tooltip would overflow the screen.
|
||||
/// </param>
|
||||
public void Show(double mouseX, double mouseY, IReadOnlyList<TuiTooltipLine> lines,
|
||||
bool preferRight = true)
|
||||
{
|
||||
_content.Children.Clear();
|
||||
|
||||
if (lines.Count == 0)
|
||||
{
|
||||
_border.IsVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
int maxChars = 0;
|
||||
foreach (var line in lines)
|
||||
{
|
||||
_content.Children.Add(new TextBlock
|
||||
{
|
||||
Text = line.Text,
|
||||
Foreground = line.Foreground,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Padding = new Thickness(0),
|
||||
Margin = new Thickness(0),
|
||||
FontSize = 1,
|
||||
});
|
||||
if (line.Text.Length > maxChars)
|
||||
maxChars = line.Text.Length;
|
||||
}
|
||||
|
||||
double tipW = maxChars + 4;
|
||||
double screenW = _rootPanel.Bounds.Width;
|
||||
|
||||
const double gap = 1;
|
||||
double gx;
|
||||
if (preferRight)
|
||||
{
|
||||
gx = mouseX + gap;
|
||||
if (gx + tipW > screenW)
|
||||
gx = mouseX - tipW - gap;
|
||||
}
|
||||
else
|
||||
{
|
||||
gx = mouseX - tipW - gap;
|
||||
if (gx < 0)
|
||||
gx = mouseX + gap;
|
||||
}
|
||||
|
||||
Canvas.SetLeft(_border, Math.Max(0, gx));
|
||||
Canvas.SetTop(_border, Math.Max(0, mouseY));
|
||||
_border.IsVisible = true;
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
_border.IsVisible = false;
|
||||
_content.Children.Clear();
|
||||
}
|
||||
|
||||
public bool IsVisible => _border.IsVisible;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,6 @@ MCC.LoadBot(new PacketCadenceCaptureBot());
|
|||
|
||||
//MCCScript Extensions
|
||||
|
||||
using System.Threading;
|
||||
|
||||
public class PacketCadenceCaptureBot : ChatBot
|
||||
{
|
||||
private const int CaptureDurationSeconds = 5;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue