Nightmare Residuum

✅ 可玩

residuum

更新 2bf9100 2026-10-19 源码 下载 ZIP 上游 michaelprograms/nightmare-residuum 落后 9 提交

▶ 开始游玩 · Play Now

A from-scratch space-themed LPMud that takes only its name and thematic inspiration from the classic Nightmare Mudlib, distinct from this collection's `nightmare3`/`nightmare4` in both codebase and setting. A new arrival wakes in Origin, a small orbital station (a central chamber with corridors to a tank room, a nanite-injection vendor, and a transporter) whose android greeter walks them through a two-stage character-creation process: first a species choice, then a "tank" console where `download <class>` installs one of six classes -- warrior, mystic, scoundrel, ranger, psionist, or paladin -- and `encode`/`randomize` sets physical attributes before `done` finalizes the specimen and releases it into the world. Leaving the station via Origin's transporter pad drops a character onto Planet, a fully procedural surface generated live from Perlin/simplex noise: every coordinate computes its own biome, heat, humidity, and elevation on the fly (tropical rainforest, desert, tundra, and more), populated with wildlife (curious raccoons, feral cats, large ants, lurking vultures, plasma snails, rabid rats, watchful crows, wild dogs) to hunt or be hunted by. Architecturally the mudlib runs a genuine account/character split -- one login account can hold several independent characters, each with its own save file and its own class/level/stats/experience/defeat-count progression -- and drives its whole verb/command/ability layer through a custom dispatch registry rather than the driver's native `add_action()`. The very first character ever created on a fresh copy of the world is automatically granted immortal rank. The codebase ships its own in-game unit-test and code-coverage framework (`D_TEST`, invoked live via a `test` command) directly alongside the game content. English-language content.

README

An LPMud "inspired by the Nightmare Mudlib" but written from scratch for modern FluffOS rather than ported from a period MudOS archive. Distinct from both nightmare3 and nightmare4 elsewhere in this collection -- its own codebase, its own space-themed example domain, and a full account/character split (your login connection and your in-game character are two separate objects, so one account can hold several characters).

Source: michaelprograms/nightmare-residuum (submodule-patch, pin dee36ca2, mudlib root work/lib). Catalog FluffOS-compat in patches/.

Highlights

Play online

https://mudlibs.fluffos.info/residuum/

Admin account

Warning: Mud@2026 is a public default password for local play
only. Change it before exposing this host publicly.

Local run

cd libs/residuum
~/src/fluffos/build-debug/src/driver config.fluffos

Game port: 40237.

NOTES · 移植与修复记录

Nightmare Residuum -- porting notes

Source: git clone https://github.com/michaelprograms/nightmare-residuum (commit d9336dbcad231b34ac751e9e41946b52efbbd309, cloned 2026-08-26). The repo's own description: "An LPMud inspired by the Nightmare Mudlib but utilizing modern fluffos driver features." Top level bundles a driver/ git submodule (tracking fluffos/fluffos, not fetched), a native/noise Rust FFI addon for procedural planet rendering, and a tools/planet-render helper -- all ignored per this project's convention (own driver build at ~/src/fluffos, no Rust toolchain invoked). Mudlib root is the repo's lib/ directory: 712 raw files, 4.3MB -- by far the smallest and most modern archive processed in this session so far. Slug residuum, number 935, port 40237.

Hosting (2026-09-16) — real submodule-patch

work/ is a git submodule of michaelprograms/nightmare-residuum @ 86e64a0b2499e9636fa66169d0470edb6f8b53e6 (main; scheduled safe rebase 2026-10-05 from 6c08edc5, +1 astronomy test coverage, 4 catalog patches still apply). Mudlib root is work/lib (upstream.mudlib_subdir, same pattern as lima). Upstream ships .c; catalog follows that (no .c→.lpc rename in the submodule).

Catalog FluffOS-compat in patches/:

Slice-width fixes from the old vendored .lpc tree are intentionally not carried forward — they existed only because of the catalog rename.

0. Confirmed genuinely distinct from nightmare3/nightmare4

Before investing effort: this is NOT a period MudOS snapshot at all -- it's a from-scratch modern codebase that merely takes thematic inspiration from the original Nightmare lib (its own README says so explicitly, linking to fluffos/nightmare3 as the inspiration, not the source). Confirmed via file layout (lib/std, lib/secure, lib/daemon, lib/cmd, lib/domain, lib/realm -- no overlap with either sibling's directory names or file contents beyond generic LPC-family naming), example domain (domain/Origin, a small space station, vs nightmare3's Praxis and nightmare4's Ylsrim), and architecture (a genuine account/character split with aD_TEST unit-test framework built in from day one -- 227 of the 677 real .lpc/.c files are .test.lpc/.mock.lpc companions, something neither sibling has any equivalent of). Not a duplicate.

