Guides

MUSHclient with MUSHclient

MUSHclient plugins require a specific XML manifest structure paired with Lua scripts that hook into the client's trigger and alias systems. This guide walks through creating a production-ready plugin that registers dynamic triggers, handles game output, persists configuration across sessions, and packages correctly for distribution.

60 minutes7 steps
MUSHclient with MUSHclient illustration
Placeholder illustration shown while custom artwork is being produced.
1

Structure the XML Manifest

Create a valid XML file with the muclient DOCTYPE declaration. The plugin element must include a unique ID (GUID), name, author, and language attributes. Reference the Lua script file using the script tag. Define static triggers and aliases within trigger and alias tags if not creating them programmatically. Save the file with .xml extension.

AutoMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE muclient>
<muclient>
<plugin
  name="AutoMapper"
  author="Dev"
  id="12345678-1234-1234-1234-123456789012"
  language="Lua"
  purpose="Room tracking"
  date_written="2024-01-15"
  requires="4.50"
  version="1.0"
>
<script>AutoMapper.lua</script>
</plugin>
</muclient>

⚠ Common Pitfalls

  • Omitting the XML declaration causes parsing failures
  • Duplicate plugin IDs conflict with existing installations
  • Incorrect encoding declarations corrupt special characters
2

Initialize Lua State in OnPluginInstall

Implement OnPluginInstall to run code when the plugin loads. Retrieve saved state using world.GetPluginVariable and initialize global configuration tables. Check world.GetInfo(1) to verify connection status before attempting to send commands. Set up a plugin-specific table to avoid namespace pollution.

AutoMapper.lua
function OnPluginInstall()
  local saved = world.GetPluginVariable("room_count")
  PluginVars = {
    count = tonumber(saved) or 0,
    active = true
  }
  world.Note("AutoMapper initialized. Rooms tracked: " .. PluginVars.count)
end

⚠ Common Pitfalls

  • Assuming variables persist between sessions without GetPluginVariable
  • Calling world.Send before checking connection status
  • Initializing global variables without local scope protection
3

Register Dynamic Triggers Using AddTriggerEx

Use world.AddTriggerEx to create triggers programmatically within OnPluginInstall. Set the regexp parameter to 1 for PCRE patterns. Use send_to value 12 to execute script callbacks. Store trigger names in a table for later cleanup. The callback function receives trigger_name, line, and wildcards table.

AutoMapper.lua
function OnPluginInstall()
  TriggerList = {}
  
  local id = world.AddTriggerEx(
    "room_detect",           -- name
    "^\[Exits: (.+)\]",      -- pattern (PCRE)
    "handle_room_exits",     -- script function
    0,                       -- omit_from_log
    0,                       -- omit_from_output
    1,                       -- keep_evaluating
    1,                       -- regexp (1=enabled)
    0,                       -- ignore_case
    0,                       -- expand_variables
    0,                       -- custom_colour
    12,                      -- send_to (12=script)
    100                      -- sequence
  )
  table.insert(TriggerList, id)
end

⚠ Common Pitfalls

  • Forgetting to set regexp=1 causes literal matching failures
  • Incorrect send_to values (must be 12 for script execution)
  • Duplicate trigger names raise errors without checking world.IsTrigger
4

Implement Callback Handlers for Game Output

Define Lua functions matching the names passed to AddTriggerEx. Process capture groups via the wildcards table (index 1 for first capture). Use world.Note for local debugging output that never reaches the MUD server. Call world.StripANSI if processing color codes. Update plugin state and save using SetPluginVariable.

AutoMapper.lua
function handle_room_exits(name, line, wildcards)
  local exits = wildcards[1]
  PluginVars.count = PluginVars.count + 1
  
  world.Note("Detected exits: " .. exits)
  world.SetPluginVariable("room_count", tostring(PluginVars.count))
  
  -- Trigger another action based on exits
  if string.find(exits, "north") then
    -- Logic here
  end
end

⚠ Common Pitfalls

  • Assuming wildcards indices match visual groups (check with Note)
  • Modifying trigger lists from within callbacks without using temporary triggers
  • Not handling nil values when capture groups fail
5

Create User Commands with AddAlias

Register aliases using world.AddAlias to expose plugin commands to users. Set regexp=1 and define patterns like ^/map (.+)$. Leave the text parameter empty and set the script parameter to your handler function name. Aliases intercept user input before it reaches the MUD.

AutoMapper.lua
function OnPluginInstall()
  -- Command pattern: /map status
  world.AddAlias(
    "map_cmd",               -- name
    "^/map (.+)$",          -- match pattern
    "",                     -- text to send (empty for script)
    "handle_map_command",   -- script function
    0, 0, 1, 0, 0, 0, 0, 0   -- flags: regexp enabled
  )
end

function handle_map_command(name, line, wildcards)
  local cmd = wildcards[1]
  if cmd == "status" then
    world.Note("Rooms mapped: " .. PluginVars.count)
  elseif cmd == "reset" then
    PluginVars.count = 0
    world.SetPluginVariable("room_count", "0")
  end
end

⚠ Common Pitfalls

  • Creating aliases without ^$ anchors that match partial input
  • Forgetting to return 1 to stop command processing (if needed)
  • Conflicting with existing MUD commands without prefixing
6

Persist Configuration Across Sessions

Store complex data as JSON strings or delimited text using world.SetPluginVariable. Retrieve in OnPluginInstall with GetPluginVariable. Implement OnPluginSaveState for explicit save points. Handle nil returns gracefully when variables do not exist yet (first run).

AutoMapper.lua
function save_config()
  local data = string.format("%d|%s", 
    PluginVars.count, 
    PluginVars.active and "1" or "0"
  )
  world.SetPluginVariable("config", data)
end

function load_config()
  local raw = world.GetPluginVariable("config")
  if raw and raw ~= "" then
    local count, active = string.match(raw, "(%d+)|(%d)")
    PluginVars.count = tonumber(count) or 0
    PluginVars.active = (active == "1")
  end
end

⚠ Common Pitfalls

  • Exceeding storage limits with large datasets
  • Not handling malformed data on load
  • Calling SetPluginVariable too frequently in high-frequency triggers
7

Package and Debug the Plugin

Combine the XML and Lua files into a .mcp (MUSHclient Plugin) file by creating a ZIP archive and renaming the extension. Install via Plugins > Add in MUSHclient. Enable 'Show Plugin Errors' in Edit > Preferences > Scripts. Use world.Note extensively for trace logging. Check the output window for Lua syntax errors during load.

⚠ Common Pitfalls

  • UTF-8 BOM markers causing XML parsing errors
  • Missing file references in XML that load before the script exists
  • Not implementing OnPluginDisconnect to clean up triggers when unloading

What you built

Test the packaged .mcp file by installing it through the Plugins menu while connected to a MUD. Verify trigger firing using world.Note statements in callbacks. Ensure OnPluginInstall completes without errors by checking the output window. For distribution, include a README detailing the plugin ID to prevent conflicts with other plugins.