Merge branch 'master' into copilot/implement-automatic-mining-handling

This commit is contained in:
Anon 2026-03-30 19:13:39 +02:00 committed by GitHub
commit be20b97478
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 4467 additions and 2463 deletions

View 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);
}

View 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);
}
}
}

View 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);
}
}
}

View file

@ -45,11 +45,14 @@ 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();
@ -1356,6 +1359,42 @@ namespace MinecraftClient
}
}
/// <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>
@ -4198,6 +4237,67 @@ namespace MinecraftClient
}
}
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

View file

@ -228,9 +228,26 @@ 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;
@ -271,6 +288,13 @@ 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

View file

@ -91,6 +91,7 @@ namespace MinecraftClient.Protocol.Handlers
private int currentDimension;
private bool isOnlineMode = false;
private readonly BlockingCollection<Tuple<int, Queue<byte>>> packetQueue = new();
private readonly Dictionary<string, bool> legacyAchievementProgress = new(StringComparer.Ordinal);
private float LastYaw, LastPitch;
private double lastSentX, lastSentY, lastSentZ;
private float lastSentYaw, lastSentPitch;
@ -120,6 +121,7 @@ namespace MinecraftClient.Protocol.Handlers
Tuple<Thread, CancellationTokenSource>? netReader = null; // reader thread
readonly ILogger log;
readonly RandomNumberGenerator randomGen;
private bool legacyAchievementsInitialized;
public Protocol18Handler(TcpClient Client, int protocolVersion, IMinecraftComHandler handler,
ForgeInfo? forgeInfo, int rawProtocolVersion = 0)
@ -3132,6 +3134,19 @@ namespace MinecraftClient.Protocol.Handlers
case PacketTypesIn.RecipeBookSettings:
break;
case PacketTypesIn.Statistics:
if (protocolVersion < MC_1_12_Version)
HandleLegacyStatistics(packetData);
break;
case PacketTypesIn.Advancements:
HandleAdvancements(packetData);
break;
case PacketTypesIn.SelectAdvancementTab:
HandleSelectAdvancementTab(packetData);
break;
default:
return false; //Ignored packet
}
@ -3139,6 +3154,218 @@ namespace MinecraftClient.Protocol.Handlers
return true; //Packet processed
}
/// <summary>
/// Handle the Statistics packet for pre-1.12 legacy achievements.
/// </summary>
private void HandleLegacyStatistics(Queue<byte> packetData)
{
int statCount = dataTypes.ReadNextVarInt(packetData);
for (int i = 0; i < statCount; i++)
{
string statId = dataTypes.ReadNextString(packetData);
int value = dataTypes.ReadNextVarInt(packetData);
if (statId.StartsWith("achievement.", StringComparison.Ordinal))
legacyAchievementProgress[statId] = value > 0;
}
List<Achievement> added = new(LegacyAchievementCatalog.Ids.Count + legacyAchievementProgress.Count);
foreach (string achievementId in LegacyAchievementCatalog.Ids)
added.Add(CreateLegacyAchievement(achievementId, legacyAchievementProgress.TryGetValue(achievementId, out bool completed) && completed));
foreach (var (achievementId, completed) in legacyAchievementProgress)
{
if (!LegacyAchievementCatalog.Contains(achievementId))
added.Add(CreateLegacyAchievement(achievementId, completed));
}
handler.OnAchievementsUpdate(added, [], reset: !legacyAchievementsInitialized);
legacyAchievementsInitialized = true;
}
/// <summary>
/// Handle the Advancements packet (1.12+).
/// </summary>
private void HandleAdvancements(Queue<byte> packetData)
{
bool reset = dataTypes.ReadNextBool(packetData);
// --- Added advancements ---
int addedCount = dataTypes.ReadNextVarInt(packetData);
var added = new List<Achievement>(addedCount);
var addedDefinitions = new Dictionary<string, (string? title, string? description, AchievementType type, bool isHidden, List<List<string>> requirements)>(addedCount);
for (int i = 0; i < addedCount; i++)
{
string id = dataTypes.ReadNextString(packetData);
// Parent
bool hasParent = dataTypes.ReadNextBool(packetData);
if (hasParent)
dataTypes.ReadNextString(packetData); // parentId - read and discard
// Display
string? title = null;
string? description = null;
var type = AchievementType.Task;
bool isHidden = false;
bool hasDisplay = dataTypes.ReadNextBool(packetData);
if (hasDisplay)
{
title = dataTypes.ReadNextChat(packetData);
description = dataTypes.ReadNextChat(packetData);
dataTypes.ReadNextItemSlot(packetData, itemPalette); // icon - read and discard
int frameType = dataTypes.ReadNextVarInt(packetData);
type = frameType switch
{
1 => AchievementType.Challenge,
2 => AchievementType.Goal,
_ => AchievementType.Task
};
int flags = dataTypes.ReadNextInt(packetData);
isHidden = (flags & 0x04) != 0;
if ((flags & 0x01) != 0)
dataTypes.ReadNextString(packetData); // background texture - read and discard
dataTypes.ReadNextFloat(packetData); // x
dataTypes.ReadNextFloat(packetData); // y
}
// Criteria and requirements differ by version
var requirements = new List<List<string>>();
if (protocolVersion < MC_1_20_2_Version)
{
// Builder-based (pre-1.20.2): criteria names list, then requirements
int criteriaCount = dataTypes.ReadNextVarInt(packetData);
for (int c = 0; c < criteriaCount; c++)
dataTypes.ReadNextString(packetData); // criterion name only, no trigger data
}
// Requirements (all versions)
int reqGroupCount = dataTypes.ReadNextVarInt(packetData);
for (int g = 0; g < reqGroupCount; g++)
{
int groupSize = dataTypes.ReadNextVarInt(packetData);
var group = new List<string>(groupSize);
for (int s = 0; s < groupSize; s++)
group.Add(dataTypes.ReadNextString(packetData));
requirements.Add(group);
}
// sendsTelemetryEvent (added in 1.20, present in all versions since)
if (protocolVersion >= MC_1_20_Version)
dataTypes.ReadNextBool(packetData);
addedDefinitions[id] = (title, description, type, isHidden, requirements);
}
// --- Removed advancement IDs ---
int removedCount = dataTypes.ReadNextVarInt(packetData);
var removedIds = new List<string>(removedCount);
for (int i = 0; i < removedCount; i++)
removedIds.Add(dataTypes.ReadNextString(packetData));
// --- Progress updates ---
int progressCount = dataTypes.ReadNextVarInt(packetData);
var progressMap = new Dictionary<string, Dictionary<string, bool>>(progressCount);
for (int i = 0; i < progressCount; i++)
{
string id = dataTypes.ReadNextString(packetData);
int criteriaEntries = dataTypes.ReadNextVarInt(packetData);
var criteria = new Dictionary<string, bool>(criteriaEntries);
for (int c = 0; c < criteriaEntries; c++)
{
string criterionName = dataTypes.ReadNextString(packetData);
bool isDone = dataTypes.ReadNextBool(packetData);
if (isDone)
dataTypes.ReadNextLong(packetData); // epochMs - read and discard
criteria[criterionName] = isDone;
}
progressMap[id] = criteria;
}
// showAdvancements boolean added in 1.21.11+
if (protocolVersion >= MC_1_21_11_Version)
dataTypes.ReadNextBool(packetData); // showAdvancements - read and discard
// Build Achievement records from definitions + progress
foreach (var (id, def) in addedDefinitions)
{
progressMap.TryGetValue(id, out var criteria);
criteria ??= new Dictionary<string, bool>();
bool isCompleted = ComputeAdvancementCompleted(def.requirements, criteria);
var readOnlyReqs = def.requirements.ConvertAll<IReadOnlyList<string>>(static g => g.AsReadOnly());
added.Add(new Achievement(id, def.title, def.description, def.type, def.isHidden, isCompleted, readOnlyReqs.AsReadOnly(), criteria));
}
// Also build Achievement records for progress-only updates (no definition change)
foreach (var (id, criteria) in progressMap)
{
if (!addedDefinitions.ContainsKey(id))
added.Add(new Achievement(id, null, null, AchievementType.Task, false, false, [], criteria));
}
handler.OnAchievementsUpdate(added, removedIds, reset);
}
private static Achievement CreateLegacyAchievement(string id, bool isCompleted)
{
Dictionary<string, bool> criteria = new(StringComparer.Ordinal)
{
[id] = isCompleted
};
IReadOnlyList<string>[] requirements = [[id]];
return new Achievement(id, null, null, AchievementType.Legacy, false, isCompleted, requirements, criteria);
}
/// <summary>
/// Compute whether an advancement is completed based on AND-of-ORs requirements.
/// </summary>
private static bool ComputeAdvancementCompleted(List<List<string>> requirements, Dictionary<string, bool> criteria)
{
// Zero requirements = automatically done
if (requirements.Count == 0)
return true;
// Each OR-group must have at least one satisfied criterion
foreach (var 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>
/// Handle the SelectAdvancementTab packet.
/// </summary>
private void HandleSelectAdvancementTab(Queue<byte> packetData)
{
bool hasTab = dataTypes.ReadNextBool(packetData);
string? tabId = hasTab ? dataTypes.ReadNextString(packetData) : null;
handler.OnSelectAdvancementTab(tabId);
}
private void HandleUnlockRecipes(Queue<byte> packetData)
{
int action = dataTypes.ReadNextVarInt(packetData);
@ -3290,6 +3517,29 @@ namespace MinecraftClient.Protocol.Handlers
private string ReadSlotDisplayLabel(Queue<byte> packetData)
{
int slotDisplayType = dataTypes.ReadNextVarInt(packetData);
// 26.1 changed the slot display registry order, inserting 3 new types:
// Pre-26.1: 0=empty, 1=any_fuel, 2=item, 3=item_stack, 4=tag, 5=smithing_trim, 6=with_remainder, 7=composite
// 26.1+: 0=empty, 1=any_fuel, 2=with_any_potion, 3=only_with_component, 4=item, 5=item_stack, 6=tag, 7=dyed, 8=smithing_trim, 9=with_remainder, 10=composite
if (protocolVersion >= MC_26_1_Version)
{
return slotDisplayType switch
{
0 => "Empty",
1 => "Any Fuel",
2 => ReadWithAnyPotionSlotDisplayLabel(packetData),
3 => ReadOnlyWithComponentSlotDisplayLabel(packetData),
4 => Item.GetTypeString(itemPalette.FromId(dataTypes.ReadNextVarInt(packetData))),
5 => dataTypes.ReadNextItemSlot(packetData, itemPalette)?.GetTypeString() ?? "Empty",
6 => "#" + dataTypes.ReadNextString(packetData),
7 => ReadDyedSlotDisplayLabel(packetData),
8 => ReadSmithingTrimSlotDisplayLabel(packetData),
9 => ReadWithRemainderSlotDisplayLabel(packetData),
10 => ReadCompositeSlotDisplayLabel(packetData),
_ => $"slot_display_{slotDisplayType}",
};
}
return slotDisplayType switch
{
0 => "Empty",
@ -3304,6 +3554,34 @@ namespace MinecraftClient.Protocol.Handlers
};
}
/// <summary>
/// Reads a with_any_potion slot display (26.1+): contains a nested SlotDisplay.
/// </summary>
private string ReadWithAnyPotionSlotDisplayLabel(Queue<byte> packetData)
{
return ReadSlotDisplayLabel(packetData);
}
/// <summary>
/// Reads an only_with_component slot display (26.1+): contains a nested SlotDisplay and a DataComponentType VarInt ID.
/// </summary>
private string ReadOnlyWithComponentSlotDisplayLabel(Queue<byte> packetData)
{
string sourceLabel = ReadSlotDisplayLabel(packetData);
_ = dataTypes.ReadNextVarInt(packetData); // DataComponentType registry id
return sourceLabel;
}
/// <summary>
/// Reads a dyed slot display (26.1+): contains two nested SlotDisplays (dye + target).
/// </summary>
private string ReadDyedSlotDisplayLabel(Queue<byte> packetData)
{
_ = ReadSlotDisplayLabel(packetData); // dye
string targetLabel = ReadSlotDisplayLabel(packetData); // target
return targetLabel;
}
private string ReadSmithingTrimSlotDisplayLabel(Queue<byte> packetData)
{
string baseLabel = ReadSlotDisplayLabel(packetData);

View file

@ -530,6 +530,20 @@ namespace MinecraftClient.Protocol
/// <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

File diff suppressed because it is too large Load diff

View file

@ -566,6 +566,9 @@ 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>

View file

@ -2269,6 +2269,18 @@ 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);
@ -7174,5 +7186,104 @@ namespace MinecraftClient {
return ResourceManager.GetString("cmd.minimap.position_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);
}
}
}
}

View file

@ -830,6 +830,12 @@ 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>
@ -2527,4 +2533,37 @@ see item details.</value>
<data name="cmd.minimap.position_set" xml:space="preserve">
<value>Minimap position 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>

View file

@ -514,6 +514,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. */
@ -1121,6 +1129,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>

View file

@ -1210,6 +1210,9 @@ 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;

View 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);
}
}
}

