-
Notifications
You must be signed in to change notification settings - Fork 0
Mudlet Samples
Audience: Player Status: ✅ Ready
Copy-paste Mudlet recipes: a one-click auto-login trigger, plus recipes that turn TelosMUD's GMCP surface into a rich UI — HP/mana gauges, a live automap, and keyword tab-completion. The GMCP recipes register a handler for a specific GMCP event and read the payload documented in the GMCP Reference.
Mudlet enables GMCP automatically and, on each incoming message, parses the JSON into the global gmcp table and raises an event named after the package — e.g. a Char.Vitals message updates gmcp.Char.Vitals and raises the event gmcp.Char.Vitals. You wire a recipe up by registering a handler for that event name. The simplest way is one line in a Mudlet Script:
registerAnonymousEventHandler("gmcp.Char.Vitals", "updateVitals")Content-defined keys: TelosMUD's
Char.Vitalspayload uses the content pack's resource refs. The built-in demo world useshp/mana, so the fields aregmcp.Char.Vitals.hp/.maxhp/.mana/.maxmana. On a different pack, use that pack's refs.
When you connect, the gate prints your one-click OAuth link and waits for you to sign in (Player Reference → Connecting). This is a plain text trigger — not GMCP — that spots that prompt and pops the link open in your browser for you.
The gate prints three lines: the prompt, a blank line, then the indented URL. Build a multiline (AND) trigger with these three regex conditions and a line margin of 3, so all three match across consecutive lines:
^To sign in, open this link in your browser:$
^$
^\s+(https?://\S+/login/\S+)$
Then set the trigger's script to open the captured URL — matches[2] is the capture group from the third line:
openUrl(matches[2])openUrl launches your default browser; you approve on GitHub, and the OAuth page closes itself after a few seconds and hands you back to the MUD. (If you'd rather pin it to your own server, replace https?://\S+ with your host, e.g. https://play\.your-mud\.example.)
Matching note: this keys off the gate's login screen text, which is engine code, not content — so a world changing its content (rooms, mobs, even its login message of the day) leaves this trigger working. Only a fork that edits the auth code's prompt wording would require updating the first regex line to match.
A pair of Geyser gauges driven by Char.Vitals. Put the setup in a Script (runs once) and register the handler:
-- run once (Script body)
hpBar = hpBar or Geyser.Gauge:new({
name = "hpBar", x = "70%", y = "2%", width = "28%", height = "22px"
})
hpBar:setColor(140, 20, 20)
manaBar = manaBar or Geyser.Gauge:new({
name = "manaBar", x = "70%", y = "26px", width = "28%", height = "22px"
})
manaBar:setColor(20, 60, 160)
function updateVitals()
local v = gmcp.Char.Vitals
if v.hp and v.maxhp then
hpBar:setValue(v.hp, v.maxhp, "HP " .. v.hp .. "/" .. v.maxhp)
end
if v.mana and v.maxmana then
manaBar:setValue(v.mana, v.maxmana, "MP " .. v.mana .. "/" .. v.maxmana)
end
end
registerAnonymousEventHandler("gmcp.Char.Vitals", "updateVitals")Char.Vitals fires whenever a pool changes, so the bars track combat and regen live. For a stats panel, do the same against gmcp.Char.Stats; for a "standing / fighting / dead" indicator (and your current target), read gmcp.Char.Status.state and .target.
Room.Info carries {num, name, zone, coord?, exits{dir→num}}, where num is a stable hash of the room — consistent across shards and restarts, which is exactly what a mapper needs. A minimal mapper that creates rooms on the fly and draws exits:
local DIRS = { north=1, south=2, east=3, west=4, up=5, down=6,
northeast=7, northwest=8, southeast=9, southwest=10 }
function onRoomInfo()
local r = gmcp.Room.Info
local id = r.num
if not roomExists(id) then
addRoom(id)
setRoomName(id, r.name)
if r.coord then
setRoomCoordinates(id, r.coord[1], r.coord[2], r.coord[3])
end
-- put the room on an area named after its zone
local area = getAreaTable()[r.zone] or addAreaName(r.zone)
setRoomArea(id, r.zone)
end
for dir, dest in pairs(r.exits or {}) do
if DIRS[dir] then setExit(id, dest, dir) end
end
centerview(id)
end
registerAnonymousEventHandler("gmcp.Room.Info", "onRoomInfo")Layer gmcp.Room.Players on top to show who (players and mobs) is in the current room — it's the visible, canSee-filtered occupant list.
TelosMUD emits your inventory over Char.Items.List (a full snapshot per location) and then Char.Items.Add / .Update / .Remove deltas. Feed those item names into Mudlet's command-line suggestion list so pressing Tab completes keywords for get, wear, drop, etc.:
-- rebuild suggestions from the current inventory snapshot
function refreshItemSuggestions()
clearCmdLineSuggestions()
local items = gmcp.Char.Items.List and gmcp.Char.Items.List.items or {}
for _, item in ipairs(items) do
if item.name then
addCmdLineSuggestion(item.name) -- Tab now completes this keyword
end
end
end
registerAnonymousEventHandler("gmcp.Char.Items.List", "refreshItemSuggestions")
registerAnonymousEventHandler("gmcp.Char.Items.Add", "refreshItemSuggestions")
registerAnonymousEventHandler("gmcp.Char.Items.Remove", "refreshItemSuggestions")Char.Items.List arrives on login, reconnect, and handoff arrival; the Add/Remove deltas keep the set current as you loot and drop. (For a fuller build you'd track the item set incrementally rather than re-reading a snapshot on every delta, but re-reading is the simplest correct recipe.) You can extend the same pattern to room occupants (Room.Players) so Tab also completes the name of the mob you're about to kill.
A classic layout: no channel chatter in the main output, and every channel collected into one side console. Two facts about how the engine delivers a channel make this work:
- Every channel line you're subscribed to arrives twice — once as a normal text line in the main window, and (for GMCP clients) once as a
Comm.Channel.Textframe{channel, talker, text}. The text line comes first, then the GMCP mirror. -
channels off <chan>on the MUD unsubscribes you entirely — no text and no GMCP. That's genuinely leaving the channel; you stop receiving it everywhere.
So the recipe is: gag the main-window copy client-side (you stay subscribed, still receiving) and render the GMCP copy into a separate miniconsole. Create the console once in a Script:
-- run once (Script body): a docked chat console on the right
chatWin = chatWin or Geyser.MiniConsole:new({
name = "chatWin", x = "70%", y = "2%", width = "29%", height = "40%"
})
chatWin:setColor(12, 12, 16)
chatWin:enableScrollBar()
-- one color per channel (fall back to grey)
local CHAN_COLOR = { gossip = "yellow", newbie = "cyan", guild = "green" }
function onChannelText()
local m = gmcp.Comm.Channel.Text
if not m then return end
local color = CHAN_COLOR[m.channel] or "grey"
cecho("chatWin", "<"..color..">["..m.channel.."] <white>"..(m.talker or "")..": "..(m.text or "").."\n")
end
registerAnonymousEventHandler("gmcp.Comm.Channel.Text", "onChannelText")Then gag the main-window copy. The channel line's on-screen format is content-defined (the demo pack renders [gossip] Name: text), so match your pack's format. A single regex trigger on the channel prefix, with deleteLine() as its script, keeps the main window clean:
^\[(gossip|newbie|guild)\]
deleteLine() -- trigger script: drop this line from the main window onlyBecause the text line is processed before the GMCP frame arrives, this standing gag fires in time — the line never shows in the main window, but onChannelText still populates chatWin.
Hide vs. leave. Hiding the console (chatWin:hide() / :show(), e.g. from an alias) is purely client-side — you're still subscribed and messages keep accumulating. To actually drop a channel — stop receiving it as text and GMCP — use the MUD command channels off <chan>; channels on <chan> re-joins. Build one console per channel instead of a shared one by keying the miniconsoles off m.channel if you prefer separate tabs.
You can also show a roster of who's currently listening on a channel: advertise Comm.Channel.Players in Core.Supports, then register gmcp.Comm.Channel.Players and read its players name-list into a side panel keyed by its channel. The roster is aggregated across shards and refreshes every couple of seconds.
Most of GMCP is server-push, but a rich client can also ask the world to open a container. Send a Char.Items.Contents request with the container's item id (the id from a Char.Items.List entry, e.g. i42); the world replies with a Char.Items.List frame whose location is that container id. Advertise Char.Items.Contents in Core.Supports first.
-- ask the world for a container's contents (call from an alias or a button)
function openContainer(itemId)
sendGMCP("Char.Items.Contents " .. yajl.to_string({ container = itemId }))
end
-- render the reply: a Char.Items.List keyed to the container id
function onItemsList()
local L = gmcp.Char.Items.List
if not L then return end
if L.location ~= "inv" and L.location ~= "room" then
-- container panel: L.location is the container's id; draw L.items into it
end
end
registerAnonymousEventHandler("gmcp.Char.Items.List", "onItemsList")The request is rate-limited (about five per second) and reach-scoped — you can only open a container you're carrying or can see on the floor — so a button that spams requests is throttled server-side, and a stale or guessed id simply returns nothing.
See the GMCP Reference for the full payload of every package, and Player Reference for the non-GMCP basics.
TelosMUD — Wiki under construction.
- Builder Reference
- Builder Commands
- Trust Tier Model
- Pack Authoring
- Pack MUD Settings
- Pack Lua Scripting
- Pack Lua Hooks
- Pack Entity Reference
- Building Instanced Zones
- Engine Developer Reference
- Architecture Overview
- Entity Component Model
- Zone Runtime & Actor Model
- Instanced Zones
- Command Parser & Targeting
- Edge & Protocol
- GMCP Reference
- Persistence & Durability
- Content Loading & Hot Reload
- Abilities & Effects
- Combat System
- Loot, Spawns & Crafting
- Accounts & Auth Internals
- Orchestration & Directors
- Scoped Event Bus
- Cross-Shard Handoff
- Lua Sandbox Internals
- Distributed Systems Model
- RPC & Protobuf