From 0a463f2380c568e2e73542890750d3e581fa2cc1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sun, 22 Mar 2026 23:50:40 +0000
Subject: [PATCH 1/7] Initial plan
From a2d032b5497a2e06d432e295e78031bb972d02e9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 23 Mar 2026 00:03:55 +0000
Subject: [PATCH 2/7] Add Discord Rich Presence ChatBot integration
- Add DiscordRichPresence NuGet package (v1.143.0)
- Create DiscordRpc.cs ChatBot with configurable presence display
- Wire config in Settings.cs ChatBotConfig class
- Register bot in McClient.cs RegisterBots()
- Add translation strings to Translations.resx and Designer
- Add config comments to ConfigComments.resx and Designer
- Support placeholders: {server_host}, {server_port}, {username},
{health}, {max_health}, {food}, {dimension}, {gamemode},
{x}, {y}, {z}, {player_count}, {protocol}
Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com>
Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/43388021-4264-47e6-8403-7a1bca66d86f
---
MinecraftClient/ChatBots/DiscordRpc.cs | 309 ++++++++++++++++++
MinecraftClient/McClient.cs | 1 +
MinecraftClient/MinecraftClient.csproj | 1 +
.../ConfigComments/ConfigComments.Designer.cs | 99 ++++++
.../ConfigComments/ConfigComments.resx | 33 ++
.../Translations/Translations.Designer.cs | 72 ++++
.../Resources/Translations/Translations.resx | 24 ++
MinecraftClient/Settings.cs | 7 +
8 files changed, 546 insertions(+)
create mode 100644 MinecraftClient/ChatBots/DiscordRpc.cs
diff --git a/MinecraftClient/ChatBots/DiscordRpc.cs b/MinecraftClient/ChatBots/DiscordRpc.cs
new file mode 100644
index 00000000..0e17b910
--- /dev/null
+++ b/MinecraftClient/ChatBots/DiscordRpc.cs
@@ -0,0 +1,309 @@
+using System;
+using System.Diagnostics;
+using DiscordRPC;
+using DiscordRPC.Logging;
+using MinecraftClient.Mapping;
+using MinecraftClient.Scripting;
+using Tomlet.Attributes;
+
+namespace MinecraftClient.ChatBots
+{
+ ///
+ /// Displays a Discord Rich Presence status showing the player's
+ /// current Minecraft session information (server, health, dimension, etc.).
+ /// Requires a Discord Application ID from https://discord.com/developers/applications
+ ///
+ public class DiscordRpc : ChatBot
+ {
+ public static Configs Config = new();
+
+ [TomlDoNotInlineObject]
+ public class Configs
+ {
+ [NonSerialized]
+ private const string BotName = "DiscordRpc";
+
+ public bool Enabled = false;
+
+ [TomlInlineComment("$ChatBot.DiscordRpc.ApplicationId$")]
+ public string ApplicationId = string.Empty;
+
+ [TomlInlineComment("$ChatBot.DiscordRpc.PresenceDetails$")]
+ public string PresenceDetails = "Playing on {server_host}:{server_port}";
+
+ [TomlInlineComment("$ChatBot.DiscordRpc.PresenceState$")]
+ public string PresenceState = "{dimension} - HP: {health}/{max_health}";
+
+ [TomlInlineComment("$ChatBot.DiscordRpc.LargeImageKey$")]
+ public string LargeImageKey = "mcc_icon";
+
+ [TomlInlineComment("$ChatBot.DiscordRpc.LargeImageText$")]
+ public string LargeImageText = "Minecraft Console Client";
+
+ [TomlInlineComment("$ChatBot.DiscordRpc.SmallImageKey$")]
+ public string SmallImageKey = string.Empty;
+
+ [TomlInlineComment("$ChatBot.DiscordRpc.SmallImageText$")]
+ public string SmallImageText = string.Empty;
+
+ [TomlInlineComment("$ChatBot.DiscordRpc.ShowElapsedTime$")]
+ public bool ShowElapsedTime = true;
+
+ [TomlInlineComment("$ChatBot.DiscordRpc.ShowPlayerCount$")]
+ public bool ShowPlayerCount = true;
+
+ [TomlInlineComment("$ChatBot.DiscordRpc.UpdateIntervalSeconds$")]
+ public int UpdateIntervalSeconds = 10;
+
+ public void OnSettingUpdate()
+ {
+ ApplicationId ??= string.Empty;
+ PresenceDetails ??= string.Empty;
+ PresenceState ??= string.Empty;
+ LargeImageKey ??= string.Empty;
+ LargeImageText ??= string.Empty;
+ SmallImageKey ??= string.Empty;
+ SmallImageText ??= string.Empty;
+
+ if (UpdateIntervalSeconds < 1)
+ {
+ UpdateIntervalSeconds = 10;
+ LogToConsole(BotName, Translations.bot_DiscordRpc_invalid_interval);
+ }
+ }
+ }
+
+ private DiscordRpcClient? rpcClient;
+ private int tickCounter;
+ private int updateIntervalTicks;
+ private Timestamps? sessionTimestamps;
+ private float lastHealth;
+
+ public override void Initialize()
+ {
+ if (string.IsNullOrWhiteSpace(Config.ApplicationId))
+ {
+ LogToConsole(Translations.bot_DiscordRpc_missing_app_id);
+ UnloadBot();
+ return;
+ }
+
+ try
+ {
+ rpcClient = new DiscordRpcClient(Config.ApplicationId.Trim())
+ {
+ Logger = Settings.Config.Logging.DebugMessages
+ ? new ConsoleLogger(LogLevel.Trace)
+ : new ConsoleLogger(LogLevel.None)
+ };
+
+ rpcClient.OnReady += (_, e) =>
+ {
+ LogToConsole(string.Format(Translations.bot_DiscordRpc_connected, e.User.Username));
+ };
+
+ rpcClient.OnConnectionFailed += (_, e) =>
+ {
+ LogToConsole(string.Format(Translations.bot_DiscordRpc_connection_failed, e.FailedPipe));
+ };
+
+ rpcClient.Initialize();
+ updateIntervalTicks = Settings.DoubleToTick(Config.UpdateIntervalSeconds);
+
+ if (Config.ShowElapsedTime)
+ sessionTimestamps = Timestamps.Now;
+
+ lastHealth = Handler.GetHealth();
+
+ SetPresence();
+ LogToConsole(Translations.bot_DiscordRpc_initialized);
+ }
+ catch (Exception e)
+ {
+ LogToConsole(string.Format(Translations.bot_DiscordRpc_init_error, e.Message));
+ LogDebugToConsole(e.StackTrace ?? string.Empty);
+ UnloadBot();
+ }
+ }
+
+ public override void OnUnload()
+ {
+ if (rpcClient is { IsDisposed: false })
+ {
+ rpcClient.ClearPresence();
+ rpcClient.Dispose();
+ }
+
+ rpcClient = null;
+ }
+
+ public override void AfterGameJoined()
+ {
+ if (Config.ShowElapsedTime)
+ sessionTimestamps = Timestamps.Now;
+
+ SetPresence();
+ }
+
+ public override void Update()
+ {
+ tickCounter++;
+ if (tickCounter < updateIntervalTicks)
+ return;
+
+ tickCounter = 0;
+ SetPresence();
+ }
+
+ public override void OnHealthUpdate(float health, int food)
+ {
+ lastHealth = health;
+ }
+
+ public override bool OnDisconnect(DisconnectReason reason, string message)
+ {
+ if (rpcClient is { IsDisposed: false })
+ rpcClient.ClearPresence();
+
+ return false;
+ }
+
+ private void SetPresence()
+ {
+ if (rpcClient is null or { IsDisposed: true })
+ return;
+
+ try
+ {
+ string details = ReplacePlaceholders(Config.PresenceDetails);
+ string state = ReplacePlaceholders(Config.PresenceState);
+
+ var presence = new RichPresence
+ {
+ Details = TruncateForDiscord(details, 128),
+ State = TruncateForDiscord(state, 128)
+ };
+
+ // Assets (images)
+ var assets = new Assets();
+ bool hasAssets = false;
+
+ if (!string.IsNullOrWhiteSpace(Config.LargeImageKey))
+ {
+ assets.LargeImageKey = Config.LargeImageKey.Trim();
+ assets.LargeImageText = TruncateForDiscord(
+ ReplacePlaceholders(Config.LargeImageText), 128);
+ hasAssets = true;
+ }
+
+ if (!string.IsNullOrWhiteSpace(Config.SmallImageKey))
+ {
+ assets.SmallImageKey = Config.SmallImageKey.Trim();
+ assets.SmallImageText = TruncateForDiscord(
+ ReplacePlaceholders(Config.SmallImageText), 128);
+ hasAssets = true;
+ }
+
+ if (hasAssets)
+ presence.Assets = assets;
+
+ // Timestamps
+ if (Config.ShowElapsedTime && sessionTimestamps is not null)
+ presence.Timestamps = sessionTimestamps;
+
+ // Player count as party
+ if (Config.ShowPlayerCount)
+ {
+ string[] onlinePlayers = GetOnlinePlayers();
+ int playerCount = onlinePlayers.Length;
+ if (playerCount > 0)
+ {
+ presence.Party = new Party
+ {
+ ID = $"mcc_{GetServerHost()}_{GetServerPort()}",
+ Size = playerCount,
+ Max = Math.Max(playerCount, playerCount)
+ };
+ }
+ }
+
+ rpcClient.SetPresence(presence);
+ }
+ catch (Exception e)
+ {
+ LogDebugToConsole(string.Format(Translations.bot_DiscordRpc_update_error, e.Message));
+ }
+ }
+
+ private string ReplacePlaceholders(string template)
+ {
+ if (string.IsNullOrEmpty(template))
+ return string.Empty;
+
+ string serverHost = GetServerHost();
+ int serverPort = GetServerPort();
+ string username = GetUsername();
+ float health = Handler.GetHealth();
+ int food = Handler.GetSaturation();
+ Location location = GetCurrentLocation();
+ string[] onlinePlayers = GetOnlinePlayers();
+ int gamemode = GetGamemode();
+ int protocolVersion = GetProtocolVersion();
+
+ string dimensionName = "Unknown";
+ try
+ {
+ var dim = World.GetDimension();
+ dimensionName = dim.Name ?? "Unknown";
+
+ // Clean up the dimension name for display
+ if (dimensionName.StartsWith("minecraft:"))
+ dimensionName = dimensionName["minecraft:".Length..];
+
+ dimensionName = dimensionName switch
+ {
+ "overworld" => "Overworld",
+ "the_nether" => "The Nether",
+ "the_end" => "The End",
+ _ => dimensionName
+ };
+ }
+ catch
+ {
+ // World may not be available
+ }
+
+ string gamemodeStr = gamemode switch
+ {
+ 0 => "Survival",
+ 1 => "Creative",
+ 2 => "Adventure",
+ 3 => "Spectator",
+ _ => "Unknown"
+ };
+
+ return template
+ .Replace("{server_host}", serverHost)
+ .Replace("{server_port}", serverPort.ToString())
+ .Replace("{username}", username)
+ .Replace("{health}", ((int)Math.Ceiling(health)).ToString())
+ .Replace("{max_health}", "20")
+ .Replace("{food}", food.ToString())
+ .Replace("{dimension}", dimensionName)
+ .Replace("{gamemode}", gamemodeStr)
+ .Replace("{x}", ((int)location.X).ToString())
+ .Replace("{y}", ((int)location.Y).ToString())
+ .Replace("{z}", ((int)location.Z).ToString())
+ .Replace("{player_count}", onlinePlayers.Length.ToString())
+ .Replace("{protocol}", protocolVersion.ToString());
+ }
+
+ private static string TruncateForDiscord(string value, int maxLength)
+ {
+ if (string.IsNullOrEmpty(value))
+ return string.Empty;
+
+ return value.Length <= maxLength ? value : value[..(maxLength - 3)] + "...";
+ }
+ }
+}
diff --git a/MinecraftClient/McClient.cs b/MinecraftClient/McClient.cs
index b5ee2278..d0f86a2e 100644
--- a/MinecraftClient/McClient.cs
+++ b/MinecraftClient/McClient.cs
@@ -430,6 +430,7 @@ namespace MinecraftClient
if (Config.ChatBot.ScriptScheduler.Enabled) { BotLoad(new ScriptScheduler()); }
if (Config.ChatBot.TelegramBridge.Enabled) { BotLoad(new TelegramBridge()); }
if (Config.ChatBot.ItemsCollector.Enabled) { BotLoad(new ItemsCollector()); }
+ if (Config.ChatBot.DiscordRpc.Enabled) { BotLoad(new DiscordRpc()); }
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MCC_FILE_INPUT")))
BotLoad(new FileInputBot());
}
diff --git a/MinecraftClient/MinecraftClient.csproj b/MinecraftClient/MinecraftClient.csproj
index 6b0e4198..a25750c2 100644
--- a/MinecraftClient/MinecraftClient.csproj
+++ b/MinecraftClient/MinecraftClient.csproj
@@ -31,6 +31,7 @@
+
diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs
index 4dcf11cd..c01667d5 100644
--- a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs
+++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs
@@ -898,6 +898,105 @@ namespace MinecraftClient {
}
}
+ ///
+ /// Looks up a localized string similar to Show a Discord Rich Presence status with your current Minecraft session info..
+ ///
+ internal static string ChatBot_DiscordRpc {
+ get {
+ return ResourceManager.GetString("ChatBot.DiscordRpc", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Your Discord Application ID..
+ ///
+ internal static string ChatBot_DiscordRpc_ApplicationId {
+ get {
+ return ResourceManager.GetString("ChatBot.DiscordRpc.ApplicationId", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The top line of the Rich Presence display. Supports placeholders..
+ ///
+ internal static string ChatBot_DiscordRpc_PresenceDetails {
+ get {
+ return ResourceManager.GetString("ChatBot.DiscordRpc.PresenceDetails", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The second line of the Rich Presence display. Supports placeholders..
+ ///
+ internal static string ChatBot_DiscordRpc_PresenceState {
+ get {
+ return ResourceManager.GetString("ChatBot.DiscordRpc.PresenceState", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The key of the large image asset uploaded to your Discord application..
+ ///
+ internal static string ChatBot_DiscordRpc_LargeImageKey {
+ get {
+ return ResourceManager.GetString("ChatBot.DiscordRpc.LargeImageKey", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Tooltip text for the large image. Supports placeholders..
+ ///
+ internal static string ChatBot_DiscordRpc_LargeImageText {
+ get {
+ return ResourceManager.GetString("ChatBot.DiscordRpc.LargeImageText", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The key of the small image asset uploaded to your Discord application (leave empty to hide)..
+ ///
+ internal static string ChatBot_DiscordRpc_SmallImageKey {
+ get {
+ return ResourceManager.GetString("ChatBot.DiscordRpc.SmallImageKey", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Tooltip text for the small image. Supports placeholders..
+ ///
+ internal static string ChatBot_DiscordRpc_SmallImageText {
+ get {
+ return ResourceManager.GetString("ChatBot.DiscordRpc.SmallImageText", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Show elapsed session time in the Discord presence..
+ ///
+ internal static string ChatBot_DiscordRpc_ShowElapsedTime {
+ get {
+ return ResourceManager.GetString("ChatBot.DiscordRpc.ShowElapsedTime", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Show the online player count as a party size in the Discord presence..
+ ///
+ internal static string ChatBot_DiscordRpc_ShowPlayerCount {
+ get {
+ return ResourceManager.GetString("ChatBot.DiscordRpc.ShowPlayerCount", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to How often (in seconds) to refresh the Discord presence. Minimum: 1.
+ ///
+ internal static string ChatBot_DiscordRpc_UpdateIntervalSeconds {
+ get {
+ return ResourceManager.GetString("ChatBot.DiscordRpc.UpdateIntervalSeconds", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Relay messages between players and servers, like a mail plugin
///This bot can store messages when the recipients are offline, and send them when they join the server
diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx
index 53be0a22..0581b79c 100644
--- a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx
+++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx
@@ -828,6 +828,39 @@ If the connection to the Minecraft game server is blocked by the firewall, set E
A Chat Bot that collects items on the ground
+
+ Show a Discord Rich Presence status with your current Minecraft session info.\nRequires a Discord Application ID from https://discord.com/developers/applications\nYou can customize what is shown using placeholders: {server_host}, {server_port}, {username}, {health}, {max_health}, {food}, {dimension}, {gamemode}, {x}, {y}, {z}, {player_count}, {protocol}
+
+
+ Your Discord Application ID. Create one at https://discord.com/developers/applications
+
+
+ The top line of the Rich Presence display. Supports placeholders.
+
+
+ The second line of the Rich Presence display. Supports placeholders.
+
+
+ The key of the large image asset uploaded to your Discord application.
+
+
+ Tooltip text for the large image. Supports placeholders.
+
+
+ The key of the small image asset uploaded to your Discord application (leave empty to hide).
+
+
+ Tooltip text for the small image. Supports placeholders.
+
+
+ Show elapsed session time in the Discord presence.
+
+
+ Show the online player count as a party size in the Discord presence.
+
+
+ How often (in seconds) to refresh the Discord presence. Minimum: 1
+
Remotely control the client using Web Sockets.\n# This is useful if you want to implement an application that can remotely and asynchronously execute procedures in MCC.\n# Example implementation written in JavaScript: https://github.com/milutinke/MCC.js.git\n# The protocol specification will be available in the documentation soon.
diff --git a/MinecraftClient/Resources/Translations/Translations.Designer.cs b/MinecraftClient/Resources/Translations/Translations.Designer.cs
index af3da4ab..d1f31606 100644
--- a/MinecraftClient/Resources/Translations/Translations.Designer.cs
+++ b/MinecraftClient/Resources/Translations/Translations.Designer.cs
@@ -1095,6 +1095,69 @@ namespace MinecraftClient {
}
}
+ ///
+ /// Looks up a localized string similar to Please provide a valid Discord Application ID!.
+ ///
+ internal static string bot_DiscordRpc_missing_app_id {
+ get {
+ return ResourceManager.GetString("bot.DiscordRpc.missing_app_id", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Connected to Discord as {0}.
+ ///
+ internal static string bot_DiscordRpc_connected {
+ get {
+ return ResourceManager.GetString("bot.DiscordRpc.connected", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Failed to connect to Discord (pipe {0}). Is Discord running?.
+ ///
+ internal static string bot_DiscordRpc_connection_failed {
+ get {
+ return ResourceManager.GetString("bot.DiscordRpc.connection_failed", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Discord Rich Presence initialized successfully..
+ ///
+ internal static string bot_DiscordRpc_initialized {
+ get {
+ return ResourceManager.GetString("bot.DiscordRpc.initialized", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Failed to initialize Discord Rich Presence: {0}.
+ ///
+ internal static string bot_DiscordRpc_init_error {
+ get {
+ return ResourceManager.GetString("bot.DiscordRpc.init_error", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Error updating Discord presence: {0}.
+ ///
+ internal static string bot_DiscordRpc_update_error {
+ get {
+ return ResourceManager.GetString("bot.DiscordRpc.update_error", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Invalid update interval, must be at least 1 second. Using default of 10 seconds..
+ ///
+ internal static string bot_DiscordRpc_invalid_interval {
+ get {
+ return ResourceManager.GetString("bot.DiscordRpc.invalid_interval", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to The bot is already farming!.
///
@@ -2089,6 +2152,15 @@ namespace MinecraftClient {
}
}
+ ///
+ /// Looks up a localized string similar to DiscordRpc.
+ ///
+ internal static string botname_DiscordRpc {
+ get {
+ return ResourceManager.GetString("botname.DiscordRpc", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Farmer.
///
diff --git a/MinecraftClient/Resources/Translations/Translations.resx b/MinecraftClient/Resources/Translations/Translations.resx
index 26117d75..b16e7620 100644
--- a/MinecraftClient/Resources/Translations/Translations.resx
+++ b/MinecraftClient/Resources/Translations/Translations.resx
@@ -463,6 +463,27 @@ cooldown: {6}
An unknown error has occured!
+
+ Please provide a valid Discord Application ID! Get one at https://discord.com/developers/applications
+
+
+ Connected to Discord as {0}
+
+
+ Failed to connect to Discord (pipe {0}). Is Discord running?
+
+
+ Discord Rich Presence initialized successfully.
+
+
+ Failed to initialize Discord Rich Presence: {0}
+
+
+ Error updating Discord presence: {0}
+
+
+ Invalid update interval, must be at least 1 second. Using default of 10 seconds.
+
The bot is already farming!
@@ -770,6 +791,9 @@ Add the ID of this chat to "Authorized_Chat_Ids" field in the configuration file
DiscordBridge
+
+ DiscordRpc
+
Farmer
diff --git a/MinecraftClient/Settings.cs b/MinecraftClient/Settings.cs
index 0fc34523..0309dc17 100644
--- a/MinecraftClient/Settings.cs
+++ b/MinecraftClient/Settings.cs
@@ -1466,6 +1466,13 @@ namespace MinecraftClient
ChatBots.ItemsCollector.Config.OnSettingUpdate();
}
}
+
+ [TomlPrecedingComment("$ChatBot.DiscordRpc$")]
+ public ChatBots.DiscordRpc.Configs DiscordRpc
+ {
+ get { return ChatBots.DiscordRpc.Config; }
+ set { ChatBots.DiscordRpc.Config = value; ChatBots.DiscordRpc.Config.OnSettingUpdate(); }
+ }
}
}
From 8743129e72371f31ba9880468441291f7b3afd80 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 23 Mar 2026 00:08:08 +0000
Subject: [PATCH 3/7] Address code review: fix party max, rename food variable,
add ShowServerAddress option
Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com>
Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/43388021-4264-47e6-8403-7a1bca66d86f
---
MinecraftClient/ChatBots/DiscordRpc.cs | 14 +++++++++-----
.../ConfigComments/ConfigComments.Designer.cs | 9 +++++++++
.../Resources/ConfigComments/ConfigComments.resx | 3 +++
3 files changed, 21 insertions(+), 5 deletions(-)
diff --git a/MinecraftClient/ChatBots/DiscordRpc.cs b/MinecraftClient/ChatBots/DiscordRpc.cs
index 0e17b910..3daa3ef4 100644
--- a/MinecraftClient/ChatBots/DiscordRpc.cs
+++ b/MinecraftClient/ChatBots/DiscordRpc.cs
@@ -46,6 +46,9 @@ namespace MinecraftClient.ChatBots
[TomlInlineComment("$ChatBot.DiscordRpc.SmallImageText$")]
public string SmallImageText = string.Empty;
+ [TomlInlineComment("$ChatBot.DiscordRpc.ShowServerAddress$")]
+ public bool ShowServerAddress = true;
+
[TomlInlineComment("$ChatBot.DiscordRpc.ShowElapsedTime$")]
public bool ShowElapsedTime = true;
@@ -222,7 +225,7 @@ namespace MinecraftClient.ChatBots
{
ID = $"mcc_{GetServerHost()}_{GetServerPort()}",
Size = playerCount,
- Max = Math.Max(playerCount, playerCount)
+ Max = playerCount
};
}
}
@@ -240,11 +243,12 @@ namespace MinecraftClient.ChatBots
if (string.IsNullOrEmpty(template))
return string.Empty;
- string serverHost = GetServerHost();
+ string serverHost = Config.ShowServerAddress ? GetServerHost() : "Hidden";
int serverPort = GetServerPort();
+ string serverPortStr = Config.ShowServerAddress ? serverPort.ToString() : "****";
string username = GetUsername();
float health = Handler.GetHealth();
- int food = Handler.GetSaturation();
+ int foodLevel = Handler.GetSaturation();
Location location = GetCurrentLocation();
string[] onlinePlayers = GetOnlinePlayers();
int gamemode = GetGamemode();
@@ -284,11 +288,11 @@ namespace MinecraftClient.ChatBots
return template
.Replace("{server_host}", serverHost)
- .Replace("{server_port}", serverPort.ToString())
+ .Replace("{server_port}", serverPortStr)
.Replace("{username}", username)
.Replace("{health}", ((int)Math.Ceiling(health)).ToString())
.Replace("{max_health}", "20")
- .Replace("{food}", food.ToString())
+ .Replace("{food}", foodLevel.ToString())
.Replace("{dimension}", dimensionName)
.Replace("{gamemode}", gamemodeStr)
.Replace("{x}", ((int)location.X).ToString())
diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs
index c01667d5..0caddf01 100644
--- a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs
+++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs
@@ -970,6 +970,15 @@ namespace MinecraftClient {
}
}
+ ///
+ /// Looks up a localized string similar to Show the server address (host and port) in the Discord presence..
+ ///
+ internal static string ChatBot_DiscordRpc_ShowServerAddress {
+ get {
+ return ResourceManager.GetString("ChatBot.DiscordRpc.ShowServerAddress", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Show elapsed session time in the Discord presence..
///
diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx
index 0581b79c..2039e499 100644
--- a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx
+++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx
@@ -852,6 +852,9 @@ If the connection to the Minecraft game server is blocked by the firewall, set E
Tooltip text for the small image. Supports placeholders.
+
+ Show the server address (host and port) in the Discord presence. When disabled, {server_host} and {server_port} are masked for privacy.
+
Show elapsed session time in the Discord presence.
From 02686de79c85e5f9362ae2e910f51116ef662e97 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 23 Mar 2026 00:13:03 +0000
Subject: [PATCH 4/7] Add granular privacy controls: ShowCoordinates,
ShowHealth, ShowDimension, ShowGamemode
Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com>
Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/43388021-4264-47e6-8403-7a1bca66d86f
---
MinecraftClient/ChatBots/DiscordRpc.cs | 90 ++++++++++++-------
.../ConfigComments/ConfigComments.Designer.cs | 36 ++++++++
.../ConfigComments/ConfigComments.resx | 14 ++-
3 files changed, 106 insertions(+), 34 deletions(-)
diff --git a/MinecraftClient/ChatBots/DiscordRpc.cs b/MinecraftClient/ChatBots/DiscordRpc.cs
index 3daa3ef4..6e5c60cb 100644
--- a/MinecraftClient/ChatBots/DiscordRpc.cs
+++ b/MinecraftClient/ChatBots/DiscordRpc.cs
@@ -49,6 +49,18 @@ namespace MinecraftClient.ChatBots
[TomlInlineComment("$ChatBot.DiscordRpc.ShowServerAddress$")]
public bool ShowServerAddress = true;
+ [TomlInlineComment("$ChatBot.DiscordRpc.ShowCoordinates$")]
+ public bool ShowCoordinates = true;
+
+ [TomlInlineComment("$ChatBot.DiscordRpc.ShowHealth$")]
+ public bool ShowHealth = true;
+
+ [TomlInlineComment("$ChatBot.DiscordRpc.ShowDimension$")]
+ public bool ShowDimension = true;
+
+ [TomlInlineComment("$ChatBot.DiscordRpc.ShowGamemode$")]
+ public bool ShowGamemode = true;
+
[TomlInlineComment("$ChatBot.DiscordRpc.ShowElapsedTime$")]
public bool ShowElapsedTime = true;
@@ -254,50 +266,62 @@ namespace MinecraftClient.ChatBots
int gamemode = GetGamemode();
int protocolVersion = GetProtocolVersion();
- string dimensionName = "Unknown";
- try
+ string healthStr = Config.ShowHealth ? ((int)Math.Ceiling(health)).ToString() : "?";
+ string maxHealthStr = Config.ShowHealth ? "20" : "?";
+ string foodStr = Config.ShowHealth ? foodLevel.ToString() : "?";
+ string xStr = Config.ShowCoordinates ? ((int)location.X).ToString() : "?";
+ string yStr = Config.ShowCoordinates ? ((int)location.Y).ToString() : "?";
+ string zStr = Config.ShowCoordinates ? ((int)location.Z).ToString() : "?";
+
+ string dimensionName = Config.ShowDimension ? "Unknown" : "Hidden";
+ if (Config.ShowDimension)
{
- var dim = World.GetDimension();
- dimensionName = dim.Name ?? "Unknown";
-
- // Clean up the dimension name for display
- if (dimensionName.StartsWith("minecraft:"))
- dimensionName = dimensionName["minecraft:".Length..];
-
- dimensionName = dimensionName switch
+ try
{
- "overworld" => "Overworld",
- "the_nether" => "The Nether",
- "the_end" => "The End",
- _ => dimensionName
- };
- }
- catch
- {
- // World may not be available
+ var dim = World.GetDimension();
+ dimensionName = dim.Name ?? "Unknown";
+
+ // Clean up the dimension name for display
+ if (dimensionName.StartsWith("minecraft:"))
+ dimensionName = dimensionName["minecraft:".Length..];
+
+ dimensionName = dimensionName switch
+ {
+ "overworld" => "Overworld",
+ "the_nether" => "The Nether",
+ "the_end" => "The End",
+ _ => dimensionName
+ };
+ }
+ catch
+ {
+ // World may not be available
+ }
}
- string gamemodeStr = gamemode switch
- {
- 0 => "Survival",
- 1 => "Creative",
- 2 => "Adventure",
- 3 => "Spectator",
- _ => "Unknown"
- };
+ string gamemodeStr = Config.ShowGamemode
+ ? gamemode switch
+ {
+ 0 => "Survival",
+ 1 => "Creative",
+ 2 => "Adventure",
+ 3 => "Spectator",
+ _ => "Unknown"
+ }
+ : "Hidden";
return template
.Replace("{server_host}", serverHost)
.Replace("{server_port}", serverPortStr)
.Replace("{username}", username)
- .Replace("{health}", ((int)Math.Ceiling(health)).ToString())
- .Replace("{max_health}", "20")
- .Replace("{food}", foodLevel.ToString())
+ .Replace("{health}", healthStr)
+ .Replace("{max_health}", maxHealthStr)
+ .Replace("{food}", foodStr)
.Replace("{dimension}", dimensionName)
.Replace("{gamemode}", gamemodeStr)
- .Replace("{x}", ((int)location.X).ToString())
- .Replace("{y}", ((int)location.Y).ToString())
- .Replace("{z}", ((int)location.Z).ToString())
+ .Replace("{x}", xStr)
+ .Replace("{y}", yStr)
+ .Replace("{z}", zStr)
.Replace("{player_count}", onlinePlayers.Length.ToString())
.Replace("{protocol}", protocolVersion.ToString());
}
diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs
index 0caddf01..cd3a09a5 100644
--- a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs
+++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs
@@ -979,6 +979,42 @@ namespace MinecraftClient {
}
}
+ ///
+ /// Looks up a localized string similar to Show the player coordinates in the Discord presence..
+ ///
+ internal static string ChatBot_DiscordRpc_ShowCoordinates {
+ get {
+ return ResourceManager.GetString("ChatBot.DiscordRpc.ShowCoordinates", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Show health and food level in the Discord presence..
+ ///
+ internal static string ChatBot_DiscordRpc_ShowHealth {
+ get {
+ return ResourceManager.GetString("ChatBot.DiscordRpc.ShowHealth", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Show the current dimension in the Discord presence..
+ ///
+ internal static string ChatBot_DiscordRpc_ShowDimension {
+ get {
+ return ResourceManager.GetString("ChatBot.DiscordRpc.ShowDimension", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Show the current gamemode in the Discord presence..
+ ///
+ internal static string ChatBot_DiscordRpc_ShowGamemode {
+ get {
+ return ResourceManager.GetString("ChatBot.DiscordRpc.ShowGamemode", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Show elapsed session time in the Discord presence..
///
diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx
index 2039e499..b68844fa 100644
--- a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx
+++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx
@@ -853,7 +853,19 @@ If the connection to the Minecraft game server is blocked by the firewall, set E
Tooltip text for the small image. Supports placeholders.
- Show the server address (host and port) in the Discord presence. When disabled, {server_host} and {server_port} are masked for privacy.
+ Show the server address (host and port) in the Discord presence. When disabled, {server_host} and {server_port} are masked.
+
+
+ Show the player coordinates in the Discord presence. When disabled, {x}, {y}, {z} are masked.
+
+
+ Show health and food level in the Discord presence. When disabled, {health}, {max_health}, {food} are masked.
+
+
+ Show the current dimension in the Discord presence. When disabled, {dimension} is masked.
+
+
+ Show the current gamemode in the Discord presence. When disabled, {gamemode} is masked.
Show elapsed session time in the Discord presence.
From 5a9786cb0bb23ebdc80b253f7f0c89468d61878c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 23 Mar 2026 00:41:40 +0000
Subject: [PATCH 5/7] Add Discord RPC setup tutorial, Discord Bridge
AllowOtherBotMessages config, and enhance integration tests
Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com>
Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/a41f3e83-c88d-4c57-971b-54c380891556
---
.../references/command-matrix.md | 38 ++++++++
.../scripts/run_full_spectrum_test.sh | 87 +++++++++++++++++++
MinecraftClient/ChatBots/DiscordBridge.cs | 17 +++-
.../ConfigComments/ConfigComments.Designer.cs | 15 +++-
.../ConfigComments/ConfigComments.resx | 11 ++-
5 files changed, 164 insertions(+), 4 deletions(-)
diff --git a/.skills/mcc-integration-testing/references/command-matrix.md b/.skills/mcc-integration-testing/references/command-matrix.md
index 9880f2f8..31077374 100644
--- a/.skills/mcc-integration-testing/references/command-matrix.md
+++ b/.skills/mcc-integration-testing/references/command-matrix.md
@@ -9,9 +9,19 @@ This skill uses a fixed set of stable commands for local offline integration tes
- `inventory player list`
- `/gamemode creative`
- `inventory creativegive 36 Diamond 16`
+- `inventory creativegive 37 IronSword 1`
+- `inventory creativegive 38 GoldenApple 8`
+- `inventory creativeclear 38`
- `entity`
- `/time query daytime`
+- `look up`
+- `look down`
+- `look east`
+- `/gamemode survival`
+- `respawn`
+- `/tp CursorBot 0 -60 0`
- `smoke_test_from_mcc_full_spectrum`
+- `integration_test_chat_response`
Notes:
- Lines starting with `/` are sent to the server as chat/commands.
@@ -24,6 +34,11 @@ Notes:
- `gamerule logAdminCommands true`
- `time set day`
- `weather clear`
+- `say Hello from the server console`
+- `msg CursorBot This is a private whisper`
+- `effect give CursorBot minecraft:speed 30 1`
+- `effect give CursorBot minecraft:regeneration 10 1`
+- `kill CursorBot`
## Representative entity coverage
@@ -34,6 +49,21 @@ Notes:
- `execute as CursorBot at @s run summon minecraft:villager ~-2 ~ ~`
- `execute as CursorBot at @s run summon minecraft:allay ~-4 ~ ~`
- `execute as CursorBot at @s run summon minecraft:armor_stand ~ ~ ~2`
+- `execute as CursorBot at @s run summon minecraft:item_display ~-6 ~ ~ {item:{id:"minecraft:diamond",count:1}}`
+- `execute as CursorBot at @s run summon minecraft:spider ~10 ~ ~`
+- `execute as CursorBot at @s run summon minecraft:pig ~-8 ~ ~`
+
+## Block placement coverage
+
+- `execute as CursorBot at @s run fill ~1 ~ ~1 ~3 ~2 ~3 minecraft:stone`
+- `execute as CursorBot at @s run setblock ~5 ~ ~5 minecraft:chest`
+- `execute as CursorBot at @s run setblock ~5 ~1 ~5 minecraft:furnace`
+- `execute as CursorBot at @s run setblock ~6 ~ ~5 minecraft:crafting_table`
+
+## Dimension change coverage
+
+- `execute in minecraft:the_nether run tp CursorBot 0 64 0`
+- `execute in minecraft:overworld run tp CursorBot 0 -60 0`
## Representative particle coverage
@@ -41,13 +71,21 @@ Notes:
- `execute as CursorBot at @s run particle minecraft:end_rod ~ ~1 ~ 0.5 0.5 0.5 0.01 20 force`
- `execute as CursorBot at @s run particle minecraft:explosion ~ ~1 ~ 0 0 0 0 1 force`
- `execute as CursorBot at @s run particle minecraft:totem_of_undying ~ ~1 ~ 0.5 0.5 0.5 0.1 20 force`
+- `execute as CursorBot at @s run particle minecraft:flame ~ ~1 ~ 0.2 0.2 0.2 0.02 30 force`
+- `execute as CursorBot at @s run particle minecraft:heart ~ ~2 ~ 0.3 0.3 0.3 0 5 force`
## Representative sound coverage
- `execute as CursorBot at @s run playsound minecraft:entity.lightning_bolt.thunder master CursorBot ~ ~ ~ 1 1 0`
- `execute as CursorBot at @s run playsound minecraft:block.note_block.bell master CursorBot ~ ~ ~ 1 1 0`
+- `execute as CursorBot at @s run playsound minecraft:entity.experience_orb.pickup master CursorBot ~ ~ ~ 1 1 0`
## Explosion coverage
- `execute as CursorBot at @s run summon minecraft:tnt ~3 ~ ~`
- `execute as CursorBot at @s run summon minecraft:tnt ~6 ~ ~`
+
+## Kill and respawn cycle
+
+- `kill CursorBot` (via RCON, requires survival mode)
+- `respawn` (via MCC command after death)
diff --git a/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh b/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh
index 1ae6e983..02bc4f9f 100755
--- a/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh
+++ b/.skills/mcc-integration-testing/scripts/run_full_spectrum_test.sh
@@ -155,6 +155,7 @@ run_server_command "time set day"
run_server_command "weather clear"
sleep 2
+# ── Phase 1: Basic status and info commands ──
run_mcc_command "health"
run_mcc_command "list"
run_mcc_command "inventory player list"
@@ -165,6 +166,34 @@ run_mcc_command "entity"
run_mcc_command "/time query daytime"
run_mcc_command "smoke_test_from_mcc_full_spectrum"
+# ── Phase 2: Movement and look commands ──
+run_mcc_command "/tp CursorBot 0 -60 0"
+sleep 3
+run_mcc_command "look up"
+sleep 1
+run_mcc_command "look down"
+sleep 1
+run_mcc_command "look east"
+sleep 1
+
+# ── Phase 3: Advanced inventory operations ──
+run_mcc_command "inventory creativegive 37 IronSword 1"
+run_mcc_command "inventory creativegive 38 GoldenApple 8"
+run_mcc_command "inventory player list"
+run_mcc_command "inventory creativeclear 38"
+run_mcc_command "inventory player list"
+
+# ── Phase 4: Block placement and interaction ──
+run_server_command "execute as CursorBot at @s run fill ~1 ~ ~1 ~3 ~2 ~3 minecraft:stone"
+sleep 2
+run_server_command "execute as CursorBot at @s run setblock ~5 ~ ~5 minecraft:chest"
+sleep 1
+run_server_command "execute as CursorBot at @s run setblock ~5 ~1 ~5 minecraft:furnace"
+sleep 1
+run_server_command "execute as CursorBot at @s run setblock ~6 ~ ~5 minecraft:crafting_table"
+sleep 1
+
+# ── Phase 5: Entity spawning (expanded coverage) ──
run_server_command "execute as CursorBot at @s run summon minecraft:cow ~2 ~ ~"
run_server_command "execute as CursorBot at @s run summon minecraft:zombie ~4 ~ ~"
run_server_command "execute as CursorBot at @s run summon minecraft:creeper ~6 ~ ~"
@@ -172,41 +201,99 @@ run_server_command "execute as CursorBot at @s run summon minecraft:skeleton ~8
run_server_command "execute as CursorBot at @s run summon minecraft:villager ~-2 ~ ~"
run_server_command "execute as CursorBot at @s run summon minecraft:allay ~-4 ~ ~"
run_server_command "execute as CursorBot at @s run summon minecraft:armor_stand ~ ~ ~2"
+run_server_command "execute as CursorBot at @s run summon minecraft:item_display ~-6 ~ ~ {item:{id:\"minecraft:diamond\",count:1}}"
+run_server_command "execute as CursorBot at @s run summon minecraft:spider ~10 ~ ~"
+run_server_command "execute as CursorBot at @s run summon minecraft:pig ~-8 ~ ~"
sleep 2
run_mcc_command "entity"
+# ── Phase 6: Effects and environment ──
+run_server_command "effect give CursorBot minecraft:speed 30 1"
+sleep 2
+run_mcc_command "health"
+run_server_command "effect give CursorBot minecraft:regeneration 10 1"
+sleep 2
+run_mcc_command "health"
+
+# ── Phase 7: Gamemode cycling ──
+run_mcc_command "/gamemode survival"
+sleep 2
+run_mcc_command "health"
+run_mcc_command "/gamemode creative"
+sleep 2
+
+# ── Phase 8: Dimension change (nether) ──
+run_server_command "execute in minecraft:the_nether run tp CursorBot 0 64 0"
+sleep 4
+run_mcc_command "health"
+run_server_command "execute in minecraft:overworld run tp CursorBot 0 -60 0"
+sleep 4
+
+# ── Phase 9: Server chat and whisper ──
+run_server_command "say Hello from the server console"
+sleep 2
+run_server_command "msg CursorBot This is a private whisper"
+sleep 2
+run_mcc_command "integration_test_chat_response"
+
+# ── Phase 10: Particles, sounds, and explosions ──
run_server_command "execute as CursorBot at @s run particle minecraft:happy_villager ~ ~1 ~ 0.5 0.5 0.5 0 12 force"
run_server_command "execute as CursorBot at @s run particle minecraft:end_rod ~ ~1 ~ 0.5 0.5 0.5 0.01 20 force"
run_server_command "execute as CursorBot at @s run particle minecraft:explosion ~ ~1 ~ 0 0 0 0 1 force"
run_server_command "execute as CursorBot at @s run particle minecraft:totem_of_undying ~ ~1 ~ 0.5 0.5 0.5 0.1 20 force"
+run_server_command "execute as CursorBot at @s run particle minecraft:flame ~ ~1 ~ 0.2 0.2 0.2 0.02 30 force"
+run_server_command "execute as CursorBot at @s run particle minecraft:heart ~ ~2 ~ 0.3 0.3 0.3 0 5 force"
run_server_command "execute as CursorBot at @s run playsound minecraft:entity.lightning_bolt.thunder master CursorBot ~ ~ ~ 1 1 0"
run_server_command "execute as CursorBot at @s run playsound minecraft:block.note_block.bell master CursorBot ~ ~ ~ 1 1 0"
+run_server_command "execute as CursorBot at @s run playsound minecraft:entity.experience_orb.pickup master CursorBot ~ ~ ~ 1 1 0"
run_server_command "execute as CursorBot at @s run summon minecraft:tnt ~3 ~ ~"
sleep 2
run_server_command "execute as CursorBot at @s run summon minecraft:tnt ~6 ~ ~"
+# ── Phase 11: Kill and respawn cycle ──
+run_mcc_command "/gamemode survival"
+sleep 2
+run_server_command "kill CursorBot"
+sleep 4
+run_mcc_command "respawn"
+sleep 4
+run_mcc_command "health"
+run_mcc_command "/gamemode creative"
+sleep 2
+
sleep 6
capture_server_logs
+# ── Assertions: MCC log ──
assert_contains "$MCC_LOG" "Server was successfully joined." "MCC never joined the server"
assert_contains "$MCC_LOG" "[FileInput] > inventory player list" "Inventory command was not executed"
assert_contains "$MCC_LOG" "[FileInput] > entity" "Entity command was not executed"
assert_contains "$MCC_LOG" "[FileInput] > /gamemode creative" "Creative mode command was not executed from MCC"
assert_contains "$MCC_LOG" "Requested Diamond x16 in slot #36" "Creative inventory give did not succeed"
assert_contains "$MCC_LOG" "smoke_test_from_mcc_full_spectrum" "Client-originated chat was not observed"
+assert_contains "$MCC_LOG" "[FileInput] > look up" "Look command was not executed"
+assert_contains "$MCC_LOG" "[FileInput] > /gamemode survival" "Survival mode switch was not executed"
+assert_contains "$MCC_LOG" "[FileInput] > respawn" "Respawn command was not executed"
+assert_contains "$MCC_LOG" "[FileInput] > health" "Health command was not executed"
+assert_contains "$MCC_LOG" "integration_test_chat_response" "Chat response test message was not observed"
assert_not_contains "$MCC_LOG" "Please enable InventoryHandling" "Inventory handling is still disabled"
assert_not_contains "$MCC_LOG" "Please enable EntityHandling" "Entity handling is still disabled"
assert_not_contains "$MCC_LOG" "You must be in Creative gamemode" "Creative mode was not active when creativegive ran"
assert_not_contains "$MCC_LOG" "Failed to load settings" "MCC failed to reload its config"
+assert_not_contains "$MCC_LOG" "NullReferenceException" "A NullReferenceException occurred during the test"
+# ── Assertions: Server log ──
assert_contains "$SERVER_FILE_LOG" "CursorBot joined the game" "Server never saw CursorBot join"
assert_contains "$SERVER_FILE_LOG" "smoke_test_from_mcc_full_spectrum" "Server never received the client chat message"
assert_contains "$SERVER_FILE_LOG" "Displaying particle minecraft:happy_villager" "Particle events were not recorded on the server"
assert_contains "$SERVER_FILE_LOG" "Played sound minecraft:block.note_block.bell to CursorBot" "Sound events were not recorded on the server"
assert_contains "$SERVER_FILE_LOG" "Summoned new Primed TNT" "TNT summon did not occur on the server"
+assert_contains "$SERVER_FILE_LOG" "integration_test_chat_response" "Server never received the chat response test message"
+assert_contains "$SERVER_FILE_LOG" "Hello from the server console" "Server say command was not logged"
+assert_contains "$SERVER_FILE_LOG" "Killed CursorBot" "Server kill command did not execute"
assert_not_contains "$SERVER_FILE_LOG" "Sending unknown packet 'clientbound/minecraft:disconnect'" "Server hit the disconnect packet regression during the test"
cat <
- /// Looks up a localized string similar to Automatically farms crops for you (plants, breaks and bonemeals them).
+ /// Looks up a localized string similar to When enabled, messages from other Discord bots in the channel will be relayed to Minecraft chat..
+ ///
+ internal static string ChatBot_DiscordBridge_AllowOtherBotMessages {
+ get {
+ return ResourceManager.GetString("ChatBot.DiscordBridge.AllowOtherBotMessages", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Automatically farms cropsfor you (plants, breaks and bonemeals them).
///Crop types available: Beetroot, Carrot, Melon, Netherwart, Pumpkin, Potato, Wheat.
///Usage: "/farmer start" command and "/farmer stop" command.
///NOTE: This a newly added bot, it is not perfect and was only tested in 1.19.2, there are some minor issues like not being able to bonemeal carrots/potatoes sometimes.
@@ -899,7 +908,9 @@ namespace MinecraftClient {
}
///
- /// Looks up a localized string similar to Show a Discord Rich Presence status with your current Minecraft session info..
+ /// Looks up a localized string similar to Show a Discord Rich Presence status with your current Minecraft session info.
+ ///Setup:
+ ///1. Go to https://discord.com/developers/applications and log in with your Discord account. [rest of string was truncated]";.
///
internal static string ChatBot_DiscordRpc {
get {
diff --git a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx
index b68844fa..9c6152df 100644
--- a/MinecraftClient/Resources/ConfigComments/ConfigComments.resx
+++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.resx
@@ -390,6 +390,9 @@ For Discord message formatting, check the following: https://mccteam.github.io/r
Your Discord Bot token.
+
+ 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.
+
Automatically farms crops for you (plants, breaks and bonemeals them).
Crop types available: Beetroot, Carrot, Melon, Netherwart, Pumpkin, Potato, Wheat.
@@ -829,7 +832,13 @@ If the connection to the Minecraft game server is blocked by the firewall, set E
A Chat Bot that collects items on the ground
- Show a Discord Rich Presence status with your current Minecraft session info.\nRequires a Discord Application ID from https://discord.com/developers/applications\nYou can customize what is shown using placeholders: {server_host}, {server_port}, {username}, {health}, {max_health}, {food}, {dimension}, {gamemode}, {x}, {y}, {z}, {player_count}, {protocol}
+ Show a Discord Rich Presence status with your current Minecraft session info.
+Setup:
+1. Go to https://discord.com/developers/applications and log in with your Discord account.
+2. Click "New Application", give it a name (e.g. "MCC") and confirm.
+3. On the application page, copy the "Application ID" and paste it in the "ApplicationId" field below.
+4. (Optional) Go to "Rich Presence" -> "Art Assets" to upload custom images for LargeImageKey/SmallImageKey.
+Note: This does NOT require a Bot Token, only an Application ID. Discord must be running on the same machine as MCC.
Your Discord Application ID. Create one at https://discord.com/developers/applications
From 6c1449439ce8c83732c0d75334dfeff03b5c28fa Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 23 Mar 2026 00:49:34 +0000
Subject: [PATCH 6/7] Add Discord RPC E2E test scripts and address code review
feedback
Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com>
Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/a41f3e83-c88d-4c57-971b-54c380891556
---
.../discord-rpc-e2e/fake_discord_ipc.py | 222 ++++++++++++++
.../scripts/discord-rpc-e2e/fake_mc_server.py | 281 ++++++++++++++++++
.../discord-rpc-e2e/run_e2e_discord_rpc.py | 280 +++++++++++++++++
MinecraftClient/ChatBots/DiscordBridge.cs | 3 +-
4 files changed, 785 insertions(+), 1 deletion(-)
create mode 100644 .skills/mcc-integration-testing/scripts/discord-rpc-e2e/fake_discord_ipc.py
create mode 100644 .skills/mcc-integration-testing/scripts/discord-rpc-e2e/fake_mc_server.py
create mode 100644 .skills/mcc-integration-testing/scripts/discord-rpc-e2e/run_e2e_discord_rpc.py
diff --git a/.skills/mcc-integration-testing/scripts/discord-rpc-e2e/fake_discord_ipc.py b/.skills/mcc-integration-testing/scripts/discord-rpc-e2e/fake_discord_ipc.py
new file mode 100644
index 00000000..f17a9914
--- /dev/null
+++ b/.skills/mcc-integration-testing/scripts/discord-rpc-e2e/fake_discord_ipc.py
@@ -0,0 +1,222 @@
+#!/usr/bin/env python3
+"""
+Fake Discord IPC server.
+Listens on a Unix domain socket at the Discord IPC path and handles
+the discord-rpc-csharp handshake + SET_ACTIVITY commands.
+Logs all received presence updates to stdout and a log file.
+"""
+import json
+import os
+import socket
+import struct
+import sys
+import time
+import threading
+
+# Discord IPC opcodes
+OP_HANDSHAKE = 0
+OP_FRAME = 1
+OP_CLOSE = 2
+OP_PING = 3
+OP_PONG = 4
+
+LOG_FILE = "/tmp/e2e-test/discord_rpc.log"
+SOCKET_PATH = None
+
+presence_received = threading.Event()
+all_presences = []
+
+
+def log(msg):
+ timestamp = time.strftime("%H:%M:%S")
+ line = f"[{timestamp}] [FakeDiscordIPC] {msg}"
+ print(line, flush=True)
+ with open(LOG_FILE, "a") as f:
+ f.write(line + "\n")
+
+
+def get_socket_path():
+ """Determine where discord-rpc-csharp will look for the IPC socket."""
+ runtime_dir = os.environ.get("XDG_RUNTIME_DIR")
+ if runtime_dir:
+ return os.path.join(runtime_dir, "discord-ipc-0")
+
+ tmpdir = os.environ.get("TMPDIR", "/tmp")
+ return os.path.join(tmpdir, "discord-ipc-0")
+
+
+def read_message(conn):
+ """Read a Discord IPC message: 4-byte LE opcode + 4-byte LE length + JSON."""
+ header = b""
+ while len(header) < 8:
+ chunk = conn.recv(8 - len(header))
+ if not chunk:
+ return None, None
+ header += chunk
+
+ opcode, length = struct.unpack(">= 7
+ if value != 0:
+ byte |= 0x80
+ result.append(byte)
+ if value == 0:
+ break
+ return bytes(result)
+
+
+def read_varint(data, offset=0):
+ """Decode a VarInt, return (value, new_offset)."""
+ result = 0
+ shift = 0
+ while True:
+ byte = data[offset]
+ offset += 1
+ result |= (byte & 0x7F) << shift
+ if (byte & 0x80) == 0:
+ break
+ shift += 7
+ if shift >= 35:
+ raise ValueError("VarInt too large")
+ # Sign extension for negative values
+ if result & (1 << 31):
+ result -= 1 << 32
+ return result, offset
+
+
+def read_varint_from_socket(sock):
+ """Read a VarInt from socket one byte at a time."""
+ result = 0
+ shift = 0
+ while True:
+ byte_data = sock.recv(1)
+ if not byte_data:
+ raise ConnectionError("Connection closed")
+ byte = byte_data[0]
+ result |= (byte & 0x7F) << shift
+ if (byte & 0x80) == 0:
+ break
+ shift += 7
+ if shift >= 35:
+ raise ValueError("VarInt too large")
+ if result & (1 << 31):
+ result -= 1 << 32
+ return result
+
+
+def write_string(s):
+ """Encode a Minecraft string (VarInt length + UTF-8 bytes)."""
+ encoded = s.encode("utf-8")
+ return write_varint(len(encoded)) + encoded
+
+
+def read_string(data, offset):
+ """Decode a Minecraft string."""
+ length, offset = read_varint(data, offset)
+ s = data[offset:offset + length].decode("utf-8")
+ return s, offset + length
+
+
+def make_packet(packet_id, payload=b""):
+ """Wrap data into a Minecraft packet: [length VarInt][packet_id VarInt][payload]."""
+ pid = write_varint(packet_id)
+ packet_data = pid + payload
+ return write_varint(len(packet_data)) + packet_data
+
+
+def read_packet(sock):
+ """Read a full Minecraft packet from socket. Returns (packet_id, payload_bytes)."""
+ length = read_varint_from_socket(sock)
+ if length <= 0:
+ return None, b""
+ data = b""
+ while len(data) < length:
+ chunk = sock.recv(length - len(data))
+ if not chunk:
+ raise ConnectionError("Connection closed mid-packet")
+ data += chunk
+ packet_id, offset = read_varint(data, 0)
+ return packet_id, data[offset:]
+
+
+# -- Packet builders --
+
+def build_status_response():
+ """Build Status Response (0x00) packet."""
+ status = {
+ "version": {"name": MC_VERSION, "protocol": PROTOCOL_VERSION},
+ "players": {"max": 20, "online": 1, "sample": []},
+ "description": {"text": "MCC Discord RPC Test Server"},
+ "enforcesSecureChat": False,
+ "previewsChat": False
+ }
+ return make_packet(0x00, write_string(json.dumps(status)))
+
+
+def build_ping_response(payload_bytes):
+ """Build Ping Response (0x01) packet."""
+ return make_packet(0x01, payload_bytes)
+
+
+def build_login_success(username):
+ """Build Login Success (0x02) packet for 1.20.1."""
+ player_uuid = uuid.uuid3(uuid.NAMESPACE_DNS, f"OfflinePlayer:{username}")
+ uuid_bytes = player_uuid.bytes
+ name_bytes = write_string(username)
+ num_properties = write_varint(0) # No properties
+ return make_packet(0x02, uuid_bytes + name_bytes + num_properties)
+
+
+def build_keep_alive(keep_alive_id=0):
+ """Build Keep Alive (0x24 for 1.20.1) packet."""
+ return make_packet(0x24, struct.pack(">q", keep_alive_id))
+
+
+def handle_client(conn, addr):
+ """Handle a single MCC client connection."""
+ log(f"Client connected from {addr}")
+ state = "handshake" # handshake -> status or login -> play
+ username = None
+
+ try:
+ while True:
+ packet_id, payload = read_packet(conn)
+ if packet_id is None:
+ break
+
+ if state == "handshake":
+ if packet_id == 0x00:
+ # Handshake packet
+ proto, off = read_varint(payload, 0)
+ host, off = read_string(payload, off)
+ port = struct.unpack(">H", payload[off:off+2])[0]
+ off += 2
+ next_state, off = read_varint(payload, off)
+ log(f"Handshake: protocol={proto}, host={host}, port={port}, next_state={next_state}")
+
+ if next_state == 1:
+ state = "status"
+ elif next_state == 2:
+ state = "login"
+
+ elif state == "status":
+ if packet_id == 0x00:
+ # Status Request
+ log("Status Request received, sending response")
+ conn.sendall(build_status_response())
+ elif packet_id == 0x01:
+ # Ping
+ log("Ping received, sending Pong")
+ conn.sendall(build_ping_response(payload))
+ break # Status connection is done
+
+ elif state == "login":
+ if packet_id == 0x00:
+ # Login Start
+ username, off = read_string(payload, 0)
+ log(f"Login Start: username={username}")
+
+ # Send Login Success (offline mode - no encryption)
+ log(f"Sending Login Success for {username}")
+ conn.sendall(build_login_success(username))
+
+ state = "play"
+
+ # Signal that client joined (login phase complete)
+ client_joined.set()
+ log(f"*** {username} has logged in successfully! ***")
+
+ # Keep the connection alive without sending JoinGame
+ # MCC's DiscordRpc bot initializes during the login phase,
+ # before JoinGame is processed. We keep the connection open
+ # so the async RPC send can complete.
+ log("Keeping connection alive (not sending JoinGame to avoid NBT complexity)...")
+
+ # Start keep-alive loop
+ ka_thread = threading.Thread(
+ target=keep_alive_loop, args=(conn,), daemon=True
+ )
+ ka_thread.start()
+
+ elif state == "play":
+ # Just absorb play-state packets from the client silently
+ pass
+
+ except (ConnectionError, ConnectionResetError, BrokenPipeError) as e:
+ log(f"Client disconnected: {e}")
+ except Exception as e:
+ log(f"Error: {e}")
+ finally:
+ conn.close()
+ if username:
+ log(f"{username} disconnected")
+
+
+def keep_alive_loop(conn):
+ """Send keep-alive packets every 10 seconds."""
+ ka_id = 0
+ try:
+ while True:
+ time.sleep(10)
+ ka_id += 1
+ conn.sendall(build_keep_alive(ka_id))
+ except Exception:
+ pass
+
+
+def main():
+ with open(LOG_FILE, "w") as f:
+ f.write("")
+
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ sock.bind((HOST, PORT))
+ sock.listen(5)
+
+ log(f"Fake MC Server listening on {HOST}:{PORT} (protocol {PROTOCOL_VERSION}, {MC_VERSION})")
+ log("Waiting for MCC connections...")
+
+ try:
+ while True:
+ conn, addr = sock.accept()
+ t = threading.Thread(target=handle_client, args=(conn, addr), daemon=True)
+ t.start()
+ except KeyboardInterrupt:
+ log("Shutting down...")
+ finally:
+ sock.close()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.skills/mcc-integration-testing/scripts/discord-rpc-e2e/run_e2e_discord_rpc.py b/.skills/mcc-integration-testing/scripts/discord-rpc-e2e/run_e2e_discord_rpc.py
new file mode 100644
index 00000000..b509216f
--- /dev/null
+++ b/.skills/mcc-integration-testing/scripts/discord-rpc-e2e/run_e2e_discord_rpc.py
@@ -0,0 +1,280 @@
+#!/usr/bin/env python3
+"""
+End-to-end test orchestrator for Discord RPC ChatBot.
+Starts all components, waits for MCC to connect, and verifies RPC presence is sent.
+"""
+import os
+import signal
+import subprocess
+import sys
+import time
+
+MCC_DIR = "/home/runner/work/Minecraft-Console-Client/Minecraft-Console-Client"
+TEST_DIR = "/tmp/e2e-test"
+MC_LOG = f"{TEST_DIR}/fake_mc_server.log"
+RPC_LOG = f"{TEST_DIR}/discord_rpc.log"
+MCC_LOG = f"{TEST_DIR}/mcc_output.log"
+
+
+def log(msg):
+ print(f"\033[1;36m[E2E-TEST]\033[0m {msg}", flush=True)
+
+
+def wait_for_log(log_file, marker, timeout=30, label=""):
+ """Wait for a specific string to appear in a log file."""
+ start = time.time()
+ while time.time() - start < timeout:
+ try:
+ with open(log_file, "r") as f:
+ content = f.read()
+ if marker in content:
+ return True
+ except FileNotFoundError:
+ pass
+ time.sleep(0.5)
+ log(f"TIMEOUT waiting for '{marker}' in {label or log_file}")
+ return False
+
+
+def read_log(log_file):
+ try:
+ with open(log_file, "r") as f:
+ return f.read()
+ except FileNotFoundError:
+ return ""
+
+
+def main():
+ os.makedirs(TEST_DIR, exist_ok=True)
+
+ pids = []
+
+ # Clean up old files
+ for f in [MC_LOG, RPC_LOG, MCC_LOG]:
+ if os.path.exists(f):
+ os.remove(f)
+
+ # Remove any leftover mcc_input.txt
+ input_file = os.path.join(MCC_DIR, "mcc_input.txt")
+ if os.path.exists(input_file):
+ os.remove(input_file)
+
+ try:
+ # -- Step 1: Start fake Discord IPC --
+ log("Starting fake Discord IPC server...")
+ discord_proc = subprocess.Popen(
+ [sys.executable, f"{TEST_DIR}/fake_discord_ipc.py"],
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
+ env={**os.environ, "XDG_RUNTIME_DIR": "/tmp"}
+ )
+ pids.append(discord_proc.pid)
+ time.sleep(1)
+
+ if not wait_for_log(RPC_LOG, "Fake Discord IPC listening", timeout=5, label="Discord IPC"):
+ log("FAIL: Discord IPC server did not start")
+ return 1
+ log(" OK: Discord IPC server running")
+
+ # -- Step 2: Start fake Minecraft server --
+ log("Starting fake Minecraft server...")
+ mc_proc = subprocess.Popen(
+ [sys.executable, f"{TEST_DIR}/fake_mc_server.py"],
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT
+ )
+ pids.append(mc_proc.pid)
+ time.sleep(1)
+
+ if not wait_for_log(MC_LOG, "Fake MC Server listening", timeout=5, label="MC Server"):
+ log("FAIL: MC server did not start")
+ return 1
+ log(" OK: Minecraft server running on 127.0.0.1:25565")
+
+ # -- Step 3: Create MCC configuration --
+ log("Creating MCC configuration...")
+ ini_path = os.path.join(MCC_DIR, "MinecraftClient.ini")
+ with open(ini_path, "w") as f:
+ f.write("""
+[Main]
+[Main.General]
+Account = { Login = "RpcTestBot", Password = "-" }
+ServerIP = "127.0.0.1:25565"
+MinecraftVersion = "1.20.1"
+
+[Main.Advanced]
+Language = "en_us"
+EnableSentry = false
+
+[Logging]
+DebugMessages = true
+
+[ChatBot]
+[ChatBot.DiscordRpc]
+Enabled = true
+ApplicationId = "123456789012345678"
+PresenceDetails = "Playing on {server_host}:{server_port}"
+PresenceState = "{dimension} - HP: {health}/{max_health}"
+LargeImageKey = "mcc_icon"
+LargeImageText = "Minecraft Console Client"
+SmallImageKey = ""
+SmallImageText = ""
+ShowServerAddress = true
+ShowCoordinates = true
+ShowHealth = true
+ShowDimension = true
+ShowGamemode = true
+ShowElapsedTime = true
+ShowPlayerCount = true
+UpdateIntervalSeconds = 5
+""")
+ log(" OK: MinecraftClient.ini created")
+
+ # -- Step 4: Start MCC --
+ log("Starting MCC...")
+ mcc_env = {
+ **os.environ,
+ "MCC_FILE_INPUT": "1",
+ "XDG_RUNTIME_DIR": "/tmp" # So DiscordRPC library finds our fake IPC socket
+ }
+
+ mcc_proc = subprocess.Popen(
+ ["dotnet", "run", "--project", "MinecraftClient", "-c", "Release",
+ "--no-build", "--", "RpcTestBot", "-", "127.0.0.1:25565"],
+ cwd=MCC_DIR,
+ stdout=open(MCC_LOG, "w"),
+ stderr=subprocess.STDOUT,
+ env=mcc_env
+ )
+ pids.append(mcc_proc.pid)
+
+ # -- Step 5: Wait for MCC to join the server --
+ log("Waiting for MCC to connect to server...")
+ mc_joined = wait_for_log(MC_LOG, "has logged in successfully", timeout=30, label="MC join")
+
+ if mc_joined:
+ log(" OK: MCC connected to fake Minecraft server")
+ else:
+ log(" WARN: Could not confirm MC join in server log")
+ # Check MCC log for more info
+ mcc_content = read_log(MCC_LOG)
+ if "Server was successfully joined" in mcc_content:
+ log(" OK: MCC reports successful join in its own log")
+ mc_joined = True
+
+ # -- Step 6: Wait for Discord RPC presence --
+ log("Waiting for Discord RPC presence update...")
+ # Give MCC time to initialize the Discord RPC bot and set presence
+ rpc_activity = wait_for_log(RPC_LOG, "SET_ACTIVITY received", timeout=30, label="RPC activity")
+
+ if rpc_activity:
+ log(" OK: Discord RPC presence received!")
+ else:
+ log(" INFO: Checking if RPC client attempted connection...")
+ rpc_content = read_log(RPC_LOG)
+ if "HANDSHAKE received" in rpc_content:
+ log(" OK: RPC handshake succeeded, waiting longer for activity...")
+ rpc_activity = wait_for_log(RPC_LOG, "SET_ACTIVITY received", timeout=20, label="RPC activity (extended)")
+ elif "Client connected" in rpc_content:
+ log(" PARTIAL: RPC client connected but no activity sent yet")
+
+ # -- Step 7: Collect and display results --
+ log("")
+ log("=" * 60)
+ log("END-TO-END TEST RESULTS")
+ log("=" * 60)
+
+ # Check all criteria
+ mc_server_ok = "Fake MC Server listening" in read_log(MC_LOG)
+ discord_ipc_ok = "Fake Discord IPC listening" in read_log(RPC_LOG)
+ mcc_content = read_log(MCC_LOG)
+ rpc_content = read_log(RPC_LOG)
+
+ mcc_connected = "has logged in successfully" in read_log(MC_LOG) or "Server was successfully joined" in mcc_content
+ rpc_handshake = "HANDSHAKE received" in rpc_content
+ rpc_ready = "Sent READY response" in rpc_content
+ rpc_presence = "SET_ACTIVITY received" in rpc_content
+
+ # Extract presence details from RPC log
+ presence_details = ""
+ presence_state = ""
+ for line in rpc_content.split("\n"):
+ if "Details :" in line:
+ presence_details = line.split("Details :")[1].strip()
+ if "State :" in line:
+ presence_state = line.split("State :")[1].strip()
+
+ results = [
+ ("Fake MC Server started", mc_server_ok),
+ ("Fake Discord IPC started", discord_ipc_ok),
+ ("MCC connected to server", mcc_connected),
+ ("RPC handshake completed", rpc_handshake),
+ ("RPC READY sent to client", rpc_ready),
+ ("RPC presence set", rpc_presence),
+ ]
+
+ all_pass = True
+ for label, ok in results:
+ status = "\033[1;32mPASS\033[0m" if ok else "\033[1;31mFAIL\033[0m"
+ log(f" [{status}] {label}")
+ if not ok:
+ all_pass = False
+
+ if presence_details:
+ log(f"\n Presence Details: {presence_details}")
+ if presence_state:
+ log(f" Presence State : {presence_state}")
+
+ log("")
+
+ # Print relevant logs
+ log("--- MCC Output (last 30 lines) ---")
+ mcc_lines = mcc_content.strip().split("\n")
+ for line in mcc_lines[-30:]:
+ log(f" {line}")
+
+ log("")
+ log("--- Discord RPC Log ---")
+ rpc_lines = rpc_content.strip().split("\n")
+ for line in rpc_lines:
+ log(f" {line}")
+
+ log("")
+ log("--- MC Server Log ---")
+ mc_content = read_log(MC_LOG)
+ mc_lines = mc_content.strip().split("\n")
+ for line in mc_lines:
+ log(f" {line}")
+
+ log("")
+
+ if all_pass:
+ log("\033[1;32m*** ALL TESTS PASSED - Discord RPC integration is fully working! ***\033[0m")
+ return 0
+ else:
+ log("\033[1;33m*** SOME TESTS DID NOT PASS ***\033[0m")
+ return 1
+
+ finally:
+ # Clean up all processes
+ log("\nCleaning up processes...")
+ for pid in pids:
+ try:
+ os.kill(pid, signal.SIGTERM)
+ except ProcessLookupError:
+ pass
+
+ time.sleep(1)
+
+ for pid in pids:
+ try:
+ os.kill(pid, signal.SIGKILL)
+ except ProcessLookupError:
+ pass
+
+ # Clean up config
+ ini_path = os.path.join(MCC_DIR, "MinecraftClient.ini")
+ if os.path.exists(ini_path):
+ os.remove(ini_path)
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/MinecraftClient/ChatBots/DiscordBridge.cs b/MinecraftClient/ChatBots/DiscordBridge.cs
index e2c5e757..5a20906e 100644
--- a/MinecraftClient/ChatBots/DiscordBridge.cs
+++ b/MinecraftClient/ChatBots/DiscordBridge.cs
@@ -384,7 +384,8 @@ namespace MinecraftClient.ChatBots
if (string.IsNullOrEmpty(message) || string.IsNullOrWhiteSpace(message))
return;
- // Relay messages from other bots when configured, but never process commands from them
+ // Relay messages from other bots when configured, but never process commands from them.
+ // Skip relay when direction is Discord-only (Discord -> MC disabled).
if (e.Author.IsBot)
{
if (Config.Allow_Other_Bot_Messages && bridgeDirection != BridgeDirection.Discord)
From 221b82643da526ffc5bea8791d9ce2a54519a45b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 23 Mar 2026 01:05:48 +0000
Subject: [PATCH 7/7] Fix DiscordRpc to send presence only after game join,
apply C# best practices, remove temp E2E scripts
Co-authored-by: milutinke <441903+milutinke@users.noreply.github.com>
Agent-Logs-Url: https://github.com/MCCTeam/Minecraft-Console-Client/sessions/80d151a5-aee7-4ae6-8497-9e7487d25136
---
.../discord-rpc-e2e/fake_discord_ipc.py | 222 --------------
.../scripts/discord-rpc-e2e/fake_mc_server.py | 281 ------------------
.../discord-rpc-e2e/run_e2e_discord_rpc.py | 280 -----------------
MinecraftClient/ChatBots/DiscordBridge.cs | 2 +-
MinecraftClient/ChatBots/DiscordRpc.cs | 61 ++--
5 files changed, 29 insertions(+), 817 deletions(-)
delete mode 100644 .skills/mcc-integration-testing/scripts/discord-rpc-e2e/fake_discord_ipc.py
delete mode 100644 .skills/mcc-integration-testing/scripts/discord-rpc-e2e/fake_mc_server.py
delete mode 100644 .skills/mcc-integration-testing/scripts/discord-rpc-e2e/run_e2e_discord_rpc.py
diff --git a/.skills/mcc-integration-testing/scripts/discord-rpc-e2e/fake_discord_ipc.py b/.skills/mcc-integration-testing/scripts/discord-rpc-e2e/fake_discord_ipc.py
deleted file mode 100644
index f17a9914..00000000
--- a/.skills/mcc-integration-testing/scripts/discord-rpc-e2e/fake_discord_ipc.py
+++ /dev/null
@@ -1,222 +0,0 @@
-#!/usr/bin/env python3
-"""
-Fake Discord IPC server.
-Listens on a Unix domain socket at the Discord IPC path and handles
-the discord-rpc-csharp handshake + SET_ACTIVITY commands.
-Logs all received presence updates to stdout and a log file.
-"""
-import json
-import os
-import socket
-import struct
-import sys
-import time
-import threading
-
-# Discord IPC opcodes
-OP_HANDSHAKE = 0
-OP_FRAME = 1
-OP_CLOSE = 2
-OP_PING = 3
-OP_PONG = 4
-
-LOG_FILE = "/tmp/e2e-test/discord_rpc.log"
-SOCKET_PATH = None
-
-presence_received = threading.Event()
-all_presences = []
-
-
-def log(msg):
- timestamp = time.strftime("%H:%M:%S")
- line = f"[{timestamp}] [FakeDiscordIPC] {msg}"
- print(line, flush=True)
- with open(LOG_FILE, "a") as f:
- f.write(line + "\n")
-
-
-def get_socket_path():
- """Determine where discord-rpc-csharp will look for the IPC socket."""
- runtime_dir = os.environ.get("XDG_RUNTIME_DIR")
- if runtime_dir:
- return os.path.join(runtime_dir, "discord-ipc-0")
-
- tmpdir = os.environ.get("TMPDIR", "/tmp")
- return os.path.join(tmpdir, "discord-ipc-0")
-
-
-def read_message(conn):
- """Read a Discord IPC message: 4-byte LE opcode + 4-byte LE length + JSON."""
- header = b""
- while len(header) < 8:
- chunk = conn.recv(8 - len(header))
- if not chunk:
- return None, None
- header += chunk
-
- opcode, length = struct.unpack(">= 7
- if value != 0:
- byte |= 0x80
- result.append(byte)
- if value == 0:
- break
- return bytes(result)
-
-
-def read_varint(data, offset=0):
- """Decode a VarInt, return (value, new_offset)."""
- result = 0
- shift = 0
- while True:
- byte = data[offset]
- offset += 1
- result |= (byte & 0x7F) << shift
- if (byte & 0x80) == 0:
- break
- shift += 7
- if shift >= 35:
- raise ValueError("VarInt too large")
- # Sign extension for negative values
- if result & (1 << 31):
- result -= 1 << 32
- return result, offset
-
-
-def read_varint_from_socket(sock):
- """Read a VarInt from socket one byte at a time."""
- result = 0
- shift = 0
- while True:
- byte_data = sock.recv(1)
- if not byte_data:
- raise ConnectionError("Connection closed")
- byte = byte_data[0]
- result |= (byte & 0x7F) << shift
- if (byte & 0x80) == 0:
- break
- shift += 7
- if shift >= 35:
- raise ValueError("VarInt too large")
- if result & (1 << 31):
- result -= 1 << 32
- return result
-
-
-def write_string(s):
- """Encode a Minecraft string (VarInt length + UTF-8 bytes)."""
- encoded = s.encode("utf-8")
- return write_varint(len(encoded)) + encoded
-
-
-def read_string(data, offset):
- """Decode a Minecraft string."""
- length, offset = read_varint(data, offset)
- s = data[offset:offset + length].decode("utf-8")
- return s, offset + length
-
-
-def make_packet(packet_id, payload=b""):
- """Wrap data into a Minecraft packet: [length VarInt][packet_id VarInt][payload]."""
- pid = write_varint(packet_id)
- packet_data = pid + payload
- return write_varint(len(packet_data)) + packet_data
-
-
-def read_packet(sock):
- """Read a full Minecraft packet from socket. Returns (packet_id, payload_bytes)."""
- length = read_varint_from_socket(sock)
- if length <= 0:
- return None, b""
- data = b""
- while len(data) < length:
- chunk = sock.recv(length - len(data))
- if not chunk:
- raise ConnectionError("Connection closed mid-packet")
- data += chunk
- packet_id, offset = read_varint(data, 0)
- return packet_id, data[offset:]
-
-
-# -- Packet builders --
-
-def build_status_response():
- """Build Status Response (0x00) packet."""
- status = {
- "version": {"name": MC_VERSION, "protocol": PROTOCOL_VERSION},
- "players": {"max": 20, "online": 1, "sample": []},
- "description": {"text": "MCC Discord RPC Test Server"},
- "enforcesSecureChat": False,
- "previewsChat": False
- }
- return make_packet(0x00, write_string(json.dumps(status)))
-
-
-def build_ping_response(payload_bytes):
- """Build Ping Response (0x01) packet."""
- return make_packet(0x01, payload_bytes)
-
-
-def build_login_success(username):
- """Build Login Success (0x02) packet for 1.20.1."""
- player_uuid = uuid.uuid3(uuid.NAMESPACE_DNS, f"OfflinePlayer:{username}")
- uuid_bytes = player_uuid.bytes
- name_bytes = write_string(username)
- num_properties = write_varint(0) # No properties
- return make_packet(0x02, uuid_bytes + name_bytes + num_properties)
-
-
-def build_keep_alive(keep_alive_id=0):
- """Build Keep Alive (0x24 for 1.20.1) packet."""
- return make_packet(0x24, struct.pack(">q", keep_alive_id))
-
-
-def handle_client(conn, addr):
- """Handle a single MCC client connection."""
- log(f"Client connected from {addr}")
- state = "handshake" # handshake -> status or login -> play
- username = None
-
- try:
- while True:
- packet_id, payload = read_packet(conn)
- if packet_id is None:
- break
-
- if state == "handshake":
- if packet_id == 0x00:
- # Handshake packet
- proto, off = read_varint(payload, 0)
- host, off = read_string(payload, off)
- port = struct.unpack(">H", payload[off:off+2])[0]
- off += 2
- next_state, off = read_varint(payload, off)
- log(f"Handshake: protocol={proto}, host={host}, port={port}, next_state={next_state}")
-
- if next_state == 1:
- state = "status"
- elif next_state == 2:
- state = "login"
-
- elif state == "status":
- if packet_id == 0x00:
- # Status Request
- log("Status Request received, sending response")
- conn.sendall(build_status_response())
- elif packet_id == 0x01:
- # Ping
- log("Ping received, sending Pong")
- conn.sendall(build_ping_response(payload))
- break # Status connection is done
-
- elif state == "login":
- if packet_id == 0x00:
- # Login Start
- username, off = read_string(payload, 0)
- log(f"Login Start: username={username}")
-
- # Send Login Success (offline mode - no encryption)
- log(f"Sending Login Success for {username}")
- conn.sendall(build_login_success(username))
-
- state = "play"
-
- # Signal that client joined (login phase complete)
- client_joined.set()
- log(f"*** {username} has logged in successfully! ***")
-
- # Keep the connection alive without sending JoinGame
- # MCC's DiscordRpc bot initializes during the login phase,
- # before JoinGame is processed. We keep the connection open
- # so the async RPC send can complete.
- log("Keeping connection alive (not sending JoinGame to avoid NBT complexity)...")
-
- # Start keep-alive loop
- ka_thread = threading.Thread(
- target=keep_alive_loop, args=(conn,), daemon=True
- )
- ka_thread.start()
-
- elif state == "play":
- # Just absorb play-state packets from the client silently
- pass
-
- except (ConnectionError, ConnectionResetError, BrokenPipeError) as e:
- log(f"Client disconnected: {e}")
- except Exception as e:
- log(f"Error: {e}")
- finally:
- conn.close()
- if username:
- log(f"{username} disconnected")
-
-
-def keep_alive_loop(conn):
- """Send keep-alive packets every 10 seconds."""
- ka_id = 0
- try:
- while True:
- time.sleep(10)
- ka_id += 1
- conn.sendall(build_keep_alive(ka_id))
- except Exception:
- pass
-
-
-def main():
- with open(LOG_FILE, "w") as f:
- f.write("")
-
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- sock.bind((HOST, PORT))
- sock.listen(5)
-
- log(f"Fake MC Server listening on {HOST}:{PORT} (protocol {PROTOCOL_VERSION}, {MC_VERSION})")
- log("Waiting for MCC connections...")
-
- try:
- while True:
- conn, addr = sock.accept()
- t = threading.Thread(target=handle_client, args=(conn, addr), daemon=True)
- t.start()
- except KeyboardInterrupt:
- log("Shutting down...")
- finally:
- sock.close()
-
-
-if __name__ == "__main__":
- main()
diff --git a/.skills/mcc-integration-testing/scripts/discord-rpc-e2e/run_e2e_discord_rpc.py b/.skills/mcc-integration-testing/scripts/discord-rpc-e2e/run_e2e_discord_rpc.py
deleted file mode 100644
index b509216f..00000000
--- a/.skills/mcc-integration-testing/scripts/discord-rpc-e2e/run_e2e_discord_rpc.py
+++ /dev/null
@@ -1,280 +0,0 @@
-#!/usr/bin/env python3
-"""
-End-to-end test orchestrator for Discord RPC ChatBot.
-Starts all components, waits for MCC to connect, and verifies RPC presence is sent.
-"""
-import os
-import signal
-import subprocess
-import sys
-import time
-
-MCC_DIR = "/home/runner/work/Minecraft-Console-Client/Minecraft-Console-Client"
-TEST_DIR = "/tmp/e2e-test"
-MC_LOG = f"{TEST_DIR}/fake_mc_server.log"
-RPC_LOG = f"{TEST_DIR}/discord_rpc.log"
-MCC_LOG = f"{TEST_DIR}/mcc_output.log"
-
-
-def log(msg):
- print(f"\033[1;36m[E2E-TEST]\033[0m {msg}", flush=True)
-
-
-def wait_for_log(log_file, marker, timeout=30, label=""):
- """Wait for a specific string to appear in a log file."""
- start = time.time()
- while time.time() - start < timeout:
- try:
- with open(log_file, "r") as f:
- content = f.read()
- if marker in content:
- return True
- except FileNotFoundError:
- pass
- time.sleep(0.5)
- log(f"TIMEOUT waiting for '{marker}' in {label or log_file}")
- return False
-
-
-def read_log(log_file):
- try:
- with open(log_file, "r") as f:
- return f.read()
- except FileNotFoundError:
- return ""
-
-
-def main():
- os.makedirs(TEST_DIR, exist_ok=True)
-
- pids = []
-
- # Clean up old files
- for f in [MC_LOG, RPC_LOG, MCC_LOG]:
- if os.path.exists(f):
- os.remove(f)
-
- # Remove any leftover mcc_input.txt
- input_file = os.path.join(MCC_DIR, "mcc_input.txt")
- if os.path.exists(input_file):
- os.remove(input_file)
-
- try:
- # -- Step 1: Start fake Discord IPC --
- log("Starting fake Discord IPC server...")
- discord_proc = subprocess.Popen(
- [sys.executable, f"{TEST_DIR}/fake_discord_ipc.py"],
- stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
- env={**os.environ, "XDG_RUNTIME_DIR": "/tmp"}
- )
- pids.append(discord_proc.pid)
- time.sleep(1)
-
- if not wait_for_log(RPC_LOG, "Fake Discord IPC listening", timeout=5, label="Discord IPC"):
- log("FAIL: Discord IPC server did not start")
- return 1
- log(" OK: Discord IPC server running")
-
- # -- Step 2: Start fake Minecraft server --
- log("Starting fake Minecraft server...")
- mc_proc = subprocess.Popen(
- [sys.executable, f"{TEST_DIR}/fake_mc_server.py"],
- stdout=subprocess.PIPE, stderr=subprocess.STDOUT
- )
- pids.append(mc_proc.pid)
- time.sleep(1)
-
- if not wait_for_log(MC_LOG, "Fake MC Server listening", timeout=5, label="MC Server"):
- log("FAIL: MC server did not start")
- return 1
- log(" OK: Minecraft server running on 127.0.0.1:25565")
-
- # -- Step 3: Create MCC configuration --
- log("Creating MCC configuration...")
- ini_path = os.path.join(MCC_DIR, "MinecraftClient.ini")
- with open(ini_path, "w") as f:
- f.write("""
-[Main]
-[Main.General]
-Account = { Login = "RpcTestBot", Password = "-" }
-ServerIP = "127.0.0.1:25565"
-MinecraftVersion = "1.20.1"
-
-[Main.Advanced]
-Language = "en_us"
-EnableSentry = false
-
-[Logging]
-DebugMessages = true
-
-[ChatBot]
-[ChatBot.DiscordRpc]
-Enabled = true
-ApplicationId = "123456789012345678"
-PresenceDetails = "Playing on {server_host}:{server_port}"
-PresenceState = "{dimension} - HP: {health}/{max_health}"
-LargeImageKey = "mcc_icon"
-LargeImageText = "Minecraft Console Client"
-SmallImageKey = ""
-SmallImageText = ""
-ShowServerAddress = true
-ShowCoordinates = true
-ShowHealth = true
-ShowDimension = true
-ShowGamemode = true
-ShowElapsedTime = true
-ShowPlayerCount = true
-UpdateIntervalSeconds = 5
-""")
- log(" OK: MinecraftClient.ini created")
-
- # -- Step 4: Start MCC --
- log("Starting MCC...")
- mcc_env = {
- **os.environ,
- "MCC_FILE_INPUT": "1",
- "XDG_RUNTIME_DIR": "/tmp" # So DiscordRPC library finds our fake IPC socket
- }
-
- mcc_proc = subprocess.Popen(
- ["dotnet", "run", "--project", "MinecraftClient", "-c", "Release",
- "--no-build", "--", "RpcTestBot", "-", "127.0.0.1:25565"],
- cwd=MCC_DIR,
- stdout=open(MCC_LOG, "w"),
- stderr=subprocess.STDOUT,
- env=mcc_env
- )
- pids.append(mcc_proc.pid)
-
- # -- Step 5: Wait for MCC to join the server --
- log("Waiting for MCC to connect to server...")
- mc_joined = wait_for_log(MC_LOG, "has logged in successfully", timeout=30, label="MC join")
-
- if mc_joined:
- log(" OK: MCC connected to fake Minecraft server")
- else:
- log(" WARN: Could not confirm MC join in server log")
- # Check MCC log for more info
- mcc_content = read_log(MCC_LOG)
- if "Server was successfully joined" in mcc_content:
- log(" OK: MCC reports successful join in its own log")
- mc_joined = True
-
- # -- Step 6: Wait for Discord RPC presence --
- log("Waiting for Discord RPC presence update...")
- # Give MCC time to initialize the Discord RPC bot and set presence
- rpc_activity = wait_for_log(RPC_LOG, "SET_ACTIVITY received", timeout=30, label="RPC activity")
-
- if rpc_activity:
- log(" OK: Discord RPC presence received!")
- else:
- log(" INFO: Checking if RPC client attempted connection...")
- rpc_content = read_log(RPC_LOG)
- if "HANDSHAKE received" in rpc_content:
- log(" OK: RPC handshake succeeded, waiting longer for activity...")
- rpc_activity = wait_for_log(RPC_LOG, "SET_ACTIVITY received", timeout=20, label="RPC activity (extended)")
- elif "Client connected" in rpc_content:
- log(" PARTIAL: RPC client connected but no activity sent yet")
-
- # -- Step 7: Collect and display results --
- log("")
- log("=" * 60)
- log("END-TO-END TEST RESULTS")
- log("=" * 60)
-
- # Check all criteria
- mc_server_ok = "Fake MC Server listening" in read_log(MC_LOG)
- discord_ipc_ok = "Fake Discord IPC listening" in read_log(RPC_LOG)
- mcc_content = read_log(MCC_LOG)
- rpc_content = read_log(RPC_LOG)
-
- mcc_connected = "has logged in successfully" in read_log(MC_LOG) or "Server was successfully joined" in mcc_content
- rpc_handshake = "HANDSHAKE received" in rpc_content
- rpc_ready = "Sent READY response" in rpc_content
- rpc_presence = "SET_ACTIVITY received" in rpc_content
-
- # Extract presence details from RPC log
- presence_details = ""
- presence_state = ""
- for line in rpc_content.split("\n"):
- if "Details :" in line:
- presence_details = line.split("Details :")[1].strip()
- if "State :" in line:
- presence_state = line.split("State :")[1].strip()
-
- results = [
- ("Fake MC Server started", mc_server_ok),
- ("Fake Discord IPC started", discord_ipc_ok),
- ("MCC connected to server", mcc_connected),
- ("RPC handshake completed", rpc_handshake),
- ("RPC READY sent to client", rpc_ready),
- ("RPC presence set", rpc_presence),
- ]
-
- all_pass = True
- for label, ok in results:
- status = "\033[1;32mPASS\033[0m" if ok else "\033[1;31mFAIL\033[0m"
- log(f" [{status}] {label}")
- if not ok:
- all_pass = False
-
- if presence_details:
- log(f"\n Presence Details: {presence_details}")
- if presence_state:
- log(f" Presence State : {presence_state}")
-
- log("")
-
- # Print relevant logs
- log("--- MCC Output (last 30 lines) ---")
- mcc_lines = mcc_content.strip().split("\n")
- for line in mcc_lines[-30:]:
- log(f" {line}")
-
- log("")
- log("--- Discord RPC Log ---")
- rpc_lines = rpc_content.strip().split("\n")
- for line in rpc_lines:
- log(f" {line}")
-
- log("")
- log("--- MC Server Log ---")
- mc_content = read_log(MC_LOG)
- mc_lines = mc_content.strip().split("\n")
- for line in mc_lines:
- log(f" {line}")
-
- log("")
-
- if all_pass:
- log("\033[1;32m*** ALL TESTS PASSED - Discord RPC integration is fully working! ***\033[0m")
- return 0
- else:
- log("\033[1;33m*** SOME TESTS DID NOT PASS ***\033[0m")
- return 1
-
- finally:
- # Clean up all processes
- log("\nCleaning up processes...")
- for pid in pids:
- try:
- os.kill(pid, signal.SIGTERM)
- except ProcessLookupError:
- pass
-
- time.sleep(1)
-
- for pid in pids:
- try:
- os.kill(pid, signal.SIGKILL)
- except ProcessLookupError:
- pass
-
- # Clean up config
- ini_path = os.path.join(MCC_DIR, "MinecraftClient.ini")
- if os.path.exists(ini_path):
- os.remove(ini_path)
-
-
-if __name__ == "__main__":
- sys.exit(main())
diff --git a/MinecraftClient/ChatBots/DiscordBridge.cs b/MinecraftClient/ChatBots/DiscordBridge.cs
index 5a20906e..9f5905de 100644
--- a/MinecraftClient/ChatBots/DiscordBridge.cs
+++ b/MinecraftClient/ChatBots/DiscordBridge.cs
@@ -381,7 +381,7 @@ namespace MinecraftClient.ChatBots
string message = e.Message.Content.Trim();
- if (string.IsNullOrEmpty(message) || string.IsNullOrWhiteSpace(message))
+ if (string.IsNullOrWhiteSpace(message))
return;
// Relay messages from other bots when configured, but never process commands from them.
diff --git a/MinecraftClient/ChatBots/DiscordRpc.cs b/MinecraftClient/ChatBots/DiscordRpc.cs
index 6e5c60cb..02a72049 100644
--- a/MinecraftClient/ChatBots/DiscordRpc.cs
+++ b/MinecraftClient/ChatBots/DiscordRpc.cs
@@ -1,5 +1,4 @@
using System;
-using System.Diagnostics;
using DiscordRPC;
using DiscordRPC.Logging;
using MinecraftClient.Mapping;
@@ -88,11 +87,11 @@ namespace MinecraftClient.ChatBots
}
}
- private DiscordRpcClient? rpcClient;
- private int tickCounter;
- private int updateIntervalTicks;
- private Timestamps? sessionTimestamps;
- private float lastHealth;
+ private DiscordRpcClient? _rpcClient;
+ private int _tickCounter;
+ private int _updateIntervalTicks;
+ private Timestamps? _sessionTimestamps;
+ private float _lastHealth;
public override void Initialize()
{
@@ -105,32 +104,26 @@ namespace MinecraftClient.ChatBots
try
{
- rpcClient = new DiscordRpcClient(Config.ApplicationId.Trim())
+ _rpcClient = new DiscordRpcClient(Config.ApplicationId.Trim())
{
Logger = Settings.Config.Logging.DebugMessages
? new ConsoleLogger(LogLevel.Trace)
: new ConsoleLogger(LogLevel.None)
};
- rpcClient.OnReady += (_, e) =>
+ _rpcClient.OnReady += (_, e) =>
{
LogToConsole(string.Format(Translations.bot_DiscordRpc_connected, e.User.Username));
};
- rpcClient.OnConnectionFailed += (_, e) =>
+ _rpcClient.OnConnectionFailed += (_, e) =>
{
LogToConsole(string.Format(Translations.bot_DiscordRpc_connection_failed, e.FailedPipe));
};
- rpcClient.Initialize();
- updateIntervalTicks = Settings.DoubleToTick(Config.UpdateIntervalSeconds);
+ _rpcClient.Initialize();
+ _updateIntervalTicks = Settings.DoubleToTick(Config.UpdateIntervalSeconds);
- if (Config.ShowElapsedTime)
- sessionTimestamps = Timestamps.Now;
-
- lastHealth = Handler.GetHealth();
-
- SetPresence();
LogToConsole(Translations.bot_DiscordRpc_initialized);
}
catch (Exception e)
@@ -143,49 +136,51 @@ namespace MinecraftClient.ChatBots
public override void OnUnload()
{
- if (rpcClient is { IsDisposed: false })
+ if (_rpcClient is { IsDisposed: false })
{
- rpcClient.ClearPresence();
- rpcClient.Dispose();
+ _rpcClient.ClearPresence();
+ _rpcClient.Dispose();
}
- rpcClient = null;
+ _rpcClient = null;
}
public override void AfterGameJoined()
{
if (Config.ShowElapsedTime)
- sessionTimestamps = Timestamps.Now;
+ _sessionTimestamps = Timestamps.Now;
+ _lastHealth = Handler.GetHealth();
+ _tickCounter = 0;
SetPresence();
}
public override void Update()
{
- tickCounter++;
- if (tickCounter < updateIntervalTicks)
+ _tickCounter++;
+ if (_tickCounter < _updateIntervalTicks)
return;
- tickCounter = 0;
+ _tickCounter = 0;
SetPresence();
}
public override void OnHealthUpdate(float health, int food)
{
- lastHealth = health;
+ _lastHealth = health;
}
public override bool OnDisconnect(DisconnectReason reason, string message)
{
- if (rpcClient is { IsDisposed: false })
- rpcClient.ClearPresence();
+ if (_rpcClient is { IsDisposed: false })
+ _rpcClient.ClearPresence();
return false;
}
private void SetPresence()
{
- if (rpcClient is null or { IsDisposed: true })
+ if (_rpcClient is null or { IsDisposed: true })
return;
try
@@ -223,8 +218,8 @@ namespace MinecraftClient.ChatBots
presence.Assets = assets;
// Timestamps
- if (Config.ShowElapsedTime && sessionTimestamps is not null)
- presence.Timestamps = sessionTimestamps;
+ if (Config.ShowElapsedTime && _sessionTimestamps is not null)
+ presence.Timestamps = _sessionTimestamps;
// Player count as party
if (Config.ShowPlayerCount)
@@ -242,7 +237,7 @@ namespace MinecraftClient.ChatBots
}
}
- rpcClient.SetPresence(presence);
+ _rpcClient.SetPresence(presence);
}
catch (Exception e)
{
@@ -282,7 +277,7 @@ namespace MinecraftClient.ChatBots
dimensionName = dim.Name ?? "Unknown";
// Clean up the dimension name for display
- if (dimensionName.StartsWith("minecraft:"))
+ if (dimensionName.StartsWith("minecraft:", StringComparison.Ordinal))
dimensionName = dimensionName["minecraft:".Length..];
dimensionName = dimensionName switch