View 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));
}
}
}

View file

@ -28,6 +28,7 @@ namespace MinecraftClient.Tui
{
Orientation = Orientation.Vertical,
Margin = new Thickness(1, 0, 0, 0),
VerticalAlignment = VerticalAlignment.Center,
};
AddMotd(infoPanel, info);
@ -47,7 +48,7 @@ namespace MinecraftClient.Tui
Background = new SolidColorBrush(Color.FromArgb(240, 20, 20, 20)),
Padding = new Thickness(1, 0),
Child = contentPanel,
Margin = new Thickness(0, 1),
Margin = new Thickness(0),
};
}
@ -180,114 +181,8 @@ namespace MinecraftClient.Tui
private static Run Value(string text, IBrush color) =>
new(text) { Foreground = color };
#region Favicon Rendering
private static Grid BuildFaviconGrid(string base64Png, int displaySize)
{
byte[] pngBytes;
try
{
pngBytes = Convert.FromBase64String(base64Png);
}
catch
{
return new Grid();
}
int srcWidth, srcHeight;
byte[] rgba;
try
{
(srcWidth, srcHeight, rgba) = DecodePngToRgba(pngBytes);
}
catch
{
return new Grid();
}
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;
}
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);
}
private static (int Width, int Height, byte[] Rgba) DecodePngToRgba(byte[] png)
{
using var image = new ImageMagick.MagickImage(png);
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);
}
#endregion
private static Grid BuildFaviconGrid(string base64Png, int displaySize) =>
IconGridBuilder.BuildFromBase64(base64Png, displaySize);
private static class McColors
{