Documentation / Under the hood
Architecture
Emunah is a tree of plain Lua modules loaded straight from a git checkout. A handful of ideas carry most of the design, and each exists because of a specific failure it closed off.
Module layout
src/emunah.lua the loader: the manifest, loading and reloading
src/emunah/
core/ util log event config timers queue act outgoing
gmcp/ init vitals status afflictions defences items skills room comm ire
have/ capabilities -- "can I actually do this right now"
curing/ curelist afflist deflist situations engine defkeepup
detect/ init patterns opponent opponent_patterns diag
ui/ theme gag echo alert layout chyron vitals affpanel chat roompanel map roomarea names
mapper/ data route info lines locate moves guard -- the map, walks, locator lines
namedb/ api capture -- with namedb.lua: who everyone is
class/ adapter priest priest_pvp -- the class interface, and Priest
walker denizens ih watch bashing pvp allies -- hunting and fighting
loot shop elist riftlist shield antitheft rites pipes riding keys presences
help conf commands -- every command and setting as data, files, the dispatcher
conf/ one file of settings per area, and your own cure ranks and rules
test/ mock_mudlet run bench profile latency ui_preview
tools/ build-xml build-trigger-package build-mapper-lines build-commands-page
fight-report ci
The load order is written out in the manifest rather than inferred, because later modules read earlier ones as they load: the interface reads the theme, curing reads the affliction table, PvP reads the name database.
One gate for every command
Achaea refuses commands for reasons that have nothing to do with the command: you are stunned, lying down, entangled, or the balance it needs has not come back. Those rules once lived at each call site, and whichever copy missed a new rule went on firing into refusals once per prompt.
core/act.lua is now the only place that knows them. Every automated command declares what it needs, and the gate decides when that can be met:
emunah.act.send("perform penitence " .. target, { standing = true, eq = true })
emunah.act.send("order " .. id .. " follow me", { bal = true, eq = true })
Stun, sleep, unconsciousness and death hold everything, whatever a command declares. Prone holds only what needs you upright, so curing carries on while you are knocked down, which is exactly when it matters most. A walk's every step goes through the same gate.
One slot per balance
core/queue.lua is not one queue but one slot per balance. A more urgent action replaces what is waiting in its slot; a less urgent one is dropped rather than stacked, because a backlog of stale cures is worse than none. A queued action is checked again at the moment it would send, since the reason for it, or a block on it, can change while it waits. A command you type goes first: Emunah's own sends on the same balance wait a moment behind it.
One line per prompt
What a prompt decides leaves together. Every command Emunah decides between a prompt's balance changes and the end of its tick (the cures, keep-up, the attack, the next step of a walk), and everything sent when a timer lapses, is collected and sent as one line joined by the game's own command separator: CONFIG SEPARATOR | in the game. The game splits the line and runs each command in order, so one line goes over the wire where there were several. This is how the reference system sends too, ten commands to a line.
The separator is learned from the game, never assumed. On the first prompt of every session Emunah asks config commandseparator and hides the answer; a CONFIG SEPARATOR you type stops the joining until the game answers it. Until the game has named one, every command goes on a line of its own, because joining with a separator the game does not have would turn two commands into one it cannot run. Mudlet's own ;; is not used: Mudlet splits at it before sending, so it never saved a line.
Batching changes no rule about balance. Each command passed the gate, and marked what it spends, when it was added, so a later command of the same prompt that needs that balance is held exactly as it would have been alone. A batch never carries two commands on one balance. Emunah also writes two commands that must run in order as one: a herb only in the rift goes as outr then eat, and every restock trip due goes at once. They leave as one line with the game's separator, or as consecutive lines without one. queue.chain off sends every command alone.
Sent is not executed
The game runs a command some time after it arrives, and until then it goes on reporting the balance that command is about to spend as available. Two systems reading that honestly would both send, and the second would be refused.
So a balance is treated as spent from the moment something of Emunah's goes out on it until the game answers: the queue holds the slot, and any other command that needs that balance waits behind it, whatever the next GMCP update says. When the game states the cost, such as "Balance used: 3.2s.", the exact figure replaces the estimate.
Generated, never hand-edited
Three files are built from a source and must be regenerated, not edited:
| File | Built from |
|---|---|
EmunahTriggers.xml | The reference system's trigger set, by tools/build-trigger-package.py. Only lines whose meaning is certain from the source are taken, translated to the server's names, and every one only reports to Emunah's detection, which applies the anti-illusion rules. |
src/emunah/mapper/lines.lua | The IRE Mudlet Mapper's locator lines, by tools/build-mapper-lines.py. |
website/commands.html | src/emunah/help.lua, by tools/build-commands-page.lua. |
The reload model
emreload pulls the latest main, then rebuilds the whole namespace from disk: every module is dropped and required again in manifest order. Only a short, explicit list survives: event handlers, timers, and a small scratch space modules use to carry state across a reload, such as which pipes are lit or whether bliss is up.
It re-executes the loader file itself rather than calling the loader already in memory. That function was built around the manifest as it was when first read, so a module just added to the manifest would otherwise be silently skipped while everything else looked up to date.
Failure modes closed off
Each of these was a real bug, and each is pinned by a test.
| Failure | What prevents it |
|---|---|
| Event handlers stacking on every reload, so one affliction queued three cures after three reloads | core/event.lua tracks handlers on a registry that outlives the reload and removes the previous generation first. |
| A failed module load installing its error message as the module | A failed load aborts with the file's name and rolls the whole reload back. |
| Inventory updates rebuilt from the old record, so they never landed | Updates are built from the incoming record. |
| Removing an absent room from a list deleting the last one instead | The walker checks before it removes. |
| Balance and equilibrium arriving as the strings "1" and "0", where "0" is true in Lua | Every GMCP boolean goes through one conversion. |
Evidence before code
Every game mechanic Emunah relies on is recorded in docs/game/ with what established it: a HELP file, a GMCP trace, a timestamped transcript, or the account of a player who cast it. A plausible guess about the game is never shipped as fact; where the exact text or mechanic is unknown, the feature waits.
On curing and balances the reference system's logic is trusted over Emunah's own. But it is several years old and the game has moved on, so where a transcript from today's game disagrees, the transcript wins, and the disagreement is written down where the code departs from the reference. A line the reference system lacks is recorded and asked about, never guessed at.
Testing
The suite of 3847 behavioural tests runs the real modules against test/mock_mudlet.lua, with no client involved. The mock is strict on purpose: each mocked widget has only the methods the real one has, and trigger patterns are matched as the regular expressions Mudlet actually uses. Two shipped bugs once got past a looser mock that accepted anything; a strict one turns "silently wrong in the client" into a failing test. GitHub Actions runs sh tools/ci.sh, which compiles every first-party file as Lua 5.1 and then runs that suite. The site is published only after that passes on main.
Every behavioural fix carries a test that would have caught the original report, and the suite must be green before anything is committed. The suite also checks the documentation against the code: every command is documented, nothing documented is missing, the figures on this site match their sources, and no page on it has an em dash.