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 < MC disabled). + if (e.Author.IsBot) + { + if (Config.Allow_Other_Bot_Messages && bridgeDirection != BridgeDirection.Discord) + SendText(message); + return; + } + + if (!Config.OwnersIds.Contains(e.Author.Id)) return; if (bridgeDirection == BridgeDirection.Discord) diff --git a/MinecraftClient/ChatBots/DiscordRpc.cs b/MinecraftClient/ChatBots/DiscordRpc.cs new file mode 100644 index 00000000..02a72049 --- /dev/null +++ b/MinecraftClient/ChatBots/DiscordRpc.cs @@ -0,0 +1,332 @@ +using System; +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.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; + + [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); + + 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; + + _lastHealth = Handler.GetHealth(); + _tickCounter = 0; + 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 = 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 = Config.ShowServerAddress ? GetServerHost() : "Hidden"; + int serverPort = GetServerPort(); + string serverPortStr = Config.ShowServerAddress ? serverPort.ToString() : "****"; + string username = GetUsername(); + float health = Handler.GetHealth(); + int foodLevel = Handler.GetSaturation(); + Location location = GetCurrentLocation(); + string[] onlinePlayers = GetOnlinePlayers(); + int gamemode = GetGamemode(); + int protocolVersion = GetProtocolVersion(); + + 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) + { + try + { + var dim = World.GetDimension(); + dimensionName = dim.Name ?? "Unknown"; + + // Clean up the dimension name for display + if (dimensionName.StartsWith("minecraft:", StringComparison.Ordinal)) + 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 = 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}", healthStr) + .Replace("{max_health}", maxHealthStr) + .Replace("{food}", foodStr) + .Replace("{dimension}", dimensionName) + .Replace("{gamemode}", gamemodeStr) + .Replace("{x}", xStr) + .Replace("{y}", yStr) + .Replace("{z}", zStr) + .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..9ccbede3 100644 --- a/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs +++ b/MinecraftClient/Resources/ConfigComments/ConfigComments.Designer.cs @@ -772,7 +772,16 @@ namespace MinecraftClient { } /// - /// 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. @@ -898,6 +907,152 @@ namespace MinecraftClient { } } + /// + /// 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 { + 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 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 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.. + /// + 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..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. @@ -828,6 +831,60 @@ 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. +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 + + + 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 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. + + + 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(); } + } } }