diff --git a/modules/gmcp/files/data-overlays/config.yaml b/modules/gmcp/files/data-overlays/config.yaml new file mode 100644 index 00000000..31b01ce6 --- /dev/null +++ b/modules/gmcp/files/data-overlays/config.yaml @@ -0,0 +1,41 @@ +################################################################################ +# If modified, these settings will be copied into your config overrides +# folder under `Modules.gmcp`: +# +# Modules: +# gmcp: +# mapper_version: "1" +# mapper_url: "https://github.com/GoMudEngine/MudletMapper/releases/latest/download/GoMudMapper.mpackage" +# ui_version: "1" +# ui_url: "https://github.com/GoMudEngine/MudletUI/releases/latest/download/GoMudUI.mpackage" +# map_version: "1" +# map_url: "https://github.com/GoMudEngine/MudletMapper/releases/latest/download/gomud.dat" +# discord_application_id: "1298377884154724412" +# discord_invite_url: "https://discord.gg/FaauSYej3n" +# discord_details: "Using GoMudEngine" +# discord_state: "Exploring the world" +# discord_large_image_key: "server-icon" +# discord_small_image_key: "character-icon" +################################################################################ + +# Mudlet client configuration + +# Mapper configuration +mapper_version: "1" +mapper_url: "https://github.com/GoMudEngine/MudletMapper/releases/latest/download/GoMudMapper.mpackage" + +# UI configuration +ui_version: "1" +ui_url: "https://github.com/GoMudEngine/MudletUI/releases/latest/download/GoMudUI.mpackage" + +# Map data configuration +map_version: "1" +map_url: "https://github.com/GoMudEngine/MudletMapper/releases/latest/download/gomud.dat" + +# Discord Rich Presence configuration +discord_application_id: "1298377884154724412" # Default GoMud Discord Server +discord_invite_url: "https://discord.gg/FaauSYej3n" # Default GoMud Discord Server +discord_details: "Using GoMudEngine" +discord_state: "Exploring the world" +discord_large_image_key: "server-icon" +discord_small_image_key: "character-icon" \ No newline at end of file diff --git a/modules/gmcp/files/data-overlays/keywords.yaml b/modules/gmcp/files/data-overlays/keywords.yaml index d657be1a..f636bb6c 100644 --- a/modules/gmcp/files/data-overlays/keywords.yaml +++ b/modules/gmcp/files/data-overlays/keywords.yaml @@ -8,6 +8,8 @@ help: - checkclient - mudletmap - mudletui + integration: + - discord # Aliases for keywords when typing: help help-aliases: diff --git a/modules/gmcp/files/data-overlays/mudlet-config.yaml b/modules/gmcp/files/data-overlays/mudlet-config.yaml deleted file mode 100644 index 5a6135cf..00000000 --- a/modules/gmcp/files/data-overlays/mudlet-config.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# Mudlet client configuration - -# Mapper configuration -mapper_version: "1" -mapper_url: "https://github.com/GoMudEngine/MudletMapper/releases/latest/download/GoMudMapper.mpackage" - -# UI configuration -ui_version: "1" -ui_url: "https://github.com/GoMudEngine/MudletUI/releases/latest/download/GoMudUI.mpackage" - -# Map data configuration -map_version: "1" -map_url: "https://github.com/GoMudEngine/MudletMapper/releases/latest/download/gomud.dat" \ No newline at end of file diff --git a/modules/gmcp/files/datafiles/templates/help/checkclient.template b/modules/gmcp/files/datafiles/templates/help/checkclient.template index f2beb9a4..ee9bceb7 100644 --- a/modules/gmcp/files/datafiles/templates/help/checkclient.template +++ b/modules/gmcp/files/datafiles/templates/help/checkclient.template @@ -6,7 +6,7 @@ The checkclient command displays information about cli checkclient - Shows details about detected client features and available enhancements -NOTES: +Notes: - This command is particularly useful for Mudlet users to see what special features are available - It will detect if you're using Mudlet and show relevant package information - The command runs automatically when you log in to provide helpful information diff --git a/modules/gmcp/files/datafiles/templates/help/client.template b/modules/gmcp/files/datafiles/templates/help/client.template index 3e48ac57..aec96d34 100644 --- a/modules/gmcp/files/datafiles/templates/help/client.template +++ b/modules/gmcp/files/datafiles/templates/help/client.template @@ -10,7 +10,7 @@ This game supports various MUD clients with special features to enhance your gam Other Clients - Basic support with standard MUD protocol features For the best experience, we recommend using a client that supports GMCP. -GMCP FEATURES: +GMCP Features: - Character information and status updates - Room and environment data - Communication channels diff --git a/modules/gmcp/files/datafiles/templates/help/discord.template b/modules/gmcp/files/datafiles/templates/help/discord.template new file mode 100644 index 00000000..65c01493 --- /dev/null +++ b/modules/gmcp/files/datafiles/templates/help/discord.template @@ -0,0 +1,29 @@ +.: Help for discord + +The discord command allows Mudlet client users to customize what information is shown in their Discord status. + +Usage: + + discord - Shows current settings and available commands + + discord info on|off - Enable/disable Discord application info + discord status on|off - Enable/disable Discord status updates + + discord area on|off - Show/hide current area in status + discord party on|off - Show/hide party information + discord name on|off - Show/hide character name + discord level on|off - Show/hide character level + + +Notes: +- These commands only work if you are using a Mudlet client +- All settings default to enabled when not set +- Character name and level are shown in the format: "Name (lvl. X)" +- Party information includes party size and current area (if enabled) +- The info setting controls sending the Discord application ID +- The status setting controls sending presence updates to Discord +- Disabling either info or status will clear any cached data +- Settings persist between sessions +- Running discord without arguments shows current settings + +See also: help client, checkclient \ No newline at end of file diff --git a/modules/gmcp/files/datafiles/templates/help/mudletmap.template b/modules/gmcp/files/datafiles/templates/help/mudletmap.template index 357e4360..d615975e 100644 --- a/modules/gmcp/files/datafiles/templates/help/mudletmap.template +++ b/modules/gmcp/files/datafiles/templates/help/mudletmap.template @@ -6,7 +6,7 @@ The mudletmap command sends map data to Mudlet clients mudletmap - Sends or refreshes map data to your Mudlet client -NOTES: +Notes: - This command only works if you are using a Mudlet client - The map data helps with navigation and visualization of the game world - This command runs automatically when you log in with Mudlet diff --git a/modules/gmcp/files/datafiles/templates/help/mudletui.template b/modules/gmcp/files/datafiles/templates/help/mudletui.template index a504674c..b37bfddb 100644 --- a/modules/gmcp/files/datafiles/templates/help/mudletui.template +++ b/modules/gmcp/files/datafiles/templates/help/mudletui.template @@ -11,7 +11,7 @@ The mudletui command allows Mudlet client users to man mudletui hide - Hides automatic Mudlet UI prompts when logging in mudletui show - Re-enables automatic Mudlet UI prompts when logging in -NOTES: +Notes: - These commands only work if you are using a Mudlet client - The UI package enhances your gameplay experience with custom windows and controls - If the installation doesn't work automatically, check for prompts in your Mudlet client diff --git a/modules/gmcp/gmcp.Mudlet.go b/modules/gmcp/gmcp.Mudlet.go index e94f67f0..fab974b8 100644 --- a/modules/gmcp/gmcp.Mudlet.go +++ b/modules/gmcp/gmcp.Mudlet.go @@ -2,11 +2,13 @@ package gmcp import ( "embed" + "fmt" "strings" "github.com/GoMudEngine/GoMud/internal/configs" "github.com/GoMudEngine/GoMud/internal/events" "github.com/GoMudEngine/GoMud/internal/mudlog" + "github.com/GoMudEngine/GoMud/internal/parties" "github.com/GoMudEngine/GoMud/internal/plugins" "github.com/GoMudEngine/GoMud/internal/rooms" "github.com/GoMudEngine/GoMud/internal/usercommands" @@ -31,12 +33,21 @@ type MudletConfig struct { // Map data configuration MapVersion string `json:"map_version" yaml:"map_version"` MapURL string `json:"map_url" yaml:"map_url"` + + // Discord Rich Presence configuration + DiscordApplicationID string `json:"discord_application_id" yaml:"discord_application_id"` + DiscordInviteURL string `json:"discord_invite_url" yaml:"discord_invite_url"` + DiscordLargeImageKey string `json:"discord_large_image_key" yaml:"discord_large_image_key"` + DiscordDetails string `json:"discord_details" yaml:"discord_details"` + DiscordState string `json:"discord_state" yaml:"discord_state"` + DiscordSmallImageKey string `json:"discord_small_image_key" yaml:"discord_small_image_key"` } // GMCPMudletModule handles Mudlet-specific GMCP functionality type GMCPMudletModule struct { - plug *plugins.Plugin - config MudletConfig + plug *plugins.Plugin + config MudletConfig + mudletUsers map[int]bool // Track which users are using Mudlet clients } // GMCPMudletDetected is an event fired when a Mudlet client is detected @@ -47,131 +58,295 @@ type GMCPMudletDetected struct { func (g GMCPMudletDetected) Type() string { return `GMCPMudletDetected` } +// GMCPDiscordStatusRequest is an event fired when a client requests Discord status information +type GMCPDiscordStatusRequest struct { + UserId int +} + +func (g GMCPDiscordStatusRequest) Type() string { return `GMCPDiscordStatusRequest` } + +// GMCPDiscordMessage is an event fired when a client sends a Discord-related GMCP message +type GMCPDiscordMessage struct { + ConnectionId uint64 + Command string + Payload []byte +} + +func (g GMCPDiscordMessage) Type() string { return `GMCPDiscordMessage` } + func init() { - // Set up a default configuration first + // Create module with basic structure g := GMCPMudletModule{ - plug: plugins.New(`gmcp.Mudlet`, `1.0`), - config: MudletConfig{ - MapperVersion: "1", // Default value - MapperURL: "https://github.com/GoMudEngine/MudletMapper/releases/latest/download/GoMudMapper.mpackage", // Default value - UIVersion: "1", // Default value - UIURL: "https://github.com/GoMudEngine/MudletUI/releases/latest/download/GoMudUI.mpackage", // Default value - MapVersion: "1", // Default value - MapURL: "https://github.com/GoMudEngine/MudletMapper/releases/latest/download/gomud.dat", // Default value - }, + plug: plugins.New(`gmcp.Mudlet`, `1.0`), + mudletUsers: make(map[int]bool), } - // Attach embedded filesystem without logging errors - _ = g.plug.AttachFileSystem(files) - - // Load config values from plugin config system - if mapperVersion, ok := g.plug.Config.Get(`MapperVersion`).(string); ok { - g.config.MapperVersion = mapperVersion - } - if mapperURL, ok := g.plug.Config.Get(`MapperURL`).(string); ok { - g.config.MapperURL = mapperURL - } - if uiVersion, ok := g.plug.Config.Get(`UIVersion`).(string); ok { - g.config.UIVersion = uiVersion - } - if uiURL, ok := g.plug.Config.Get(`UIURL`).(string); ok { - g.config.UIURL = uiURL - } - if mapVersion, ok := g.plug.Config.Get(`MapVersion`).(string); ok { - g.config.MapVersion = mapVersion - } - if mapURL, ok := g.plug.Config.Get(`MapURL`).(string); ok { - g.config.MapURL = mapURL + // Attach filesystem with proper error handling + if err := g.plug.AttachFileSystem(files); err != nil { + panic(err) } + // Register callbacks for load/save + g.plug.Callbacks.SetOnLoad(g.load) + g.plug.Callbacks.SetOnSave(g.save) + // Register event listeners events.RegisterListener(events.PlayerSpawn{}, g.playerSpawnHandler) + events.RegisterListener(events.PlayerDespawn{}, g.playerDespawnHandler) events.RegisterListener(GMCPMudletDetected{}, g.mudletDetectedHandler) + events.RegisterListener(GMCPDiscordStatusRequest{}, g.discordStatusRequestHandler) + events.RegisterListener(GMCPDiscordMessage{}, g.discordMessageHandler) + events.RegisterListener(events.RoomChange{}, g.roomChangeHandler) + events.RegisterListener(events.PartyUpdated{}, g.partyUpdateHandler) - // Register the Mudlet-specific user commands - set as hidden (true for first bool) + // Register the Mudlet-specific user commands g.plug.AddUserCommand("mudletmap", g.sendMapCommand, true, false) g.plug.AddUserCommand("mudletui", g.sendUICommand, false, false) g.plug.AddUserCommand("checkclient", g.checkClientCommand, true, false) + g.plug.AddUserCommand("discord", g.discordCommand, true, false) } -// sendUICommand is a user command that sends UI-related GMCP messages to Mudlet clients -func (g *GMCPMudletModule) sendUICommand(rest string, user *users.UserRecord, room *rooms.Room, flags events.EventFlag) (bool, error) { - // Only send if the client is Mudlet - connId := user.ConnectionId() +// Helper function to load a config string from the plugin's configuration +func loadConfigString(p *plugins.Plugin, key string) string { + if val, ok := p.Config.Get(key).(string); ok { + return val + } + return "" +} + +// load handles loading configuration from the plugin's storage +func (g *GMCPMudletModule) load() { + // Load config values directly from embedded config or overrides + g.config.MapperVersion = loadConfigString(g.plug, "mapper_version") + g.config.MapperURL = loadConfigString(g.plug, "mapper_url") + g.config.UIVersion = loadConfigString(g.plug, "ui_version") + g.config.UIURL = loadConfigString(g.plug, "ui_url") + g.config.MapVersion = loadConfigString(g.plug, "map_version") + g.config.MapURL = loadConfigString(g.plug, "map_url") + g.config.DiscordApplicationID = loadConfigString(g.plug, "discord_application_id") + g.config.DiscordInviteURL = loadConfigString(g.plug, "discord_invite_url") + g.config.DiscordLargeImageKey = loadConfigString(g.plug, "discord_large_image_key") + g.config.DiscordDetails = loadConfigString(g.plug, "discord_details") + g.config.DiscordState = loadConfigString(g.plug, "discord_state") + g.config.DiscordSmallImageKey = loadConfigString(g.plug, "discord_small_image_key") +} + +// save handles saving configuration to the plugin's storage +func (g *GMCPMudletModule) save() { + g.plug.WriteStruct(`mudlet_config`, g.config) +} + +// Helper function to check if a user is using a Mudlet client +func (g *GMCPMudletModule) isMudletClient(userId int) bool { + if userId < 1 { + return false + } + + // First check our cache of known Mudlet users + if known, ok := g.mudletUsers[userId]; ok { + return known + } + + // If not in cache, check the connection + connId := users.GetConnectionId(userId) + if connId == 0 { + return false + } + + // Check the cache to see if this is a Mudlet client if gmcpData, ok := gmcpModule.cache.Get(connId); ok && gmcpData.Client.IsMudlet { - // Process command arguments - args := strings.Fields(rest) - if len(args) > 0 { - switch args[0] { - case "install": - // Send UI install message - g.sendMudletUIInstall(user.UserId) - user.SendText("\nUI installation package sent to your Mudlet client. If it doesn't install automatically, you may need to accept the installation prompt in Mudlet.\n") - // Set a flag to prevent the checkclient message from showing again - user.SetConfigOption("mudlet_ui_prompt_disabled", true) - case "remove": - // Send UI remove message - g.sendMudletUIRemove(user.UserId) - user.SendText("\nUI removal command sent to your Mudlet client.\n") - case "update": - // Send UI update message - g.sendMudletUIUpdate(user.UserId) - user.SendText("\nManual UI update check sent to your Mudlet client.\n") - case "hide": - // Set a flag to prevent the checkclient message from showing again - user.SetConfigOption("mudlet_ui_prompt_disabled", true) - user.SendText("\nThe Mudlet UI prompt has been hidden. You won't see these messages again when logging in.\n") - user.SendText("You can use mudletui show in the future if you want to see the prompts again.\n") - case "show": - // Remove the flag to allow the checkclient message to show again - user.SetConfigOption("mudlet_ui_prompt_disabled", false) - user.SendText("\nThe Mudlet UI prompt has been re-enabled. You'll see these messages again when logging in.\n") - user.SendText("You can use mudletui hide in the future if you want to hide the prompts again.\n") - default: - // Unknown command - user.SendText("\nUsage: mudletui install|remove|update|hide|show\n\nType 'help mudletui' for more information.\n") + // Store for future reference + g.mudletUsers[userId] = true + return true + } + + return false +} + +// Helper function to get user config option with default boolean value +func getUserBoolOption(user *users.UserRecord, key string, defaultValue bool) bool { + val := user.GetConfigOption(key) + if val == nil { + return defaultValue + } + if boolVal, ok := val.(bool); ok { + return boolVal + } + return defaultValue +} + +// Helper to send GMCP event +func sendGMCP(userId int, module string, payload interface{}) { + if userId < 1 { + return + } + events.AddToQueue(GMCPOut{ + UserId: userId, + Module: module, + Payload: payload, + }) +} + +// Helper function to create and send Discord Info message +func (g *GMCPMudletModule) sendDiscordInfo(userId int) { + if userId < 1 { + return + } + + user := users.GetByUserId(userId) + if user == nil { + return + } + + // Check if Discord.Info is enabled + if !getUserBoolOption(user, "discord_enable_info", true) { + mudlog.Debug("GMCP", "type", "Mudlet", "action", "Discord.Info package sending disabled for user", "userId", userId) + return + } + + // Send Discord Info payload + payload := struct { + ApplicationID string `json:"applicationid"` + InviteURL string `json:"inviteurl"` + }{ + ApplicationID: g.config.DiscordApplicationID, + InviteURL: g.config.DiscordInviteURL, + } + + sendGMCP(userId, "External.Discord.Info", payload) + mudlog.Debug("GMCP", "type", "Mudlet", "action", "Sent Discord Info", "userId", userId) +} + +// sendDiscordStatus sends the current Discord status information +func (g *GMCPMudletModule) sendDiscordStatus(userId int) { + if userId < 1 { + return + } + + // Get the user record + user := users.GetByUserId(userId) + if user == nil { + mudlog.Error("GMCP", "type", "Mudlet", "action", "Failed to get user record for Discord status", "userId", userId) + return + } + + // Check if Discord.Status is enabled + if !getUserBoolOption(user, "discord_enable_status", true) { + mudlog.Debug("GMCP", "type", "Mudlet", "action", "Discord.Status package sending disabled for user", "userId", userId) + return + } + + // Get the current room + room := rooms.LoadRoom(user.Character.RoomId) + if room == nil { + mudlog.Error("GMCP", "type", "Mudlet", "action", "Failed to get room for Discord status", "userId", userId, "roomId", user.Character.RoomId) + return + } + + // Check display preferences + showArea := getUserBoolOption(user, "discord_show_area", true) + showParty := getUserBoolOption(user, "discord_show_party", true) + showName := getUserBoolOption(user, "discord_show_name", true) + showLevel := getUserBoolOption(user, "discord_show_level", true) + + // Build the details string based on preferences + detailsStr := g.config.DiscordDetails + if showName || showLevel { + detailsStr = "" + if showName { + detailsStr = user.Character.Name + } + if showLevel { + if detailsStr != "" { + detailsStr += " " } - } else { - // No arguments provided - show status and available commands - mudName := configs.GetServerConfig().MudName.String() - - // Check current status of prompt display - promptDisabled := user.GetConfigOption("mudlet_ui_prompt_disabled") - promptStatus := "ENABLED" - if promptDisabled != nil && promptDisabled.(bool) { - promptStatus = "HIDDEN" + if showName { + detailsStr += fmt.Sprintf("(lvl. %d)", user.Character.Level) + } else { + detailsStr += fmt.Sprintf("Level %d", user.Character.Level) } + } + } + + // Create Discord Status payload + payload := struct { + Details string `json:"details"` + State string `json:"state"` + Game string `json:"game"` + LargeImageKey string `json:"large_image_key"` + SmallImageKey string `json:"small_image_key"` + StartTime int64 `json:"starttime"` + PartySize int `json:"partysize,omitempty"` + PartyMax int `json:"partymax,omitempty"` + }{ + Details: detailsStr, + State: g.config.DiscordState, + Game: configs.GetServerConfig().MudName.String(), + LargeImageKey: g.config.DiscordLargeImageKey, + SmallImageKey: g.config.DiscordSmallImageKey, + StartTime: user.GetConnectTime().Unix(), + } + + // Show area if enabled + if showArea { + payload.State = fmt.Sprintf("Exploring %s", room.Zone) + } - user.SendText("\n" + mudName + " Mudlet UI Management\n") - user.SendText("Status:\n") - user.SendText(" Login message display: " + promptStatus + "\n") - user.SendText("Available Commands:\n") - user.SendText(" mudletui install - Install the Mudlet UI package\n") - user.SendText(" mudletui remove - Remove the Mudlet UI package\n") - user.SendText(" mudletui update - Manually check for updates to the Mudlet UI package\n") - user.SendText(" mudletui hide - Hide login messages\n") - user.SendText(" mudletui show - Enable login messages\n\n") - user.SendText("For more information, type help mudletui\n") + // Show party info if enabled and in a party + if party := parties.Get(userId); party != nil && showParty { + payload.PartySize = len(party.GetMembers()) + payload.PartyMax = 10 + if showArea { + payload.State = fmt.Sprintf("Group in %s", room.Zone) + } else { + payload.State = "In group" } + } - // Return true to indicate the command was handled - return true, nil - } else { - // Client is not Mudlet - user.SendText("\nThis command is only available for Mudlet clients. You are currently using: " + gmcpData.Client.Name + "\n") + // Send the Discord Status message + sendGMCP(userId, "External.Discord.Status", payload) + mudlog.Debug("GMCP", "type", "Mudlet", "action", "Sent Discord status update", "userId", userId, "zone", room.Zone) +} + +// Send empty Discord status to clear it +func (g *GMCPMudletModule) clearDiscordStatus(userId int) { + payload := struct { + Details string `json:"details"` + State string `json:"state"` + Game string `json:"game"` + LargeImageKey string `json:"large_image_key"` + SmallImageKey string `json:"small_image_key"` + }{ + Details: "", + State: "", + Game: "", + LargeImageKey: "", + SmallImageKey: "", } - // Command was handled - return true, nil + sendGMCP(userId, "External.Discord.Status", payload) } -// sendMudletUIInstall sends the UI installation GMCP message +// Send Mudlet map configuration +func (g *GMCPMudletModule) sendMudletMapConfig(userId int) { + if userId < 1 { + return + } + + mapConfig := map[string]string{ + "url": g.config.MapURL, + } + + sendGMCP(userId, "Client.Map", mapConfig) + mudlog.Debug("GMCP", "type", "Mudlet", "action", "Sent Mudlet map config", "userId", userId) +} + +// Send Mudlet UI package installation message func (g *GMCPMudletModule) sendMudletUIInstall(userId int) { if userId < 1 { return } - // Create a payload for UI installation payload := struct { Version string `json:"version"` URL string `json:"url"` @@ -180,101 +355,74 @@ func (g *GMCPMudletModule) sendMudletUIInstall(userId int) { URL: g.config.UIURL, } - // Send the Client.GUI message - events.AddToQueue(GMCPOut{ - UserId: userId, - Module: "Client.GUI", - Payload: payload, - }) - + sendGMCP(userId, "Client.GUI", payload) mudlog.Debug("GMCP", "type", "Mudlet", "action", "Sent Mudlet UI install config", "userId", userId) } -// sendMudletUIRemove sends the UI remove GMCP message +// Send Mudlet UI package removal message func (g *GMCPMudletModule) sendMudletUIRemove(userId int) { if userId < 1 { return } - // Create a payload for UI removal payload := struct { GoMudUI string `json:"gomudui"` }{ GoMudUI: "remove", } - // Send the Client.GUI message - events.AddToQueue(GMCPOut{ - UserId: userId, - Module: "Client.GUI", - Payload: payload, - }) - + sendGMCP(userId, "Client.GUI", payload) mudlog.Debug("GMCP", "type", "Mudlet", "action", "Sent Mudlet UI remove command", "userId", userId) } -// sendMudletUIUpdate sends the UI update GMCP message +// Send Mudlet UI package update message func (g *GMCPMudletModule) sendMudletUIUpdate(userId int) { if userId < 1 { return } - // Create a payload for UI update payload := struct { GoMudUI string `json:"gomudui"` }{ GoMudUI: "update", } - // Send the Client.GUI message - events.AddToQueue(GMCPOut{ - UserId: userId, - Module: "Client.GUI", - Payload: payload, - }) - + sendGMCP(userId, "Client.GUI", payload) mudlog.Debug("GMCP", "type", "Mudlet", "action", "Sent Mudlet UI update command", "userId", userId) } -// sendMapCommand is a user command that sends the map URL to Mudlet clients -func (g *GMCPMudletModule) sendMapCommand(rest string, user *users.UserRecord, room *rooms.Room, flags events.EventFlag) (bool, error) { - // Only send if the client is Mudlet - connId := user.ConnectionId() - if gmcpData, ok := gmcpModule.cache.Get(connId); ok && gmcpData.Client.IsMudlet { - // Send the map URL - g.sendMudletMapConfig(user.UserId) - - // Return true to indicate the command was handled (but don't show any output to the user) - return true, nil +// Send mapper configuration to Mudlet client +func (g *GMCPMudletModule) sendMudletConfig(userId int) { + if userId < 1 { + return } - // Return false to indicate the command wasn't handled (if not a Mudlet client) - // This allows other handlers to potentially process it - return false, nil -} + // Send mapper info + payload := struct { + Version string `json:"version"` + URL string `json:"url"` + }{ + Version: g.config.MapperVersion, + URL: g.config.MapperURL, + } + sendGMCP(userId, "Client.GUI", payload) -// sendMudletMapConfig sends the Mudlet map configuration via GMCP -func (g *GMCPMudletModule) sendMudletMapConfig(userId int) { - if userId < 1 { + // Get the user record + user := users.GetByUserId(userId) + if user == nil { return } - // Create a payload for the Client.Map message - mapConfig := map[string]string{ - "url": g.config.MapURL, - } + // Send Discord info if enabled + g.sendDiscordInfo(userId) - // Send the Client.Map message - events.AddToQueue(GMCPOut{ - UserId: userId, - Module: "Client.Map", - Payload: mapConfig, - }) + // Send Discord status + g.sendDiscordStatus(userId) - mudlog.Debug("GMCP", "type", "Mudlet", "action", "Sent Mudlet map config", "userId", userId) + mudlog.Info("GMCP", "type", "Mudlet", "action", "Sent Mudlet package config", "userId", userId) } -// playerSpawnHandler sends Mudlet-specific GMCP when a player connects +// playerSpawnHandler handles when a player connects func (g *GMCPMudletModule) playerSpawnHandler(e events.Event) events.ListenerReturn { evt, typeOk := e.(events.PlayerSpawn) if !typeOk { @@ -283,17 +431,32 @@ func (g *GMCPMudletModule) playerSpawnHandler(e events.Event) events.ListenerRet } // Check if the client is Mudlet - if gmcpData, ok := gmcpModule.cache.Get(evt.ConnectionId); ok { - if gmcpData.Client.IsMudlet { - // Send Mudlet-specific GMCP - g.sendMudletConfig(evt.UserId) - } + if gmcpData, ok := gmcpModule.cache.Get(evt.ConnectionId); ok && gmcpData.Client.IsMudlet { + // Send Mudlet-specific GMCP + g.sendMudletConfig(evt.UserId) } return events.Continue } -// mudletDetectedHandler handles the event when a Mudlet client is detected +// playerDespawnHandler handles when a player disconnects +func (g *GMCPMudletModule) playerDespawnHandler(e events.Event) events.ListenerReturn { + evt, typeOk := e.(events.PlayerDespawn) + if !typeOk { + mudlog.Error("Event", "Expected Type", "PlayerDespawn", "Actual Type", e.Type()) + return events.Cancel + } + + // Clean up the mudletUsers map entry for this user + if evt.UserId > 0 { + delete(g.mudletUsers, evt.UserId) + mudlog.Debug("GMCP", "type", "Mudlet", "action", "Cleaned up Mudlet user entry", "userId", evt.UserId) + } + + return events.Continue +} + +// mudletDetectedHandler handles when a Mudlet client is detected func (g *GMCPMudletModule) mudletDetectedHandler(e events.Event) events.ListenerReturn { evt, typeOk := e.(GMCPMudletDetected) if !typeOk { @@ -308,54 +471,315 @@ func (g *GMCPMudletModule) mudletDetectedHandler(e events.Event) events.Listener return events.Continue } -// sendMudletConfig sends the Mudlet configuration via GMCP -func (g *GMCPMudletModule) sendMudletConfig(userId int) { - if userId < 1 { - return +// discordStatusRequestHandler handles Discord status requests +func (g *GMCPMudletModule) discordStatusRequestHandler(e events.Event) events.ListenerReturn { + evt, typeOk := e.(GMCPDiscordStatusRequest) + if !typeOk { + mudlog.Error("Event", "Expected Type", "GMCPDiscordStatusRequest", "Actual Type", e.Type()) + return events.Cancel } - // Create a GUI payload with mapper version and url - guiPayload := struct { - Version string `json:"version"` - URL string `json:"url"` - }{ - Version: g.config.MapperVersion, - URL: g.config.MapperURL, + // Send both Discord info and status + g.sendDiscordInfo(evt.UserId) + g.sendDiscordStatus(evt.UserId) + + mudlog.Info("GMCP", "type", "Mudlet", "action", "Processed Discord status request", "userId", evt.UserId) + return events.Continue +} + +// discordMessageHandler handles Discord-related GMCP messages +func (g *GMCPMudletModule) discordMessageHandler(e events.Event) events.ListenerReturn { + evt, typeOk := e.(GMCPDiscordMessage) + if !typeOk { + mudlog.Error("Event", "Expected Type", "GMCPDiscordMessage", "Actual Type", e.Type()) + return events.Cancel } - // Send the Client.GUI message with mapper version and URL - events.AddToQueue(GMCPOut{ - UserId: userId, - Module: "Client.GUI", - Payload: guiPayload, - }) + // Find the user ID for this connection + userId := 0 + for _, user := range users.GetAllActiveUsers() { + if user.ConnectionId() == evt.ConnectionId { + userId = user.UserId + break + } + } + + if userId == 0 { + return events.Cancel + } + + // Log the message + mudlog.Info("Mudlet GMCP Discord", "type", evt.Command, "userId", userId, "payload", string(evt.Payload)) + + // Handle different Discord commands + switch evt.Command { + case "Hello": + g.sendDiscordInfo(userId) + case "Get": + user := users.GetByUserId(userId) + if user != nil && user.Character != nil { + events.AddToQueue(GMCPDiscordStatusRequest{ + UserId: userId, + }) + } + } + + return events.Continue +} + +// roomChangeHandler updates Discord status when players change areas +func (g *GMCPMudletModule) roomChangeHandler(e events.Event) events.ListenerReturn { + evt, typeOk := e.(events.RoomChange) + if !typeOk { + return events.Cancel + } + + // Only handle player movements (not mobs) + if evt.UserId == 0 || evt.MobInstanceId > 0 { + return events.Continue + } + + // Check if this is a Mudlet client + if !g.isMudletClient(evt.UserId) { + return events.Continue + } + + // Load rooms and check for zone change + oldRoom := rooms.LoadRoom(evt.FromRoomId) + newRoom := rooms.LoadRoom(evt.ToRoomId) + if oldRoom == nil || newRoom == nil { + return events.Continue + } + + // Update Discord status on zone change + if oldRoom.Zone != newRoom.Zone { + g.sendDiscordStatus(evt.UserId) + } + + return events.Continue +} + +// partyUpdateHandler updates Discord status for party members +func (g *GMCPMudletModule) partyUpdateHandler(e events.Event) events.ListenerReturn { + evt, typeOk := e.(events.PartyUpdated) + if !typeOk { + mudlog.Error("Event", "Expected Type", "PartyUpdated", "Actual Type", e.Type()) + return events.Cancel + } + + // Update Discord status for all Mudlet users in the party + for _, userId := range evt.UserIds { + if g.isMudletClient(userId) { + g.sendDiscordStatus(userId) + } + } - mudlog.Debug("GMCP", "type", "Mudlet", "action", "Sent Mudlet package config", "userId", userId) + return events.Continue } -// checkClientCommand checks if the player is using Mudlet and shows information if they are +// Helper function for handling command toggles +func (g *GMCPMudletModule) handleToggleCommand(user *users.UserRecord, settingName string, value bool, enableMsg string, disableMsg string) { + user.SetConfigOption(settingName, value) + if value { + user.SendText("\n" + enableMsg + "\n") + } else { + user.SendText("\n" + disableMsg + "\n") + } + + // Update Discord status if this was a Discord-related setting + if strings.HasPrefix(settingName, "discord_") { + g.sendDiscordStatus(user.UserId) + } +} + +// sendUICommand handles UI-related commands +func (g *GMCPMudletModule) sendUICommand(rest string, user *users.UserRecord, room *rooms.Room, flags events.EventFlag) (bool, error) { + // Only proceed if client is Mudlet + connId := user.ConnectionId() + if gmcpData, ok := gmcpModule.cache.Get(connId); !ok || !gmcpData.Client.IsMudlet { + user.SendText("\nThis command is only available for Mudlet clients. You are currently using: " + gmcpData.Client.Name + "\n") + return true, nil + } + + // Process arguments + args := strings.Fields(rest) + if len(args) == 0 { + // No arguments - show status and help + mudName := configs.GetServerConfig().MudName.String() + + // Check prompt display status + var promptStatus string + if getUserBoolOption(user, "mudlet_ui_prompt_disabled", false) { + promptStatus = "HIDDEN" + } else { + promptStatus = "ENABLED" + } + + user.SendText("\n" + mudName + " Mudlet UI Management\n") + user.SendText("Status:\n") + user.SendText(" Login message display: " + promptStatus + "\n") + user.SendText("Available Commands:\n") + user.SendText(" mudletui install - Install the Mudlet UI package\n") + user.SendText(" mudletui remove - Remove the Mudlet UI package\n") + user.SendText(" mudletui update - Manually check for updates to the Mudlet UI package\n") + user.SendText(" mudletui hide - Hide login messages\n") + user.SendText(" mudletui show - Enable login messages\n\n") + user.SendText("For more information, type help mudletui\n") + return true, nil + } + + // Handle specific commands + switch args[0] { + case "install": + g.sendMudletUIInstall(user.UserId) + user.SetConfigOption("mudlet_ui_prompt_disabled", true) + user.SendText("\nUI installation package sent to your Mudlet client. If it doesn't install automatically, you may need to accept the installation prompt in Mudlet.\n") + + case "remove": + g.sendMudletUIRemove(user.UserId) + user.SendText("\nUI removal command sent to your Mudlet client.\n") + + case "update": + g.sendMudletUIUpdate(user.UserId) + user.SendText("\nManual UI update check sent to your Mudlet client.\n") + + case "hide": + g.handleToggleCommand(user, "mudlet_ui_prompt_disabled", true, + "The Mudlet UI prompt has been hidden.", + "") + user.SendText("You can use mudletui show in the future if you want to see the prompts again.\n") + + case "show": + g.handleToggleCommand(user, "mudlet_ui_prompt_disabled", false, + "The Mudlet UI prompt has been re-enabled.", + "") + user.SendText("You can use mudletui hide in the future if you want to hide the prompts again.\n") + + default: + user.SendText("\nUsage: mudletui install|remove|update|hide|show\n\nType 'help mudletui' for more information.\n") + } + + return true, nil +} + +// sendMapCommand sends map configuration +func (g *GMCPMudletModule) sendMapCommand(rest string, user *users.UserRecord, room *rooms.Room, flags events.EventFlag) (bool, error) { + // Only send if the client is Mudlet + connId := user.ConnectionId() + if gmcpData, ok := gmcpModule.cache.Get(connId); ok && gmcpData.Client.IsMudlet { + g.sendMudletMapConfig(user.UserId) + return true, nil + } + return false, nil +} + +// checkClientCommand checks if client is Mudlet and shows info func (g *GMCPMudletModule) checkClientCommand(rest string, user *users.UserRecord, room *rooms.Room, flags events.EventFlag) (bool, error) { - // Get the connection ID and check if the client is Mudlet + // Check if client is Mudlet connId := user.ConnectionId() if gmcpData, ok := gmcpModule.cache.Get(connId); ok && gmcpData.Client.IsMudlet { - // Check if the user has disabled the prompt - promptDisabled := user.GetConfigOption("mudlet_ui_prompt_disabled") - if promptDisabled != nil && promptDisabled.(bool) { - // User has disabled the prompt, so don't show the message + // Skip if prompt is disabled + if getUserBoolOption(user, "mudlet_ui_prompt_disabled", false) { return true, nil } - // Show a brief intro message + // Show Mudlet help user.SendText("\n\nWe have detected you are using Mudlet as a client.\n") - - // Use the standard help system to show the mudletui help usercommands.Help("mudletui", user, room, flags) + } + return true, nil +} + +// discordCommand handles Discord-related settings +func (g *GMCPMudletModule) discordCommand(rest string, user *users.UserRecord, room *rooms.Room, flags events.EventFlag) (bool, error) { + // Only proceed if client is Mudlet + connId := user.ConnectionId() + if gmcpData, ok := gmcpModule.cache.Get(connId); !ok || !gmcpData.Client.IsMudlet { + user.SendText("\nThis command is only available for Mudlet clients. You are currently using: " + gmcpData.Client.Name + "\n") + return true, nil + } - // Command was handled + // Process arguments + args := strings.Fields(rest) + if len(args) == 0 { + user.SendText("\nUsage: discord area on|off|party on|off|name on|off|level on|off|info on|off|status on|off\n") return true, nil } - // Client is not Mudlet - return true but don't show any message - // (Return true anyway to avoid command showing up in help) + // Handle different settings + if len(args) >= 2 { + switch args[0] { + case "area": + if args[1] == "on" { + g.handleToggleCommand(user, "discord_show_area", true, "Area display in Discord status enabled.", "") + } else if args[1] == "off" { + g.handleToggleCommand(user, "discord_show_area", false, "Area display in Discord status disabled.", "") + } else { + user.SendText("\nUsage: discord area on|off\n") + } + + case "party": + if args[1] == "on" { + g.handleToggleCommand(user, "discord_show_party", true, "Party display in Discord status enabled.", "") + } else if args[1] == "off" { + g.handleToggleCommand(user, "discord_show_party", false, "Party display in Discord status disabled.", "") + } else { + user.SendText("\nUsage: discord party on|off\n") + } + + case "name": + if args[1] == "on" { + g.handleToggleCommand(user, "discord_show_name", true, "Character name display in Discord status enabled.", "") + } else if args[1] == "off" { + g.handleToggleCommand(user, "discord_show_name", false, "Character name display in Discord status disabled.", "") + } else { + user.SendText("\nUsage: discord name on|off\n") + } + + case "level": + if args[1] == "on" { + g.handleToggleCommand(user, "discord_show_level", true, "Level display in Discord status enabled.", "") + } else if args[1] == "off" { + g.handleToggleCommand(user, "discord_show_level", false, "Level display in Discord status disabled.", "") + } else { + user.SendText("\nUsage: discord level on|off\n") + } + + case "info": + if args[1] == "on" { + g.handleToggleCommand(user, "discord_enable_info", true, "Discord.Info package sending enabled.", "") + g.sendDiscordInfo(user.UserId) + } else if args[1] == "off" { + g.handleToggleCommand(user, "discord_enable_info", false, "Discord.Info package sending disabled.", "") + // Send empty Discord.Info payload + sendGMCP(user.UserId, "External.Discord.Info", struct { + ApplicationID string `json:"applicationid"` + InviteURL string `json:"inviteurl"` + }{ + ApplicationID: "", + InviteURL: "", + }) + } else { + user.SendText("\nUsage: discord info on|off\n") + } + + case "status": + if args[1] == "on" { + g.handleToggleCommand(user, "discord_enable_status", true, "Discord.Status package sending enabled.", "") + g.sendDiscordStatus(user.UserId) + } else if args[1] == "off" { + g.handleToggleCommand(user, "discord_enable_status", false, "Discord.Status package sending disabled.", "") + g.clearDiscordStatus(user.UserId) + } else { + user.SendText("\nUsage: discord status on|off\n") + } + + default: + user.SendText("\nUsage: discord area on|off|party on|off|name on|off|level on|off|info on|off|status on|off\n") + } + } else { + user.SendText("\nUsage: discord area on|off|party on|off|name on|off|level on|off|info on|off|status on|off\n") + } + return true, nil } diff --git a/modules/gmcp/gmcp.go b/modules/gmcp/gmcp.go index 925d395b..b4a96a41 100644 --- a/modules/gmcp/gmcp.go +++ b/modules/gmcp/gmcp.go @@ -362,6 +362,37 @@ func (g *GMCPModule) HandleIAC(connectionId uint64, iacCmd []byte) bool { if err := json.Unmarshal(payload, &decoded); err == nil { mudlog.Debug("GMCP LOGIN", "username", decoded.Name, "password", strings.Repeat(`*`, len(decoded.Password))) } + + // Handle Discord-related messages + default: + // Check if it's a Discord message + if strings.HasPrefix(command, "External.Discord") { + // Try to find the user ID associated with this connection + userId := 0 + for _, user := range users.GetAllActiveUsers() { + if user.ConnectionId() == connectionId { + userId = user.UserId + break + } + } + + if userId > 0 { + // Extract the Discord command (Hello, Get, Status) + discordCommand := "" + if parts := strings.Split(command, "."); len(parts) >= 3 { + discordCommand = parts[2] // External.Discord.Hello -> Hello + } + + // Dispatch a GMCPDiscordMessage event + events.AddToQueue(GMCPDiscordMessage{ + ConnectionId: connectionId, + Command: discordCommand, + Payload: payload, + }) + + mudlog.Debug("GMCP", "type", "Discord", "command", discordCommand, "userId", userId) + } + } } return true @@ -424,7 +455,7 @@ func (g *GMCPModule) dispatchGMCP(e events.Event) events.ListenerReturn { var prettyJSON bytes.Buffer json.Indent(&prettyJSON, v, "", "\t") fmt.Print(gmcp.Module + ` `) - fmt.Println(string(prettyJSON.Bytes())) + fmt.Println(prettyJSON.String()) } // Regular code follows... @@ -446,7 +477,7 @@ func (g *GMCPModule) dispatchGMCP(e events.Event) events.ListenerReturn { var prettyJSON bytes.Buffer json.Indent(&prettyJSON, []byte(v), "", "\t") fmt.Print(gmcp.Module + ` `) - fmt.Println(string(prettyJSON.Bytes())) + fmt.Println(prettyJSON.String()) } // Regular code follows... @@ -473,7 +504,7 @@ func (g *GMCPModule) dispatchGMCP(e events.Event) events.ListenerReturn { var prettyJSON bytes.Buffer json.Indent(&prettyJSON, payload, "", "\t") fmt.Print(gmcp.Module + ` `) - fmt.Println(string(prettyJSON.Bytes())) + fmt.Println(prettyJSON.String()) } // Regular code follows...