Wilderness

✅ 可玩

wilderness

更新 7daa927 2026-09-04 源码 下载 ZIP

▶ 开始游玩 · Play Now

A production game built on the LIMA mudlib framework (v1.0a9) -- the same natural-language-parser, capability-security, wizard-Unix-shell codebase already onboarded standalone as libs/lima -- but an independent, much older (May 2000) snapshot with the framework's own AUTO_WIZ demo shortcut disabled, so real characters start as ordinary mortals rather than instant wizards. New characters float in 'The Void,' whose own room text describes an inky blackness where 'a wispy tendril of smoke will waft by your face' before a circular portal that always stays fixed in front of you; stepping through it drops you into a wizard-built area such as /wiz/branin/workroom, 'a swirling maelstrom of color' with a meeping snake inside. Beyond the entry point, the built world spans a labyrinth, caves, a beach, an attic with two Beavis-and-Butthead-style easter-egg NPCs, a cult area with its own foyer and portal, and dozens of individually-built rooms and objects -- a sword in a stone, a bungee jump, a car wash, a working camera/photograph system, guild-guard NPCs. Character creation is an 8-attribute point-buy stat allocation plus a size choice and a short personality quiz, on top of the LIMA framework's centralized natural-language command parser, capability-based security daemon, wizard Unix-like shells, Intermud-3 support, and a modular combat system (death/health/wield/armor/pulse/blows/formula/advancement submodules). Shares Harvey Mudd College academic hosting (cs.hmc.edu) with this collection's libs/lpmud141, though it's an unrelated, much newer (MudOS v22.2-era) codebase -- unlike lpmud141, which required a from-scratch master-object reconstruction.

README

A production game built on the LIMA Mudlib -- the same natural-language- parser, capability-security, wizard-Unix-shell framework already onboarded in this collection standalone as libs/lima, but this is an independent, much older snapshot (dated May 2000) with the framework's own AUTO_WIZ demo convenience turned off. Real characters here start as ordinary mortals in a real, populated game world -- floating in "The Void" and stepping through a randomized portal into one of several wizard-built areas -- not as instant wizards in a bare-bones framework demo.

