Guides

Mudlet with Mudlet

Combat-intensive MUDs require dozens of triggers that parse health bars, afflictions, and enemy actions. Naive implementations create trigger bloat that degrades client performance and causes collision errors where multiple patterns match simultaneously. This guide implements a production-grade architecture using Lua table-based state management, programmatic trigger generation, and event-driven decoupling to maintain sub-16ms response times during high-frequency combat.

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

Architect Namespace and State Tables

Establish a dedicated Lua namespace to prevent function collisions with other packages. Create a state table to track combat entities rather than storing data in trigger names or global variables, which reduces memory overhead and enables serialization for debugging.

combat_core.lua
-- Initialize namespace
myCombat = myCombat or {}

-- State table structure
myCombat.state = {
  active = false,
  target = nil,
  vitals = { hp = 0, maxHp = 0, mp = 0 },
  afflictions = {},
  tempTriggers = {}
}

-- Accessor function to enforce encapsulation
function myCombat.getTarget()
  return myCombat.state.target
end

⚠ Common Pitfalls

  • Avoid polluting the global namespace; never declare variables without the myCombat. prefix
  • Do not store large string buffers in the state table during combat; this causes garbage collection spikes
2

Generate Triggers Programmatically

Use permRegexTrigger via script rather than the GUI to create version-controlled, reproducible trigger groups. This enables dynamic group creation based on MUD-specific patterns and allows deletion of entire groups when unloading packages.

trigger_setup.lua
-- Create a trigger group for organization
myCombat.groupId = permGroup("CombatEngine", " triggers")

-- Programmatically create a health bar parser
myCombat.hpTrigger = permRegexTrigger(
  "HP_Parser",
  "^\\[(
)\\/(
)hp\\]$",
  [[myCombat.handleHp(matches[2], matches[3])]],
  {group="CombatEngine"}
)

-- Enable by default
enableTrigger("CombatEngine")

⚠ Common Pitfalls

  • Regex patterns without anchors (^ and $) cause excessive CPU usage by matching partial lines repeatedly
  • Trigger names containing spaces or special characters break when referenced in killTrigger() calls
3

Implement Event-Driven Decoupling

Decouple trigger detection from action execution using Mudlet's raiseEvent system. Triggers should parse lines and emit events; separate handler functions subscribe to these events. This prevents trigger chains from blocking and allows multiple systems to react to the same combat event.

event_handlers.lua
-- Trigger action only raises event
function myCombat.handleHp(current, max)
  local curr, mx = tonumber(current), tonumber(max)
  raiseEvent("combat.vitalsUpdate", curr, mx, "hp")
end

-- Event handler registered separately
function myCombat.onVitalsUpdate(event, current, max, type)
  myCombat.state.vitals[type] = current
  myCombat.state.vitals["max" .. type:gsub("^%l", string.upper)] = max
  
  -- Update UI only when threshold crossed
  if current / max < 0.25 then
    cecho("<red>WARNING: Low " .. type .. "!\\n")
  end
end

-- Register handler
registerAnonymousEventHandler("combat.vitalsUpdate", "myCombat.onVitalsUpdate")

⚠ Common Pitfalls

  • Event name collisions with Mudlet built-in events (sysDownloadDone, sysConnectionEvent) cause unpredictable execution order
  • Capture groups from regex are not automatically passed to event handlers; explicitly pass matches[n] as arguments
4

Manage Temporary Trigger Lifecycles

Create temporary triggers for transient combat states (specific buffs, afflictions, or timed effects) using tempRegexTrigger. Store their IDs in a dedicated table and implement a cleanup function that iterates through and kills them when combat ends to prevent memory leaks.

temp_management.lua
-- Store temp trigger IDs
myCombat.state.tempTriggers = {}

function myCombat.addTempAfflictionTrigger(afflictionName, pattern)
  local id = tempRegexTrigger(pattern, [[myCombat.handleAffliction("]] .. afflictionName .. [[")]])
  if id then
    table.insert(myCombat.state.tempTriggers, id)
    cecho("<green>Added temp trigger for " .. afflictionName .. " (ID: " .. id .. ")\\n")
  end
end

-- Cleanup function called on combat end
function myCombat.cleanup()
  for _, id in ipairs(myCombat.state.tempTriggers) do
    killTrigger(id)
  end
  myCombat.state.tempTriggers = {}
  myCombat.state.active = false
end

⚠ Common Pitfalls

  • Failing to store temp trigger IDs makes them impossible to kill; they persist until Mudlet restart
  • Temp triggers are not saved to profiles; ensure your init script recreates them on login if they represent permanent character abilities
5

