- Lua 99.5%
- Shell 0.5%
| dev | ||
| docs | ||
| examples | ||
| scripts | ||
| tests | ||
| tools | ||
| types | ||
| .gitignore | ||
| .luarc.json | ||
| AGENTS.md | ||
| init.lua | ||
| README.md | ||
| TODO.md | ||
nfrune
Shared Lua scripts for Rune. Each Rune instance keeps its
own config directory and loads this checkout from that directory's init.lua.
Disclosure/Disclaimer Whatever
About 90% of stuff here is human design, with an AI writing it out. I daily drive this, and there seems to be no issues (other than bug caused by stupid design/intentional bs). I try to not hammer/stress the server as much as possible (well I have 200 ping, I don't wanna make it worse haha.).
Code isn't too complicated too, except....
The mapper + its renderer. That is 100% vibed. Maybe one day I'll read that code and improve it...
nfblight refers to my blightmud config.
Connect a Rune instance
Copy examples/config-init.lua to the instance's config directory as init.lua.
Edit the address and login placeholders only in that private copy. The example
loads the public checkout, shows where to load an optional private companion,
connects on startup, and retains connection state across /reload.
Start Rune with that config directory, or type /reload in an existing session.
The example delegates connection attempts to scripts.session_control. The
device bridge publishes a local timed-hold state, allowing /hold 30m from the
Telegram gateway to suppress initial connection and reconnection while another
MUD client is being tested.
Or use your own loading script.
Add a shared script
Add a Lua module under scripts/, then add a require() line to init.lua.
Module names use dots and omit .lua; for example, scripts/chat.lua is
loaded with require("scripts.chat"). Normal Lua loading stuff.
Before adding a stateful or multi-module feature, read the Lua architecture guide. It describes the preferred module boundaries, lifecycle, listener contracts, comments, and test layout.
Instance-specific settings and secrets should stay in that instance's config directory. The shared modules can read durable per-instance values through Rune's storage APIs when needed.
Cross-instance IPC
IPC uses a small authenticated broker bound to loopback. Its Go tools live in
the sibling ~/projects/nfrune/ipc repository. Build them once from there:
cd ~/projects/nfrune/ipc
mkdir -p .local/bin
go build -o .local/bin/nfrune-ipc ./cmd/nfrune-ipc
go build -o .local/bin/nfrune-deck ./cmd/nfrune-deck
The first loaded Rune instance starts the broker automatically. Each instance then publishes its character state and receives short-lived targeted commands.
/ipc status
/ipc peers
/ipc slot 2
/ipc restart
bm Healer look
bm 2 look
bm all score
bm other follow Leader
b2 recall
ba score
When exactly one other instance is active, the target may be omitted:
bm north
Each config directory can persistently claim slot 1 through 4 with
/ipc slot <number>. The b1–b4 aliases send to those stable slots, while
ba sends to every active instance. Missing and duplicate slots are rejected
instead of being resolved by peer ordering.
The broker descriptor and authentication token live under
$XDG_RUNTIME_DIR/nfrune-ipc/ with user-only permissions. Commands are kept
only in bounded memory, delivered in per-instance order, and expire after five
seconds rather than being replayed after a stale connection.
The notification path publishes incoming tells and selected game lifecycle
events to the Telegram gateway. Lifecycle alerts cover character death,
reaching level 50, TNL transitioning to zero at level 50 or higher, and an armed
automove run pausing, becoming blocked, or stopping because its MUD connection
was lost. If a party member falls behind, automove sends an immediate recovery
alert and retraces up to five confirmed movement steps, resuming the sweep as
soon as the party regroups. Exhausting or losing that return path pauses the
run and sends the corresponding stop alert. Explicit /automove pause commands
remain silent. Initial GMCP state
after startup, reload, or a character change establishes a baseline and does not
emit progression alerts.
Remote /send commands are single-line MUD commands. Rune slash commands are
refused by default, except a small allowlist (/reload, /automove status,
/automove resume) that an operator may invoke remotely. The authoritative
allowlist lives in scripts/remote_commands.lua, which the IPC controller
consults before executing any remotely received slash command; keep it in sync
with ALLOWED_REMOTE_RUNE_COMMANDS in the nfrune-gateway source.
The scripts resolve nfrune-ipc and nfrune-deck from an explicit
NFRUNE_IPC_BROKER or NFRUNE_DECK_BINARY override first, then from PATH,
and finally from the development checkout's .local/bin directory. The NFRune
Home Manager gateway module installs the packaged tools and supplies the
explicit immutable paths automatically.
Zellij command deck
Define nfrune.deck_actions in each Rune config-directory init.lua before
loading the shared init.lua; see examples/config-init.lua. Each instance
advertises its own actions through IPC, and the deck shows the actions belonging
to the selected character.
When Rune is running inside Zellij, press capital X on an empty input to open
the floating deck. Select the executing IPC slot, then an action key, then an
ally slot when the action uses targeting = "ally". Use %s in that action's
command where the selected character name belongs.
Actions use type = "mud" for game commands or type = "rune" for internal
Rune slash-command handlers (write the command without the leading slash).
Rune actions travel through the internal event channel and are never sent to
the game. The deck never focuses or writes into another Rune pane.
Learned mapper V1
The learned mapper can import nfblight's version-2 map without modifying the original Blightmud store:
/mapper import <character>
/map source learned
The importer reads the default Blightmud store under the user's data directory.
Pass an explicit store path after the character when it lives elsewhere. Rune's
copy is written to mapper-v2.ndlua inside that instance's config directory.
It is loaded incrementally so large maps do not block Rune's script deadline.
Use /mapper status, /mapper room, and /mapper save to inspect or flush the
learned map. /map source hybrid uses learned rooms as the base while applying
fresh BIGMAP route, destination, terrain, and link state. Switch back to the
server-provided BIGMAP with /map source local.
Use /mapper find or Alt-F to open the incrementally built room picker. It
matches room names and descriptions containing the area, zone, vnum, terrain,
and stub state. Selecting a room calculates a route; /mapper walk loads its
directions into NukeFire with path go <directions>. It does not move the
character. Use the in-game path step or path step <number> command to
advance under server control.
Zone statistics
Every complete zinfo report is captured into the current Rune instance's
durable store. Compare the latest reports with:
/zstats
/zstats <page>
/zstats all
/zstats page <number>
/zstats next
/zstats prev
/zstats show <zone>
/zstats compare <zone> <zone> ...
/zstats sort hp|exp|raw|mobexp|damage [page]
/zstats copy
EXP/1k is the server-adjusted experience per 1,000 average mob HP; Raw
excludes the zone reward scalar. Run /zstats scrape to inspect every available
GPS destination using one zinfo gps <code> request at a time. The sweep has
one retry per destination and persists its cursor across reloads:
/zstats scrape
/zstats scrape missing
/zstats scrape status
/zstats scrape stop
/zstats scrape resume
Fender's Revenge dock announcements are retained per Rune instance. Buoy rooms are learned from their room descriptions and kept separately from imported mapper data:
/fender
/fender rooms
/fender route
/fender cycle
/fender history [count]
/fender ui [on|off|toggle]
/fender route selects the learned room for the latest announced dock; follow
it with /mapper walk to load the directions into NukeFire's path system. The
active dock is shown as a yellow B in the compact map when it is visible.
The current dock is also shown in the map details (or compact mobile footer);
use /fender ui off to hide it.
The tracker retains the latest 100 announcements. After observing a repeated
dock, /fender cycle reports the latest completed loop and every measured
stop-to-stop interval.
Item catalog
NukeFire's public item CSV is cached once under
${XDG_CACHE_HOME:-$HOME/.cache}/nfrune/ and shared by all Rune instances.
Press Alt-I or use:
/items
/items show <vnum>
/items copy <vnum>
/items status
/items refresh
/items reload
/items scan
/items scan status
/items scan stop
/items equipment scan
/items equipment status
/items inscribe
/items inscribe status
/items inscribe stop
/items scores
/items weights [class]
/items score-weights
/items score <vnum> [class]
/items score coverage [class]
/items score validate [class]
/items rank <slot> [class] [page]
/items upgrades <score> <eq|tat|imp> <location> [class] [page]
/items audit-mat
/items rebalance <silver|embercore|dinobrain>
/items rebalance-dry <silver|embercore|dinobrain>
The picker searches item names, VNUMs, types, wear locations, abilities, flags,
classes, damage, and affects. Selecting an item only shows its reference
details; it never sends a MUD command. /items refresh downloads and atomically
replaces the shared cache, while /items reload makes another running instance
read the updated cache.
/items equipment scan captures the local character's eq, tattoo, and
implant reports, matches equipped and installed items against the catalog,
calculates their class scores, and shares a compact snapshot with connected IPC
peers. Manually requested reports refresh their respective section passively.
Likely wear and removal messages mark the snapshot stale until the next scan;
persisted snapshots survive /reload but also start stale.
After a manually submitted id <item>, tatstat <slot>, impstat <slot>,
aucs <number>, or aucstat <number|id ...>
succeeds, the identification header is matched against the catalog and an
estimated score is shown for every connected IPC character after the complete
identification block. Calibrated profiles are
available for Ninja, Heretic, Slinger, Mutant, Voidstriker, and Kaiju; unobserved
stats remain neutral rather than receiving guessed weights. When that character
has a matching equipment snapshot, the
report also shows the weakest applicable equipped score and candidate gain.
Unsupported classes, class restrictions, remort requirements, stale equipment,
catalog variants, weapons, and other uncertain estimates are labeled. Scripted
identification jobs do not produce this report.
compare explain <item> output is captured passively for score research.
/items scores reports the collected sample count, while /items weights
summarizes exact linear weights and flags nonlinear or rounded terms. Score
observations are bounded per Rune instance and written to separate shards under
the shared ${XDG_CACHE_HOME:-$HOME/.cache}/nfrune/scores/ directory. Coverage
and weight reports merge every shard, allowing all characters to contribute
without concurrent Rune processes overwriting one another.
Item scoring weights come from the game's setweights
The scorer uses the per-class weights the server exposes via the setweights
command, not a hardcoded table. /items score-weights sends setweights,
captures the block, and stores the weights per class (via rune.store) so the
scorer honors the server's current values. Until refreshed, a bundled snapshot
of the captured weights (Ninja, Heretic, Voidstriker, Kaiju) is used as the
fallback; Slinger and Mutant fall back to the legacy calibrated values until
their setweights is captured.
Two stats — armor class and fightspeed — score with a negative-is-better
convention: a negative item value is good and yields positive points
(weight × |value|), matching the game's Candidate score details output
(e.g. armor class -80 → +80). The ac_apply "armor value" term uses the
game's separate armor weight. /items score coverage [class] reconciles each
measured stat's ratio against the stored weight and flags any that drift (a
stale store or a server-side weight change).
/items score coverage [class] reports which observed score terms can be
reproduced exactly from the catalog, which have catalog inputs but unresolved
transforms, and which depend on server-only live state.
/items score <vnum> [class] prints the locally reconstructed score and every
contribution. /items score validate [class] compares those estimates against
the deduplicated shared observations, reporting clean prototype accuracy
separately from variants and server-only bonuses.
/items rank <slot> [class] [page] ranks the newest catalog record for each
VNUM, filters out class-incompatible items, and shows ten results per page.
Remort requirements remain visible but do not filter or influence the ranking.
A ~ prefix marks estimates affected by known catalog uncertainty.
/items upgrades searches upward from a supplied score at an equipment,
tattoo, or implant location. Results are class-compatible, ordered by the
smallest score gain first, and are not filtered by remort requirement. For
example, /items upgrades 850 tat hand shows the nearest tattoo-hand upgrades
for the current class.
/items scan reads the current inventory and runs compare explain for each
entry in sequence. It waits for each response before sending the next command,
captures successful score details through the same passive collector, and
continues past items the server cannot compare. Use /items scan status to
check progress or /items scan stop to cancel it.
/items inscribe reads the inventory and finds entries without an inscription
marker. For each item it verifies a one-word selector with id, then asks the
Artisan to inscribe it. Equipped items are never touched. A failed Artisan
response stops the run, including insufficient-credit failures. Use
/items inscribe status or /items inscribe stop to inspect or stop it.
When an id keyword resolves to a different item, that keyword is retried as
numbered selectors from 1.<keyword> through 5.<keyword>. A not-found
response skips numbering and moves directly to the next word. Selectors are
accepted only when id returns the expected item name.
/items audit-mat reads each connected character's mat inventory and reports
their Silver disc, Embercore, and Dinobrain counts. /items rebalance <silver|embercore|dinobrain> equalizes a material across the connected
characters using a multi-pass approach:
- It captures the connected participant set and runs a distributed
mataudit. - It computes exact per-character targets (the total divided as evenly as possible, so final counts differ by at most one) and picks a single donor (richest) and recipient (poorest).
- The donor locally
gathers exactly the transfer amount,give alls it to the recipient, and the recipient runsaddmatto store it — strictly serialized, advancing only on confirmed game output. - A fresh
mataudit runs after every transfer, and the operation repeats until balanced or a safety stop fires (timeout, failure, missing participant, no progress, conservation mismatch, or max passes).
Each pass transfers to exactly one recipient, so piles never pool. Characters
must be able to trade (same location) for a transfer to succeed. /items rebalance-dry runs a single audit and prints the current counts, exact targets,
and the next proposed transfer without dispatching anything.
Rune 0.11 development doctor
For an opt-in bounded runtime diagnostic, set nfrune.dev_doctor = true in
an instance's private config before loading the shared init.lua. Then use:
/dev
It reports registration counts/names, current state dimensions, declared visible
and hidden layout leaves, and a short cleaned output preview. It does not install
an extra bind or timer, retain full MUD output, or expose credentials. The fake
runtime tests prove lifecycle behavior; a live Rune session still needs a manual
/dev check after loading the configuration.
API reference
Rune's upstream API documentation is not committed. Generate a local snapshot
under reference/rune/ when developing or reviewing Rune API usage:
Refresh it with:
./scripts/update-rune-reference.sh
The refresh requires Git and network access. The generated snapshot is ignored by Git.
Editor support
types/rune.lua describes the Rune global and callback data for LuaLS-compatible
editors. .luarc.json adds it as a workspace library and configures Lua 5.1.
The type file is editor-only and must not be loaded by Rune.
When refreshing the API reference, review types/rune.lua for corresponding API
changes; the updater intentionally does not guess type signatures from prose.