info.html · 介绍 · play.html · 游玩 · llms.txt · 下载与本地运行 · ZIP · GitHub
A small, genuinely historical LPMud reference world -- "HMC LP-mud" (Harvey Mudd College), version 1.4.1-A, bundled with its own classic C-language LPMud driver (the mudlib content here runs under this project's own FluffOS build instead). A university-account game, not a commercial title: new characters arrive in the Endoplasmatorium ("a place where people without bodies come to get them") next to a clinic, a village green, and an elevator connecting up to the wizards' hall. The Adventurers' Guild runs a classic experience-and-gold leveling ladder with twenty title ranks, from "the utter novice" up through journeyman/master magician and sorcerer tiers to "the apprentice Wizard". Beyond the village (green, road, shore, bank, pub, church, and a general store) lies a real overworld: a ten-room forest chain, a mountain pass and peak, a giant's lair and treasure path, a sea and sea floor, and a chain of thirteen numbered planar rooms. A soul-verb social/emote command set (`smile`, etc.) and basic wizard tooling round out the toolkit. The game world is deliberately small and thin -- a real early-1990s single-author teaching project, not an omission in this port.
README
A historically significant early LPMud reference mudlib -- "HMC LP-mud" (Harvey Mudd College), version 1.4.1-A, bundled together with the original classic C-language LPMud driver it shipped with (ignored here; this project uses its own FluffOS build). Only the mudlib itself was ported. This is a genuinely small, from-a-university-account game, not a commercial title: a starter guild, an Endoplasmatorium, a village green, a clinic, an elevator, some mines, a maze, basic wizard tooling and soul/emote commands -- thin by design, not by omission.
Source: lpmud-1.4.1-A-r10.tar.gz, recovered via the Wayback Machine
from mudmagic.com (codes/dl/2154/lpmud-1.4.1-A-r10.tar.gz), a now-defunct
MUD community site. Slug lpmud141, number 941, port 40243.
Historical note
This driver's own source has no concept of a "master object" anywhere --
confirmed by reading it directly, not by absence of a file. It predates
the security-daemon/uid architecture that every later LPMud/MudOS/FluffOS
descendant (including every other lib in this collection) standardized
on. Bringing this online required authoring a secure/master.lpc and
secure/simul_efun.lpc from scratch (pure driver-glue reproducing this
driver's own hardcoded C behavior), plus fixes for several fundamental
dialect differences: a two-call add_action()/add_verb() verb
registration idiom, a move_object(item, dest) that moves an arbitrary
object rather than just the caller, and a reset(arg==0) "first-time
setup" convention that this driver's own object lifecycle doesn't fire
synchronously the way this archive expects. Full technical detail,
including exactly what was authored vs. what's original archive content,
is in NOTES.md.
Status
Boots clean. 146 of 150 .lpc files pass a full lpcc_check.sh batch
compile sweep; the 4 that don't are confirmed test-harness-only false
positives (two files that are genuinely include-only/template fragments
never meant to compile standalone, and two files whose first-run setup
code needs a real connected player that the standalone compile-checker
doesn't provide) -- see NOTES.md for the per-file detail.
Verified live end to end with a real driver and a raw socket client:
registering a brand-new character (letters-only name, matching this
archive's own original valid_name() rule), arriving in a correctly
lit and described starting room, moving between several different rooms,
soul on plus emote commands (e.g. smile), score, quit, and
reconnecting into the same saved character afterward -- including across
a full driver restart.
No outbound network connections of any kind (confirmed by source inspection, not just by absence of symptoms).
WASM status: playable. Clean WASM boot and full session (register,
look, score, quit) with zero fixes needed. See NOTES.md \S10.
Local run
cd libs/lpmud141
~/src/fluffos/build-debug/src/driver config.fluffosGame port: 40243.
Connect and give a name (1-11 lowercase letters, no digits -- this archive's own original rule) to register a new character, or an existing name to log back in; you'll be prompted for a password either way (typed twice for a new character).
NOTES · 移植与修复记录
LPmud 1.4.1-A -- porting notes
Source: lpmud-1.4.1-A-r10.tar.gz, recovered from the Wayback Machine
(originally hosted at mudmagic.com, a now-defunct MUD community site, at
/codes/dl/2154/lpmud-1.4.1-A-r10.tar.gz). The CDX API found two working
snapshots (2006-11-07 and 2006-12-09, identical digest,
F45N5YSOL3EUATOFS2SYVS6EN7BIFC6R, 178589 bytes); downloaded via the
id_ URL suffix (raw bytes, no Wayback chrome) and confirmed as a valid,
non-corrupted gzip/tar (tar -tzf, 374 members) before use. Slug
lpmud141, number 941, port 40243.
The archive bundles the original classic C-language LPMud driver source
(lang.y, interpret.c, simulate.c, object.c, backend.c, comm1.c,
ed.c, etc. -- ~15,000 lines) alongside the real payload, mudlib/ (338
files). Per this project's standing convention, the bundled driver was
never touched or built -- only mudlib/ was ported, against this
project's own FluffOS build.
0. This is NOT a normal onboarding: no master object, ever
Before any conversion work, grep -rn master across the entire bundled
driver source returns zero hits. This is not an oversight or a
stripped-down archive -- this driver genuinely predates the
"master object" security-daemon architecture that LPMud 3.x/MudOS/FluffOS
(and by extension every other lib in this 940+-lib corpus) standardized
on. Confirmed by reading the driver's own connection-handling code
(comm1.c's new_player()): it hardcodes
ob = clone_object("obj/player")->u.ob; then logon(ob) (which just
applys a hardcoded "logon" function name) for every new connection --
no master, no ACL, no uid model of any kind. Boot-time object loading is
equally hardcoded: backend.c's load_first_objects() always loads
room/void first, then reads room/init_file line by line loading
each named object, then loads room/rum2.c as an unconditional final
kludge (its own comment: "For some reason the environment of the last
castle preloaded doesn't get its long description!").
FluffOS unconditionally requires a master object (a master file config
directive; PACKAGE_UIDS applies; connect()/epilog()/preload() at
boot). Since none exists anywhere in this archive (mudlib/ or the driver
directory), secure/master.lpc and secure/simul_efun.lpc were
authored from scratch -- see §2 below for exactly what's in them and
why each piece is there. This is pure driver-glue reproducing the
driver's own hardcoded C behavior in LPC, not invented mudlib content;
every actual room/NPC/item file in mudlib/ is untouched archive
material.
One nearby archive file *did* get relocated: security.h (wizard-level
and invisibility-level #defines -- SENIOR, GOD, NO_SHORT,
INVIS_TELEPORT, etc.), referenced by 3 mudlib files via
#include "../../security.h". In the original tarball layout that
resolves one directory *above* mudlib/, i.e. into the driver's own
source tree (where the file genuinely lives, lpmud/security.h) -- an
artifact of the original single combined driver+mudlib checkout, not a
missing-content gap. Copied into work/security.h (the mudlib root) and
the 3 #include lines repointed to "/security.h"; the constants
themselves are untouched, real archive content.
1. Conversion
Pure-ASCII English archive (LC_ALL=C grep -rlP '[\x80-\xFF]' found zero
non-ASCII files) -- scripts/convert_lib.sh ran with zero encoding work
needed: 148 .c files renamed to .lpc, 2 literal .c" references
fixed, 1 local angle-bracket #include converted to quotes, 0 files hit
by the static->nosave sweep (this codebase never uses static at
all).
2. secure/master.lpc and secure/simul_efun.lpc (authored, §0)
master.lpc: get_root_uid()/get_bb_uid() (both "root", matching
this codebase's single-tier trust -- it never had a uid model to begin
with); permissive valid_read()/valid_write()/valid_socket()/
valid_seteuid()/valid_hide()/valid_shadow() (same reasoning);
valid_override() (needed for the efun:: syntax used in the global
auto.h, §3); connect() reproducing comm1.c's hardcoded
clone_object("obj/player"); epilog()/preload() reproducing
backend.c's load_first_objects() exactly -- always /room/void
first, then every non-comment non-blank line of room/init_file (read
fresh every boot, so wizard castles appended by create_wizard(), §4,
keep preloading across restarts); creator_file()/domain_file()/
author_file() (silence harmless-but-noisy "no function defined"
warnings); log_error()/error_handler() for compile/runtime
diagnostics.
A real bug was found and fixed in error_handler() itself while
building it: an early draft did ret += "Object: " + ... + " at " +
mp["file"] + ":" + mp["line"] + "\n" -- concatenating a string with
mp["line"] (an int) via +, which this driver does not implicitly
convert. That throws *inside the error handler itself*, and with no
handler for a from-error_handler error, the driver aborts the whole
load completely silently (lpcc's own main_lpcc.cc wraps
find_object() in a bare catch(...) with no diagnostic printed) --
"Fail to load object X" with zero explanation was the only visible
symptom, and it looked exactly like a real compile/runtime bug in X for
several debugging cycles before the root cause (a bug in the *handler*,
not in the failing object) was found by temporarily replacing
error_handler() with a sprintf("%O", mp) dump. Fixed with
sprintf("%s:%d", mp["file"], mp["line"]) and an objectp() guard
around mp["object"] instead of a bare truthiness check. Worth
remembering for future onboardings: an uncaught error inside a
from-scratch master.lpc's own error_handler() is indistinguishable
from a silent object-load failure unless you specifically suspect and
test the handler in isolation.
simul_efun.lpc: see §5 for the driver-only-efun shims it provides, and
§3 for resolve_ob().
3. add_action()/add_verb(): a two-call idiom, not a bare-function-value bug
This driver's add_action and add_verb are two separate one-argument
driver instructions (lexical.l: both are real lexer tokens,
F_ADD_ACTION/F_ADD_VERB -- not efuns in the modern sense at all,
literally keywords in this dialect). add_action(fn) registers a
pending function name; add_verb(cmd) binds the verb to whatever was
most recently registered. Confirmed by reading interpret.c/
simulate.c's F_ADD_ACTION/F_ADD_VERB cases, and by the archive's
own usage: room/pub2.lpc calls add_action("order") twice with
two different add_verb()s ("order" and "buy"), proving the two
calls are genuinely decoupled, not just cosmetic sugar over a 2-arg
add_action(fn, cmd).
This driver's own add_action spec is void add_action(string|function,
string|string*, void|int) -- the verb argument is mandatory here
(only the trailing flag is optional), so a literal single-argument
add_action(fn) call would fail to compile even before considering
add_verb() at all.
Confirmed exhaustively via a small Python balanced-paren/string-aware
scanner before touching anything: every single add_action(...) call
in the archive (308 of them, across 58 files including two .h files --
room/std.h's TWO_EXIT-style movement macros) is immediately followed
by exactly one add_verb(...) call, with nothing but whitespace
between them, 1:1 paired, zero exceptions. Mechanically merged into
single two-argument add_action(fn, cmd) calls tree-wide (dropping the
separate add_verb call). This is the correct, semantics-preserving
translation, not a workaround -- confirmed against the pub2.lpc two-verb
case (add_action("order", "order"); add_action("order", "buy");,
i.e. the same function reachable via two different words, exactly as
pub2.lpc's own author intended) and live-tested (§7): "smile" and
other social commands, "east"/"west"/"south" movement, all correctly
verb-matched.
4. move_object(item, dest): moves an arbitrary object, not just the caller
The single most consequential driver-dialect gap in this archive.
interpret.c's F_MOVE_OBJECT case takes two arguments (the object
being moved, and its destination) and moves *that* object -- not
necessarily this_object(). Used ~200 times across the whole obj/ and
room/ tree (move_object(weapon, guard), move_object(this_player(),
"room/church"), etc.) -- this is how the archive's own obj/player.lpc
moves a fresh character into their starting room in the first place.
FluffOS's real move_object() efun signature is void
move_object(object|string) -- one argument, always moving
current_object. Getting an *other* object to move requires the
standard LPC idiom item->move_object(dest) (call item's own
move_object function via call_other(), which runs with
current_object == item, so a bare move_object(dest) inside that
function correctly targets item). All ~200 call sites were mechanically
rewritten to this form (a Python balanced-paren/top-level-comma-aware
scanner split each call into exactly 2 arguments -- 0 anomalies -- and
emitted A->move_object(B), or plain move_object(B) when A was
already textually this_object()).
That rewrite alone was not sufficient and looked, for a while, like
it hadn't worked at all: call_other() on this driver never falls back
to a real efun of the same name if the target object's own *compiled
program* has no LPC function called move_object -- and nothing in this
archive defines one. item->move_object(dest) was therefore silently
resolving to nothing (a call_other() to a nonexistent function just
returns 0, no error) -- confirmed by a from-scratch character never
actually arriving in a room (environment() was 0 after login, with
zero errors logged anywhere). Fixed by giving *every* object in the
mudlib a real move_object() function, for free, via the global include
file (secure/auto.h, injected into every single compile):
varargs void move_object(mixed dest) {
efun::move_object(dest);
}efun:: (not a bare call) is required inside this function -- a bare
move_object(dest) here would recurse into this very function forever,
since a local definition always shadows a same-named efun for plain
calls within its own file. valid_override() had to be added to
master.lpc for the efun:: syntax to even compile (this driver gates
it behind a valid_override() master apply -- see secure/master.lpc).
A second, narrower problem layered on top of this: resolve_ob()
(simul_efun.lpc) handles destination strings. This driver's original
find_object() silently fell back to load_object() when the named
object wasn't already resident (simulate.c: ob = find_object2(str); if
(ob) return ob; return load_object(str);) -- but FluffOS's real
find_object() efun is a pure in-memory lookup with no such fallback,
and the real move_object() efun's own string-destination handling calls
exactly that non-compiling find_object() internally, throwing
"move_object failed: could not find destination" the first time
anything tries to move something into a room that hasn't been touched
yet (which, for a brand-new character's very first room, is *every*
boot). resolve_ob(x) (find-or-load, matching the original semantics
exactly) wraps every destination argument at every rewritten call site:
A->move_object(resolve_ob(B)).
5. reset(arg) with arg==0: expected to fire synchronously, doesn't on this driver
The second most consequential gap, and the hardest to diagnose --
new characters arrived in a real room (§4 fixed) but every room was
pitch dark (test_dark() -> set_light(0) <= 0) despite every room's
own reset(arg) { if (arg) return; set_light(1); ... } -- the extremely
common "arg 0 means first-time setup" idiom, confirmed as this driver's
own convention by reading simulate.c: apply("reset", ob, 0) is
called synchronously right after object creation (both for fresh
compiles and clones), *before* anything else touches the object.
FluffOS's own object lifecycle does not do this. Read directly out
of ~/src/fluffos/src/vm/internal/base/object.cc and backend.cc,
since this gap is subtle enough that guessing would have been wrong:
call_create() (called for every compile *and* every clone) calls
set_nextreset(ob) -- scheduling next_reset a full time to reset
interval in the future (randomized-halved if that config option is on)
-- then runs create(), then immediately sets O_RESET_STATE.
Every later reset check (try_reset()'s lazy on-touch path *and*
backend.cc's eager periodic-scan path) requires both "time's up" AND
"O_RESET_STATE not set" -- and the latter is already true the instant
create() returns, for every object, always. A brand-new object's own
reset(arg==0) therefore does not fire again until a full reset
interval (900-1800s here) has elapsed, full stop -- regardless of lazy
vs. eager reset config, regardless of anyone visiting the room in the
meantime. (This is presumably a non-issue for the rest of this 940+-lib
corpus because their room/NPC base classes do first-time setup in
create() directly rather than depending on reset(0) -- but this
archive, again, predates that split.)
Fixed the same way as §4 -- a universal default, injected via
secure/auto.h into every compile, confirmed to collide with nothing
(grep -rln "^create(" across the whole archive returns zero files --
nothing here defines its own create() at all, since the concept
didn't exist on the original driver):
void create() {
call_other(this_object(), "reset", 0);
}call_other(), not a bare call, deliberately: plenty of files (most
plain items) have no reset() of their own, and a bare call to an
undefined function is a compile error, while call_other() to a
nonexistent function just harmlessly returns 0. This one create()
addition alone took every room in the archive from permanently dark and
empty (or, for shops/NPC spawns, permanently unstocked) to fully
populated on first visit -- confirmed live, §7.
Known, accepted, harmless side effect: two archive files'
reset()/create()-adjacent code assumes a real connected player
(this_player()) is present at first-reset time, which is true in
actual gameplay but not in lpcc's standalone compile-check harness (no
interactive session exists there) --
obj/wiz_soul.lpc:16 (restore_object("players/" +
call_other(this_player(),"query_real_name",0))) and obj/crash.lpc:6
(a one-shot "a wizard has arrived" announcement effect, shout(...+
call_other(this_player(),"query_name")+...)). Both throw Bad argument
1 to EFUN call_other() ... Got: int(0) *only* under lpcc_check.sh;
confirmed via a real boot (obj/wiz_soul is one of the 5
room/init_file-preloaded objects) that this is silently absorbed by
master.lpc's preload() (catch(load_object(file))) with no visible
effect, and confirmed live that a real wizard's soul on (which clones
obj/wiz_soul fresh, in a real player's connection context) works
correctly (§7). Not fixed -- fixing it would mean guarding every
this_player() use against a test-harness-only condition that never
occurs in real play, which is exactly the kind of harness-only false
positive this project's own methodology (§10.4) says to identify and
leave alone rather than "fix."
6. Smaller driver-compat fixes
statustype keyword:obj/torch.lpcandobj/weapon.lpcdeclarestatus is_lit;/status wielded;-- this driver's own boolean-ish type synonym forint(confirmed: unrelated to the "reserved word" class below, this driver's grammar genuinely acceptsstatusas a type name). Not a valid type on this driver; both replaced withint(2 files, 2 declarations).classused as a plain variable name:room/fortress.lpcandroom/orc_vall.lpcboth declareint n,i,class,value,weight;--classis a genuine reserved word on this driver build (__STRUCT_CLASS__is predefined), so the whole multi-variable declaration failed to parse, and every use downstream on the same line cascaded into further "Undefined variable" errors forvalue/weighttoo (one root cause, not three -- AGENTS.md §6.4). Renamed towclassin both files (5 renamed uses each).new(hd)function name:obj/bullboard.lpcnames its "post a new message" handlernew(bound to thenotecommand viaadd_action("new", "note")) -- also a reserved word on this driver (struct/class instantiation). Renamed the function and its registration tonew_note. While in this function anyway, fixed a same-function author typo blocking compile oncenewno longer masked it:envorinment(this_object())->environment(this_object())(AGENTS.md §6.6 -- pre-existing author typo, not a conversion artifact, fixed because the compiler flags it).crypt()/snoop()need mandatory 2nd/1st args: this driver'scrypt(string, string|int)has no 1-argument form (the classic driver auto-generated a salt);obj/player.lpc's 2 password-set call sites (password = crypt(password)) becamecrypt(password, 0)(a fresh random salt per this project's own documented convention, see AGENTS.md §7.14), and its 2 password-*verify* sites (crypt(p) != password) becamecrypt(p, password) != password(verify against the stored hash's own salt prefix, the standard idiom). Separately,snoop()with zero arguments (this driver's "stop my own snoop" form) needed the real efun's 1-argument form:snoop(this_player()).
7. Verification performed (live, real driver, real socket client)
Raw Python socket scripts (connect(), send/recv with short sleeps,
no tmux_mud.sh) against ~/src/fluffos/build-debug/src/driver
config.fluffos, run from libs/lpmud141/ (required -- log directory :
/log resolves against the driver's own CWD, AGENTS.md §5.2):
- New character registration: connect -> name prompt (
valid_name()requires 1-11 lowercase a-z letters, no digits -- this archive's own rule, predating non-ASCII names entirely, so a plain English name liketestwizardis the correct/only valid input, not a workaround) -> "New character." -> password (typed twice, matching) -> guild-advance message -> full MOTD -> arrives lit, in the correct starting room (room/church, "Endoplasmatorium") with its real long description. look: full room description, no longer "It is too dark." (§5's headline fix).score: real stats (50 HP, level 1, title "the utter novice"), not all-zero.soul on+ emote commands:smile-> "You smile happily." (a realadd_action/add_verb-registered command, §3, reaching a correctly-move_object()-attached clone, §4).- Movement:
east(Endoplasmatorium -> clinic, full description),west(back),south(-> village green) -- confirmed multiple distinct rooms all independently reset/lit/described correctly, not just the login room. i(inventory): correct empty-inventory output (not "too dark").quit: "Saving <Name>." -- and reconnecting with the *same* character name + password afterward (including across a full driver process restart,kill+ relaunch) correctly restores the saved character (persisted stats, incrementedage), not a fresh "New character." -- confirmed twice, across two separate restarts.- Outbound network:
grep -rln "socket_connect\|socket_create\|resolve("across the whole mudlib returns nothing -- no outbound connections of any kind, as expected for an archive this old. - No admin-account seeding (AGENTS.md §1.5): this codebase's only privilege concept is a per-character
levelfield promoted viaobj/wiz_soul.lpc's in-game "grant" commands (real gameplay, not an account/wizard-rank system with a login-time seed hook) -- N/A here, same asimud/lil.
8. Deliberately not fixed / not in scope
- Everything in §5's "known, accepted, harmless side effect" note (
wiz_soul/crashunder thelpccharness only). obj/soul_com.lpcandroom/def_castle.lpcboth faillpcc_check.shstandalone (Undefined variable 'cap_name'/'NAME'/'DEST') -- both are genuinely include-only/template files, never meant to be compiled or loaded on their own:soul_com.lpcis#include-d verbatim at the end ofobj/soul.lpc(which declaresstring cap_name;*before* the#include, so the combined file compiles and loads fine -- confirmed,obj/soulis one of the 146 passes);def_castle.lpcis a templatecreate_wizard()(secure/ simul_efun.lpc) concatenates a generated#define NAME .../#define DEST ...header onto before writing a new file, never compiled asroom/def_castle.lpcdirectly. Both arelpcc_check.shbatch-sweep false positives from feeding every.lpcfile to the compiler indiscriminately, not real bugs -- same class as this project's documented precedent for test-fixture/mixin files (AGENTS.md,lil'ssingle/tests/compiler/fail/).- Content is thin by design, not by omission: this is a genuine 1990s university reference mudlib (
README's own contact is[email protected], "HMC LP-mud" -- Harvey Mudd College), not a commercial game. A handful of rooms, a starter guild, soul/emote commands, basic combat, wizard tools. Left exactly as shipped -- no new rooms, NPCs, or commands invented, per this project's standing rule.
9. Deep functional test (round two, 2026-08-27)
One continuous session against a live ~/src/fluffos/build-debug/src/driver
config.fluffos, raw Python socket scripts (no tmux_mud.sh), following
AGENTS.md §10.7's methodology plus the seven standing cross-cutting bug
patterns (§7.121/§8.3a/§7.122/§7.123/§7.124/§7.126/§7.129).
9.1 Standing cross-cutting patterns: all clean
- §7.121 (float in a declared-
intfunction):grep -rn '\bfloat\b'across the wholework/tree returns zero hits -- this codebase never usesfloatat all. - §8.3a (
privatedemoted toDECL_HIDDEN, breakingadd_actiondispatch): the onlyprivatehit in the whole tree issecure/master.lpc'sprivate nomask void preload(string file)-- a MASTER APPLY, invoked by the driver viaapply_master_ob()withORIGIN_DRIVER, which this driver's ownapply_low()maps to aDECL_HIDDENpermission requirement (confirmed by reading~/src/fluffos/src/vm/internal/apply.cc) --private(DECL_PRIVATE, a strictly higher bit value) passes that check fine. Not the §8.3a shape at all (that class is specifically aboutadd_action/call_out-style *external* dispatch onto an inheritedprivatefunction). - §7.122 (autoload/class-item duplication on reconnect): this archive's own analogous mechanism is
obj/player.lpc'sauto_load/compute_auto_str()/load_auto_obj()(used for exactly one real marker item,obj/shout_curse.lpc, a "can't shout" curse). Manually traced every call path (fresh login, existing-player login, and the "already playing / throw the other copy out" merge flow) and confirmed this driver's plainsave_object()does NOT independently serialize carried inventory the way the TMI-2-lineage bug's driver did, andload_auto_obj()is only ever reached on a genuinewhere==0fresh-placement login, never on the merge path -- no duplication found, confirmed clean by code trace (not live-reproduced, since the shipped archive has exactly one auto-load item type and reproducing a real cursed character was out of scope for this pass). - §7.123 (bare file-scope
IDENT = (...)statement): zero hits fromgrep -rnE '^[A-Za-z_][A-Za-z0-9_]* *= *[\(\[]'acrosswork/. - §7.124 (percentage/fraction unit-mismatch literal): zero hits from
grep -rnE '= *0\.[0-9]+;'-- consistent with there being nofloatusage at all in this codebase. - §7.126 (stale pre-
.c-to-.lpcextension in.odoor/save data): N/A -- this archive has no AREA/coordinate-grid engine and no.o-stored room-exit mechanism at all; every exit is a literal string in source (room/std.h's*_EXITmacros).room/init_file(a plain-text preload listing, not.odata) DOES still list its five entries with a stale.csuffix (obj/player.c,obj/soul.c, etc.) -- confirmed harmless:secure/master.lpc'sepilog()/preload()reads this file verbatim andcatch(load_object(file))s each line, and a live boot shows all five loading with zero entries inlog/log_catch/log/log, confirmingload_object()on this driver correctly resolves a.c-suffixed path to the real.lpcfile. - §7.129 (
tell_room()wrapper forwarding a defaulted0excludearg intomessage()): N/A --secure/simul_efun.lpcdefines notell_room()wrapper at all; everytell_room()call site in the archive (obj/chat.lpc,obj/door.lpc,obj/wiz_soul.lpc,room/sub/door_trap.lpc) resolves to the real native efun directly, which already handles an omitted 3rd argument correctly.
9.2 Re-examined secure/master.lpc/secure/simul_efun.lpc: no missed edge cases
Re-read both files in full against this pass's own findings. preload()
being private is intentional and correct (see §9.1 above, not a
missed §8.3a case). error_handler() still has the objectp() guard
and sprintf("%s:%d", ...) fix documented in the onboarding NOTES (§2);
no new issue found there. resolve_ob()/transfer()/create_wizard()
in simul_efun.lpc were re-checked line by line while root-causing §9.3
below -- create_wizard()'s own find_player(owner) call was found to
be silently dead (see §9.3), but the function itself is otherwise
correct once that dependency works.
9.3 New finding: find_living()/find_player() never matched anything, archive-wide -- fixed (also filed as AGENTS.md §7.131)
Bug: this driver's find_living()/find_player() require an
explicit, one-time set_living_name() registration (a hashed lookup) --
completely unlike the archive's own original driver, whose
find_living_object()/find_player() (object.c) scan every live
object and call its own id() LFUN per search, with no registration
step at all. Since nothing in this archive ever called
set_living_name() (confirmed: zero hits, grep -rn set_living_name),
both efuns silently returned 0 for every name, always -- no crash, no
compile error, no debug.log signal.
Blast radius (all confirmed dead before the fix, all confirmed
working after): obj/player.lpc's tell/whisper commands (always
"No player with that name."); the "already playing / throw the other
copy out" duplicate-login guard in move_player_to_start() (completely
unreachable -- reconnecting under the same name while an old,
disconnected-but-undestructed body was still alive silently created a
SECOND live body with the same name, sharing the same save file, rather
than ever prompting to merge them -- reproduced live: two simultaneous
"Dualaa"/"Dualbb"-named bodies visible in the same room after a plain
reconnect); every obj/wiz_soul.lpc wizard command that targets a
player/NPC by name (heal, stat, trans/teleport, goto, promote,
snoop, at, echo_to); secure/simul_efun.lpc's own
create_wizard() (find_player(owner) always fails, so the entire
from-scratch castle-creation flow -- see NOTES.md's own onboarding
writeup -- was unreachable); room/post.lpc's online-mail delivery;
obj/wand.lpc's zap target; and room/alley.lpc's Trixie NPC give-back
puzzle (transfer(obj, find_living(lower_case(who))) silently resolved
to a no-op). room/vill_road2.lpc's/new.vill_road2.lpc's "harry" NPC
and every room/NPC's own !living(x)-guarded respawn-on-reset check
were confirmed UNAFFECTED in practice (harry is preprocessor-disabled
via #define HARRY 0 in the live room, and new.vill_road2.lpc is a
dead/unreferenced draft file; every respawn guard already has its own
redundant local-object-variable check that doesn't depend on
find_living() at all).
Fix (3 files, obj/player.lpc/obj/monster.lpc/obj/monster.talk.lpc):
set_living_name(n) added to the two general-purpose NPC classes'
shared set_name(n) function, and to the player body -- but NOT simply
inlined into logon2() where name itself is set, because of a
second-order trap that only surfaces once the primary fix makes
find_player() matches possible at all: the ORIGINAL move_player_to_start()
excludes self from its own "already playing" search by temporarily
blanking the local name variable (correct on the classic per-call-scan
driver; a no-op against this driver's separate hashed registration), and
set_living_name() inserts at the HEAD of a same-name hash chain rather
than replacing an existing entry -- so a self-registration made before
the search made every single login (including a brand-new character's
very first one) find *itself* and report "You are already playing!".
Fixed by deferring set_living_name(name) in player.lpc until
move_player_to_start() has already confirmed no live duplicate exists
(self excluded by simple absence from the table, not by a name-blanking
trick that no longer does anything).
Third bug found only once the above two were live-reachable for the
first time in this project's testing: try_throw_out()'s own
restore_object("players/" + name) call (its default, no-noclear
form) resets EVERY declared variable on the object first, then applies
only what the save file can hold -- since object-type variables were
never in scope to save, this wiped both myself (set moments earlier in
logon2()) and soul (set moments earlier by this same function's own
soul("on") call) back to 0. The very next line,
move_player_to_start()'s myself->move_object(resolve_ob(where)),
then crashed with *Bad argument 1 to EFUN call_other() ... Got:
int(0) (confirmed in work/log/log), leaving the surviving body with
no environment at all -- reproduced live as look reporting "It is too
dark." (not a real lighting bug: set_light(0)'s "walk up to the
top-level environment" query had no environment to walk up to).
logon2() already has an explicit comment about this exact driver
behavior for its own earlier restore_object() call ("Don't do this
before the restore!") -- try_throw_out()'s later one just didn't carry
the same treatment. Fixed by re-setting myself = this_player(); and
re-attaching soul via present("wiz_soul"|"soul", myself) (the
already-cloned soul object is still physically present -- restore_object()
never touches real inventory, only declared variables) right after that
restore_object() call.
Verified live, full sequence: register a fresh character -> look
(lit, full description) -> disconnect without quit (leaving the body
alive, un-destructed) -> reconnect under the same name -> "You are
already playing! Throw the other copy out ?" now correctly fires (it
never did before this fix) -> y -> look (fully lit, not "too dark")
-> score (correct restored stats) -> soul on ("You already have
one.", confirming the soul reference was correctly re-attached to the
already-present clone rather than silently duplicating it) -> quit ->
reconnect one more time (ordinary, no false "already playing", correct
restored data). Separately, a two-socket simultaneous-connection test
confirmed tell <name> <msg> now actually delivers across two live
players (previously always "No player with that name."). debug.log
and work/log/{log,log_catch,compile} checked clean (zero errors)
across the entire sequence, both before and after the fix (the crash
this fixes writes to work/log/log via master.lpc's own
error_handler(), not debug.log).
Combat (a fresh character vs. room/yard.lpc's beggar NPC, using the
free "small knife" in the same room) exercised cleanly post-fix: gradual
HP exchange on both sides, normal experience/hit_point progression
via repeated score checks, no anomalies. An earlier same-session
combat run (before these fixes landed) had shown one character die
after only 1-2 visible incoming hits from a weak (weapon_class 3,
unarmed) NPC -- root-caused to exactly this bug: rapid manual reconnect
testing under the SAME character name (with the "already playing" guard
dead) had left multiple simultaneous same-named bodies alive, and the
specific body that then fought the beggar was very likely a corrupted
one carrying stale state from the same myself/soul-clearing failure
mode above, not a separate combat bug -- not independently reproduced
after the fix, and combat itself behaves correctly in every post-fix
test.
Test-harness note: work/players/ was found containing only the
original archive's own shipped guest.o (raw/mudlib/players/guest.o,
level 2, "Guest" character with 397 experience, matching a real prior
session on the original driver) -- accidentally deleted mid-session
during test-character cleanup, then restored from raw/ and confirmed
still loads correctly (this driver's plain-text save format parses the
classic driver's own save file without any conversion needed -- a
requirement of the guest login path, since obj/player.lpc's own
logon2() special-cases an empty password specifically for that
account). All other test characters created during this pass were
deleted afterward (not committed) per this project's git add -u
convention for avoiding test-save clutter.
10. WASM status audit (2026-09-01)
playable. Booted ~/src/fluffos/build-wasm/src against this lib via
scripts/wasm_client.js -- clean boot, zero fixes needed. Full session
verified: register a brand-new character (letters-only name), land in
the Endoplasmatorium lit and described, look, score, quit (with
autosave). No sockets/pcre/uptime-gate issues -- consistent with this
lib's own "no outbound network connections of any kind" finding above.