Build Collision Detection Logging

Implement a debug wrapper that logs every trigger firing to a dedicated miniconsole. This identifies when multiple triggers match the same line (collisions) or when triggers fire in unexpected order, which is critical for debugging combat logic that relies on sequential parsing.

debug_system.lua
-- Create debug console
myCombat.debugConsole = myCombat.debugConsole or Geyser.MiniConsole:new({
  name="combatDebug",
  x="70%", y="0%",
  width="30%", height="40%",
  color="black"
})

-- Wrapper for trigger execution
function myCombat.debugTrigger(triggerName, matchesTable, callback)
  local timestamp = getTime(true, "yyyy-MM-dd hh:mm:ss.zzz")
  local matchDisplay = table.concat(matchesTable, " | ", 1, math.min(3, #matchesTable))
  
  cecho(myCombat.debugConsole, 
    "<gray>[" .. timestamp .. "] <yellow>" .. triggerName .. ": <white>" .. matchDisplay .. "\\n")
  
  -- Execute actual logic
  local status, err = pcall(callback)
  if not status then
    cecho(myCombat.debugConsole, "<red>ERROR: " .. err .. "\\n")
  end
end

⚠ Common Pitfalls

  • Using display(matches) in production exposes game data to logs that might be shared accidentally
  • Debug output to the main window during high-frequency combat (10+ lines/second) causes interface lag; always use a miniconsole
6

Optimize Pattern Execution Order

Reorder triggers to place high-frequency, simple patterns (prompt detection, health updates) at the top of the trigger group and complex multi-line or backtracking patterns at the bottom. Mudlet evaluates triggers sequentially; failing fast on common lines reduces CPU usage.

optimization.lua
-- Reorder triggers programmatically by killing and recreating in sequence
-- Note: Mudlet evaluates triggers in creation order within a group

function myCombat.optimizeOrder()
  -- Disable group during reorganization
  disableTrigger("CombatEngine")
  
  -- Store definitions
  local triggers = {
    {name="Prompt_Fast", pattern="^<\\d+/%d+hp", code="myCombat.parsePrompt()"},
    {name="Target_Switch", pattern="^You target (.+)\\.", code="myCombat.setTarget(matches[2])"},
    {name="Complex_Affliction", pattern="^.*(\\w+) affects you with (\\w+)\\.", code="myCombat.parseAffliction(matches[2], matches[3])"}
  }
  
  -- Clear existing
  killGroup("CombatEngine")
  permGroup("CombatEngine", "triggers")
  
  -- Recreate in optimized order
  for _, t in ipairs(triggers) do
    permRegexTrigger(t.name, t.pattern, t.code, {group="CombatEngine"})
  end
  
  enableTrigger("CombatEngine")
end

⚠ Common Pitfalls

  • Patterns with greedy quantifiers (.*) on long lines cause catastrophic backtracking; use specific character classes ([^>]+) instead
  • Case-sensitive regex fails on MUDs with inconsistent capitalization; use [Tt]arget or (?i) inline flags if supported
7

Package for Distribution

Wrap the system as a Mudlet package (.xml) with unique namespacing and initialization guards. Include an uninstall function that cleanly removes all triggers, timers, and event handlers to prevent orphaned objects when users upgrade versions.

package_manager.lua
-- Initialization guard
if myCombat and myCombat.initialized then
  return
end

myCombat = myCombat or {}
myCombat.initialized = true

function myCombat.uninstall()
  -- Cleanup triggers
  if myCombat.groupId then
    killGroup("CombatEngine")
  end
  
  -- Kill temp triggers
  for _, id in ipairs(myCombat.state.tempTriggers or {}) do
    killTrigger(id)
  end
  
  -- Unregister events
  -- Note: Mudlet does not provide unregisterAnonymousEventHandler;
  -- set handler to empty function instead
  myCombat.onVitalsUpdate = function() end
  
  cecho("<green>Combat module uninstalled successfully\\n")
end

-- Auto-initialize on load
myCombat.init()

⚠ Common Pitfalls

  • Packages without uninstall functions leave ghost triggers that duplicate when reinstalled, causing double execution
  • Hardcoding profile-specific paths (getMudletHomeDir() .. "/myFile") breaks when sharing packages across Windows and Unix systems

What you built

This architecture separates data from presentation, uses events for loose coupling, and manages trigger lifecycles explicitly. Test under load by enabling the debug console and verifying that temp trigger IDs are cleaned up after combat. For further optimization, profile trigger execution using `lua showTriggers()` output and migrate high-frequency patterns to substring triggers (begin/end patterns) which execute faster than regex in Mudlet's engine.