Source: wlib051100.tar.gz (https://www.cs.hmc.edu/~jhsu/wilderness/), credited in its own USAGE file to the original LIMA Mudlib authors (John Viega/Rust, Greg Stein/Deathblade, Tim Hollebeek/Beek, security system by Reimer Behrends/Ellery at TMI-2). Slug wilderness, number 949, port 40251.

Shared academic origin with lpmud141, unrelated codebase

This archive and libs/lpmud141 ("HMC LP-mud") were both hosted on Harvey Mudd College's cs.hmc.edu -- the same computer science department's LPMud hosting tradition spanned at least two very different eras and codebases. lpmud141 is a mid-1990s archive so old its bundled driver predates the "master object" security architecture altogether (a from-scratch master.lpc/simul_efun.lpc had to be authored just to run it on a modern driver -- see that lib's own NOTES.md). Wilderness, by contrast, is a genuine MudOS v22.2-era game with a full, standard master-object security daemon out of the box -- no architectural reconstruction needed, just the usual driver-dialect porting work (see this lib's own NOTES.md for the details, including a genuinely infinite-recursion bug found and fixed in a stringify-macro misuse, and a driver compile-flag conflict shared with -- but distinct from -- libs/lima's own).

Status

Boots clean under a dedicated FluffOS build (see "Local run" below -- this lib needs different driver compile flags than every other lib in this collection, including libs/lima). 853 of 887 .lpc files pass a full lpcc_check.sh batch compile sweep; the remainder are confirmed batch-tool artifacts, one suspected driver-level (not mudlib) struct- naming bug in an unreachable file, and pre-existing incomplete/dead archive content (an unfinished drink-themed room, two easter-egg "attic" NPCs, an optional contrib bulletin-board module, etc.) -- full per-file detail in NOTES.md.

Verified live end to end with a real driver and a raw socket client: registering a brand-new mortal character (8-attribute point-buy stat allocation, a size choice, a short personality quiz), arriving correctly lit and described in the starting room, stepping through its portal into a real, furnished part of the world, moving between rooms, look/score/inventory, quit, and reconnecting into the same saved character afterward. A from-scratch admin account (fluffos/Mud@2026, this project's standing convention) was seeded via the archive's own "promote the first character created" bootstrap and verified functional (who, admtool).

No outbound network connections observed or expected in normal play (Intermud-3/imud_d support exists in the code but isn't preloaded by default in this archive's own shipped data/config/preload).

WASM status: playable. The site serves a dedicated WASM driver (scripts/custom_drivers/wilderness/) built from ~/src/fluffos-wilderness with #define ARRAY_RESERVED_WORD. Admin login (fluffos / Mud@2026) into Grand Hall, plus look / score / quit, verified with scripts/wasm_client.js. The shared driver and the lima/swmud/spacemud binary both fail this lib's check_config. Play: https://mudlibs.fluffos.info/wilderness/

Local run

This lib requires a dedicated FluffOS build, not the project's shared default driver -- its own secure/check_config.c refuses to boot on any driver that isn't compiled with a specific flag set (a security check this codebase inherited from the LIMA framework). See NOTES.md for exactly which flags and how the dedicated ~/src/fluffos-wilderness worktree was built (a few minutes, one-time, if that checkout is ever lost). Once built:

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

Game port: 40251.

Connect and give a name (letters only) to register a new character, or an existing name to log back in; you'll be prompted for a password either way. A brand-new character walks through 8 attribute-point prompts, a size choice, and a short personality quiz before arriving in the game world.

NOTES · 移植与修复记录

Wilderness -- porting notes

Source: https://www.cs.hmc.edu/~jhsu/wilderness/wlib051100.tar.gz (dated May 2000; the archive's own USAGE file credits it as a build of the LIMA Mudlib, authored by John Viega/Rust, Greg Stein/Deathblade, and Tim Hollebeek/Beek at "Lima Bean", with the security system contributed by Reimer Behrends/Ellery at TMI-2). 2,642 files. README describes itself as "the ALPHA release of the Lima Mudlib"; include/config.h sets AUTODOC_MUDNAME "wilderness", confirming this specific archive is a real game instance built on that base, not the reference Lima Bean codebase itself. Slug wilderness, number 949, port 40251. Same cs.hmc.edu (Harvey Mudd College) academic hosting as libs/lpmud141, but an entirely unrelated codebase from a different era of that department's LPMud hosting -- no code lineage between the two.

0. This is the SAME driver-flag conflict as libs/lima, but a different snapshot with a genuinely different requirement

secure/check_config.c (present in this archive, confirming a real MudOS-era master-object architecture -- no from-scratch master.lpc needed here, unlike lpmud141) refuses to boot unless the driver was compiled with NO_LIGHT/NO_ADD_ACTION/NO_WIZARDS defined, OLD_ED/PACKAGE_UIDS undefined, plus several always-satisfied-by-this- project's-default-driver checks (SANE_EXPLODE_STRING, MUDLIB_ERROR_HANDLER, PACKAGE_CONTRIB, PACKAGE_PARSER) -- the exact LIMA-lineage flag set documented in AGENTS.md §7.46 and solved once already for libs/lima via a dedicated driver worktree (~/src/fluffos-lima).

One flag genuinely differs from libs/lima's own requirement, and this matters: libs/lima (a 2026-era fluffos/lima fork) requires #undef ARRAY_RESERVED_WORD; this 2000-era Wilderness snapshot's own check_config.c requires the OPPOSITE, #define ARRAY_RESERVED_WORD (confirmed: line-by-line diff of both check_config.lpcs shows this is the ONLY substantive difference in the whole need() list -- everything else is identical modulo formatting). The two requirements are mutually exclusive on one driver binary, so libs/lima's existing ~/src/fluffos-lima worktree could NOT be reused as-is. A second, separate worktree was built instead:

cd ~/src/fluffos && git worktree add ~/src/fluffos-wilderness <same commit as fluffos-lima> --detach

src/local_options edited identically to fluffos-lima's own changes (NO_ADD_ACTION/NO_WIZARDS/NO_LIGHT from #undef to #define, OLD_ED from #define to #undef), PLUS one more line neither fluffos's nor fluffos-lima's local_options had touched:

/* wilderness (Lima-lineage, 2000-era snapshot) requires this DEFINED --
 * the opposite of libs/lima's modern fork, which requires it undef'd. */
#define ARRAY_RESERVED_WORD

(ARRAY_RESERVED_WORD isn't exposed in local_options.h's own comment block at all -- it's a base/internal/options_internal.h default [#undef] that local_options is free to override, since options_incl.h includes options_internal.h BEFORE local_options.)

Built with the identical CMake flags libs/lima used (-DCMAKE_BUILD_TYPE=Debug -DMARCH_NATIVE=OFF -DUSE_JEMALLOC=ON -DSTATIC=OFF -DPACKAGE_UIDS=OFF), targets driver+lpcc. This lib requires ~/src/fluffos-wilderness/build-debug/{src/driver,src/lpcc} -- neither the project's shared default driver nor ~/src/fluffos-lima will boot it (check_config.lpc's ARRAY_RESERVED_WORD check alone guarantees exactly one of libs/lima/libs/wilderness fails on any single shared binary). Like the fluffos-lima worktree, this one lives outside libs/wilderness/ and outside git version control -- rebuild from the steps above if this checkout is ever lost (a few minutes, one-time).

scripts/lpcc_check.sh's hardcoded LPCC=~/src/fluffos/build-debug/src/lpcc path was changed to LPCC="${LPCC:-$HOME/src/fluffos/build-debug/src/lpcc}" (env-var overridable, same default when unset) so this and any future LIMA-lineage lib can point it at their own special lpcc without editing the shared script every time: LPCC=~/src/fluffos-wilderness/build-debug/src/lpcc bash scripts/lpcc_check.sh libs/wilderness/config.fluffos libs/wilderness/work.

Practical implication for AGENTS.md §7.46: that entry's "out of scope for now" verdict (citing sgzmudsgz, since purged) is stale -- libs/lima already proved the flag conflict is solvable with a dedicated worktree, and this lib proves the fix generalizes to a second, independently-configured LIMA-lineage snapshot with a genuinely different flag requirement. §7.46 has been updated accordingly.

1. Conversion

Pure ASCII archive (LC_ALL=C grep -rlP '[\x80-\xFF]' found zero non-ASCII files among 2,642) -- scripts/convert_lib.sh ran with UTF-8 as the source encoding (skip transcode, rename + reference-fixup only), matching libs/lima's own precedent for an already-ASCII archive. 887 .c files renamed to .lpc, 163 literal .c" references fixed, 11 local angle-bracket #includes converted to quotes, 6 files hit by the static->nosave sweep.

2. ARRAY_RESERVED_WORD's bare array produces TYPE_UNKNOWN, not TYPE_ANY -- breaks any double-indexing or strict-typed use, ~285 files affected corpus-wide

The single largest bug class in this lib, and worth cataloging in detail since ARRAY_RESERVED_WORD is a real, still-supported driver feature (not a legacy-only quirk) that could recur in any future LIMA-lineage or other classic-MudOS archive.

Root cause (confirmed by reading this driver's own compiler source, not by guessing): grammar.y's basic_type: opt_atomic_type L_ARRAY production computes the array's element type from opt_atomic_type, and opt_atomic_type's empty alternative (grammar_rules_types.cc:rule_opt_basic_type_empty()) returns TYPE_UNKNOWN (0) -- NOT TYPE_ANY (1) as a surface reading of the grammar comment ("defaults to TYPE_ANY") would suggest. So a BARE array declaration (no preceding element type, e.g. array x = ..., as opposed to int array x / string array x which supply an explicit opt_atomic_type) compiles to TYPE_UNKNOWN | TYPE_MOD_ARRAY. Indexing it once (x[i]) hits the default: branch of rule_primary_expr_index()/rule_expr_index() (grammar_rules_exprs.cc), which strips the array bit and yields bare TYPE_UNKNOWN for the element -- and a SECOND index on that result (x[i][j]), or passing it to a strictly-typed parameter, or an argument-type check, then falls through to type_error("Value indexed has a bad type ", TYPE_UNKNOWN) (prints as "unknown") or a Bad type for argument/Bad type of argument used for range error, because TYPE_UNKNOWN doesn't match any of the switch's other cases the way TYPE_ANY would have.

On the classic MudOS driver this archive actually shipped for, bare array was almost certainly equivalent to today's mixed * (freely re-indexable, no static-type friction) -- this archive uses the idiom pervasively (~285 real declarations across the whole tree: local variables holding unique_array()/explode()/similar results, function parameters, varargs/... collector parameters, function return types) with the clear intent of "array of anything, don't care what's inside, may itself contain nested arrays." This driver's stricter, TYPE_UNKNOWN-yielding interpretation of the SAME keyword is a genuine dialect gap, not a mudlib bug -- exactly the kind of driver-compat issue this project fixes proactively.

Fix: mechanical sweep changing every bare array type token (in an actual declaration position -- function return type, parameter type, or local variable type; never inside a comment or string literal) to mixed *, which is TYPE_ANY | TYPE_MOD_ARRAY and behaves exactly as intended (freely re-indexable, passes any argument-type check expecting mixed/mixed *). Already-typed forms (int array, string array, object array, class Foo array) were left untouched -- they already carry a real, non-TYPE_UNKNOWN element type and aren't affected by this bug.

Implementation: a small Python script (scratchpad/fix_array_type.py, not committed -- throwaway tooling) walked every .lpc/.h file, tracked block-comment state across lines, and replaced \barray\b tokens NOT immediately preceded by (int|string|object|mixed|mapping|float|function|buffer|class NAME) with mixed *. First pass had a real bug, caught by manual audit before trusting the result: the script's string-literal handling was incomplete (it correctly avoided misreading ////* */ comment delimiters found INSIDE a string, but never actually excluded the string's OWN contents from substitution) -- 5 genuine string literals containing the word "array" as legitimate text got corrupted into "mixed *" mid-sentence (secure/daemons/imail_d.lpc's "BCC list should be an array" error message, secure/daemons/lpscript_d.lpc's two code-generation strings that literally emit the text "array patterns = ({ " as LPScript source, include/driver/type.h's #define ARRAY "array" string constant, and trans/obj/dataedit.lpc's case "array": switch label). Found by re-running the same sweep against a freshly-reconverted pristine copy and diffing every changed file's full content (not just spot-checking), then grep-verified corpus-wide for any "[^"]*mixed \*[^"]*" pattern (i.e., "mixed *" appearing inside a quoted string, always wrong) -- exactly 5 hits, all fixed by hand back to their original text. A second full-tree audit after the fix (masking already-typed prefixes and checking for any REMAINING bare-array match in a real code position, ignoring the same 5 confirmed-legitimate string literals) found zero further instances. Also caught in the same sweep: two domains/std/2.4.5/-tree files (domains/std/2.4.5/obj/tmp_leo.lpc, domains/lpscript/tmp_harry.lpc) whose array patterns/array num declarations were somehow NOT converted by the sweep's first real run against the live work/ tree despite matching the same pattern (root cause not fully pinned down -- possibly a tail -N truncation in this session's own review of the sweep's file list hid them from an earlier verification pass, not a flaw in the sweep logic itself, since a later masked full-tree scan found and fixed them cleanly); fixed identically by hand once found via the same audit.

Verified: lpcc_check.sh failure count dropped from 285/887 to 144/887 after this one fix alone -- the dominant bug class in this lib by a wide margin.

3. Same LIMA-lineage .c->.lpc extension-width bugs libs/lima already found, PLUS new instances in files lima's own conversion didn't touch

libs/lima's own NOTES.md documents [0..<3]/[<2..]-style fixed 2-character slices (correct for stripping .c, wrong for .lpc) in cmd_d.lpc, spell_d.lpc, and quest_d.lpc. This archive -- an older, independent LIMA snapshot, not derived from libs/lima's modern fork -- has the exact same idiom, confirming it's baked into the LIMA lineage itself rather than being fork-specific drift. All of libs/lima's three known files needed the identical fix here too, PLUS four more instances libs/lima's own sweep never encountered (either because that fork's cmd_d.lpc/etc. genuinely differ, or because this session searched more exhaustively):

One genuinely NEW instance in this archive that has no libs/lima analog at all: contrib/http_connection.lpc:93's file[<1..<2] == ".lpc" uses a BACKWARDS range (<1 = last-char index, <2 = second-to-last -- since <1's underlying index is numerically LARGER than <2's, [<1..<2] is an empty/invalid range regardless of what string length or extension width is involved). This is a pre-existing authoring bug unrelated to the .c->.lpc rename (it would have been equally broken checking for the 2-character .c too) -- left unfixed, consistent with this project's "don't guess at content intent" policy; contrib/ is optional bonus code and http_d isn't preloaded anyway.

4. secure/simul_efun/string.lpc's chr(0) crashes create() -- this driver rejects embedding a literal NUL byte via sprintf("%c", 0)

create() builds a 256-entry ascii lookup table (for(i=0; i<256; i++){ if(i==0)ascii+=" "; ascii += chr(i); }, where chr(i) is this lib's own sprintf("%c",i) simul_efun) later sliced up into case-conversion tables (_lower/_upper/_swapcase). The author's own if(i==0)ascii+=" " line shows they already knew byte value 0 needed a substitute placeholder (a literal NUL inside an LPC string is problematic on C-string-based drivers) -- but the very next unconditional statement, ascii += chr(i), still ran for i==0 anyway and called chr(0) regardless, which THIS driver's sprintf("%c", ...) outright rejects: *(s)printf(): Incorrect argument to type %c, must be valid UTF8 char. (arg: 0), thrown from inside simul_efun's own create() -- fatal at boot (The simul_efun ... object must be loadable), before master.lpc even attempts to load. This is also a genuine PRE-EXISTING bug independent of the driver crash: the original code appended BOTH the placeholder space AND (before crashing here) the real chr(0) for i==0, which would have made ascii 257 characters long instead of 256 and shifted every subsequent index by one relative to its real byte value -- i.e. the intended fix (skip the real chr(0) call entirely when substituting the placeholder) also happens to fix a latent indexing bug, not just the crash.

Fixed with an else, so i==0 gets ONLY the placeholder and chr(0) is never actually invoked:

for(i=0; i < 256; i++){
   if(i==0)
	ascii+=" ";
   else
	ascii += chr(i);
}

5. secure/master.lpc's virtual-object compile fallback + a STR()-vs-STRINGIZE() stringify-before-expand macro bug together produce genuine infinite recursion, not just a cosmetic path

secure/master.lpc's compile_object(path) implements a classic virtual-object fallback: walk UP path's directory components looking for a real .lpc ancestor file, and if found, call ancestor->virtual_create(remaining_suffix) via call_other(). This is normal, working LIMA-lineage code -- the bug is what path gets fed into it.

std/classes/event_info.lpc and include/combat_modules.h's MODULE() macro both used this header's OWN STR(y) (#define STR(y) #y -- a raw C-preprocessor stringify, which does NOT macro-expand its argument first, per standard #-operator semantics) on BLOW_MODULE, itself a macro (#define BLOW_MODULE simple, selected by include/combat_modules.h's own config). STR(BLOW_MODULE) therefore stringifies the LITERAL TOKEN TEXT "BLOW_MODULE", not its expansion "simple" -- inherit __DIR__ "event_info/" STR(BLOW_MODULE); compiles to inherit "/std/classes/event_info/BLOW_MODULE";, a path that never corresponds to a real file (the real files are event_info/simple.lpc / event_info/types.lpc). The header's OWN author already anticipated this exact trap and defined the correct two-level-indirection fix right next to the broken one -- #define STRINGIZE(z) STR(z) (the extra macro layer causes z to be expanded as a normal macro argument BEFORE STR stringifies it, the standard C idiom for "stringify the expansion, not the token") -- and even used STRINGIZE(BLOW_MODULE) correctly in a SIBLING file (std/adversary/blows/base.lpc). event_info.lpc and the MODULE() macro itself just used the wrong one of the two.

This isn't merely a wrong/dead path, though -- it's genuinely infinitely recursive, confirmed by instrumenting compile_object() with a temporary write_file() debug line and reproducing live: walking up from the bogus path /std/classes/event_info/BLOW_MODULE finds /std/classes/event_info.lpc (the file containing the broken inherit line itself!) as the nearest real ancestor, so the driver tries to call_other() into /std/classes/event_info to find its virtual_create function -- which requires COMPILING /std/classes/event_info.lpc first -- which re-triggers resolving its OWN unresolved inherit "/std/classes/event_info/BLOW_MODULE" -- which calls compile_object() again for the exact same bogus path -- forever. 295 identical debug-log entries for the one path confirmed this before the fix. The recursion eventually trips this driver's hardcoded CFG_MAX_CALL_DEPTH (150, a compile-time C++ array bound, NOT the runtime-configurable maximum call depth setting) and aborts with Too deep recursion., repeatedly, every time ANYTHING tries to load a combat-capable object (i.e. constantly, since std/adversary.lpc inherits event_info-consuming modules) -- surfaced first as spurious noise during boot (8 occurrences while preloading unrelated daemons) then, far more consequentially, as a hard block on creating the very first player body (std/body.lpc -> std/adversary.lpc -> event_info), immediately followed by a cascading *Bad argument 1 to EFUN call_other() ... Got: int(0) once the recursion aborted mid-setup and left the body in a broken half-initialized state. No character could ever finish logging in before this fix.

Fixed by changing both misuses to STRINGIZE():

// include/combat_modules.h
#define MODULE(x, y) SUBDIR x "/" STRINGIZE(y)   // was STR(y)
// std/classes/event_info.lpc
inherit __DIR__ "event_info/" STRINGIZE(BLOW_MODULE);   // was STR(BLOW_MODULE)

Corpus-wide grep confirmed these were the ONLY two STR(...) call sites in the whole tree not already using the correct STRINGIZE(...) form.

A related, initially-misleading config workaround that turned out to be unnecessary: before root-causing the above, inherit chain size was raised from 30 (borrowed from libs/lima's own config) to 300 in config.fluffos, reasoning that this archive's much larger combat-module composition (std/adversary.lpc alone inherits 8 MODULE()/SUBDIR combat submodules on top of std/body.lpc's own ~20) might legitimately need a higher num_objects_this_thread budget than Lima's own simpler body class. This DID stop the original Inherit chain too deep: > 30 symptom, but only by giving the SAME underlying infinite recursion 10x more iterations before hitting the (also-configurable) inherit-count ceiling instead -- it then surfaced as Too deep recursion against the driver's separate, non-configurable 150-deep C++ call-stack limit instead. The 300 value was kept in the final config regardless (this archive's combat system genuinely does load more distinct files per player-body creation than Lima's own simpler composition, confirmed by the corrected boot completing cleanly with zero Inherit chain too deep noise even after the real recursion bug was fixed), but it was NOT the actual fix -- worth flagging explicitly so a future reader doesn't mistake a symptom-mitigating config bump for the real root cause the way this session initially did.

6. private protected -- both access modifiers on one declaration, illegal here (AGENTS.md §7.48 sibling)

std/modules/m_exit.lpc:306: private protected string eval_dest(mixed arg) -- this driver hard-errors on Multiple access modifiers for ANY two-modifier combination among private/protected/public; the classic driver apparently tolerated (or silently resolved) stacking them. Confirmed as genuine original archive content (byte-identical in raw/, not a conversion artifact). eval_dest() is called from a SIBLING file that inherits m_exit.lpc (std/modules/m_exit_obj.lpc), i.e. exactly the §7.48 "inherited-private" shape this project already catalogs -- and protected (which permits inherited access, unlike a strictly-enforced private) is clearly the intended single modifier here. This was the ONLY private protected/public private/etc. dual-modifier instance anywhere in the whole tree (corpus-wide regex grep after the fix confirms zero remaining). Fixed by dropping private, keeping protected.

7. Same ENSURE(x /* comment */) inline-block-comment-in-macro-argument driver bug libs/lima already found, same file even

libs/lima's NOTES.md documents this driver's real preprocessor bug: a /* ... */ block comment written INSIDE a macro call's argument list breaks macro expansion entirely (Undefined function ENSURE, cascading into unrelated syntax errors). This archive hits the EXACT SAME macro in the EXACT SAME FILE (daemons/spell_d.lpc's register_spell(), ENSURE(spell_name /* You didn't provide a spell name. */); and the following line) -- unsurprising, since ENSURE/assert.h is shared LIMA-lineage infrastructure, but confirms this driver bug (not yet fixed upstream, not in this project's scope to fix at the driver level) is a recurring hazard for ANY future LIMA-derived archive. Fixed with the same equivalent, meaning-preserving rewrite libs/lima used -- move the comment OUTSIDE the macro call:

ENSURE(spell_name); /* You didn't provide a spell name. */
ENSURE(valid_spell_name(spell_name)); /* Your spell name wasn't valid. */

This one fix unblocked daemons/spell_d.lpc itself plus the 5 spell files that fail to compile whenever their base daemon does (domains/std/spells/{unlock,test,stock-mage/std_mage_spell, stock-mage/frotz}, daemons/spell_d -- confirmed via lpcc_check.sh's before/after failure list).

8. Too-narrow declared parameter types where the function body/doc comment already documents wider real usage (recurring pattern, libs/lima's own §2 bug #2 precedent, 4 more instances found here)

libs/lima's own NOTES.md documents exactly this shape once (set_droppable(int g) declared too narrow when the body already branches on functionp(g)||stringp(g) and the doc comment says so). This archive has the SAME root shared-module file (m_gettable.lpc) -- confirming this is a LIMA-lineage-wide authoring habit (declare the common case, let the body handle the documented wider contract, and never notice the mismatch until a driver enforces static argument types) -- plus three more independent instances found by the same "declared type narrower than the function's own body/doc comment" read-through:

9. std/book.lpc's remove_contents(): foreach loop ignores its own loop variable

remove_contents(string array headings...) { foreach(string heading in flatten_array(headings)) remove_synonym(headings); } -- passes the WHOLE outer array headings to remove_synonym() on every iteration instead of the per-iteration loop variable heading. A genuine pre-existing authoring typo (would have behaved wrong -- silently removing nothing useful, or erroring on the type mismatch depending on driver leniency -- on the original driver too), only surfaced now because this driver's stricter remove_synonym(string syn) argument type-check turns the always-passing-an-array mistake into a hard compile error instead of a silent runtime no-op. Fixed: remove_synonym(heading).

10. std/modules/m_exit.lpc's per-direction accessors dereference exits[direction] unconditionally -- crashes any room that queries/sets an enter/exit message, description, or check for a direction that was never added via set_exits()/add_exit()

16 call sites across 13 functions (query_enter_msg, set_enter_msg, add_enter_msg, remove_enter_msg, list_enter_msgs, query_exit_msg, set_exit_msg, add_exit_msg, remove_exit_msg, list_exit_msgs, query_exit_description, set_exit_description, set_exit_check) all did exits[direction]->SOMETHING with no check that exits[direction] (a class move_data, populated only by add_exit()/set_exits()) actually exists yet. When it doesn't, this is 0->SOMETHING -- *Tried to take a member of something that isn't a class., uncaught, aborting the calling object's create()/setup() entirely. (One accessor, query_exit_check(), already had its own member_array(direction,keys(exits))==-1 guard -- confirmed as the correct existing pattern, extended to the other 13.)

Live-reproduced via domains/std/Car_Wash.lpc (the connected "example room" area reachable one hop east of Grand Hall, so this is genuinely on the explorable path, not deep dead content): its setup() calls set_exits((["west":"Example_Room1"])) (registering ONLY "west") then set_enter_msg("east", "As $N $venter the car wash...") -- an "east" direction this room never actually has an exit for (the room's only real connectivity is the reciprocal "west" back to Example_Room1; there is no design reason this room needs an "east" exit at all, so this reads as leftover/copy-pasted content rather than a intended-but-broken feature. Whether the message was meant to live on Example_Room1's own "east" exit instead -- set_enter_msg's own doc comment: "This message will be displayed in the DESTINATION room" -- is exactly this kind of "can't guess original intent, don't invent new content" case this project's methodology says to leave alone).

The SAME bug, via a different trigger, also crashed EVERY object carrying a sword: std/sword.lpc/domains/std/sword.lpc/ wiz/mog/sword.lpc all called set_combat_messages("combat-sword") -- a message-type key that was never actually added to daemons/messages_d.lpc's data table (only "unarmed", "edged", "blunt", "piercing", "weapon", "poke", "fish", and "combat-torch" exist there; sibling sword-like weapons crystal_sword.lpc/broadsword.lpc/flaming_zweihander.lpc/ scimitar.lpc all correctly use "edged", and all three sword.lpc copies ALSO call set_weapon_skill("edged") two lines later, confirming "edged" -- not "combat-sword" -- was always the intended category). set_combat_messages()'s own body (std/modules/m_damage_source.lpc) already guards this with a real error() on an unknown type ("No messages of that type."), so THIS particular gap was already a hard, correctly-signposted crash rather than a silent 0-dereference -- but it took down every sword-carrying object's create(), including std/adversary.lpc's base mudlib_setup()->set_combat_messages("unarmed") call for the object that TRIES to wield the sword (an NPC picking up a chest-spawned sword, or a room's set_objects() eagerly creating one at make_objects_if_needed() time), confirmed live via domains/std/rooms/caves/Grotto/domains/std/Attic/domains/std/Shop (all spawn a chest or an NPC that ends up holding /domains/std/objects/sword, which inherits SWORD = /std/sword) all failing to update/load until this was fixed. Both bugs needed fixing together to get these rooms clean: set_combat_messages()'s argument corrected to "edged" in all three sword.lpc copies, PLUS the m_exit.lpc defensive guards (since even with the sword crash fixed, Car_Wash.lpc's own unrelated bad direction would still have crashed it).

Fix applied to all 16 sites: an if(!exits[direction]) return; (void setters) / return 0; (list-type getters) / fall through to the existing query_default_exit_message() default (message-text getters, matching what already happens when the resolved message list is empty) guard immediately before the first dereference in each function, added without touching the earlier present(direction)->is_exit() special-case branch each function already has for the OTHER (complex-exit-object) exit representation. This is a defensive-guard fix in the same spirit as this project's many other "old driver tolerated a stray 0-dereference, this driver doesn't" fixes (e.g. the get_dir()-returns-0-not-empty-array class) -- restoring the original archive's presumably-silent behavior for a case its own author never guarded against, not inventing new game behavior.

11. Compile-sweep results and confirmed non-bugs (lpcc_check.sh, 887 files)

After all fixes above: 853 pass, 34 fail. Every remaining failure was individually triaged (via lpcc_check.sh's batch output AND, wherever the batch tool's own error text looked suspicious, a live update <path> from a real wizard session to cross-check against actual boot behavior per this project's own "batch mode can be a spurious artifact" precedent):

12. Verification performed (live, real driver via ~/src/fluffos-wilderness, raw Python socket client)

13. Not attempted this session

WASM status left "" (native-only verification per this session's scope, matching how libs/lima's own WASM pass was deferred as future work -- a WASM build of this lib would need the SAME ARRAY_RESERVED_WORD-flipped local_options change layered onto an emcmake cmake --preset wasm build, analogous to what libs/lima's NOTES.md already flags as its own outstanding WASM work).

§10.7 deep functional test (2026-08-31, round two)

Full continuous playthrough on the dedicated ~/src/fluffos-wilderness native driver, going well beyond the onboarding pass's registration/ recursion-fix verification above -- specifically to re-confirm the §5 infinite-recursion fix and the §10 sword/exit fixes hold under fresh, independent exploration, and to exercise systems (economy, real combat, quit/reconnect, a long-sit boot watch) the onboarding pass didn't reach. Used the seeded fluffos/Mud@2026 admin account for exploration/instrumentation, plus a fresh throwaway mortal registration (Questorwild/Test12345, real male human, 50/50/.../50 attribute spread, deleted after use per this project's no-test-clutter convention) to independently re-verify the mortal registration and restore paths on an account other than the onboarding session's own.

Bug found and fixed: domains/std/rooms/beach/Outside_Cave.lpc's object_arrived hook destructed the PLAYER'S OWN BODY on ordinary entry -- new AGENTS.md §7.189

Found via ordinary exploration, no combat or edge-case input involved: goto-ing (or simply walking, confirmed both ways) from the connected, portal-reachable Grotto room out into Outside_Cave destructed the player's own body object mid-move, 100% reproducible. The visible symptom was severe and immediate -- the room's own flavor text ("Fluffos sinks into the surf, and you quickly lose track of it in the violent crashing waves.") printed as if describing the PLAYER themselves, followed by a cascade of driver errors (*Bad argument 1 to EFUN call_other() ... Got: int(0), *Bad argument 1 to environment() ... Got: 0, *Parse accepted, but no do_* function found) and a fully dead connection (Can't use verb with no body. on every subsequent command) until reconnecting. Root-caused via log/runtime's stack trace to std/modules/m_smartmove.lpc:170 (environment()->call_hooks("person_arrived", ...) returning 0 right after a successful move) plus a direct read of Outside_Cave.lpc's own obj_arrived(): it checked ob->get_size() >= VERY_LARGE to pick between a "large splash" and a "sinks into the surf" message, but only the VERY_LARGE branch checked is_living() before deciding whether to destruct at all -- the small-object branch (what every ordinary humanoid player or NPC entering the room hits) unconditionally printed the sinks-into-the-surf message and called destruct(ob) with no living/vehicle check whatsoever. Confirmed this is a wilderness-specific regression, not an inherent Lima- lineage bug: the sibling domains/std/Lava_Room.lpc in this SAME codebase implements the identical "destruct small non-living arrivals" idiom correctly (if ( !o->is_living() && !o->is_vehicle() ) before its own destruct()), and BOTH libs/lima's modern fork AND libs/spacemud's independent archive ship their own copies of this exact Outside_Cave.lpc file with the correct if (!ob->is_living()) guard already present -- three of four known copies have the guard, strong evidence the fourth (this one) genuinely lost it rather than never having it. Fixed by adding the missing guard (see AGENTS.md §7.189 for the full writeup and general lesson); verified live, before/after: pre-fix, out from Grotto crashes the connection every time; post-fix, out correctly shows "You enter." + the room description, and north back into Grotto immediately afterward confirms the round trip and the player's body survive intact. The connection always recovered on reconnect (only the in-memory body was destroyed, not the save file), so this was "only" a forced-disconnect crash rather than true permadeath, but it was a 100%-reproducible crash on an entirely ordinary two-hops-from-the-portal exploration path.

Bug found and fixed: daemons/messages_d.lpc's unarmed "graze" combat message baked a hardcoded -s into the verb token, breaking this codebase's own $v conjugation contract in both directions

"unarmed"'s "none" (near-miss/graze) tier read "$N $vgrazes to $t with $p fist." -- note $vgrazes, not $vgraze -- while every sibling weapon category's identical tier ("edged", "blunt", "piercing") correctly used $vgraze. Root-caused by reading std/modules/m_messages.lpc's $v handler: for the second-person subject ("you"), it uses the verb token verbatim (so a token that already ends in -s prints wrong for "you" -- confirmed live, "You grazes to Bill the Troll with your fist."); for a third-person subject, it calls M_GRAMMAR->pluralize(str) to append the -s itself, so a token that ALREADY has one gets doubled -- confirmed with an isolated instrumented test, M_GRAMMAR->pluralize("grazes") returns "grazesses" (efun pluralize()'s real "ends in s -> add -es" rule firing on an already-conjugated word). So the bug was wrong for BOTH grammatical persons, not just cosmetically wrong for one. Fixed by dropping the baked-in -s, matching the three sibling categories: "$N $vgraze $t with $p fist.". Verified live (a fresh player-body instance was needed since set_combat_messages() caches MESSAGES_D->get_messages() once per object at setup() time, so an already-loaded body doesn't pick up a live update to messages_d.lpc until it reconnects): post-fix, the exact same fight now correctly shows "You graze Bill the Troll with your fist." Narrow enough (one file, a single message string, no corpus-wide grep hits for the same $v<word>s doubled-verb shape) that it doesn't warrant its own AGENTS.md entry.

What was tested and confirmed working

WASM status unchanged from §13 above (native-only this pass too, same ARRAY_RESERVED_WORD flag blocker as a WASM build would need).

WASM measurement (2026-09-03): still noboot on every existing WASM binary

meta.json was flipped to playable on 2026-08-31 (2ec643a7c45) to unblock the Pages deploy pipeline -- that commit equated a native §10.7 pass with WASM playability. Measured this session against both binaries the site can actually serve:

So the live play page, if packed with the shared driver, cannot boot.

That gap is now closed in the same session: ~/src/fluffos-wilderness got an emcmake cmake --preset wasm -DPACKAGE_UIDS=OFF build, the artifacts live in scripts/custom_drivers/wilderness/, and build_site.sh maps the slug there. One mudlib-side compile fix was required -- the same lima §1.3(c) dump_socket_status() / socket_status() eager-simul_efun hole -- guarded with #ifdef __PACKAGE_SOCKETS__ in secure/simul_efun/misc.lpc. /obj/secure/socket.lpc still fails to compile during the rcp_d preload (no sockets package); same graceful skip as lima/spacemud, not on the login path.

Verified with scripts/wasm_client.js against ~/src/fluffos-wilderness/build-wasm/src: fluffos / Mud@2026 → Grand Hall → lookscore ("You are an implementor.") → quit ("You have left Wilderness."). wasm_status is playable again, now actually true for the browser. Native play on ~/src/fluffos-wilderness is unchanged.