Mortal Remains

✅ 可玩

mortremains

更新 fdf581a 2026-09-12 源码 下载 ZIP 上游 clockworksoul/MortalRemains

▶ 开始游玩 · Play Now

A classic English-language LPMud, admin'd by "Cyanide" from 1997 to 2000, running the public-domain-released TMI-2 ("The Mud Institute") 1.4alpha mudlib base. A ~13,500-file archive of real 1997-2000 wizard-home content across dozens of authors (Cyanide, Chronos, Nightmask, Spooner, and many more). New characters pass through a full TMI-2 registration (password, email, real name) and a menu-driven race/gender/handedness/alignment stat-reroll wizard before landing in Decatur, the starting town. A newbie helper NPC, Alucard, stationed at the Intersection east of the town center, points new players toward starter equipment and directs them to any of five class halls scattered around town: a Pit Fighter's guild and a Sharpshooter's guild for warriors (joining awards a class-marker item, "A Pit Fighter's Scars"), a mage hall, a rogue hall, and the Temple of Talos for priests. The preloaded channel daemon makes a real outbound socket-connect attempt on every boot to a real (now long-dead) 1999-era Intermud-3 router address, reflecting the mud's original era of live inter-mud connectivity.

README

A classic English-language LPMud, admin'd by "Cyanide" from 1997 to 2000, built on the TMI-2 ("The Mud Institute") 1.4alpha mudlib base. Released into the public domain by its original admin; source cloned from git clone https://github.com/clockworksoul/MortalRemains. Slug mortremains, number 929, port 40231.

