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(); }
+ }
}
}