1. Conversion

Pure ASCII/UTF-8 archive already -- convert_lib.sh reported already_utf8=708 converted=0 lossy=0 skipped_binary=4, no encoding work needed. Renamed 677 .c files to .lpc, fixed 1051 literal .c" references, and ran the static->nosave sweep across only 2 files (this codebase's own house style already avoids static almost entirely) with no collisions.

2. No get_root_uid()/get_bb_uid()/creator_file() needed at the

mudlib-design level, but still required by THIS driver build

This driver was built with PACKAGE_UIDS, so set_master() demands master::get_root_uid()/get_bb_uid() exist or the whole process exit(-1)s at boot (AGENTS.md's general \S7.2 class) -- and every subsequent object load then needs master::creator_file() too. Unlike most archives in this catalog, this codebase's real access-control model has NOTHING to do with driver uids at all (grep confirms zero getuid()/seteuid() calls anywhere) -- it's entirely privs-based via its own D_ACCESS daemon consulting read.cfg/write.cfg. Added the same flat "everyone is Root" stub as this project's nightmare3/ nightmare4 (secure/daemon/master.lpc, next to the existing privs_file() apply):

string get_root_uid() { return "Root"; }
string get_bb_uid() { return "Backbone"; }
string creator_file(string file) { return get_root_uid(); }

3. ed_start()/ed_cmd()/query_ed_mode() -- this build has

__OLD_ED__, opposite of what this codebase's own local_options (meant for a self-built driver) asks for

The repo's own local_options (top level, for anyone building their own driver per build.sh) explicitly sets #undef OLD_ED -- i.e. it wants the new synchronous ed API. This project's shared driver build has __OLD_ED__ defined instead (confirmed via debug.log's predefine dump and by directly test-compiling a bare ed("/tmp/x") call, which compiles clean, vs ed_start/ed_cmd/query_ed_mode, which don't). Two files affected:

4. The .c->.lpc filename-slice bug -- present here too, and this

time it broke the WHOLE command/verb/ability dispatch registry AND silently disabled set_password()

The exact class flagged before starting this port (previously the dominant pattern in nightmare4, and in sunshadow's daemon/command.lpc): code that slices a filename to strip its extension using an arithmetic width that assumed the archive's original 2-character .c, now wrong against the 4-character .lpc this project's convention renames to. This driver's str[0..<n] keeps len-n+1 characters (AGENTS.md \S7.80's off-by-one), so a correct strip needs n = suffix_length + 1 -- .lpc needs [0..<5], not [0..<3]. Found in two distinct shapes, the second more severe than anything a first-pass grep for the literal .c" string would ever catch (these are all bare numeric slices, no quoted extension string involved):

- std/user/account.lpc's set_password() -- STD_USER[0..<3] is "/std/user.l", which can never equal base_name(previous_object())'s extension-free "/std/user", so this self-defense check rejected every legitimate caller. This one doesn't even need a live-connection guard to be caught by; it directly blocked the password-creation step of registration on the very first connection test (*Illegal attempt to account->set_password, log/runtime). The sibling set_name() allowlist regex two functions down has the identical STD_USER[0..<3]/D_TEST[0..<3] pattern; fixed both to [0..<5]. - secure/daemon/master.lpc (2 sites) and secure/sefun/user.lpc (the this_user() sefun) -- D_TEST[0..<3] comparisons gating a test-runner-only code path (suppressing expected-error noise during D_TEST runs, and redirecting this_user() when called from inside a test). Doesn't affect ordinary play, but is the same fallout and was fixed the same way for consistency. - secure/cmd/immortal/test.lpc and std/module/reset.lpc's set_reset_data() -- both if (x[<2..] == ".lpc") x = x[0..<3];, fixed to [<4..]/[0..<5]. - cmd/immortal/goto.lpc -- the INVERTED guard, if (path[<2..] != ".lpc") path += ".lpc";, meant "append the extension if it's not already there" but (same length-mismatch reasoning) was now unconditionally true, so goto /std/room.lpc always produced /std/room.lpc.lpc. Fixed to [<4..]. - Also swept the corpus's own D_TEST file-discovery regexes in secure/daemon/test.lpc, which independently still matched literal \.c$/\.test\.c$ against real files that no longer end that way (never disabling anything -- the .c file just doesn't exist -- but silently made this codebase's own test-coverage scanner discover zero files post-rename): rewrote the two regexes and one path exclusion to .lpc, and replaced one more x[0..<2] + "test.lpc" site with the file's own already-correct replace_string(file[0], ".lpc", ".test.lpc", 1) helper rather than re-deriving a third slice width by hand.

secure/cmd/immortal/update.lpc:114's superficially identical input[0..<2] + "test.lpc" was checked and left alone: unlike the daemon/test.lpc site, input here is NOT reliably extension-suffixed at that point (it can arrive via file_name(environment(...)), which returns the driver's own extension-free canonical object path) -- this looks like a pre-existing, load-bearing-on-context author quirk rather than something this port's .c->.lpc rename specifically broke (the exact same ambiguity would have existed against the original 2-character .c too), so left untouched per this project's policy against fixing what isn't a clear conversion regression.

5. message() never reached any target that wasn't itself the

driver's INTERACTIVE connection object -- this codebase's whole account/character split depends on a driver flag this build's local_options doesn't set

By far the most severe and most interesting finding in this port, because it isn't a bug in the mudlib source at all -- it's a genuine mismatch between what this codebase's own architecture requires and what this project's shared driver binary was compiled with.

Symptom: registration completed and write()-based output (the eval command's results, score/who's border()-rendered boxes) displayed correctly, but look, say, and the room description shown on first entering the world all produced silently empty output -- no error, nothing in log/runtime, not even catch() reporting a thrown error (catch(message("room", "TESTMSG", this_character())) returned 0).

Root cause: this codebase's account/character split means the *character* object (STD_CHARACTER, what previous_object() resolves to inside a verb like cmd/verb/look.lpc) is never itself the driver's interactive connection -- that's a separate STD_USER object the character only holds a reference to (__User). Every description, speech, and room broadcast in this mudlib goes through a message() sefun wrapper (secure/sefun/override.lpc) that calls the real efun::message(type, msg, target, exclude), relying on the *character* object having its receive_message(type, msg) apply (which forwards to __User->receive_message(...)) actually invoked by the driver's do_message() dispatch. But do_message() (src/vm/internal/simulate.cc) only calls that apply on a target object flagged O_LISTENER or truly interactive -- and O_LISTENER is *only* ever auto-set, at object-creation time, #ifdef NO_ADD_ACTION. This repo's own local_options (meant for a self-built driver, #define NO_ADD_ACTION) sets exactly that flag, which is presumably why this architecture works fine on the driver the authors actually build and test against. This project's shared driver build (~/src/fluffos/build-debug) was not compiled with NO_ADD_ACTION (confirmed directly: debug_info(0, ob)'s output prints O_ENABLE_COMMANDS where it would print O_LISTENER if the flag were set) -- so the character object's O_LISTENER flag is never set, do_message() never calls its receive_message(), and every message() call to a character (or to a room, to reach the characters standing in it) is silently swallowed. write() worked throughout only because its own implementation (also in override.lpc) calls character->receive_message(...) directly via ->, sidestepping efun::message()'s listener-flag gate entirely -- which is exactly the tell that pointed at this root cause rather than a broken room or a broken verb parser.

This is the same general class as AGENTS.md \S7.46 (a mudlib demanding driver compile-time flags this project's shared build doesn't have, previously found on a LIMA-derived lib) -- normally "not fixable at the mudlib-source level; would require a second, separately-compiled driver binary." Rebuilding a second driver just for this one lib was out of scope for this port, so instead reimplemented message() itself against receive_message(), replicating do_message()'s own dispatch logic entirely in LPC (secure/sefun/override.lpc): for each target, if it (or an item in its direct contents, one level -- matching do_message()'s own single-level recursion into a non-listener target's contents, e.g. a room reaching the characters standing in it) is interactive() or defines receive_message(), call that apply directly. This is a behavior-preserving driver-compatibility shim, not a content change -- it makes message() do exactly what this codebase already assumed it did, without touching any of the ~30 call sites across the mudlib that use it. (One acknowledged fidelity gap: the driver's internal recursion additionally forces hidden objects visible during delivery; the LPC replacement uses the plain all_inventory() efun, which respects normal hide rules -- a minor content-visibility nuance, not a functional blocker, and not worth a custom recursive walk to close.)

Verified fixed live: catch(message("room", "TESTMSG", ob)) before vs. after showed the same 0 (no error) both times, but only after the fix did "TESTMSG" actually reach the connection; a full registration-to-quit session then showed real room descriptions on look and entry, and say hello world correctly producing "You say: hello world" plus the bystander line.

6. Known gaps -- documented, not invented or mechanically forced

3 files remain uncompilable after the fixes above (99.6% pass rate, 674/677 on the full lpcc_check.sh batch sweep); none are preloaded and none are reachable from the standard registration/gameplay path exercised in \S7:

7. Verification

Booted the real FluffOS driver (~/src/fluffos/build-debug/src/driver) against config.fluffos -- Initializations complete., clean of any new fatal errors in log/debug.log/log/runtime beyond the 3 documented gaps above. Verified end to end with a raw Python socket client (no tmux_mud.sh/mudclient.py shortcuts), twice -- once as the admin account and once as an ordinary second character:

8. No outbound network connections at boot or during testing

Unlike nightmare4/nightmare3 (both run a live Intermud-3 daemon), this codebase's only socket-using file, secure/daemon/ipc.lpc, is an inbound-only loopback listener (binds 127.0.0.1, explicitly rejects any connection whose address doesn't match ^127\.0\.0\.1 [0-9]+$) for a local "Remote Creator Protocol"-style tool -- and it isn't even preloaded (secure/etc/preload.cfg has it commented out). secure/module/http.lpc (an HTTP-over-websocket responder) is likewise inbound-only. Confirmed via ss -tnp during a live boot: no established or attempted outbound connections at any point. This lib is safe for high-frequency automated reboot/deep-test loops, unlike its two Nightmare-lineage siblings in this collection.

