Documentation menu

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:

FileBuilt from
EmunahTriggers.xmlThe 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.luaThe IRE Mudlet Mapper's locator lines, by tools/build-mapper-lines.py.
website/commands.htmlsrc/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.

FailureWhat prevents it
Event handlers stacking on every reload, so one affliction queued three cures after three reloadscore/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 moduleA failed load aborts with the file's name and rolls the whole reload back.
Inventory updates rebuilt from the old record, so they never landedUpdates are built from the incoming record.
Removing an absent room from a list deleting the last one insteadThe walker checks before it removes.
Balance and equilibrium arriving as the strings "1" and "0", where "0" is true in LuaEvery 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.