info.html · 介绍 · play.html · 游玩 · llms.txt · 下载与本地运行 · ZIP · GitHub
A pristine, pre-LDMud LPMud 2.4.5 snapshot -- the mudlib distributed with Lars Pensjo's own original LPMud driver line, from right before the LDMud/MudOS fork. A real, playable small world (286 files, hundreds of rooms) rather than an infrastructure sketch: new characters land in the village church (which hides a call-button-summoned elevator behind its west wall) and can explore a green, market road, bank, shops, and an adventurers' guild that runs a real fetch quest -- 'Retrieve the Orc slayer from the evil orc shaman, and give it to Leo' -- pointing players at an orc valley with its own orc-treasure room. A twelve-room forest and a 29-tunnel mine flank the village; a giant's lair, path, and confrontation room house a cloned, aggressive level-15 giant NPC; and the village shore road leads out to an entirely separate 'south' region -- its own self-contained ~49-room forest, 18-room island archipelago, and 30-room shoreline.
README
A pristine, pre-LDMud LPMud 2.4.5 snapshot -- the mudlib distributed with Lars Pensjö's own original LPMud driver line, from the era right before the LDMud/MudOS fork. Genuinely FluffOS/MudOS-compatible: a full driver_hook/H_[A-Z_]+/set_driver_hook grep across the whole archive found zero hits (unlike, say, the Docker Hub ldmuddev/lp245 image, which is an LDMud-ported build of the same nominal version). obj/master.lpc's own header comment dates it precisely: "This is the LPmud master object, used from version 3.0... designed to be used with mudlib 2.4.6 or earlier."
This is a real, playable small world -- 286 files, hundreds of real
rooms (a village with a church, green, market road, forest, mine,
sewers, a maze, a "south" continent, wizard-only areas), NPCs
(orcs, wolves, a dragon, guards), a level/experience system, and the
classic LPMud spell/combat mechanics -- not a thin infrastructure
sketch like the already-onboarded basis.
Provenance
Recovered directly from the still-live ftp.lysator.liu.se LPMud
archive mirror
(https://ftp.lysator.liu.se/pub/lpmud/mudlibs/other/2.4.5.tar.gz),
same as basis -- no Wayback Machine recovery was actually needed,
the file is still served as-is.
Highlights (see NOTES.md for full detail)
- A genuine, severe infinite-recursion bug, present in the original 1990s source itself, exposed by this driver's
file_name()convention:room/room.lpc(the shared room base every roominherits) caches a "number word" array ("one","two", ...) on a shared singleton instance at/room/room, usingfile_name(this_object()) == "room/room"to detect "am I the singleton itself?". This driver'sfile_name()always returns a LEADING SLASH ("/room/room"); the comparison was therefore always false, so the singleton's own lookup called itself viacall_other("room/room", "query_numbers")forever. This broke the long-form description of every single room in the game the first time any exit list needed to be spelled out in words ("There are three obvious exits: ..."), with the driver's own genericdefault error messagemasking the real "Too deep recursion" error from ordinary (non-wizard) players. Fixed by comparing against"/room/room". obj/player.lpccalledmove_player()-- the function every single movement in the game funnels through -- but never defined it, and doesn't inherit anything that does. The genuine implementation exists, verbatim-compatible with player.lpc's own variable names, inobj/living.lpc(an apparent unfinished refactor toward a shared living-body base class that never got wired up to the actual player object). Copied the function in. Without this fix, this driver's "an undefined bare call only errors when actually invoked, not at compile time" behavior meant the whole compile sweep looked clean, and only a live walk between rooms revealed the break.- Two applies this driver's
PACKAGE_UIDSbuild unconditionally requires but this pre-uid-era archive never defined at all:get_root_uid()/get_bb_uid()(boot-blocking,exit(-1)) andcreator_file()(blocks every single subsequent object load). Added flat single-owner stubs (AGENTS.md §7.2). - This driver defaults to
lazy resets : 0; this whole archive'sreset(arg){if(arg)return;...}idiom (and this driver's owncall_create()always marking every object "already reset" on load, deferring the real first reset for up to 30 minutes) meant every room loaded dark/uninitialized until its own random reset timer eventually fired. Fixed generally by addingcreate() { reset(0); }to all 157 files using this idiom (mechanical, not hand-picked) plus turninglazy resetson for the runtime-reset case (moving into an object whose reset is overdue). - Old MudOS's classic
command(str, ob)(run a command as a specific object) doesn't exist here --command(str)always runs ascurrent_object. Fixed the ~10 call sites needing to run a command as a DIFFERENT object (an NPC's forced "wield", a wizard's queued auto-typer) via a smalldo_command()wrapper defined on the target classes, matching this driver'scall_other()-never- falls-back-to-an-efun semantics already established for themove_object()/transfer()dialect gap (AGENTS.md §7.158). add_action("fun"); add_verb("cmd");(a very old 2-statement idiom, 366 call sites) mechanically merged into the modern 2-argumentadd_action("fun", "cmd")this driver requires.- Reimplemented five more classic efuns this driver never had:
extract(),log_file(),cat(),creator()(stubbed permissively),previous_file().
Registration flow
Type your name -> (new character) password -> confirm password ->
email address (or none) -> gender (m/f/o) -> straight into
room/church, the starting room.
Local play
cd libs/lpmud245
~/src/fluffos/build-debug/src/driver config.fluffosPort 40262.
NOTES · 移植与修复记录
LPmud 2.4.5 -- porting notes
Source: other/2.4.5.tar.gz, recovered directly from the still-live
ftp.lysator.liu.se LPMud archive mirror
(https://ftp.lysator.liu.se/pub/lpmud/mudlibs/other/2.4.5.tar.gz).
Slug lpmud245, number 960, port 40262. 582 raw files (286 .c
sources), mudlib root at the archive's own top level. No driver_hook/
set_driver_hook/H_[A-Z_]+ usage anywhere (confirmed by grep) --
genuinely a pre-LDMud, MudOS-line-compatible snapshot, unlike the
LDMud-ported Docker Hub ldmuddev/lp245 image of nominally the same
version. obj/master.lpc's own header comment: "This is the LPmud
master object, used from version 3.0... designed to be used with
mudlib 2.4.6 or earlier."
1. Extraction and conversion
Pure ASCII/English archive, 0 lossy conversions. 286 files renamed
.c->.lpc, 5 literal .c" references fixed, 8 files
static->nosave.
2. Proactive on-sight checklist findings
statusused as a declared type (AGENTS.md §6.8) in 8 files:obj/weapon.lpc(wielded),obj/monster.lpc+obj/monster.talk.lpc(healing),obj/torch.lpc(is_lighted),room/pub2.lpc(drink),obj/simul_efun.lpc(trunc_flag). Fixed toint.newused as an ordinary variable inroom/post.lpc(a mail-status string) -- renamed tonewmsg.function/classused as ordinary variable names inroom/pub2.lpc/room/vill_road2.lpc/room/yard.lpc(renamed tofunclist) androom/orc_vall.lpc/room/fortress.lpc(renamed towclass) -- all three are the hard-reserved-keyword class documented in this session's new AGENTS.md §6.2 entry (added while onboarding the siblingbasis).bull_board.lpc'snew(hd) {...}command handler (wired viaadd_action("new", "note")) hit the samenewcollision -- renamed the function AND the add_action string topost_new.in(str) {...}(an unreferenced dead command handler) inobj/trace.lpc/obj/trace2.lpc--inis also a hard-reserved keyword (theforeach ... inclause). Renamed togo_in(dead code, never wired via add_action, so purely a compile-blocker fix).
3. Compile-time / boot-time driver-compat fixes
- Missing
global include file(same class asbasis's finding): this archive ships no dedicated mudlib-wide header at all. Addedinclude/empty.h(a placeholder, not original content) and pointedglobal include file : <empty.h>at it. - Quoted-relative-header resolution:
obj/living.h/obj/debug.hresolve fine next to their own.lpcfiles oncemaster.lpcimplementsget_include_path()(AGENTS.md §6.1's standard fix, added since this archive had none) -- butobj/living.lpc(and its dependentsobj/monster.lpc/obj/monster.talk.lpc/obj/player.lpc/obj/shut.lpc/obj/leo.lpc) quote-includes"log.h", which actually lives inroom/log.h, NOT next toliving.lpc-- a genuine cross-directory shared header in the original archive. Fixed by adding/roomtoinclude directories(get_include_path()'s:DEFAULT:fallback then finds it there). get_root_uid()/get_bb_uid()missing (AGENTS.md §7.2's documented class): this driver'sPACKAGE_UIDSbuild unconditionallyexit(-1)s at boot without them; this archive predates the uid/euid system entirely (only one incidentalseteuid()call in the whole mudlib). Added flat stubs returning"ROOT"/"BACKBONE".creator_file()missing -- a related but DISTINCT requirement fromget_root_uid()/get_bb_uid(), not previously catalogued this precisely:vm/internal/simulate.cc'sapply_master_ob(APPLY_CREATOR_FILE, ...)is called for EVERY object load (to assign a euid) wheneverPACKAGE_UIDSis on, unconditionally required,error()ing ("master object: No function creator_file() defined!") the first time ANY object beyond master/simul_efun itself gets loaded -- unlikeauthor_file()/domain_file(), which only warn-and-fall-back. Added a flat"ROOT"stub.valid_override()missing: this driver requires explicit master permission before any object may define a function sharing a real efun's name, or even callefun::something()at all (AGENTS.md §6.2's existing note on this, previously only seen via theREF_RESERVED_WORD-adjacent case) -- needed the moment themove_object(dest){efun::move_object(dest);}shims (below) got added to every object file. Added a permissive always-1 stub (a small, fully-trusted single-owner mudlib, no untrusted-wizard threat model).move_object()2-arg vs 1-arg dialect gap (AGENTS.md §7.158): 111 call sites across the whole mudlib used the classicmove_object(item, dest)form; this driver's efun takes exactly one argument (the destination) and always movesthis_object(). Mechanically rewrote every call site:move_object(this_object(), X)->efun::move_object(X)(8 sites, no call_other needed), all othermove_object(A, B)->A->move_object(B)(111 sites) -- BUT sincecall_other()never falls back to a same-named efun on a miss (confirmed the hard way: this exact "silent no-op" is documented in §7.158 as "the single worst finding, with zero error output of any kind"), every object that could ever be the "item" argument needed ITS OWNmove_object(dest) { return efun::move_object(dest); }method. Since this archive has NO shared base class at all (confirmed: zeroinheritstatements anywhere underobj//players/), added this mechanically to all 59 files underobj/andplayers/(excludingmaster.lpc/simul_efun.lpc) as a blanket precaution rather than hand-tracing every call site's actual argument type.transfer(item, dest)-- a second, distinct old MudOS efun for the exact same "privileged move" concept, not previously catalogued: 12 call sites (room/fortress.lpc,room/orc_vall.lpc,room/bank.lpc,room/vill_road2.lpc,obj/player.lpc,obj/trace.lpc/obj/trace2.lpc). Not a real efun on this driver either. Rewritten identically tomove_object()'s treatment:transfer(A, B)->A->move_object(B), reusing the same per-file shim added for themove_object()fix above. Known imprecision: a few call sites check the OLD efun's return value for success (if (transfer(...) != 0)); our shim's return value doesn't perfectly preserve that signal (see §7 below) -- acceptable for these low-stakes wizard-utility/NPC-equip call sites, not chased further.command(str, ob)2-arg vs 1-arg dialect gap -- a new bug class, not previously catalogued: old MudOS'scommand(str, ob)ranstras ifobhad typed it; this driver'scommand(str)(single arg only) always runsstras ifcurrent_object(the object whose OWN code is calling command(), NOT necessarily command_giver/this_player()) had typed it. ~12 call sites acrossobj/quicktyper.lpc(a heartbeat-driven queued-auto-typer wizard utility),obj/trace.lpc/obj/trace2.lpc,obj/player.lpc, androom/fortress.lpc/room/orc_vall.lpc(an NPC forced towielda weapon) all needed to run a command AS SOME OTHER, specific object -- impossible directly on this driver (same "call_other() never falls back to an efun" root cause asmove_object()/transfer()above: a bareob->command(str)would silently no-op). Fixed with ado_command(str) { return command(str); }wrapper added toobj/player.lpcandobj/monster.lpc(the two classes ever targeted), then called viatarget->do_command(str). Also fixedobj/player.lpc's ownsnoop_on(): the bare 0-argsnoop();and the "start snooping"snoop(ob);both needed updating for this driver's realsnoop(snooper, snoopee)signature (single-arg form STOPS snooping by that snooper, the OPPOSITE of what the original 1-argsnoop(ob)call intended) -- fixed tosnoop(this_player())andsnoop(this_player(), ob)respectively.add_action("fun"); add_verb("cmd");-- the archaic 2-statement form of command registration, not previously catalogued: 366 call sites across 111 files use this pair (an implicit-verbadd_actionimmediately followed by a separateadd_verbcall setting the real verb text) instead of the 2-argumentadd_action(fun, cmd)this driver requires (void add_action(string|function, string|string*, void|int)-- the 2nd/cmd parameter is NOT optional here).add_verbitself is not a real efun on this driver either, so this failed as "Too few arguments to add_action" for the 358 add_action(fun) calls, while the immediately-following add_verb() calls, sharing the same compiled statement's error-recovery, escaped a distinct diagnostic. Mechanically merged everyadd_action("X"); add_verb("Y");pair (any whitespace) intoadd_action("X", "Y");.- A genuine, severe infinite-recursion bug in the ORIGINAL 1990s source, exposed (not introduced) by this driver's
file_name()convention -- the most severe and interesting finding in this lib.room/room.lpc(the shared base every real roominherits) implementsconvert_number()/query_numbers()as a shared cache: the FIRST time any room needs to spell out its exit count in words ("There are three obvious exits..."), it callsquery_numbers(), which -- to avoid every room duplicating the same 10-word array -- delegates to a SINGLE shared instance at the path/room/roomitself (loaded as a real, standalone object, not through inheritance) viacall_other("room/room", "query_numbers"). That shared instance is supposed to recognize itself and populate the array directly instead of delegating again, viaif (file_name(this_object()) == "room/room") .... This driver'sfile_name()efun always returns a leading slash (efuns_main.cc's own comment: "This function now returns a leading '/'") -- so the comparison against the bare"room/room"(no slash) was ALWAYS false, even for the/room/roomobject itself, meaning its ownquery_numbers()call always took the "delegate to room/room" branch -- calling ITSELF, via call_other, forever. This is a genuine "Too deep recursion" stack overflow, hit by literally every room's long-form description whose exit list needed to be spelled out, i.e. every room with more than one exit -- reproducible on the SECOND room a new player ever enters (room/vill_green, reached viasouthfrom the startingroom/church). Made much harder to spot than a typical crash because this driver's ownmudlib error handlerconfig option (default ON) actively SUPPRESSES the real error text from ordinary (non-wizard) players, replacing it with the configureddefault error message("Something is wrong.") -- the crash was fully logged todebug.logwith a complete "Too deep recursion" trace the whole time, but nothing about the player-facing symptom hinted that an error had occurred at all (no "Undefined function", no visible stack trace, just a game message that read like legitimate (if terse) content). Diagnosed by directly readingdebug.login full after a live repro, not by trusting the player-facing output. Fixed by comparing against"/room/room"(leading slash) instead. New AGENTS.md catalog entry this session (see §7 below) -- both for thefile_name()leading-slash gotcha itself, and for the "a non-wizard player's error output can be a complete red herring; always check debug.log directly" methodology point. obj/player.lpccalledmove_player()-- literally the function every single instance of room-to-room movement in the entire mudlib funnels through -- but never defined it, and doesn't inherit anything that does (confirmed: zeroinheritstatements anywhere in this archive'sobj//players/trees). The real implementation exists, complete and directly reusable (byte- identical variable names to what player.lpc itself already declares:msgout/msgin/ghost/cap_name/mmsgout/mmsgin/is_invis/attacker_ob/hunted/brief), inobj/living.lpc-- a separate, apparently-unfinished file that looks like early work-in-progress toward a shared "living" base class that never got wired up to the actual player object. Every room's ownmove()command handler, plusobj/shut.lpc,obj/roommaker.lpc,obj/master.lpc, androom/doorway.lpc, all callthis_player()->move_player(...)expecting it to just work. Confirmed live: since this driver only errors on an undefined bare function call AT THE POINT IT'S ACTUALLY INVOKED (not at compile time -- every one of this session's "missing classic efun" discoveries,extract/log_file/cat/creator/previous_file/transfer, surfaced the identical way), the ENTIRE compile sweep (283/286, all real failures accounted for -- see §4) stayed clean despite this, and the break was only caught by an actual live walk between two rooms. Fixed by copyingmove_player()'s full body fromobj/living.lpcintoobj/player.lpcverbatim, adding the two genuinely-missing globals it also needs (int is_npc, hunting_time;--NAME_OF_GHOSTwas already available via player.lpc's existing#include "living.h").
4. Compile-sweep triage: known-acceptable remaining failures (3 of 286)
players/lars/test.lpc-- Lars Pensjö's own personal scratch/test room (players/lars/), containing literal1/0;statements as intentional driver-crash-testing debug code (a genuine 1990s development habit, not conversion damage -- confirmed against the raw archive bytes). This driver catches it at COMPILE time (constant folding) rather than the original driver's presumed runtime-only behavior. Left as-is: it's the author's own private test file, never referenced by anything else, not reachable by an ordinary player.room/def_castle.lpc-- a genuine TEMPLATE file, never meant to compile standalone.obj/master.lpc'smaster_create_wizard()reads this file's SOURCE TEXT at runtime and prepends#define NAME "..."/#define DEST "..."lines before writing the result out as a brand-new wizard's own castle file -- confirmed by reading that function directly.lpcc_check.sh's blind file-list sweep has no way to know to inject those defines; this is the template-file counterpart to AGENTS.md §10.4's "genuinely missing content" false-positive class, not a bug.obj/explore_xp.lpc-- a standalone experience-tracking utility, confirmed completely unreferenced by anything else in the archive (grep -rl "explore_xp"finds nothing outside its own file). Its ownset_name()has a defensive self-destruct (if (... && !creator(...) && !creator(previous_file())) { ... destruct(this_object()); }) that fires underlpcc_check.sh's isolated instantiation (no realprevious_object()calling context) but would not fire under a normal load in real play. Matches AGENTS.md §10.4's documented isolated-load false-positive class.
5. Live playthrough (native driver)
Full flow verified in one continuous session, port 40262: fresh
registration (name / password twice / email none / gender) reaching
room/church -> look (correct room description, all objects
mentioned) -> south (moves to room/vill_green, correct room
description including "There are three obvious exits: north, west
and east" -- the exact code path the room/room.lpc recursion bug
above broke) -> look again (correct) -> score (correct XP/gold/HP/
spell-points/age display) -> quit (clean save + disconnect). Tested
with both m/f gender selections. Re-login (existing-password
path) verified separately: password accepted, correct room
reached, movement and score both still correct. debug.log fully
clean (zero error lines) across every test session after all fixes
above.
6. WASM
Boots and plays identically under the WASM build
(~/src/fluffos/build-wasm/src, via scripts/wasm_client.js): full
registration -> look -> quit, correct output throughout.
wasm_status: playable.
7. New AGENTS.md catalog entries added this session
(Alongside the basis-onboarding entries for the closure/function-
pointer gap, new/class reserved words, and the extract/
log_file/privp/cat missing-efun list.)
1. file_name() always returns a leading / on this driver --
any archive comparing file_name(ob) == "bare/path" (no leading
slash) against a literal path constant will silently ALWAYS be
false. Watch for this specifically in "am I my own shared
singleton instance" self-identity checks, where a false negative
causes infinite self-delegation (a "Too deep recursion" crash) --
as opposed to the more common case of just silently failing to
match, which tends to surface as a missing-feature symptom instead
of a crash.
2. A non-wizard player's visible error output can be a complete red
herring when mudlib error handler is on (the default): this
driver replaces the real error text with the configured
default error message for any non-wizard command_giver,
while still logging the FULL real error (including a complete
stack trace) to debug.log every time, unconditionally. A crash
whose player-facing symptom looks like ordinary (if terse) game
content -- not an obvious error message -- should still prompt a
direct debug.log read before concluding "no bug here." This
generalizes AGENTS.md §10.3's existing point about logon()
swallowing errors to a much broader, config-driven case that can
apply to literally any command.
3. command(str, ob) (run a command as a specific, non-current
object) is not supported on this driver at all -- command(str)
always runs against current_object, never command_giver/
this_player(). Needs the same per-target do_command()-wrapper
treatment as move_object()'s 2-arg dialect gap (§7.158).
4. transfer(item, dest) -- a second classic MudOS efun for the
same "move an arbitrary object" concept move_object() also
covers on old drivers; not real on this driver either. Fix
identically: A->move_object(B) via the same per-object shim.
5. add_action("fun"); add_verb("cmd"); -- a two-statement legacy
idiom for what modern add_action(fun, cmd) (2-arg, both required
on this driver) does in one call. Mechanically mergeable via a
regex over adjacent statement pairs.
6. creator_file() is required by this driver's PACKAGE_UIDS
build for literally every object load, not just at boot like
get_root_uid()/get_bb_uid() (§7.2's existing entry) --
creator_file() deserves its own line in that catalog entry since
its absence blocks ALL subsequent loading, not just initial boot.
8. §10.7 deep functional test (2026-08-31)
Full continuous-session playthrough on the native driver (port 40262):
fresh registration -> walked the ENTIRE village crossroads area
(church -> vill_green -> vill_track -> vill_road1 -> vill_road2 ->
shop -> adv_guild -> yard) rather than just the two rooms §5's original
pass covered, tested the shop's list/buy/value commands, the
guild's list/cost/advance commands, a real multi-round combat
against the yard's Beggar NPC (hit/miss/damage messages both
directions, correct HP tracking), quit mid-combat, and a fresh
reconnect (password accepted, HP correctly persisted at the
damaged value, room reset to the starting room/church matching this
lib's existing documented save-data scope). Also ran a 200s WASM
scripts/wasm_boot_watch.sh lpmud245 200 long-sit.
Severe bug found and fixed: ~80% of the whole game world (70 of 88
rooms) started dark and/or completely exit-less on every fresh boot,
for up to 30 minutes, because the create() { reset(0); } fix already
applied by hand to a handful of custom rooms (§3's room/church.lpc
writeup) was never extended to the two shared macro headers
(room/room.h's ONE_EXIT/TWO_EXIT/THREE_EXIT/FOUR_EXIT and
room/std.h's identically-named but independent macro set) that
generate the vast majority of this archive's actual room files
Found by simply continuing the walk past where §5's original pass
stopped: south from the church into room/vill_green worked exactly
as documented, but the very next room east (room/vill_track, one hop
from spawn) printed "A dark room.", look said "It is too dark.",
and every direction command including the one that had just been
used to enter the room returned "What?" -- a genuine dead-end
softlock for any new player who wandered one room past the tested
path, not a cosmetic darkness issue.
Root cause: this whole archive uses the classic reset(arg){ if
(!arg) {...} } idiom as its room constructor (exits, light, and
descriptions are all populated inside reset(), only on the very
first, arg==0 call) -- but this driver's call_create() always
marks a freshly-loaded object as "already reset" and defers ITS real
first reset() call for up to "time to reset" seconds (room/
church.lpc's own extensive comment, §3, already documents this
exactly). church.lpc (and a handful of other hand-written rooms:
bankroom, elevator, wiz_hall) got an explicit create() {
reset(0); } added individually to force synchronous initialization --
but this archive's OTHER 88 room files are almost all generated by one
of two near-identical macro-header systems (room/room.h, inherited
via #include "room.h" + inherit "room/room", used by 68 files;
room/std.h, a fully standalone macro set with no inheritance, used
by 13 files, 4 of which -- including church.lpc itself -- override
the macro entirely with hand-written code and already had the fix).
Neither macro header itself was ever given the create() fix, so
every room instantiated purely by invoking ONE_EXIT(...)/
TWO_EXIT(...)/etc. (which is the OVERWHELMING MAJORITY of the
explorable map: every forest*/plane*/eastroad*/giant_* room,
plus the shop, the bank, and the adventurers guild) never ran its
reset() until the driver's own lazy scheduler eventually got to it.
The two macro systems differ in how bad the resulting symptom is:
room/room.h-based rooms (68 files) register their exits'add_action()calls INSIDEreset()itself (init()isn't used for movement at all here) -- so an unreset room has NO command handlers for any direction whatsoever, a hard, un-escapable dead end (confirmed live onroom/vill_track).room/std.h-based rooms (13 files, e.g.room/shop,room/adv_guild) register movement viainit()instead, which this driver DOES call synchronously every time a player enters -- so these rooms remain fully navigable even before their realreset()fires, just permanently dark ("A dark room."/"It is too dark.", no short/long description text) until it does. Confirmed live on bothroom/shopandroom/adv_guild, one and two hops respectively from the village's own main crossroads -- i.e. content every single new player would reach within their first few moves.
Fix: added create() { reset(0); } directly inside each of the 8
macro definitions (4 in room/room.h, 4 in room/std.h) rather than
hand-editing 81 individual room files -- confirmed no macro-generated
file already defines its own create() (would be a duplicate-function
compile error) and no already-fixed hand-written file (church,
bankroom, elevator, wiz_hall) invokes either macro. Verified
live, post-fix: room/vill_track is lit with both exits working on
the very first visit after a fresh boot; room/shop and
room/adv_guild are fully described (list/buy/cost/advance all
producing correct output) on first entry too. New AGENTS.md catalog
entry, §7.185 (this is a coverage gap in an ALREADY-catalogued fix
pattern -- §3's own writeup already explains the underlying mechanism
correctly -- rather than a new root cause, but the specific "a shared
macro header used by the vast majority of content files was missed by
a fix applied file-by-file to only the hand-written exceptions" shape
is worth its own entry so a future sweep checks macro headers first).
Confirmed NOT a bug -- one remaining gap, low priority, left alone
obj/quicktyper.lpc (a heartbeat-driven wizard debug/auto-typer
utility, not player-facing content) is the ONLY file under obj//
players/ still missing its own create() fix (every other file in
those trees already has one, confirming this archive's original
onboarding pass thoroughly covered obj/ by hand -- the miss was
specifically the two room macro headers, not a general gap). Its
reset() only sets an owner name and allocates a history array for
its own debug/logging use, both of which the driver's own eventual
lazy reset (or the object's own later use) still populate correctly
before they matter; no observed player-facing symptom. Not fixed,
noted here for completeness.
Regression check
Full re-walk of the fixed rooms plus every other already-verified
command (score, inventory, combat, quit, reconnect) in one final
continuous session: zero errors in the driver's own captured stdout
(this native build's debug.log is dead for the process's whole life
per AGENTS.md §10.9, so stdout is the only reliable error channel).