9. WASM status: playable, zero code fixes needed

Tested with node scripts/wasm_client.js ~/src/fluffos/build-wasm/src libs/residuum --timeout 25 --idle 1.0 driving the exact flow real players use: fluffos (account name) -> Mud@2026 (password) -> fluffos (select the existing, already-chargen'd character from §10) -> look -> score -> quit. No fresh registration was needed since this lib already carries a seeded, fully-chargen'd admin account/character from the native §10 pass (Level 2 Warrior, standing in Origin Point) -- reusing it exercises the account-menu/character- select half of the login flow that a brand-new registration wouldn't.

Transcript was completely clean end to end: Enter account name: -> Existing account 'fluffos'... -> Password: -> the full border()-rendered account menu (correctly listing "Fluffos, Level 2 Human, Origin Point") -> selecting the character produced the real room description of Origin Point (ASCII mini-map, four exits, the R-0111010 NPC's greeting) -> look re-rendered the identical room -> score produced the full character-sheet box (Warrior, Level 2, XP 896, HP/SP/MP 140/84/72, stats, connected time) -> quit cleanly dropped back to the account menu with Fluffos exits Nightmare Residuum. logged, exactly matching native behavior. Grepped the full driver console output for Error/FATAL/Undefined function/No program in object/Bad argument and found none -- no sockets-package crash, no pcre issue, no uptime() gate, no log/-path ENOENT. This lines up with §8's own finding that this lib's only socket user (secure/daemon/ipc.lpc) is inbound-only and not preloaded, so the WASM build's absent sockets package was never going to be exercised on this path in the first place -- no #ifdef __PACKAGE_SOCKETS__ guard or catch() wrap was needed anywhere. This is one of this project's "clean WASM session, zero fixes needed" cases, alongside demonangel/dreamofseven/ immaster/lpmud141/mortremains.

Since the WASM harness runs the whole mudlib out of an in-memory filesystem (MEMFS) copied at boot, none of this touched libs/residuum/work/ on disk at all -- no save-data churn to clean up, and the seeded fluffos account's on-disk save is unchanged from §10's own native-testing state.

meta.json's wasm_status set to "playable".

10. Deep functional test, round two (2026-08-27, AGENTS.md §10.7)

One continuous session via a raw Python socket client (~/src/fluffos/build-debug/src/driver config.fluffos), covering both halves of this lib's distinct account/character split as two separate steps, plus the six standing cross-cutting patterns (§7.121/§8.3a/§7.122/§7.123/§7.124/§7.126) checked explicitly by grep against this codebase.

10.1 Newbie path and chargen (previously undiscovered at onboarding)

This lib has no help newbie-style doc (doc/help/ only has syntax); the real onboarding path is architectural, not documentation-driven. Onboarding's own NOTES.md (§7) had only verified registration through the very first look in /domain/Origin/room/enter.lpc ("deep in darkness") and a quit — it never actually progressed past that. This session discovered the rest of the flow: enter.lpc's handle_receive() always fires an M_STORY intro sequence (six timed lines over several seconds) that ends by moving the character into /domain/Origin/room/tank.lpc, a second, distinct chargen step — a "STATUS" panel with download [class] (warrior/mystic/scoundrel/ranger/psionist/paladin), encode [attribute] [value], randomize, and done. A character's class stays the placeholder "adventurer" (and physical attributes stay unset, silently re-randomized on each re-entry into the tank) until done is run, which requires a real class to have been picked first ("Specimen must have a class before process is complete." if not). Only after done does the character reach the real, persistent starting room (/domain/Origin/room/center.lpc, "Origin Point"). This is not a bug — the code is internally consistent and works correctly — just an undocumented second gate worth recording so a future re-test doesn't have to rediscover it. The admin account's seeded Fluffos character (previously left mid-way through this flow, per onboarding's own transcript) was carried through download warrior + done as part of this session.

10.2 domain/Start (a large hand-built human town) is completely unreachable in this snapshot

domain/Start/human/ contains ~150 fully-built rooms (a whole town: streets, a bank, shops, apartments, a museum, a church, a school, a gym) plus starter weapons/armor items — but a corpus-wide grep (grep -rln "domain/Start" **/*.lpc, and specifically for Start/human/room/square as the likely hub) found zero references to any domain/Start file from anywhere outside domain/Start itself — no set_exit, no teleporter, no admin-only shortcut. The only way out of domain/Origin is the pad exit in /domain/Origin/room/west.lpc, which leads into the separate procedural domain/Planet coordinate-grid surface (real, reachable, and working — see §10.3), never into Start. Net effect: a brand-new character has no organic way to ever reach a shop and no starting weapon or armor is ever granted (set_class() in std/living/class.lpc only sets the class name and recalculates vitals; no gear-granting code exists anywhere). This reads as unfinished world-wiring in this snapshot rather than a programming bug (no crash, no wrong efun/API usage — the Start domain simply isn't linked into the graph yet), so it was not fixed per the scope boundary (connecting it would require deciding *where* and *how*, a content/design call). Documented here so a future pass doesn't waste time re-deriving this; also explains why a level-1 character can be beaten by a level-1 wild animal (§10.4) — they have no armor and are mechanically incapable of getting any.

10.3 Procedural planet surface works correctly; FFI-fallback error is expected, benign noise

Descending via pad correctly compiles a virtual room (/domain/Planet/virtual/room/surface/Terra/250.250, real Perlin/simplex-noise-driven biome/heat/humidity/height values, e.g. "tropical rainforest"). The first virtual-room compile of each boot throws a full driver backtrace to the console/debug.log (*get_os_env: 'NR_NOISE_SO' is not in the 'allowed os environment variables' list, from secure/sefun/noise.lpc:108's __ffi_init()) — this looked alarming live but is a caught, intentional fallback: __ffi_init() wraps the get_os_env() call in catch() and falls through to debug_message("noise: FFI disabled (NR_NOISE_SO unset)\n"), exactly as the repo's own native/noise Rust FFI addon (explicitly out of scope for this project per §0/§1) is meant to degrade when unavailable. FluffOS logs a thrown error's backtrace to the console even when an LPC catch() up the stack handles it, which is what makes a fully-expected, fully-handled condition look like a crash. Confirmed the pure-LPC fallback produces real, working terrain regardless. Not a bug; not fixed.

10.4 Combat, check_lifesigns(): real off-by-one bug, found but a fix was reverted after breaking an existing test

std/living/combat.lpc's check_lifesigns() has the code's own author-left marker:

// TODO: this should be 1
if (this_object()->query_hp() < 0) {
    dead = 1;
}

i.e. the author already knows defeat should trigger at hp <= 0 (< 1), not only at hp < 0. Changing it to < 1 looked like a trivial, author-confirmed one-token fix, but running test /std/living/combat immediately caught a regression: test_check_lifesigns()'s very first assertion (before the test ever calls set_hp()) failed with *Bad argument 1 to EFUN call_other(): Got int(0), because a freshly-new()'d STD_LIVING/mock object's query_hp() legitimately reads 0 before update_vitals() has ever run (vitals are only computed once a class is set), and < 1 now flags that transient pre-initialization state as "dead" too, destructing the object as a side effect the very first time check_lifesigns() is ever called. A real player character never hits this window in practice (vitals are established during chargen long before any combat), but the mock/test object does, which is almost certainly *why* the author's own TODO was left unresolved rather than applied outright — completing it correctly would require also guaranteeing hp is never observed at 0 pre-initialization (e.g. gating check_lifesigns() behind "vitals have been computed at least once"), which is more than a one-line change and starts to make judgment calls about combat/lifecycle sequencing. Per this project's scope boundary ("when genuinely unsure... document, don't guess"), the change was reverted back to the original < 0 + TODO comment, and is recorded here instead as a confirmed-but-deliberately-untouched author-acknowledged bug. Live-observed consequence: a character's own HP can go visibly negative (-8 in this session's test fight) before "You have been defeated!" fires — cosmetic only (the eventual defeat still happens correctly one attack-round later), not a soft-lock or crash.

10.5 Real bug fixed: /log/ write ACL silently dropped nearly all in-game action logging

secure/etc/write.cfg gated /log/ writes to ASSIST:MUDLIB only. secure/daemon/access.lpc's query_allowed() requires every object in the call stack (not just the immediate writer) to individually hold one of the path's allowed privileges (query_file_privs() classes /cmd/ files as COMMAND, confirmed in secure/include/access.h). Since D_LOG->log() is called either directly from a /cmd/*.lpc file (cmd/character/advance.lpc, cmd/character/improve.lpc) or from std/character.lpc via a call chain that passes through one (cmd/character/quit.lpc → exit_world() → D_LOG->log("character/enter", ...) for the disconnect-side log line), and COMMAND was never in /log/'s allowed set, every such write was silently denied (debug_message'd to the driver console as "/daemon/log denied write (write_file) to /log/character/...", never surfaced to any player). Confirmed live: /log/character/advance had never once been successfully created despite the feature existing and firing every time advance runs; /log/character/enter existed but was missing every disconnect-side entry, only ever recording the connect side (which is reached via std/user/character.lpc/std/user.lpc, i.e. no /cmd/ frame in that particular chain). By contrast /realm/ and /save/ — the *other* two paths ordinary player commands legitimately write through — both already include COMMAND in their own write.cfg lines, making /log/'s omission of it look like a straightforward oversight rather than a deliberate security boundary (this is a game's own first-party log directory, not user-supplied content, so no injection-style risk from allowing it). Fix: secure/etc/write.cfg, (/log/) ASSIST:MUDLIB → (/log/) ASSIST:MUDLIB:COMMAND. Verified live: re-ran test /secure/daemon/access (100% asserts pass, no regression), then a fresh quit correctly appended a "... exits from 127.0.0.1" line to /log/character/enter where it previously silently failed. This is a config/access-control bug, not a game-content decision, so it was fixed in place.

10.6 The six standing cross-cutting patterns — all checked, none found

One more near-miss worth recording for future sibling checks even though it turned out safe: domain/Planet/virtual/room/base/terrain.lpc's wildlife-spawn switch builds PLANET_NPC + npc + ".lpc" where the random(2)==1 branch's own npc value ("plasma_snail.lpc") is *already* suffixed, producing a double "plasma_snail.lpc.lpc" key — but std/module/reset.lpc's set_reset_data() independently strips exactly one trailing .lpc from every key it stores (its own [<4..]/[0..<5] guard, added for the same corpus-wide .c→.lpc slice-width class documented elsewhere in AGENTS.md), which silently repairs the double-suffix back down to the correct filename before it's ever used in new(). No live bug results, but the inconsistent suffixing in terrain.lpc's own switch (one branch bare, one branch pre-suffixed) is sloppy and would break if set_reset_data()'s defensive stripping were ever changed — not fixed, since it causes no live-observable bug today and touching it would be tidying rather than fixing.

10.7 Persistence verified across a real reconnect

Sequence: joined Fluffos, completed chargen (§10.1), fought a cloned curious_raccoon (domain/Planet/npc/curious_raccoon.lpc) unarmed/unarmored to a loss ("You have been defeated!", Defeats: 0→1), recalled to Origin Point, granted the character add_experience(1000) via eval (immortal-only, used to exercise advance/improve without a multi-hour grind — a legitimate test shortcut, not a live gameplay bug), advanced one level (2, XP 896 remaining), then a clean quit. Reconnecting on a new socket a few minutes later showed the account menu correctly reporting Level 2 Human, Origin Point; joining the character showed the exact same score (Level 2, XP 896, Defeats 1, HP 140/140 SP 84/84 MP 72/72 — HP/SP/MP had regenerated to full between sessions, which reads as ordinary passive regen, not a bug) and the exact same room (Origin Point, not reset back to the intro/tank). Both account-level (last-seen time, character list) and character-level (class, level, XP, defeat count, location) state persisted correctly. Also incidentally confirmed the earlier abrupt-disconnect (non-quit, plain socket close) behavior is NOT a bug: net_dead() → character_linkdead() intentionally does not force a save (only exit_world()/heart_beat()'s 60-second autosave do), so progress made after the last autosave tick and before an abrupt disconnect is expected to be lost — this is a normal autosave-interval tradeoff, not investigated further as a "bug."

10.8 In-game self-test framework (D_TEST) used directly as part of this pass

This lib's own test [file]/test all immortal command (secure/cmd/immortal/test.lpc → D_TEST) was used live to spot-check files touched or investigated during this session (/std/living/combat, /std/module/reset, /std/user/account, /std/user/character, /std/character, /secure/daemon/access) — all passed 100% of their own assertions both before and after the §10.5 fix, and this is exactly what caught the §10.4 regression before it was committed. Did not run the full test all (227 test files) given the time budget; the targeted subset above covers every file this session actually changed or seriously suspected.

Leftover 678 (2026-09-12) — upstream lib/ backport

michaelprograms/nightmare-residuum +17 since d9336db: mostly D_TEST coverage, one harvestable early-return drop, driver submodule bumps. Ported lib/ tests + harvestable.lpc / npc.mock.lpc to abb51fa5c1b. Ignored driver/ (this project uses ~/src/fluffos). No new rooms.

Verification / cleanup

Booted ~/src/fluffos/build-debug/src/driver config.fluffos fresh after the §10.5 fix — clean Initializations complete., zero new debug.log/console errors beyond the already-documented, expected §10.3 FFI-fallback notice. Killed by exact PID after each boot (never pkill -f). Removed all testing byproducts before committing: the throwaway qatester account+character (save/account/q/qatester.o, save/character/q/qatester/), D_TEST coverage-instrumentation files (*.lpcoverage.lpc, auto-generated by test -c), the immortal eval command's scratch file (realm/fluffos/CMD_EVAL_FILE.lpc), and this session's own driver stdout logs — kept only the seeded fluffos admin account/character save (now reflecting real, verified play: Level 2, Warrior, 896 XP, 1 Defeat, standing in Origin Point). Re-ran the port-conflict sanity check (grep -h '"port"' libs/*/meta.json | grep -oE '[0-9]{5}' | sort -n | uniq -c | awk '$1>1') — no output, clean.

深度功能测试(§10.7,2026-09-16)— post submodule-patch cutover

Re-ran a full native + WASM §10.7 pass against the cutover tree: work/ = michaelprograms/nightmare-residuum @ dee36ca2 with mudlib_subdir=lib, catalog patches 0001–0004 applied live via scripts/apply_lib_patches.py (upstream still ships .c; no catalog rename). Port 40237. Fresh world (cutover wiped the old vendored fluffos seed). Driver PID confirmed via /proc/<pid>/cwd → libs/residuum/work/lib; live debug.log is libs/residuum/log/debug.log (fd 3).

Native (scripts/tmux_mud.sh, continuous session)

WASM

node scripts/wasm_client.js ~/src/fluffos/build-wasm/src libs/residuum --timeout 45 --idle 1.0 with seeded deepres/Mud@2026 → join → look / score (L2 Warrior, Defeats 1, Origin northwest) / quit / exit. Clean. MEMFS — no host save churn.

Cleanup

Killed residuum driver by exact PID. git -C work reset --hard + apply_lib_patches.py residuum restored a clean patched tree. Play saves/logs under work/lib/save and work/lib/log are gitignored.

深度功能测试(§10.7,2026-10-05)— round three: 全量 test all、战斗消息、目录补丁

复测原因:pin 从 86e64a0b 推到 9476138(上游只给 soul.test.c 加了两行),且上一轮(2026-09-16) 之后上游改了 module/reset(收编游荡怪物、计数 ≤0 时清除)、daemon/astronomy/soul/experience 的条件分支。 round two 因时间没跑全量 test all,这次作为主要的新角度。

测法:不碰 submodule 工作树。git -C work archive HEAD 解到 scratch, apply_lib_patches.py residuum --work 打补丁,按 wasm_keep_dirs.txt 建目录,单独的 config, 即站点上的同一棵树。两种跑法:在线(第一个注册的角色自动成为 immortal,test all),以及无人在线的 driver config.fluffos -ftest(master flag("test") 带覆盖率跑全部测试后关机,与上游 CI 同一条件)。 原生驱动,ulimit -v 6291456,PID 精确结束。

修了四处(目录补丁,patches/):

1. 战斗消息全部丢失(玩家可见,严重)。 在线打一只 curious_raccoon:只有 hp: 提示在变,没有任何 "You hit … / … misses you" 之类的消息。原因:补丁 0003 在 secure/sefun/override.c 里用 LPC 重写了 message()(本驱动没有 NO_ADD_ACTION,efun 送不到非交互的角色对象),但 sefun.c 把 combat.c 作为 另一个程序 inherit 进 simul_efun 对象;在 simul_efun 对象内部,本程序没有定义的调用绑定到 efun—— lpcc 反汇编 combat_hit_message 看到的就是 EFUN: message。所以战斗类消息(hit/miss/block/parry/ evade/heal/useless/attack)全部走了 efun、全部丢掉,而 std/ 里直接调 message() 的消息走的是覆盖版本, 正常显示。修法:combat.c 里 25 处调用改成 SEFUN->message((文件里本来就用 SEFUN->possessive()), 并入补丁 0003。先试过在 combat.c 声明 varargs void message(...); 原型——在 simul_efun 里能改绑定, 但 combat.test 会单独 new() 一个 combat.c,那里原型没有函数体,报 Undefined function called: message, 所以改用 SEFUN->。修后实测战斗消息全部出现;KB 06 §7.221。 2. test all 遇到一个加载失败的测试文件就永远停住。 secure/daemon/test.c process_file() 在 load_object() 失败的分支里只打印 Error in test 就 return,不调用 fnDone,不前进到下一个文件, 也不出总结(本驱动没有 SQLite,/std/database.test.c 加载失败,第 55 个文件后就停了)。新补丁 0005: 把加载失败记为一个失败的 expect 并继续(直接跳过会让上游 CI 在测试文件编译失败时反而通过)。 3. 补丁 0001 的写法让 master 的覆盖率副本编译失败。 测试框架的覆盖率插桩在每行语句后面追加 D_TEST->line_hit(N);,0001 把三个 stub 写成单行函数 string get_root_uid() { return "Root"; },插桩落在函数 外面,master.coverage.c 语法错误,master.test 整个报错。0001 改成上游一贯的多行写法(参数名按上游 风格写成 _file)。 4. 每次启动驱动都打印 author_file()/domain_file() in the master file does not work。 master 没有这两个 apply(mudlib_stats 包在启动时问 master 文件的作者和 "/" 的 domain)。新补丁 0006:两个函数分别返回 get_root_uid()、get_bb_uid()——正是驱动原来的回退值;domain 返回 backbone uid 时驱动把对象的 domain 记为其创建者的 domain,与没有这个 apply 时一致,行为不变。

结果:无人在线的 -ftest:断言 4414/4414 全过,expect 806/808;剩下两个都是环境差异,不修:

另外记录、不修:

清理:所有驱动均按 PID 结束;测试都在 scratch 副本里,submodule 工作树没有改动。

深度功能测试(§10.7,2026-10-05)— 补丁 0006 并入 0001

scripts/rebase_upstreams.py 对每个补丁单独在原始 pin 上跑 git apply --check,不是按顺序叠加。0006 的上下文是 0001 加的 stub,单独检查失败,下一次定时 rebase 就会卡住 pin 并报 patch-check 失败。现在 0006 并入 0001(同一个 secure/daemon/master.c 块),0001–0005 各自都能单独套用;套用后的树与之前逐字节相同(KB 02 §2.3)。

Upstream pin refresh (2026-10-11) — drop museum patch 0005

Upstream 1f612d19 (*prevent a failing test from stopping test suite*) now counts a test file that fails to load and continues the suite — the same behavior our 0005-test-daemon-count-a-test-file-that-fails-to-load.patch added. That patch no longer applies on tip; removed it so the scheduled safe rebase can advance the pin. Patches remaining: 0001–0004.

Deep functional test (§10.7, 2026-10-13): compile-warning scan

work/ is the upstream submodule, so the in-place warning scan skipped it. Scanned in a scratch repo (scripts/submodule_patch_scratch.py build residuum, then lpc_warnings.py residuum): 662 files compile, 0 warnings, no patch needed. The 3 files that do not compile need efuns this driver build lacks (ed_cmd/ed_start in cmd/immortal/codesay.c) or reference an undefined variable; they are not loaded at boot. The first scratch build compiled nothing because the sefun inherits its parts as inherit "/secure/sefun/array.c" and global.h names every base as "/std/object.c"; the builder now rewrites such .c path strings in the .lpc copy (KB 02 §2.3).