A real, messy multi-author archive: ~13,500 files of genuine 1997-2000 wizard-home content from dozens of authors (Cyanide, Chronos, Nightmask, Spooner, and many more), full TMI-2 character creation (name, password, email, real name, then a menu-driven race/gender/ handedness/alignment/stat-reroll wizard), and a real starting town, Decatur. See NOTES.md for the full porting log, including the most severe bug found (a driver-dialect mismatch in deep_inherit_list() that silently blocked every new character's registration from ever completing) and the classic std/monster.lpc fix that alone recovered ~1770 cascading compile failures across the codebase's entire NPC/monster class hierarchy.

Registration flow

name -> "Is this really the name you wish to use? (y/n)" -> password -> confirm password -> email (any format accepted) -> real name (optional) -> a menu (1 reroll stats, 2 swap two stats, 3 handedness, 4 gender, 5 race, 6 alignment, 0 done, ? help) -> "Are you SURE..." confirm -> lands in the real game world (The YMAA, Decatur).

Admin account

Verified live: wizard news section on login, [NEO] marker in who, correct routing to the wizard start room, and update <path> succeeding (this project's standard admin/ACL smoke test).

Status

Boots clean, zero fatal errors. A brand-new character registers all the way into the game world through the real registration flow (not just "reaches a prompt"), and look/score/who/quit all produce correct output. 6246 of 7928 .lpc files pass a full lpcc_check.sh compile sweep -- the remainder is dominated by a completely orphaned, unreferenced ~870-file legacy subtree (adm/AFD/, dead code bundled in the archive but inherited by nothing live) plus genuinely pre-existing, non-preloaded content gaps in personal wizard-home directories (missing headers, abandoned drafts, truncated experiments) -- none of it reachable from the standard registration/gameplay path. See NOTES.md for the full breakdown.

Note: the preloaded channel daemon makes a real outbound socket-connect attempt to a hardcoded, real (long-dead) 1999-era Intermud-3 router address on every boot. This is genuine intended upstream behavior, not a bug -- see NOTES.md \S12 before adding this lib to any automated high-frequency reboot/test loop.

WASM status: playable. Clean WASM boot and full registration/game session with zero fixes needed; the outbound I3 socket attempt (see below) fails gracefully without a sockets package. See NOTES.md.

Local run

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

Game port: 40231.

NOTES · 移植与修复记录

Mortal Remains -- porting notes

Source: git clone https://github.com/clockworksoul/MortalRemains (commit 3e9c6a98f9f78629d6e693570f698a043be634d2, cloned 2026-08-26). The repo's own README.md is a short first-person note from "Cyanide", the mud's original admin, releasing the code into the public domain: "Mortal Remains was based on the TMI-2 mudlib version 1.4 alpha." Confirmed genuine and sizeable before investing further effort: 13,515 files, 85MB raw, real multi-year (1997-2000) wizard-authored content across dozens of home directories, a real historical log/runtime/log/log with entries dated back to 1999-2000, and archived player save data (.o files) for real accounts from that era. Slug mortremains, number 929, port 40231.

1. Two subtrees excluded/noted before conversion

2. Conversion

Pure ASCII archive (English-language mud) -- confirmed via a pre-check (grep -rlP '[\x80-\xFF]' found zero non-ASCII source files) before running convert_lib.sh, so the encoding step was a no-op as expected. 7928 .lpc files after the .c->.lpc rename; 9208 literal .c" references fixed; 284 local angle-bracket includes converted to quotes; 214 files touched by the static->nosave sweep. One collision from the static->nosave sweep, per AGENTS.md \S4.3's known class: u/l/lysander/workroom.lpc had a "static": "..." mapping key (a help-topic word, not a path) that the sed rewrote to "nosave":...; reverted by hand. No #define nosave static/#define protected static compatibility-shim collision found. All headers resolve through a single global /include/ directory (no <local.h>-style per-directory includes anywhere in this codebase), so no get_include_path() master apply was needed.

3. The master-boot-blocking crash: explode("/", "/") returns an

EMPTY array on this driver, not MudOS's two empty strings

The single highest-impact bug in this port, and the hardest to diagnose -- a completely silent crash with no error text printed anywhere, discovered only via gdb --batch -x 'catch throw' -x run.

Boot got as far as compiling master.lpc and simul_efun.lpc cleanly (warnings only), then aborted with just:

The simul_efun (/adm/obj/simul_efun) and master (/adm/obj/master) objects must be loadable.
Please check log files for exact error.

...with genuinely nothing else in debug.log, log/runtime, or stdout -- the driver's own mudlib_error_handler path apparently swallows the underlying error text when it fires this early in boot (worth a look for whoever hits this again, but not investigated further here since the crash itself was fully diagnosed another way). A gdb backtrace on the C++ exception throw nailed it exactly:

#1  error_handler (err=0x... "*Array index out of bounds.\n")
#2  error (fmt=0x... "Array index out of bounds.\n")
...
#5  apply_master_ob (fun=5 [APPLY_DOMAIN_FILE], num_arg=1)
#6  set_master (ob=...)

set_master() (part of the driver's PACKAGE_MUDLIB_STATS bootstrap, called from init_master() the moment master.lpc first loads) calls master->domain_file("/") to establish the backbone domain name. master.lpc's domain_file():

string domain_file( string filename )
{
    string *path;
    path = explode( filename, "/" );
    if( !path ) return "NONAME";
    switch( path[0] )        // <-- crashes here for filename == "/"
    { ... }
}

The original TMI-2 authors clearly wrote this expecting old MudOS's explode("/", "/") to return ({ "", "" }) (or at least some non-empty array) -- path[0] would then safely be "", falling through to default: return "NONAME";. This driver's sane-explode semantics instead return a genuinely empty array for that input, so path[0] throws Array index out of bounds. immediately, before master_ob is even fully live, aborting the whole boot. author_file() has the exact same latent defect (same if (!path) return... guard that never checks sizeof(path)), though it happens not to be reachable with an empty-array input from this driver's own bootstrap call (it's invoked with the master file's own path, which always explodes non-empty). Fixed both defensively:

if( !path || !sizeof(path) ) return "NONAME";

This is the same underlying "SANE_EXPLODE_STRING empty-array edge case" bug class flagged in this project's general checklist, just hitting a master-apply chokepoint instead of ordinary mudlib code, and therefore fatal to the entire boot rather than a single feature.

4. ref is a reserved word on this driver (__REF_RESERVED_WORD__)

Confirmed via the compiler source (lexer_utils.cc: {"ref", L_REF, 0}) and this driver's own advertised feature list (__REF_RESERVED_WORD__ in its startup dump) before touching anything. Four sites used ref as a plain identifier (parameter or local variable name):

cmds/object/_ref.lpc (a genuine "ref" command implementation) was checked and does NOT hit this bug -- it only uses ref_name and the quoted string "ref", never a bare ref identifier.

5. inherit textually after global variables -- four independent

instances, one with severe cascading impact

The classic \S6.1 dialect bug ("Illegal to inherit after defining global variables"), but this codebase has it as a recurring authoring habit rather than a single shared-header bug -- found independently in at least 4 completely unrelated files, plus ~200 more scattered across individual non-preloaded personal content files (see \S8 below):

6. std/user/tsh.lpc: private inherit + qualified super-call

(Name::func()) is rejected by this driver even from the SAME file that declared the private inherit

A new driver-dialect finding for this project's catalog. tsh.lpc does private inherit STACK_ADT; (a directory-stack push/pop/query ADT) and then, from within its own pushd()/popd()/dirs()/ initialize_tsh() functions, calls the base class's functions via the qualified super-call syntax: stack::push(path), stack::pop(), stack::query_stack(), stack::alloc(pushd_size). This is the idiomatic way to reach a privately-inherited base's functions and compiles fine on many LPC drivers -- but this driver's compiler (compiler.cc's function-lookup logic) rejects ANY qualified Name::func() call to a function whose inherit was declared private, unconditionally, with no special case for "the caller is the very file that declared the private inherit":

if (tmp == -1 || (ip->type_mod & DECL_PRIVATE)) {
  yyerror("Called function is private.");
  goto invalid;
}

Confirmed by compiling tsh.lpc standalone (not just as part of a larger inheritance chain) -- identical 4 errors either way, ruling out any interaction with how tsh.lpc itself later gets inherited into std/user.lpc via include/user.h. Fixed by dropping the stack:: qualifier -- privately-inherited functions remain directly (unqualified) callable from within the declaring file on this driver, so push(path), pop(), query_stack(), alloc(pushd_size) resolve correctly without the qualifier. (A separate, pre-existing dangling reference in the same file -- history::hist_alloc(hist_size), where no HISTORY_ADT is ever inherited at all -- was investigated and left alone: this driver does NOT flag it as a compile error at all for reasons not fully tracked down, and a code comment in the same block ("According to Beek, the next 2 lines should go, but that kills history completely") shows the original authors already considered this code suspect; not a driver-compat blocker either way, so left as pre-existing dead-ish content per policy.)

7. Master's valid_write()/valid_read() bare-equality fallback

never matches the driver's extension-appended filename (\S7.49)

Exactly the catalogued bug class. master.lpc's valid_write()/ valid_read() fall back to file == data_file(user) || file == user_data_file(user) when the group/path-based ACL doesn't otherwise grant access -- but data_file()/user_data_file() both return the BARE path (no extension), while this driver's save_object()/restore_object() efuns pass the FULL filename (.o already appended) to the master apply. The two never matched, so every player's own save silently failed ACL the moment a real save fired. This didn't show up at boot or at look/score -- it first fired at the password-confirmation step of brand-new character registration (std/connection.lpc's set("password", ...) triggers an autosave via save_data_conn()), with the exact symptom AGENTS.md predicts: *Denied write permission in save_object(). Fixed both functions:

return( file == data_file( user ) || file == data_file( user ) + ".o" ||
  file == user_data_file( user ) || file == user_data_file( user ) + ".o" );

8. THE most severe bug: exec()'s ACL wrapper's deep_inherit_list()

check silently denied EVERY new character's connect(), forever

The single most severe bug found in this port -- it silently blocked 100% of new character registrations from ever completing, with no compile error, no crash, and no log entry of any kind; only a generic mudlib message ("Error connecting to your body... try again...") that looked at first glance like ordinary content/flavor text.

std/connection.lpc's connect() (called as the very last step of character creation, from newuserd.lpc's finish_up3()) does:

if (!exec(body_ob, this_object())) {
    write("Error connecting to your body... try again...\n");
    body_ob->remove();
    remove();
    return 0;
}

exec here isn't the raw driver efun -- it's shadowed by a mudlib ACL wrapper in adm/simul_efun/overrides.lpc:

nomask int exec(object to_obj, object from_obj) {
    ...
    New = base_name( to_obj );
    if( New != CONNECTION && member_array("std/body.lpc",
        deep_inherit_list( to_obj ) ) == -1 )
        return 0;
    ...
}

The intent is clear: allow exec() onto the player's own connection object, OR onto anything that inherits std/body.lpc (i.e. a real player body). But this driver's deep_inherit_list() efun (array.cc) always prepends a leading slash to every returned program name (add_slash(pr->filename)), returning "/std/body.lpc" -- never the bare "std/body.lpc" the mudlib code checks for. Since body_ob for a brand-new character is never itself equal to CONNECTION, this member_array() call ALWAYS returned -1, and exec() ALWAYS returned 0 -- every single new character's connect() failed, body_ob and the connection both got destructed, and the game silently dropped the player with a plausible-sounding "try again" message that, if actually retried, would fail exactly the same way every time.

Diagnosed by temporarily instrumenting connect() to print interactive(this_object()) / interactive(body_ob) / the literal return value of exec() (confirmed: both preconditions for a *successful* exec() held -- this_object() interactive, body_ob not yet interactive -- yet exec() still returned 0), which ruled out the driver's own exec() efun and pointed straight at a mudlib override; grep -n "^int exec\|nomask int exec" then found it in one shot. Fixed the one-line string comparison:

if( New != CONNECTION && member_array("/std/body.lpc",
    deep_inherit_list( to_obj ) ) == -1 )
    return 0;

Verified with a fresh full registration end to end (see \S10) -- character creation completes, connect() succeeds, the new player lands in the real game world instead of being silently dropped.

9. Smaller fixes

10. Admin account seeding

Registered fluffos/Mud@2026 through the real, full registration flow (name -> confirm -> password -> confirm password -> email -> real name -> stat/race/gender/alignment menu -> "done" -> confirm) exactly like any other new character, landing in the real starting room (The YMAA, Decatur) with look/score/who all producing correct output, then a clean quit that closes the connection.

Granting admin here needed two separate, independent steps, neither of which alone was sufficient -- worth recording since this is a new bug-class shape for this project's \S1.5 catalog:

1. File-permission ACL (adminp() / write access): this codebase gates adminp() through master()->query_member_group(name, "adminaccess"), backed by the DATA file adm/etc/groups ((adminaccess) (admin):spooner). Added fluffos to that line ((adminaccess) (admin):spooner:fluffos) -- this alone grants full read/write ACL everywhere (adm/etc/access's (/) line grants (adminaccess)[rw]), confirmed live via update <path>. 2. Wizard command-set / rank flag: file-permission ACL is completely separate from whether the *in-game command dispatcher* ever offers wizard commands at all. That's gated by a wizard property on the CONNECTION save object (checked by std/body.lpc's init_setup(): if ((int)(link->query("wizard"))) { set("wizard", 1, MASTER_ONLY); enable_wizard(); }, called on EVERY login via logind.lpc's enter_world() -> user->BODY_OB-> setup(0) -> init_setup(0), not just first-time creation) plus the body's own PATH property (which command directories are searched). The mudlib's own normal promotion path (cmds/adm/_makewiz.lpc) requires an ALREADY-active admin to run it in-game -- but the archive's only pre-existing admin accounts (cyanide, chronos) have real, unknown 1999-era passwords, and AUTO_WIZHOOD (a global "everyone who registers becomes a wizard" switch) is #defined then immediately #undefed in config.h (deliberately disabled by the original authors -- flipping it back on would be a content/behavior change affecting every future registration, out of scope here). Instead, replicated exactly what _makewiz.lpc itself does, directly on the save DATA: appended wizard 1 to data/std/connection/f/fluffos.o, and in data/std/user/f/fluffos.o changed the body's saved "wizard":0 to "wizard":1 and "PATH":"/cmds/std" to "PATH":"/cmds/std:/cmds/object:/cmds/xtra:/cmds/file:/cmds/wiz" (the exact NEW_WIZ_PATH macro value _makewiz.lpc would have assigned). Verified live end to end on the NEXT login (not the same session): wizard news section appears, who shows the [NEO] alignment marker, login correctly routes to "The Immortal's Hall" (the wizard start room, itself needing the \S5 wizrm.lpc fix to even load), and update /std/room succeeds with "Updated and loaded." -- this project's canonical read+compile ACL smoke test.

11. Compile sweep results and known remaining gaps

lpcc_check.sh's full batch sweep, after the fixes above: 6246 pass / 1682 fail out of 7928 total .lpc files (78.8% pass rate). Given this archive's real composition -- an entirely orphaned ~870-file dead subtree (\S1) plus roughly 13,000 files of genuine, messy, multi-author 1997-2000 wizard-home content including abandoned drafts, truncated experiments, and personal scratch files -- this is consistent with (proportionally messier than, but the same *kind* of gap as) the handful of non-boot-critical failures documented on smaller, cleaner archives elsewhere in this collection. None of the remaining failures are preloaded or reachable from the standard registration/gameplay path exercised in \S10. Breakdown of the remaining 1682:

12. Genuine outbound network connection at boot -- do not sweep into

automated reboot loops

adm/daemons/channels.lpc (preloaded by default) calls I3_DAEMON->query_chanlist() from its own create(), which auto-loads adm/daemons/network/I3.lpc. That file's create() unconditionally does:

router_socket = clone_object(SOCKET, SKT_STYLE_CONNECT_M,
                             I3_ROUTER, (: read_callback :),
                             (: close_callback :) );

I3_ROUTER is "205.218.122.131 9000" -- a real, hardcoded public IP from the Intermud-3 network circa 1999-2000 (almost certainly long dead by now, but the attempt itself is real and unconditional on every boot; a live ss/ping check during this port's own test boots found no established connection or SYN-SENT socket, consistent with that address being unreachable today, not with the mudlib refraining from trying). Also opens a local listening socket on __PORT__ + 3 (40234 for this port assignment) for OOB_TCP. This is genuine, intended upstream functionality (the same class of finding as imud and dsI elsewhere in this collection), not a bug to patch away -- but per this project's established convention, mortremains should not be swept into high-frequency automated re-boot/deep-test loops the way most of this corpus safely can be, since each boot is a real outbound network event, even if the far end is currently unreachable.

13. Verification performed

深度功能测试 / Deep functional test (round two, AGENTS.md \S10.7)

First real *playthrough* pass on this lib (the onboarding session above only verified registration + look/score/who/quit + the admin promotion). Read help newbie first (points at "the fellow in the center of town" for starter equipment/leaflet/map, and help classes names Alucard at the Intersection as the class-hall directory), then played a full, continuous session as a real English-named character (Bramwell) on the native driver, using a second admin connection (fluffos/Mud@2026) only for updates and diagnosis. Found and fixed four independent, severe, live-blocking bugs plus one duplication bug -- collectively the newbie helper, ~95% of ordinary room-to-room movement, and two of the five advertised class halls were all unreachable before this pass, none of it visible to lpcc_check.sh, a clean boot, or the earlier registration-only smoke test.

1. cmds/std/_go.lpc's extension-check slice is 2 characters, not 4 -- blocks essentially every ordinary room exit whose destination path literal already includes .lpc

The single most severe bug found in this pass. cmd_go() (the function underlying every directional movement command -- north, east, etc. all alias to go <dir>) does:

fn = exits[dir];
if (fn[<2..<1] != ".lpc") {
    fn += ".lpc";
}
if (file_exists(fn)) {
    to = find_object_or_load( exits[dir] );
    ...

fn[<2..<1] is only the LAST TWO characters of the path, which can never equal the 4-character string ".lpc" -- so the append always fires. If the room author's own exits mapping literal already ends in .lpc (confirmed the overwhelming majority convention in this archive: 4597 grepped exit-value literals include .lpc explicitly, vs. 225 that don't), fn becomes a doubled ...lpc.lpc path that file_exists() correctly reports as missing, so cmd_go() prints "The mists of creation are still active in that direction" and the move silently fails -- indistinguishable from an actually-unbuilt room. Reproduced live: moving east from the very first room (startroom.lpc's only non-wizard-gated exit, to d/Prime/Central/room/road0_0.lpc, the Intersection) failed this way immediately after registration; south from the same room happened to work only because it points at WIZHALL (/d/Ancients/rooms/wizrm, no .lpc suffix in the macro) and is wizard-gated besides, which is exactly why this was invisible to the earlier onboarding pass (it never actually walked a second room).

This is very likely a port-introduced regression, not a pre-existing 1997 bug: the archive's real extension was .c, and this project's own convert_lib.sh does a blanket literal ".c"->".lpc" string rewrite as a normal part of conversion (per \S2 above, "9208 literal .c\" references fixed"). The original line was almost certainly fn[<2..<1] != ".c" -- a correct 2-character check against the original 2-character extension -- and the automated rewrite updated the string literal but had no way to know it also needed to update the slice-length arithmetic next to it. This is the exact bug class catalogued in AGENTS.md \S7.118 ("hardcoded 2-character-extension math left over from the archive's original naming"), just manifesting as an "always append a redundant extension" checking bug rather than that section's "always corrupt a derived key" stripping bug -- worth adding to that section's watch-list shape. (A cosmetically-identical but functionally harmless sibling of this exact slice-vs-literal-length mismatch was already documented pre-existing in this same lib's own \S11, in cmds/object/_update.lpc -- that one predates conversion and never bit because players conventionally omit the extension when typing update.)

Fix: fn[<2..<1] -> fn[<4..<1] (the last 4 characters, correctly matching the 4-character .lpc string). Verified live: after update /cmds/std/_go, east from the Intersection correctly loaded and entered road0_0.lpc, and the entire pit-fighter-guild navigation route below (10 more moves) worked cleanly end to end.

2. Newbie-helper NPC u/c/chronos/alucard.lpc -- and, as a direct consequence, the room that clones him -- both failed to load at all

Alucard is the exact NPC help newbie/help classes point players at for starter equipment and class-hall directions, cloned into d/Prime/Central/room/road0_0.lpc ("the Intersection", one room east of the start room) via reset(). His file has the classic \S5 "inherit after global variables" defect (string *PUNC; declared before inherit MONSTER;), the exact same bug class as 4 other individually-fixed instances documented in \S5 above. What's new here: because road0_0.lpc's own create()/reset() calls clone_object("/u/c/chronos/alucard.lpc"), and this driver propagates an inherited file's compile error up through the caller, the room itself failed to compile and load (confirmed identical in lpcc_fail.log) -- so this one inherit-ordering typo alone took out both the newbie helper AND the entire Intersection room (compounding with bug \S1 above, since bug \S1's "mists of creation" symptom was the FIRST thing masking this one until \S1 was fixed and east actually attempted to load the destination).

Fix: moved inherit MONSTER; above string *PUNC; (same pattern as \S5). Verified live end to end: after updateing both the file and the room, Bramwell walked east into the Intersection, Alucard was present, say help triggered his process_say()/help() handler, and he handed over the real starter kit (boots, leaflet, bat, armour clones) exactly as the newbie doc promises.

3. Two of the five class halls Alucard advertises had the identical \S5 defect, blocking join entirely

d/class/warrior/pit_fighter/rooms/pitfighterguild.lpc and d/class/warrior/Sharpshooter/room/SS_guild.lpc -- two independently copy-pasted files sharing the same header comment ("Pit pit fighter's guild ilz code ... Cyanide corrupted this room") -- both declare mapping skills = ([]); mapping min_levels = ([]); int *v_skills, ... before inherit GUILD;. Reproduced live: walking the pit-fighter-guild route Alucard gives ("5 south, 2 east, 2 south, and 1 west" from the Intersection) hit *No program in object '/d/class/warrior/pit_fighter/rooms/pitfighterguild'! on the final step. Per AGENTS.md \S6.4/this lib's own \S11 guidance (fix shared-root-cause bugs found live, don't blanket-sweep the ~200 scattered non-preloaded instances), both were fixed individually since they sit directly on the newbie class-acquisition path this pass is meant to exercise -- the other three halls Alucard mentions (mages, thieves, priests) do NOT have this defect (checked via lpcc_fail.log before touching anything).

Fix: reordered inherit GUILD; above the global variable block in both files (same pattern as \S5/\S2 above). Verified live: Bramwell (strength 15, meets the pit fighter's base_stat/strength >= 10 gate) successfully joined, title changed to "Cage Cleaner"/"Level 1 Pit fighter", and received "A Pit Fighter's Scars" (the class marker item). The Temple of Talos (priest hall, 2e/1s from the Intersection) was also reached and correctly, gracefully rejected join for not being chaotic evil -- confirming the rejection path itself works fine and this is content gating, not a bug.

4. d/class/rogue/obj/dummy.lpc (this lib's own safe-sparring/backstab-testing dummy) had a broken array literal, AND a crash-on-clone bug hiding behind it

This file's own long description ("This is a dummy to test you backstab out on ... type halt to stop your onslaught") makes clear intent: a zero-risk training target (execute_attack() is hardcoded to always return 0, so it can never actually hurt a player), matching AGENTS.md \S10.7 point 3's "find the lib's own safe-sparring mechanism" guidance. It failed to compile at all: set("damage", ({ 0 )} ); has ) and } swapped (should be ({ 0 }) )), cascading into further spurious errors on every line after it in the same function. Fixed the bracket swap -- but doing so exposed a SECOND, real bug underneath: adm/daemons/catalog_d.lpc's get_level() (which every wizard clone invocation runs automatically, to register the new monster in the summoning catalog) does counter = ((int *)wep->query("damage"))[1]; -- unconditionally indexing element 1 of the "damage" array. Every other monster in this codebase sets "damage" as a 2-element ({min, max}) array (confirmed via alucard.lpc's ({3, 30}) and the real combat-damage code in std/body/attack.lpc, which reads both damrange[0] and damrange[1]) -- but dummy.lpc's array had only ONE element even after the bracket fix, so cloning it live threw *Array index out of bounds. (reproduced: clone /d/class/rogue/obj/dummy as admin, confirmed via /log/runtime's stack trace pointing at catalog_d.lpc:187 <- cmd_clone <- cmd_hook).

Fix: ({ 0 }) -> ({ 0, 0 }), matching the universal 2-element convention and the object's own intent (a target that deals zero damage either way). Verified live: clone /d/class/rogue/obj/dummy succeeded cleanly after the fix (correctly auto-cataloged, no error), and a full live fight (kill dummy) confirmed the safe-sparring guarantee end to end -- every one of the dummy's attacks reported "does nothing to you" while its receive_damage() correctly echoed back each hit's damage ("Dummy screeches: You just did N points of bludgeoning damage"), with zero crashes and zero risk to the player across dozens of combat rounds.

(Separately, dummy.lpc's own long text tells players to "type halt to stop your onslaught" -- halt is in fact a wizard-only command (cmds/wiz/_halt.lpc; no player-facing equivalent exists, cmds/std/cmds/object have no _halt/_flee/_stop). An ordinary player typing halt gets a plain What?. This is pre-existing 1997 flavor-text content, not something the port touched, and fixing it would mean either adding a new player command or rewriting the dummy's documentation -- a content/behavior decision, not a programming bug, so left alone and just noted here per the task's scope boundary.)

5. Class-marker items ("A Pit Fighter's Scars") permanently duplicate across disconnect/reconnect cycles -- new AGENTS.md \S7.122

Found by accident while doing the required \S10.7 step-5 quit-then-relogin check: Bramwell's i after a real quit-and-relogin cycle showed five copies of "A Pit Fighter's Scars" (only one was ever legitimately granted, via a single join). Root-caused and fixed -- full writeup, including why the fix targets the reload side rather than reordering the save/destroy calls, is in AGENTS.md \S7.122 (new entry, since this exact auto_load/compute_autoload_array/ destroy_autoload_obj/load_autoload_obj mechanism is verbatim shared TMI-2 lineage code per its own header credit to "Truilkan@TMI", and is a plausible sibling-check candidate for any other TMI-2-based lib in this corpus). One-line summary: std/user.lpc's quit paths save the character (baking any carried auto-load item into the normal serialized inventory) before removing that item from inventory, and net_dead() never removes it at all -- so a subsequent full restore recreates the item twice (once from the leftover inventory data, once from the separate auto_load re-clone list), with no dedup) on either side. Fixed in std/user/autoload.lpc's load_autoload_obj() by skipping any auto_load entry whose base_name() already matches an object already present in inventory. Verified live: a further clean quit-and-relogin on the already-corrupted Bramwell character held steady at 5 copies (did not grow to 6) after the fix landed -- confirms no further duplication, though the fix does not retroactively clean up the pre-existing 5 (left as visible evidence on the test character rather than hand-edited away).

6. Currency/economy check (per this pass's mandate to watch for the \S7.121 float-corruption shape)

Checked std/shop.lpc, std/bank.lpc, std/coins.lpc, std/pub.lpc, std/buy_only_shop.lpc, std/container.lpc, and adm/daemons/gang_d.lpc (every file referencing credits/price/ cost/gold) for the \S7.121 shape (a function doing real float arithmetic but declared to return int with no to_int()). Clean -- this lib's economy code is plain-integer throughout with no float arithmetic anywhere in the money path; std/shop.lpc's get_ob_value() in particular already guards every branch (intp()/mapp()/pointerp() checks) before returning. No instance of this bug class found.

7. Verification performed

WASM status audit (2026-09-01)

playable. Booted ~/src/fluffos/build-wasm/src via scripts/wasm_client.js -- clean boot, zero fixes needed; the preloaded channel daemon's outbound I3 socket-connect attempt (\S12) fails silently/gracefully under WASM's no-sockets build without affecting anything else. Full session verified: real TMI-2 registration flow (name, y/n confirm, password + confirm, email, real name, the 1/2/3/4/5/6/0 stat-optimization menu), landing in The YMAA (Decatur), look/score/quit all correct.