No description
  • Lua 99.5%
  • Shell 0.5%
Find a file
2026-09-07 19:05:41 +08:00
dev Expand shared Rune tooling and multi-instance support 2026-07-29 18:14:52 +08:00
docs refactor(lua): clarify architecture and shared lifecycles 2026-08-01 00:12:13 +08:00
examples feat: add opt-in Rune development doctor 2026-09-02 22:30:55 +08:00
scripts Port progression profiles and IPC event cleanup 2026-09-07 19:05:41 +08:00
tests Port progression profiles and IPC event cleanup 2026-09-07 19:05:41 +08:00
tools Initial Rune scripting setup 2026-07-26 21:01:13 +08:00
types feat(layout): native tree layout — tablet row split, output beside map 2026-09-01 14:03:59 +08:00
.gitignore Expand shared Rune tooling and multi-instance support 2026-07-29 18:14:52 +08:00
.luarc.json Initial Rune scripting setup 2026-07-26 21:01:13 +08:00
AGENTS.md Initial Rune scripting setup 2026-07-26 21:01:13 +08:00
init.lua feat: automate post-death corpse recovery 2026-09-03 17:17:17 +08:00
README.md feat: add opt-in Rune development doctor 2026-09-02 22:30:55 +08:00
TODO.md Add group status and cross-instance IPC 2026-07-27 15:04:54 +08:00

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 b1b4 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:

  1. It captures the connected participant set and runs a distributed mat audit.
  2. 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).
  3. The donor locally gathers exactly the transfer amount, give alls it to the recipient, and the recipient runs addmat to store it — strictly serialized, advancing only on confirmed game output.
  4. A fresh mat audit 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.