diff --git a/_datafiles/config.yaml b/_datafiles/config.yaml index 3d255622..0a14ad57 100755 --- a/_datafiles/config.yaml +++ b/_datafiles/config.yaml @@ -59,6 +59,8 @@ Server: - print - inbox check - print + - mudletmap + - checkclient # - Motd - # Message of the day. This is displayed when the motd command is run. Motd: '{{ t "Motd" }}' diff --git a/modules/gmcp/files/data-overlays/keywords.yaml b/modules/gmcp/files/data-overlays/keywords.yaml new file mode 100644 index 00000000..d657be1a --- /dev/null +++ b/modules/gmcp/files/data-overlays/keywords.yaml @@ -0,0 +1,23 @@ +# Help keywords for Mudlet and client-related commands +# These will be merged with the main keywords.yaml by the help system + +help: + command: + interface: + - client + - checkclient + - mudletmap + - mudletui + +# Aliases for keywords when typing: help +help-aliases: + mudletui: [mudlet, mudletgui, mudlet-ui, mudlet ui] + mudletmap: [mudlet-map, mudlet map] + checkclient: [client-check, clientcheck] + client: [clients, mud-client, mudclient] + +# Command aliases for Mudlet-related commands +command-aliases: + mudletui: [mudlet, mgui] + mudletmap: [mudlet-map] + checkclient: [clientcheck] \ No newline at end of file diff --git a/modules/gmcp/files/data-overlays/mudlet-config.yaml b/modules/gmcp/files/data-overlays/mudlet-config.yaml new file mode 100644 index 00000000..5a6135cf --- /dev/null +++ b/modules/gmcp/files/data-overlays/mudlet-config.yaml @@ -0,0 +1,13 @@ +# 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 new file mode 100644 index 00000000..f2beb9a4 --- /dev/null +++ b/modules/gmcp/files/datafiles/templates/help/checkclient.template @@ -0,0 +1,14 @@ +.: Help for checkclient + +The checkclient command displays information about client-specific features available to you. + +Usage: + + checkclient - Shows details about detected client features and available enhancements + +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 + +See also: help mudletui, mudletmap \ No newline at end of file diff --git a/modules/gmcp/files/datafiles/templates/help/client.template b/modules/gmcp/files/datafiles/templates/help/client.template new file mode 100644 index 00000000..3e48ac57 --- /dev/null +++ b/modules/gmcp/files/datafiles/templates/help/client.template @@ -0,0 +1,21 @@ +.: Help for client + +This game supports various MUD clients with special features to enhance your gameplay experience. + +Supported Clients: + + Mudlet - Full support with custom UI, mapping, and GMCP features + Use help mudletui and help mudletmap for more information. + + Other Clients - Basic support with standard MUD protocol features + For the best experience, we recommend using a client that supports GMCP. + +GMCP FEATURES: +- Character information and status updates +- Room and environment data +- Communication channels +- Party information +- Custom UI elements (Mudlet only) +- Client Mapping support (Mudlet only) + +See also: help mudletui, help mudletmap, 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 new file mode 100644 index 00000000..357e4360 --- /dev/null +++ b/modules/gmcp/files/datafiles/templates/help/mudletmap.template @@ -0,0 +1,15 @@ +.: Help for mudletmap + +The mudletmap command sends map data to Mudlet clients for enhanced navigation. + +Usage: + + mudletmap - Sends or refreshes map data to your Mudlet client + +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 +- If your map isn't displaying properly, you can run this command to refresh it + +See also: help mudletui, checkclient \ No newline at end of file diff --git a/modules/gmcp/files/datafiles/templates/help/mudletui.template b/modules/gmcp/files/datafiles/templates/help/mudletui.template new file mode 100644 index 00000000..a504674c --- /dev/null +++ b/modules/gmcp/files/datafiles/templates/help/mudletui.template @@ -0,0 +1,23 @@ +.: Help for mudletui + +The mudletui command allows Mudlet client users to manage UI packages that enhance the gameplay experience. + +Usage: + + mudletui - Shows status information and available commands + mudletui install - Installs the GoMud UI package in your Mudlet client + mudletui remove - Removes the GoMud UI package from your Mudlet client + mudletui update - Manually check for updates to the GoMud UI package + mudletui hide - Hides automatic Mudlet UI prompts when logging in + mudletui show - Re-enables automatic Mudlet UI prompts when logging in + +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 +- You'll automatically see a message about this feature when you log in with Mudlet +- Using mudletui hide or mudletui install will hide the automatic prompts +- Using mudletui show will re-enable the automatic prompts +- Running mudletui without arguments shows current status and available commands + +See also: help client, checkclient \ No newline at end of file diff --git a/modules/gmcp/gmcp.Mudlet.go b/modules/gmcp/gmcp.Mudlet.go new file mode 100644 index 00000000..e94f67f0 --- /dev/null +++ b/modules/gmcp/gmcp.Mudlet.go @@ -0,0 +1,361 @@ +package gmcp + +import ( + "embed" + "strings" + + "github.com/GoMudEngine/GoMud/internal/configs" + "github.com/GoMudEngine/GoMud/internal/events" + "github.com/GoMudEngine/GoMud/internal/mudlog" + "github.com/GoMudEngine/GoMud/internal/plugins" + "github.com/GoMudEngine/GoMud/internal/rooms" + "github.com/GoMudEngine/GoMud/internal/usercommands" + "github.com/GoMudEngine/GoMud/internal/users" +) + +var ( + //go:embed files/* + files embed.FS +) + +// MudletConfig holds the configuration for Mudlet clients +type MudletConfig struct { + // Mapper configuration + MapperVersion string `json:"mapper_version" yaml:"mapper_version"` + MapperURL string `json:"mapper_url" yaml:"mapper_url"` + + // UI configuration + UIVersion string `json:"ui_version" yaml:"ui_version"` + UIURL string `json:"ui_url" yaml:"ui_url"` + + // Map data configuration + MapVersion string `json:"map_version" yaml:"map_version"` + MapURL string `json:"map_url" yaml:"map_url"` +} + +// GMCPMudletModule handles Mudlet-specific GMCP functionality +type GMCPMudletModule struct { + plug *plugins.Plugin + config MudletConfig +} + +// GMCPMudletDetected is an event fired when a Mudlet client is detected +type GMCPMudletDetected struct { + ConnectionId uint64 + UserId int +} + +func (g GMCPMudletDetected) Type() string { return `GMCPMudletDetected` } + +func init() { + // Set up a default configuration first + 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 + }, + } + + // 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 + } + + // Register event listeners + events.RegisterListener(events.PlayerSpawn{}, g.playerSpawnHandler) + events.RegisterListener(GMCPMudletDetected{}, g.mudletDetectedHandler) + + // Register the Mudlet-specific user commands - set as hidden (true for first bool) + g.plug.AddUserCommand("mudletmap", g.sendMapCommand, true, false) + g.plug.AddUserCommand("mudletui", g.sendUICommand, false, false) + g.plug.AddUserCommand("checkclient", g.checkClientCommand, 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() + 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") + } + } 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" + } + + 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 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") + } + + // Command was handled + return true, nil +} + +// sendMudletUIInstall sends the UI installation GMCP 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"` + }{ + Version: g.config.UIVersion, + URL: g.config.UIURL, + } + + // Send the Client.GUI message + events.AddToQueue(GMCPOut{ + UserId: userId, + Module: "Client.GUI", + Payload: payload, + }) + + mudlog.Debug("GMCP", "type", "Mudlet", "action", "Sent Mudlet UI install config", "userId", userId) +} + +// sendMudletUIRemove sends the UI remove GMCP 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, + }) + + mudlog.Debug("GMCP", "type", "Mudlet", "action", "Sent Mudlet UI remove command", "userId", userId) +} + +// sendMudletUIUpdate sends the UI update GMCP 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, + }) + + 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 + } + + // 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 +} + +// sendMudletMapConfig sends the Mudlet map configuration via GMCP +func (g *GMCPMudletModule) sendMudletMapConfig(userId int) { + if userId < 1 { + return + } + + // Create a payload for the Client.Map message + mapConfig := map[string]string{ + "url": g.config.MapURL, + } + + // Send the Client.Map message + events.AddToQueue(GMCPOut{ + UserId: userId, + Module: "Client.Map", + Payload: mapConfig, + }) + + mudlog.Debug("GMCP", "type", "Mudlet", "action", "Sent Mudlet map config", "userId", userId) +} + +// playerSpawnHandler sends Mudlet-specific GMCP when a player connects +func (g *GMCPMudletModule) playerSpawnHandler(e events.Event) events.ListenerReturn { + evt, typeOk := e.(events.PlayerSpawn) + if !typeOk { + mudlog.Error("Event", "Expected Type", "PlayerSpawn", "Actual Type", e.Type()) + return events.Cancel + } + + // 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) + } + } + + return events.Continue +} + +// mudletDetectedHandler handles the event when a Mudlet client is detected +func (g *GMCPMudletModule) mudletDetectedHandler(e events.Event) events.ListenerReturn { + evt, typeOk := e.(GMCPMudletDetected) + if !typeOk { + mudlog.Error("Event", "Expected Type", "GMCPMudletDetected", "Actual Type", e.Type()) + return events.Cancel + } + + if evt.UserId > 0 { + g.sendMudletConfig(evt.UserId) + } + + return events.Continue +} + +// sendMudletConfig sends the Mudlet configuration via GMCP +func (g *GMCPMudletModule) sendMudletConfig(userId int) { + if userId < 1 { + return + } + + // 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 the Client.GUI message with mapper version and URL + events.AddToQueue(GMCPOut{ + UserId: userId, + Module: "Client.GUI", + Payload: guiPayload, + }) + + mudlog.Debug("GMCP", "type", "Mudlet", "action", "Sent Mudlet package config", "userId", userId) +} + +// checkClientCommand checks if the player is using Mudlet and shows information if they are +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 + 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 + return true, nil + } + + // Show a brief intro message + 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) + + // Command was handled + 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) + return true, nil +} diff --git a/modules/gmcp/gmcp.go b/modules/gmcp/gmcp.go index 8c760004..26046e16 100644 --- a/modules/gmcp/gmcp.go +++ b/modules/gmcp/gmcp.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "os" "strconv" "strings" @@ -283,9 +284,24 @@ func (g *GMCPModule) HandleIAC(connectionId uint64, iacCmd []byte) bool { gmcpData.Client.Version = decoded.Version if strings.EqualFold(decoded.Client, `mudlet`) { - gmcpData.Client.IsMudlet = true + // Trigger the Mudlet detected event + userId := 0 + // Try to find the user ID associated with this connection + for _, user := range users.GetAllActiveUsers() { + if user.ConnectionId() == connectionId { + userId = user.UserId + break + } + } + + if userId > 0 { + events.AddToQueue(GMCPMudletDetected{ + ConnectionId: connectionId, + UserId: userId, + }) + } } g.cache.Add(connectionId, gmcpData) @@ -398,7 +414,7 @@ func (g *GMCPModule) dispatchGMCP(e events.Event) events.ListenerReturn { // DEBUG ONLY // TODO: REMOVE - if gmcp.UserId == 1 { + if gmcp.UserId == 1 && os.Getenv("CONSOLE_GMCP_OUTPUT") == "1" { var prettyJSON bytes.Buffer json.Indent(&prettyJSON, v, "", "\t") fmt.Print(gmcp.Module + ` `) @@ -415,7 +431,7 @@ func (g *GMCPModule) dispatchGMCP(e events.Event) events.ListenerReturn { // DEBUG ONLY // TODO: REMOVE - if gmcp.UserId == 1 { + if gmcp.UserId == 1 && os.Getenv("CONSOLE_GMCP_OUTPUT") == "1" { var prettyJSON bytes.Buffer json.Indent(&prettyJSON, []byte(v), "", "\t") fmt.Print(gmcp.Module + ` `) @@ -437,7 +453,7 @@ func (g *GMCPModule) dispatchGMCP(e events.Event) events.ListenerReturn { // DEBUG ONLY // TODO: REMOVE - if gmcp.UserId == 1 { + if gmcp.UserId == 1 && os.Getenv("CONSOLE_GMCP_OUTPUT") == "1" { var prettyJSON bytes.Buffer json.Indent(&prettyJSON, payload, "", "\t") fmt.Print(gmcp.Module + ` `)