info.html · 介绍 · play.html · 游玩 · llms.txt · 下载与本地运行 · ZIP · GitHub
A classic English-language LPMud, admin'd by "Cyanide" from 1997 to 2000, running the public-domain-released TMI-2 ("The Mud Institute") 1.4alpha mudlib base. A ~13,500-file archive of real 1997-2000 wizard-home content across dozens of authors (Cyanide, Chronos, Nightmask, Spooner, and many more). New characters pass through a full TMI-2 registration (password, email, real name) and a menu-driven race/gender/handedness/alignment stat-reroll wizard before landing in Decatur, the starting town. A newbie helper NPC, Alucard, stationed at the Intersection east of the town center, points new players toward starter equipment and directs them to any of five class halls scattered around town: a Pit Fighter's guild and a Sharpshooter's guild for warriors (joining awards a class-marker item, "A Pit Fighter's Scars"), a mage hall, a rogue hall, and the Temple of Talos for priests. The preloaded channel daemon makes a real outbound socket-connect attempt on every boot to a real (now long-dead) 1999-era Intermud-3 router address, reflecting the mud's original era of live inter-mud connectivity.
README
A classic English-language LPMud, admin'd by "Cyanide" from 1997 to 2000, built on the TMI-2 ("The Mud Institute") 1.4alpha mudlib base. Released into the public domain by its original admin; source cloned from git clone https://github.com/clockworksoul/MortalRemains. Slug mortremains, number 929, port 40231.
A real, messy multi-author archive: ~13,500 files of genuine 1997-2000
wizard-home content from dozens of authors (Cyanide, Chronos,
Nightmask, Spooner, and many more), full TMI-2 character creation
(name, password, email, real name, then a menu-driven race/gender/
handedness/alignment/stat-reroll wizard), and a real starting town,
Decatur. See NOTES.md for the full porting log, including the most
severe bug found (a driver-dialect mismatch in deep_inherit_list()
that silently blocked every new character's registration from ever
completing) and the classic std/monster.lpc fix that alone recovered
~1770 cascading compile failures across the codebase's entire
NPC/monster class hierarchy.
Registration flow
name -> "Is this really the name you wish to use? (y/n)" -> password
-> confirm password -> email (any format accepted) -> real name
(optional) -> a menu (1 reroll stats, 2 swap two stats, 3
handedness, 4 gender, 5 race, 6 alignment, 0 done, ? help) ->
"Are you SURE..." confirm -> lands in the real game world (The YMAA,
Decatur).
Admin account
- id:
fluffos - password:
Mud@2026 - privilege:
(adminaccess)group membership (adm/etc/groups, grants global read/write ACL) plus the in-game wizard rank/PATH (seeded directly into the account's save data, since this codebase's own promotion command requires an already-active admin to run it -- seeNOTES.md\S10 for why and exactly what was seeded).
Verified live: wizard news section on login, [NEO] marker in who,
correct routing to the wizard start room, and update <path>
succeeding (this project's standard admin/ACL smoke test).
Status
Boots clean, zero fatal errors. A brand-new character registers all
the way into the game world through the real registration flow (not
just "reaches a prompt"), and look/score/who/quit all produce
correct output. 6246 of 7928 .lpc files pass a full lpcc_check.sh
compile sweep -- the remainder is dominated by a completely orphaned,
unreferenced ~870-file legacy subtree (adm/AFD/, dead code bundled in
the archive but inherited by nothing live) plus genuinely pre-existing,
non-preloaded content gaps in personal wizard-home directories (missing
headers, abandoned drafts, truncated experiments) -- none of it
reachable from the standard registration/gameplay path. See NOTES.md
for the full breakdown.
Note: the preloaded channel daemon makes a real outbound
socket-connect attempt to a hardcoded, real (long-dead) 1999-era
Intermud-3 router address on every boot. This is genuine intended
upstream behavior, not a bug -- see NOTES.md \S12 before adding this
lib to any automated high-frequency reboot/test loop.
WASM status: playable. Clean WASM boot and full registration/game
session with zero fixes needed; the outbound I3 socket attempt (see
below) fails gracefully without a sockets package. See NOTES.md.
Local run
cd libs/mortremains
~/src/fluffos/build-debug/src/driver config.fluffosGame port: 40231.
NOTES · 移植与修复记录
Mortal Remains -- porting notes
Source: git clone https://github.com/clockworksoul/MortalRemains (commit
3e9c6a98f9f78629d6e693570f698a043be634d2, cloned 2026-08-26). The
repo's own README.md is a short first-person note from "Cyanide", the
mud's original admin, releasing the code into the public domain: "Mortal
Remains was based on the TMI-2 mudlib version 1.4 alpha." Confirmed
genuine and sizeable before investing further effort: 13,515 files,
85MB raw, real multi-year (1997-2000) wizard-authored content across
dozens of home directories, a real historical log/runtime/log/log
with entries dated back to 1999-2000, and archived player save data
(.o files) for real accounts from that era. Slug mortremains,
number 929, port 40231.
1. Two subtrees excluded/noted before conversion
lima-1.0a8/(top-level sibling of1.4alpha/): a completely separate, self-contained mudlib (the Lima mudlib) with its ownlib/,etc/config.lima, driver-build scaffolding. Nothing under1.4alpha/references it (confirmed by grep). Excluded fromraw//work/entirely before runningconvert_lib.sh, matching this project's convention for bundled-but-unrelated codebases.adm/AFD/(~870 files, inside the real mudlib root): a completely orphaned, self-contained alternate object-system snapshot (its ownbody.c,room.c,object/prop_logic.c, etc., with its ownattic//old/subdirectories) that nothing anywhere else in the live tree inherits, includes, or references (confirmed:grep -l "adm/AFD"across the rest of the tree returns zero hits). Left in place, undocumented as dead weight -- not deleted, not fixed. This subtree alone accounts for roughly a third of the compile sweep's remaining failures (515 of ~3287 in the first pass); none of it is reachable from the live game.
2. Conversion
Pure ASCII archive (English-language mud) -- confirmed via a
pre-check (grep -rlP '[\x80-\xFF]' found zero non-ASCII source
files) before running convert_lib.sh, so the encoding step was a
no-op as expected. 7928 .lpc files after the .c->.lpc rename;
9208 literal .c" references fixed; 284 local angle-bracket includes
converted to quotes; 214 files touched by the static->nosave
sweep. One collision from the static->nosave sweep, per AGENTS.md
\S4.3's known class: u/l/lysander/workroom.lpc had a "static":
"..." mapping key (a help-topic word, not a path) that the sed
rewrote to "nosave":...; reverted by hand. No #define nosave
static/#define protected static compatibility-shim collision found.
All headers resolve through a single global /include/ directory (no
<local.h>-style per-directory includes anywhere in this codebase),
so no get_include_path() master apply was needed.
3. The master-boot-blocking crash: explode("/", "/") returns an
EMPTY array on this driver, not MudOS's two empty strings
The single highest-impact bug in this port, and the hardest to
diagnose -- a completely silent crash with no error text printed
anywhere, discovered only via gdb --batch -x 'catch throw' -x run.
Boot got as far as compiling master.lpc and simul_efun.lpc cleanly (warnings only), then aborted with just:
The simul_efun (/adm/obj/simul_efun) and master (/adm/obj/master) objects must be loadable.
Please check log files for exact error....with genuinely nothing else in debug.log, log/runtime, or
stdout -- the driver's own mudlib_error_handler path apparently
swallows the underlying error text when it fires this early in boot
(worth a look for whoever hits this again, but not investigated
further here since the crash itself was fully diagnosed another way).
A gdb backtrace on the C++ exception throw nailed it exactly:
#1 error_handler (err=0x... "*Array index out of bounds.\n")
#2 error (fmt=0x... "Array index out of bounds.\n")
...
#5 apply_master_ob (fun=5 [APPLY_DOMAIN_FILE], num_arg=1)
#6 set_master (ob=...)set_master() (part of the driver's PACKAGE_MUDLIB_STATS
bootstrap, called from init_master() the moment master.lpc first
loads) calls master->domain_file("/") to establish the backbone
domain name. master.lpc's domain_file():
string domain_file( string filename )
{
string *path;
path = explode( filename, "/" );
if( !path ) return "NONAME";
switch( path[0] ) // <-- crashes here for filename == "/"
{ ... }
}The original TMI-2 authors clearly wrote this expecting old MudOS's
explode("/", "/") to return ({ "", "" }) (or at least some
non-empty array) -- path[0] would then safely be "", falling
through to default: return "NONAME";. This driver's sane-explode
semantics instead return a genuinely empty array for that input,
so path[0] throws Array index out of bounds. immediately, before
master_ob is even fully live, aborting the whole boot. author_file()
has the exact same latent defect (same if (!path) return... guard
that never checks sizeof(path)), though it happens not to be
reachable with an empty-array input from this driver's own bootstrap
call (it's invoked with the master file's own path, which always
explodes non-empty). Fixed both defensively:
if( !path || !sizeof(path) ) return "NONAME";This is the same underlying "SANE_EXPLODE_STRING empty-array edge case" bug class flagged in this project's general checklist, just hitting a master-apply chokepoint instead of ordinary mudlib code, and therefore fatal to the entire boot rather than a single feature.
4. ref is a reserved word on this driver (__REF_RESERVED_WORD__)
Confirmed via the compiler source (lexer_utils.cc: {"ref", L_REF,
0}) and this driver's own advertised feature list
(__REF_RESERVED_WORD__ in its startup dump) before touching
anything. Four sites used ref as a plain identifier (parameter or
local variable name):
adm/daemons/channels.lpc(preloaded) --object refparameter tocreate_channel(), plus a localobject ref;inregister_channel(). Renamed torefob.adm/daemons/ref_d.lpc--string ref;local inresolv_ref(). Renamed torefname(only the identifier; the quoted string key"ref"used elsewhere in the same function viathis_player()->query("ref")is untouched, since that's genuinely a property name, not the reserved word).cmds/file/_changelog.lpc--void pop_coder(int ref). Renamed torefidx.u/c/cyanide/working/channelsFE.lpc-- a personal wizard-sandbox draft copy ofchannels.lpcwith the identical bug shape; fixed for consistency even though it's not reachable from anything live.
cmds/object/_ref.lpc (a genuine "ref" command implementation) was
checked and does NOT hit this bug -- it only uses ref_name and the
quoted string "ref", never a bare ref identifier.
5. inherit textually after global variables -- four independent
instances, one with severe cascading impact
The classic \S6.1 dialect bug ("Illegal to inherit after defining global variables"), but this codebase has it as a recurring authoring habit rather than a single shared-header bug -- found independently in at least 4 completely unrelated files, plus ~200 more scattered across individual non-preloaded personal content files (see \S8 below):
adm/daemons/rebootd.lpc(preloaded) --int count_down = 28800;beforeinherit TIME_D; inherit DAEMON;. Reordered.adm/daemons/registryd.lpc--int active = INACTIVE; mapping reg_map = ([]);beforeinherit "/adm/daemons/timezone";. This one is called (unguarded) fromlogind.lpc's very firstget_name()handler on every login attempt (REGISTRY_D->query_active()), so a brokenregistryd.lpcmeant every single connection attempt crashed with*No program in object '/adm/daemons/registryd'!the instant a name was typed. Reordered.std/monster.lpc--object tmp; nosave int speed, hb_status;beforeinherit LIVING;. The single biggest compile-sweep win in this port:std/monster.lpcis the universal NPC/monster base class (every mob, animal, and non-player creature in the entire ~13,500-file archive ultimately inherits it), so this one fix resolved 1770 of the ~3287 first-pass compile failures in a single edit -- a textbook instance of AGENTS.md \S6.4's "one shared root cause, not N bugs." Reordered.d/Ancients/rooms/wizrm.lpc--object maker;beforeinherit ROOM;. This is "The Immortal's Hall", the room every *promoted wizard* lands in on login (distinct from the ordinary player start room) -- a broken copy meant every wizard login crashed with*No program in object '/d/Ancients/rooms/wizrm'!right after the news screen, immediately followed by a secondary*Bad argument 3 to EFUN message()from the failed move. Only surfaced once the admin account was actually promoted to wizard and logged back in (see \S7) -- ordinary player accounts never touch this file. Reordered.
6. std/user/tsh.lpc: private inherit + qualified super-call
(Name::func()) is rejected by this driver even from the SAME
file that declared the private inherit
A new driver-dialect finding for this project's catalog. tsh.lpc
does private inherit STACK_ADT; (a directory-stack push/pop/query
ADT) and then, from within its own pushd()/popd()/dirs()/
initialize_tsh() functions, calls the base class's functions via
the qualified super-call syntax: stack::push(path), stack::pop(),
stack::query_stack(), stack::alloc(pushd_size). This is the
idiomatic way to reach a privately-inherited base's functions and
compiles fine on many LPC drivers -- but this driver's compiler
(compiler.cc's function-lookup logic) rejects ANY qualified
Name::func() call to a function whose inherit was declared
private, unconditionally, with no special case for "the caller is
the very file that declared the private inherit":
if (tmp == -1 || (ip->type_mod & DECL_PRIVATE)) {
yyerror("Called function is private.");
goto invalid;
}Confirmed by compiling tsh.lpc standalone (not just as part of a
larger inheritance chain) -- identical 4 errors either way, ruling out
any interaction with how tsh.lpc itself later gets inherited into
std/user.lpc via include/user.h. Fixed by dropping the stack::
qualifier -- privately-inherited functions remain directly (unqualified)
callable from within the declaring file on this driver, so push(path),
pop(), query_stack(), alloc(pushd_size) resolve correctly without
the qualifier. (A separate, pre-existing dangling reference in the same
file -- history::hist_alloc(hist_size), where no HISTORY_ADT is
ever inherited at all -- was investigated and left alone: this driver
does NOT flag it as a compile error at all for reasons not fully
tracked down, and a code comment in the same block ("According to Beek,
the next 2 lines should go, but that kills history completely") shows
the original authors already considered this code suspect; not a
driver-compat blocker either way, so left as pre-existing dead-ish
content per policy.)
7. Master's valid_write()/valid_read() bare-equality fallback
never matches the driver's extension-appended filename (\S7.49)
Exactly the catalogued bug class. master.lpc's valid_write()/
valid_read() fall back to file == data_file(user) ||
file == user_data_file(user) when the group/path-based ACL doesn't
otherwise grant access -- but data_file()/user_data_file() both
return the BARE path (no extension), while this driver's
save_object()/restore_object() efuns pass the FULL filename
(.o already appended) to the master apply. The two never matched,
so every player's own save silently failed ACL the moment a real
save fired. This didn't show up at boot or at look/score -- it
first fired at the password-confirmation step of brand-new character
registration (std/connection.lpc's set("password", ...) triggers
an autosave via save_data_conn()), with the exact symptom AGENTS.md
predicts: *Denied write permission in save_object(). Fixed both
functions:
return( file == data_file( user ) || file == data_file( user ) + ".o" ||
file == user_data_file( user ) || file == user_data_file( user ) + ".o" );8. THE most severe bug: exec()'s ACL wrapper's deep_inherit_list()
check silently denied EVERY new character's connect(), forever
The single most severe bug found in this port -- it silently blocked 100% of new character registrations from ever completing, with no compile error, no crash, and no log entry of any kind; only a generic mudlib message ("Error connecting to your body... try again...") that looked at first glance like ordinary content/flavor text.
std/connection.lpc's connect() (called as the very last step of
character creation, from newuserd.lpc's finish_up3()) does:
if (!exec(body_ob, this_object())) {
write("Error connecting to your body... try again...\n");
body_ob->remove();
remove();
return 0;
}exec here isn't the raw driver efun -- it's shadowed by a mudlib ACL
wrapper in adm/simul_efun/overrides.lpc:
nomask int exec(object to_obj, object from_obj) {
...
New = base_name( to_obj );
if( New != CONNECTION && member_array("std/body.lpc",
deep_inherit_list( to_obj ) ) == -1 )
return 0;
...
}The intent is clear: allow exec() onto the player's own connection
object, OR onto anything that inherits std/body.lpc (i.e. a real
player body). But this driver's deep_inherit_list() efun
(array.cc) always prepends a leading slash to every returned program
name (add_slash(pr->filename)), returning "/std/body.lpc" --
never the bare "std/body.lpc" the mudlib code checks for. Since
body_ob for a brand-new character is never itself equal to
CONNECTION, this member_array() call ALWAYS returned -1, and
exec() ALWAYS returned 0 -- every single new character's
connect() failed, body_ob and the connection both got destructed,
and the game silently dropped the player with a plausible-sounding
"try again" message that, if actually retried, would fail exactly the
same way every time.
Diagnosed by temporarily instrumenting connect() to print
interactive(this_object()) / interactive(body_ob) / the literal
return value of exec() (confirmed: both preconditions for a
*successful* exec() held -- this_object() interactive, body_ob
not yet interactive -- yet exec() still returned 0), which ruled
out the driver's own exec() efun and pointed straight at a mudlib
override; grep -n "^int exec\|nomask int exec" then found it in one
shot. Fixed the one-line string comparison:
if( New != CONNECTION && member_array("/std/body.lpc",
deep_inherit_list( to_obj ) ) == -1 )
return 0;Verified with a fresh full registration end to end (see \S10) --
character creation completes, connect() succeeds, the new player
lands in the real game world instead of being silently dropped.
9. Smaller fixes
AVERAGING_NUM.0float-promotion dialect (\S6.3's catalogued class) --adm/daemons/usage_d.lpc(preloaded) and its near-identical twinadm/daemons/usage_i.lpc, bothif (times == AVERAGING_NUM.0). Rewritten as(AVERAGING_NUM * 1.0).TYPE array NAMEold-MudOS declaration dialect -- 5 sites inu/c/cyanide/public_html/cgi/autodoc.lpc(a wizard's personal web-CGI handler object, real LPC despite living underpublic_html/), e.g.string array commands = get_dir(...). Rewritten asstring *commands.- Undefined variable
k(pre-existing author typo, not a conversion artifact) --std/body/attack.lpc'soutput_attack()has TWO near-identicalforloops overcontents; the first correctly declaresfor (int k=0; ...), the second just saysfor (k=0; ...)with no type, sokwas never declared in that scope. Fixed by addingint. std/private_room.lpc(+ 10 near-identical copies across wizard homes) --void-declared functions whose bodies use a macro that unconditionally returns a value.CHECK_PROT(n)expands toif (check_prot(this_player(), n)==0) return 1;; six functions (guest_add/guest_ban/guest_remove/guest_unban/guest_list/guest_help) use this macro but were declaredvoid, producingerror: Type of returned value doesn't match function return type ( int vs void )-- 6 sites x up to 11 independently-copy-pasted files (std/private_room.lpc,std/attic/private_room.lpc, and personalworkroom.lpc/lroom.lpc/wrkrm.lpc/wrkrm2.lpccopies belonging tochronos,cyanide,zur,kyako,ashdraven(x2),gasman, anddyrin's attic (x2)) for 639 raw error lines total. Verified all 11 copies share the exact same six-function shape before applying a uniform,libs/mortremains/work-scoped sed to all of them (fixed 9/11 outright; the twowrkrm2.lpccopies --ashdraven's anddyrin's attic copy -- still fail for a separate, pre-existing, unrelated reason, see \S11). Fixed by changing the six functions' declared return type fromvoidtoint(both the forward prototypes and the definitions) -- their return value was already effectively meaningful (via the macro) and universally ignored by every caller either way, so this changes nothing observable.
10. Admin account seeding
Registered fluffos/Mud@2026 through the real, full registration
flow (name -> confirm -> password -> confirm password -> email -> real
name -> stat/race/gender/alignment menu -> "done" -> confirm) exactly
like any other new character, landing in the real starting room (The
YMAA, Decatur) with look/score/who all producing correct output,
then a clean quit that closes the connection.
Granting admin here needed two separate, independent steps, neither of which alone was sufficient -- worth recording since this is a new bug-class shape for this project's \S1.5 catalog:
1. File-permission ACL (adminp() / write access): this codebase
gates adminp() through master()->query_member_group(name,
"adminaccess"), backed by the DATA file adm/etc/groups
((adminaccess) (admin):spooner). Added fluffos to that line
((adminaccess) (admin):spooner:fluffos) -- this alone grants
full read/write ACL everywhere (adm/etc/access's (/) line
grants (adminaccess)[rw]), confirmed live via update <path>.
2. Wizard command-set / rank flag: file-permission ACL is
completely separate from whether the *in-game command dispatcher*
ever offers wizard commands at all. That's gated by a wizard
property on the CONNECTION save object (checked by
std/body.lpc's init_setup(): if ((int)(link->query("wizard")))
{ set("wizard", 1, MASTER_ONLY); enable_wizard(); }, called on
EVERY login via logind.lpc's enter_world() -> user->BODY_OB->
setup(0) -> init_setup(0), not just first-time creation) plus the
body's own PATH property (which command directories are
searched). The mudlib's own normal promotion path
(cmds/adm/_makewiz.lpc) requires an ALREADY-active admin to run
it in-game -- but the archive's only pre-existing admin accounts
(cyanide, chronos) have real, unknown 1999-era passwords, and
AUTO_WIZHOOD (a global "everyone who registers becomes a wizard"
switch) is #defined then immediately #undefed in config.h
(deliberately disabled by the original authors -- flipping it back
on would be a content/behavior change affecting every future
registration, out of scope here). Instead, replicated exactly what
_makewiz.lpc itself does, directly on the save DATA: appended
wizard 1 to data/std/connection/f/fluffos.o, and in
data/std/user/f/fluffos.o changed the body's saved
"wizard":0 to "wizard":1 and "PATH":"/cmds/std" to
"PATH":"/cmds/std:/cmds/object:/cmds/xtra:/cmds/file:/cmds/wiz"
(the exact NEW_WIZ_PATH macro value _makewiz.lpc would have
assigned). Verified live end to end on the NEXT login (not the same
session): wizard news section appears, who shows the [NEO]
alignment marker, login correctly routes to "The Immortal's Hall"
(the wizard start room, itself needing the \S5 wizrm.lpc fix to
even load), and update /std/room succeeds with "Updated and
loaded." -- this project's canonical read+compile ACL smoke test.
11. Compile sweep results and known remaining gaps
lpcc_check.sh's full batch sweep, after the fixes above: 6246
pass / 1682 fail out of 7928 total .lpc files (78.8% pass rate).
Given this archive's real composition -- an entirely orphaned ~870-file
dead subtree (\S1) plus roughly 13,000 files of genuine, messy,
multi-author 1997-2000 wizard-home content including abandoned drafts,
truncated experiments, and personal scratch files -- this is
consistent with (proportionally messier than, but the same *kind* of
gap as) the handful of non-boot-critical failures documented on
smaller, cleaner archives elsewhere in this collection. None of the
remaining failures are preloaded or reachable from the standard
registration/gameplay path exercised in \S10. Breakdown of the
remaining 1682:
- ~515 attributable to
adm/AFD/(\S1) -- entirely dead, orphaned code, not investigated further. - ~200 more scattered instances of the \S5 "inherit after global variables" bug, now diffuse across individual non-preloaded content files (personal monsters/spells/rooms/items) with no single file responsible for more than 5 occurrences -- the shared-root-cause phase is exhausted (per AGENTS.md \S6.4's own guidance: fix the *shared* causes, don't blindly bulk-fix scattered per-file instances in a 13,000-file archive). Left as a known, mechanically-identical, low-priority content gap.
- Missing headers, truncated files, and genuinely abandoned/dead experiments scattered across personal wizard homes (
u/*/) -- e.g.#include shit.h,#include /players/jyd/lib/strings.h(a path from a different, unrelated mud entirely), a wizard's own.plan.lpc/plans/plan76453.lpcscratch files. Per this project's policy, not fabricated or fixed -- these predate this port and are not driver-compat issues. - Two
wrkrm2.lpccopies (u/a/ashdraven/rooms/wrkrm2.lpcandattic/dyrin/rooms/wrkrm2.lpc) still fail after the \S9 CHECK_PROT fix, for an unrelated, pre-existing reason (warning: Non-void functions must return a valueat a barereturn;in anint-returning function, present in the ORIGINAL archive before any of this port's edits) -- confirmed via the pre-fix sweep log that this was already a separate failure mode on these two files specifically (theirwrkrm.lpcsiblings in the same two home directories compile clean). Not investigated further; personal, non-preloaded, non-boot-critical content. - A pre-existing, cosmetic author bug in
cmds/object/_update.lpc(the wizardupdatecommand itself):if(extract(file, strlen(file)-2, strlen(file)-1) != ".lpc") file += ".lpc";compares a 2-character slice against a 4-character literal, so the condition is ALWAYS true and.lpcgets appended even when the caller already included it (update /std/room.lpc-> looks for/std/room.lpc.lpc, "does not exist"). Harmless in normal use since players conventionally omit the extension (update /std/roomworks correctly, verified in \S10); left alone as original-author content, not a driver-compat issue.
12. Genuine outbound network connection at boot -- do not sweep into
automated reboot loops
adm/daemons/channels.lpc (preloaded by default) calls
I3_DAEMON->query_chanlist() from its own create(), which
auto-loads adm/daemons/network/I3.lpc. That file's create()
unconditionally does:
router_socket = clone_object(SOCKET, SKT_STYLE_CONNECT_M,
I3_ROUTER, (: read_callback :),
(: close_callback :) );I3_ROUTER is "205.218.122.131 9000" -- a real, hardcoded public IP
from the Intermud-3 network circa 1999-2000 (almost certainly long
dead by now, but the attempt itself is real and unconditional on
every boot; a live ss/ping check during this port's own test boots
found no established connection or SYN-SENT socket, consistent with
that address being unreachable today, not with the mudlib refraining
from trying). Also opens a local listening socket on __PORT__ +
3 (40234 for this port assignment) for OOB_TCP. This is genuine,
intended upstream functionality (the same class of finding as imud
and dsI elsewhere in this collection), not a bug to patch away -- but
per this project's established convention, mortremains should not
be swept into high-frequency automated re-boot/deep-test loops the
way most of this corpus safely can be, since each boot is a real
outbound network event, even if the far end is currently unreachable.
13. Verification performed
- Full clean boot with zero fatal errors,
Initializations complete.reached, driver idles normally in its event loop (confirmed by letting atimeoutSIGTERM catch it mid-epoll_wait, not mid-crash). - A brand-new character (
Fluffos) registered end to end through the REAL menu-driven creation flow (name, confirm, password, confirm password, email, real name, stat-reroll/race/gender/alignment menu, "done", confirm) -- landing in the real starting room (The YMAA, Decatur), not a void or error state. look,score,who, and a cleanquit(server closes the connection) all verified producing correct, well-formatted output post-login.- Admin account (
fluffos/Mud@2026) verified on a SEPARATE, later login: wizard news section,[NEO]marker inwho, correct routing to the wizard start room, andupdate /std/roomsucceeding ("Updated and loaded.") -- this project's canonical read+compile ACL smoke test. log/debug.logandlog/runtime(the latter a real historical file dating to 1999-2000, confirmed viastatthat this port's own test sessions never appended to it once the fixes above landed) both clean of any new fatal errors across every verification pass above.- Outbound-network check performed (\S12) -- no established connection found live, but the code path and hardcoded target address are real and documented regardless.
- WASM status: not attempted (
wasm_statusleft""per task scope).
深度功能测试(§10.7,round two) / Deep functional test (AGENTS.md §10.7)
First real *playthrough* pass on this lib (the onboarding session above
only verified registration + look/score/who/quit + the admin
promotion). Read help newbie first (points at "the fellow in the
center of town" for starter equipment/leaflet/map, and help classes
names Alucard at the Intersection as the class-hall directory), then
played a full, continuous session as a real English-named character
(Bramwell) on the native driver, using a second admin connection
(fluffos/Mud@2026) only for updates and diagnosis. Found and
fixed four independent, severe, live-blocking bugs plus one duplication
bug -- collectively the newbie helper, ~95% of ordinary room-to-room
movement, and two of the five advertised class halls were all
unreachable before this pass, none of it visible to lpcc_check.sh,
a clean boot, or the earlier registration-only smoke test.
1. cmds/std/_go.lpc's extension-check slice is 2 characters, not 4 -- blocks essentially every ordinary room exit whose destination path literal already includes .lpc
The single most severe bug found in this pass. cmd_go() (the
function underlying every directional movement command -- north,
east, etc. all alias to go <dir>) does:
fn = exits[dir];
if (fn[<2..<1] != ".lpc") {
fn += ".lpc";
}
if (file_exists(fn)) {
to = find_object_or_load( exits[dir] );
...fn[<2..<1] is only the LAST TWO characters of the path, which can
never equal the 4-character string ".lpc" -- so the append always
fires. If the room author's own exits mapping literal already ends
in .lpc (confirmed the overwhelming majority convention in this
archive: 4597 grepped exit-value literals include .lpc explicitly,
vs. 225 that don't), fn becomes a doubled ...lpc.lpc path that
file_exists() correctly reports as missing, so cmd_go() prints
"The mists of creation are still active in that direction" and the
move silently fails -- indistinguishable from an actually-unbuilt
room. Reproduced live: moving east from the very first room
(startroom.lpc's only non-wizard-gated exit, to
d/Prime/Central/room/road0_0.lpc, the Intersection) failed this way
immediately after registration; south from the same room happened to
work only because it points at WIZHALL (/d/Ancients/rooms/wizrm,
no .lpc suffix in the macro) and is wizard-gated besides, which is
exactly why this was invisible to the earlier onboarding pass (it
never actually walked a second room).
This is very likely a port-introduced regression, not a pre-existing
1997 bug: the archive's real extension was .c, and this project's
own convert_lib.sh does a blanket literal ".c"->".lpc" string
rewrite as a normal part of conversion (per \S2 above, "9208 literal
.c\" references fixed"). The original line was almost certainly
fn[<2..<1] != ".c" -- a correct 2-character check against the
original 2-character extension -- and the automated rewrite updated
the string literal but had no way to know it also needed to update the
slice-length arithmetic next to it. This is the exact bug class
catalogued in AGENTS.md \S7.118 ("hardcoded 2-character-extension math
left over from the archive's original naming"), just manifesting as an
"always append a redundant extension" checking bug rather than that
section's "always corrupt a derived key" stripping bug -- worth adding
to that section's watch-list shape. (A cosmetically-identical but
functionally harmless sibling of this exact slice-vs-literal-length
mismatch was already documented pre-existing in this same lib's own
\S11, in cmds/object/_update.lpc -- that one predates conversion and
never bit because players conventionally omit the extension when
typing update.)
Fix: fn[<2..<1] -> fn[<4..<1] (the last 4 characters,
correctly matching the 4-character .lpc string). Verified live:
after update /cmds/std/_go, east from the Intersection correctly
loaded and entered road0_0.lpc, and the entire pit-fighter-guild
navigation route below (10 more moves) worked cleanly end to end.
2. Newbie-helper NPC u/c/chronos/alucard.lpc -- and, as a direct consequence, the room that clones him -- both failed to load at all
Alucard is the exact NPC help newbie/help classes point players
at for starter equipment and class-hall directions, cloned into
d/Prime/Central/room/road0_0.lpc ("the Intersection", one room east
of the start room) via reset(). His file has the classic \S5
"inherit after global variables" defect (string *PUNC; declared
before inherit MONSTER;), the exact same bug class as 4 other
individually-fixed instances documented in \S5 above. What's new here:
because road0_0.lpc's own create()/reset() calls
clone_object("/u/c/chronos/alucard.lpc"), and this driver propagates
an inherited file's compile error up through the caller, the room
itself failed to compile and load (confirmed identical in
lpcc_fail.log) -- so this one inherit-ordering typo alone took out
both the newbie helper AND the entire Intersection room (compounding
with bug \S1 above, since bug \S1's "mists of creation" symptom was
the FIRST thing masking this one until \S1 was fixed and east
actually attempted to load the destination).
Fix: moved inherit MONSTER; above string *PUNC; (same pattern
as \S5). Verified live end to end: after updateing both the file and
the room, Bramwell walked east into the Intersection, Alucard was
present, say help triggered his process_say()/help() handler,
and he handed over the real starter kit (boots, leaflet, bat,
armour clones) exactly as the newbie doc promises.
3. Two of the five class halls Alucard advertises had the identical \S5 defect, blocking join entirely
d/class/warrior/pit_fighter/rooms/pitfighterguild.lpc and
d/class/warrior/Sharpshooter/room/SS_guild.lpc -- two independently
copy-pasted files sharing the same header comment ("Pit pit fighter's
guild ilz code ... Cyanide corrupted this room") -- both declare
mapping skills = ([]); mapping min_levels = ([]); int *v_skills, ...
before inherit GUILD;. Reproduced live: walking the pit-fighter-guild
route Alucard gives ("5 south, 2 east, 2 south, and 1 west" from the
Intersection) hit *No program in object
'/d/class/warrior/pit_fighter/rooms/pitfighterguild'! on the final
step. Per AGENTS.md \S6.4/this lib's own \S11 guidance (fix
shared-root-cause bugs found live, don't blanket-sweep the ~200
scattered non-preloaded instances), both were fixed individually since
they sit directly on the newbie class-acquisition path this pass is
meant to exercise -- the other three halls Alucard mentions (mages,
thieves, priests) do NOT have this defect (checked via lpcc_fail.log
before touching anything).
Fix: reordered inherit GUILD; above the global variable block in
both files (same pattern as \S5/\S2 above). Verified live: Bramwell
(strength 15, meets the pit fighter's base_stat/strength >= 10 gate)
successfully joined, title changed to "Cage Cleaner"/"Level 1 Pit
fighter", and received "A Pit Fighter's Scars" (the class marker
item). The Temple of Talos (priest hall, 2e/1s from the Intersection)
was also reached and correctly, gracefully rejected join for not
being chaotic evil -- confirming the rejection path itself works fine
and this is content gating, not a bug.
4. d/class/rogue/obj/dummy.lpc (this lib's own safe-sparring/backstab-testing dummy) had a broken array literal, AND a crash-on-clone bug hiding behind it
This file's own long description ("This is a dummy to test you
backstab out on ... type halt to stop your onslaught") makes clear
intent: a zero-risk training target (execute_attack() is hardcoded
to always return 0, so it can never actually hurt a player), matching
AGENTS.md \S10.7 point 3's "find the lib's own safe-sparring mechanism"
guidance. It failed to compile at all: set("damage", ({ 0 )} ); has
) and } swapped (should be ({ 0 }) )), cascading into further
spurious errors on every line after it in the same function. Fixed the
bracket swap -- but doing so exposed a SECOND, real bug underneath:
adm/daemons/catalog_d.lpc's get_level() (which every wizard
clone invocation runs automatically, to register the new monster in
the summoning catalog) does
counter = ((int *)wep->query("damage"))[1]; -- unconditionally
indexing element 1 of the "damage" array. Every other monster in
this codebase sets "damage" as a 2-element ({min, max}) array
(confirmed via alucard.lpc's ({3, 30}) and the real combat-damage
code in std/body/attack.lpc, which reads both damrange[0] and
damrange[1]) -- but dummy.lpc's array had only ONE element even
after the bracket fix, so cloning it live threw *Array index out of
bounds. (reproduced: clone /d/class/rogue/obj/dummy as admin,
confirmed via /log/runtime's stack trace pointing at
catalog_d.lpc:187 <- cmd_clone <- cmd_hook).
Fix: ({ 0 }) -> ({ 0, 0 }), matching the universal 2-element
convention and the object's own intent (a target that deals zero
damage either way). Verified live: clone /d/class/rogue/obj/dummy
succeeded cleanly after the fix (correctly auto-cataloged, no error),
and a full live fight (kill dummy) confirmed the safe-sparring
guarantee end to end -- every one of the dummy's attacks reported
"does nothing to you" while its receive_damage() correctly echoed
back each hit's damage ("Dummy screeches: You just did N points of
bludgeoning damage"), with zero crashes and zero risk to the player
across dozens of combat rounds.
(Separately, dummy.lpc's own long text tells players to "type halt
to stop your onslaught" -- halt is in fact a wizard-only command
(cmds/wiz/_halt.lpc; no player-facing equivalent exists,
cmds/std/cmds/object have no _halt/_flee/_stop). An ordinary
player typing halt gets a plain What?. This is pre-existing 1997
flavor-text content, not something the port touched, and fixing it
would mean either adding a new player command or rewriting the
dummy's documentation -- a content/behavior decision, not a
programming bug, so left alone and just noted here per the task's
scope boundary.)
5. Class-marker items ("A Pit Fighter's Scars") permanently duplicate across disconnect/reconnect cycles -- new AGENTS.md \S7.122
Found by accident while doing the required \S10.7 step-5
quit-then-relogin check: Bramwell's i after a real quit-and-relogin
cycle showed five copies of "A Pit Fighter's Scars" (only one was
ever legitimately granted, via a single join). Root-caused and fixed
-- full writeup, including why the fix targets the reload side rather
than reordering the save/destroy calls, is in AGENTS.md \S7.122 (new
entry, since this exact auto_load/compute_autoload_array/
destroy_autoload_obj/load_autoload_obj mechanism is verbatim
shared TMI-2 lineage code per its own header credit to "Truilkan@TMI",
and is a plausible sibling-check candidate for any other TMI-2-based
lib in this corpus). One-line summary: std/user.lpc's quit paths
save the character (baking any carried auto-load item into the normal
serialized inventory) before removing that item from inventory, and
net_dead() never removes it at all -- so a subsequent full restore
recreates the item twice (once from the leftover inventory data, once
from the separate auto_load re-clone list), with no dedup) on either
side. Fixed in std/user/autoload.lpc's load_autoload_obj() by
skipping any auto_load entry whose base_name() already matches an
object already present in inventory. Verified live: a further clean
quit-and-relogin on the already-corrupted Bramwell character held
steady at 5 copies (did not grow to 6) after the fix landed --
confirms no further duplication, though the fix does not retroactively
clean up the pre-existing 5 (left as visible evidence on the test
character rather than hand-edited away).
6. Currency/economy check (per this pass's mandate to watch for the \S7.121 float-corruption shape)
Checked std/shop.lpc, std/bank.lpc, std/coins.lpc, std/pub.lpc,
std/buy_only_shop.lpc, std/container.lpc, and
adm/daemons/gang_d.lpc (every file referencing credits/price/
cost/gold) for the \S7.121 shape (a function doing real float
arithmetic but declared to return int with no to_int()). Clean
-- this lib's economy code is plain-integer throughout with no float
arithmetic anywhere in the money path; std/shop.lpc's
get_ob_value() in particular already guards every branch
(intp()/mapp()/pointerp() checks) before returning. No instance
of this bug class found.
7. Verification performed
- Full continuous playthrough as
Bramwell(male human, pit fighter): register ->look/score/iat the start room -> navigate to the Intersection (bug \S1) -> meet Alucard and receive starter equipment (bug \S2) -> navigate to the Temple of Talos (correctly, gracefully rejected -- content gating, not a bug) -> navigate to the Pit Fighter Class Hall (bug \S3) ->join(received class marker item) ->score/iconfirming the class, title, and item -> live combat against the fixed training dummy (bug \S4), including ascoremid-combat showing "is attacking Dummy" state ->quit(mid-combat, correctly dropped non-wielded unequipped items) ->debug.log/log/runtime/log/loggrepped clean after every fix and after the quit -> reconnected after a real wall-clock gap, confirmed a genuine fresh restore (full news splash + "Last logon" banner, not a silent same-session reattach) with class/stats/credits all correctly persisted -- but also surfaced bug \S5 (item duplication) this way. - Distinguished the two reconnect variants explicitly per \S10.7 point 5's own warning: several intermediate reconnects during testing showed the bare
Reconnected.\nsingle-line response (a live/ interactive session reattach), while the post-quit reconnect showed the full news/banner sequence (a genuinerestore_object()) -- confirmed this distinction actually matters for this lib (only the latter path is where bug \S5 accumulates further duplicates). - Shop/economy: code-reviewed only (bug \S6 above) -- did not reach a live shop transaction with real gold in this pass; stated explicitly as unverified-live per \S10.7 point 6, not silently presented as tested.
- Death/respawn: not reached live in this pass (budget went to the five bugs above instead) -- stated explicitly as unverified-live per \S10.7 point 6.
- Admin
updateused to hot-load every fix above without a full reboot; each fix's live effect was independently confirmed (a room becoming enterable, an NPC appearing and responding, ajoinsucceeding, acloneno longer crashing, a duplicate count no longer growing) rather than just trusting a clean recompile. - Test characters left as evidence:
Bramwell(pit fighter, the main playthrough character, 5 duplicate scars pre-fix visible as evidence), plus several earlier fresh-name attempts (Wrenmoor/Corvasel/Dorrick/Halvard/Perrin) left in various partial-registration or early-exploration states as ordinary incidental test debris from iterating on the navigation script. - No new fatal errors in
log/debug.log/log/runtime/log/logfrom any of the above beyond the ones being actively diagnosed and fixed. - Driver killed cleanly by exact PID after this pass; no automated reboot loop was set up, consistent with \S12's standing caution about this lib's genuine outbound I3 connection attempt at boot.
WASM status audit (2026-09-01)
playable. Booted ~/src/fluffos/build-wasm/src via
scripts/wasm_client.js -- clean boot, zero fixes needed; the
preloaded channel daemon's outbound I3 socket-connect attempt (\S12)
fails silently/gracefully under WASM's no-sockets build without
affecting anything else. Full session verified: real TMI-2 registration
flow (name, y/n confirm, password + confirm, email, real name, the
1/2/3/4/5/6/0 stat-optimization menu), landing in The YMAA (Decatur),
look/score/quit all correct.
深度功能测试(§10.7,2026-10-03)— TMI-2 sibling fixes
Found on tmi2 (same TMI-2 1.4alpha code) and checked here on a throwaway
copy of the committed tree (git archive + fixes, fresh driver):
evalwas dead on any fresh checkout./opendoes not exist in the tree (the archive never had it;work/tmpis tracked,openis not), so the adminevalcommand (cmds/adm/_eval, needspath -add /cmds/adm) failed with*Wrong permissions for opening file /open/eval.<name>.lpc for append. "No such file or directory". Addedwork/open/.gitkeep;evalruns afterwards.suicide: the "busy" flag was never released after an abort.confirm_suicide()answered "Suicide attempt aborted." and setbusy = 1, so after onenevery player got "the suicide command is presently busy" until the daemon was reloaded; a player who vanished at a prompt wedged it the same way.busynow holds the player in the dialog and is cleared on abort; a holder that is no longer interactive does not block anyone. Reachable here by ordinary players (level 3+). Verified: prompt,n, prompt again; second player mid-dialog gives "busy" to the first; after the second disconnects the first can start the dialog again. (To reach the dialog the level-3 gate was bypassed in the throwaway copy only; the committed gate is untouched.)- A connection dropped at the "Press ENTER to continue" prompt raised an error in
net_dead()(Bad argument 3 to EFUN message(): the body has no environment yet;base_name(environment(TO))below it fails the same way), skipping the rest of the function. Guarded both uses withif (environment()). Verified: register, drop at the pager, relogin: nolog/runtimeentry, login works.
die() here has no ghost body (the death room handles the aftermath and the
body is not restored from its save file), so the coin duplication found in
tmi2 does not apply, and remove() has no seteuid(euid) line, so the
tmi2 failure of a call_out-driven first removal does not apply either.
Formatter not run (minimal edits).
深度功能测试(§10.7,2026-10-04)— negative range ends
scripts/lpc_fix_negative_ranges.py: 11 line(s) in 6 file(s) count a range from the end with < (x[a..-1] -> x[a..<1], x[-2..] -> x[<2..]; KB 06 §7.209). In this driver a negative constant end gave "" / ({ }) and a negative start the whole value, so each of these returned the wrong slice; the compile warning A negative constant as the second element of arr[x..y] is gone from the files that compile. Range lvalues (s[0..-1] = text, the prepend idiom) are left alone. A paired compile of the changed files at HEAD and in the working tree showed no new error.
深度功能测试(§10.7,2026-10-05)— here-document terminators
scripts/lpc_fix_heredoc_terminator.py --all --apply: 1 here-document block(s) repaired (1 with the terminator glued to the last text line, 0 with it indented), so End of file in text block no longer hides the room or object behind them: u/c/cattt/needswork/king.lpc. The terminator must start its line (the lexer's own test, KB 04 §6.10); a glued one lost its newline in the GBK -> UTF-8 conversion together with a half character, which stays out.
深度功能测试(§10.7,2026-10-05)— 主控 author_file()
驱动的 mudlib_stats 包在主控载入时调用一次 author_file(<主控文件>),要求返回字符串,否则每次启动都打印 author_file() in the master file does not work, using root_uid as fallback 一行,网页版每位访客的终端里都看得到。本库 adm/obj/master.lpc 的 author_file() 只为 /u/<字母>/<名字>/ 下的文件返回作者,其余返回 0。而且驱动传入的路径是 "/" 加上配置里的 /adm/obj/master,即 //adm/obj/master,sane 模式的 explode() 会保留一个开头的 ""。praise 和 find -author 直接调用 master()->author_file(),把 0 当作“没有作者”,所以没有把 0 改成字符串。现在先去掉空段(explode(...) - ({ "" })),/adm 下的文件返回 ROOT_UID(与同文件 domain_file() 的 adm 分支一致),这样也回答了开机探测;/u 分支加了长度检查(原来 /u/<x> 这样的短路径取 path[2] 会越界出错,而 author_file() 在每个对象载入时都会调用)(KB 06 §7.222)。用 lpcc config.fluffos /nonexistent.lpc 前后对比(主控和 preload 全程),只少了这一行。
深度功能测试(§10.7,2026-10-06)— 编译警告(机械部分)
scripts/lpc_warnings.py mortremains --fix:删 16 处 #pragma save_binary 之外,631 个 nosave 函数改 protected(102 个去掉修饰),删 1188 个未用局部变量,补 32 处 varargs、32 处缺返回值的 return;;lpc_audit_removed_locals.py 复核 1901 个,0 个需要处理。改动的 622 个 .lpc 在只含被跟踪文件的树里 HEAD 与新版各编一次,发现一处回退:adm/AFD/object/prop_logic.kenny.lpc 的修饰符分行写(nomask / private / nosave 各占一行),工具没看到上一行的 private,把 nosave 改成了 protected,成了编译不过的 private protected;手工改回 private,工具已修正(逐行向上看纯修饰符行)。启动警告 97 → 8,错误 0。实测 fluffos 登录、look(Immortal's Hall)、score、i、who、quit 正常。
深度功能测试(§10.7,2026-10-08)— .lpc 后缀的定宽切片
.c 改名 .lpc 时只改了字符串没改切片宽度:f[<2..] == ".lpc" 拿两个字符比四个字符,永远不成立(!= 永远成立),按后缀筛文件、去后缀的代码因此从不运行或截成 x.l(KB 06 §7.238)。scripts/lpc_lpc_suffix_slice.py --apply 改了 9 处比较、1 处去后缀(如 obj/tools/paintbrush.lpc);改动的文件 HEAD 与工作树分别加载(新进程),无回退。
深度功能测试(§10.7,2026-10-10)— 两个重复的全局变量(照 tmi2 修法)
重新整库扫描(分块扫描器)发现 2096 条 Redeclaration of global variable,分布在 1054 个文件里,根源只有两处,都在 TMI-2 基础类中,tmi2 已于 2026-10-03 修过(4db43f60e6b):
std/living/spells.lpc的object target与std/body/attack.lpc的战斗目标target同名。法术目标只在 spells.lpc 内部使用,改名为spell_target(与 tmi2 的文件逐字相同,玩家可见的文字不变)。std/monster.lpc又声明了一次mapping alias,它的create()本想清空身体(std/body/alias.lpc)的别名表,清空的却是自己的副本;怪物的add_alias/remove_alias操作的是一个 0。删掉重复声明。
/d/ 下 300 个怪物文件及三个基础类在 HEAD 与工作树各载入一次:246 -> 246 通过,无回退;抽样 40 个文件的重复声明警告全部消失。
深度功能测试(§10.7,2026-10-10)— 编译警告整理(续):返回类型、基础类
std/cmd_m.lpc的clean_up()声明为void,而驱动按返回值决定是否再调用它(0 表示不再调用);照 tmi2 改为int clean_up() { destruct(this_object()); return 0; },八个命令的int clean_up()因此与基类一致。std/object/ob_logic.lpc的set()原型写成void,定义(std/object/prop.lpc)返回int,照 tmi2 改正。- 60 个房间、物品的
int reset()与基类void reset()不符(驱动不看reset()的返回值),改为void(scripts/lpc_fix_return_types.py);kyton.lpc的heart_beat()、三件兵器的unwield()(return ::unwield(...)改为调用后返回)、两个return ::reset();同样处理。 cmds/object/_load.lpc:原型int help();与返回字符串的定义不符;改正原型后暴露出cmd_load()里的return help();——不带参数的load把帮助文字当返回值交给命令分派器,屏幕上什么也不显示。改为write(help()); return 1;。lpc_twin_port.py从 tmi2 移植 4 个与之逐字相同的文件(prune_logdir、_debugmalloc、_diff、_fref里只在#if分支里用的局部变量移进分支);u/c/chronos/objmaker/toy.lpc多余的inherit OBJECT;删去。
改动的文件及继承 cmd_m/ob_logic 的 119 个物件 HEAD 与工作树各载入一次:112 -> 113 通过,无回退,这批文件的警告 92 -> 12。剩下的主要是 logind.lpc、user.lpc(及 adm/AFD/ 下的旧副本)里只在 #if 分支用到的局部变量,需逐个移进分支。
- 2026-10-10(续):只在不启用的
#if分支里用到的局部变量移进分支:adm/daemons/logind.lpc(NO_REMOTE_LOGINS的a1..a4/local_host/pattern、NO_SHUTDOWN_LOGIN的sd_time,与 tmi2 相同)、std/user.lpc及std/attic、adm/AFD下 7 份旧副本的before/after(整段放在函数开头的#ifdef PROFILING里——tmi2 当初把两个声明分开放进第一个if里,启用PROFILING时后面的用法会找不到变量,tmi2 一并改正)、std/shop.lpc的tmp(#ifndef ONE_SHELF)、cmds/std/_channels.lpc的建频道变量(#ifdef NO_NEW_CHANNELS ... #else)、learn.lpc的调试串foo。9 份user.lpc的原型protected int create_ghost();改为与std/body.lpc一致的object;adm/AFD/USER.lpc两参数的move_object(this_object(), ob)写成move_object(ob)(逗号表达式本来就是这个效果)。改动的 12 个文件 HEAD 与工作树载入 6 -> 6,无回退,警告 38 -> 0。d/class/rogue/Assasins/obj/dagger.lpc在一个函数里面定义了另一个函数,本来就编译不过,未动。 - 2026-10-10(续):8 份
user.lpc的PROFILING代码里rsuage()是rusage()的笔误,改正;std/user.lpc开、关PROFILING都能编译。
深度功能测试(§10.7,2026-10-11):“无副作用表达式”警告复查
adm/AFD/lock.lpc(及attic/、old/两份副本):if (str == "none"); {多了分号,pick_lock()无论参数都会执行。去掉分号。adm/daemons/ref_d.lpc:(: $(tmp1), $(tmp2) :)是逗号表达式,只返回函数名字符串而不是tmp1->tmp2的函数指针;改为(: call_other, $(tmp1), $(tmp2) :)。d/Ocean/Main/mon/turtle.lpc、turt.lpc:唯一一句闲聊写在set("chat_output", ({之前,数组是空的;挪进数组。d/Prime/Park/room/path9.lpc:set("listen"); "You hear birds…",把描述丢了;改为set("listen", "…")。cmds/wiz/_resist.lpc是作者没写完的命令,按原样保留。7 个对象载入无回退。
Compile warnings (2026-10-13)
scripts/toolkit_fix.sh round plus hand fixes; no warnings left in compiling files. The 20 changed objects load the same at HEAD and in the tree (HEAD PASS 16, tree PASS 16, regress 0).
std/monster.lpcprototypedcreate_ghost()asint; the definition it inherits fromstd/body.lpcreturns the ghostobject. Prototype fixed.std/board/bboard.lpcoverrode the object'sint clean_up(int inherited)withvoid clean_up(); it is nowint clean_up(int inherited) { return 0; }, which keeps the board's choice (0: never clean up).adm/daemons/flock.lpcdefinedPRIVATEasnosaveand used it on two functions; it is nowprotected.reset()asvoidinadm/AFD/Dark/Prime/rooms/field1.lpcandd/Planar/Baator/phlegethos/out01.lpc(the latter by the toolkit, itsreturn 1;becamereturn;); an emptyif (!str) { }stub removed fromcmds/wiz/_resist.lpc;u/d/darkg/private/pornomag.lpcnames theclean_up()the driver already used; the shops'tmplocal moved where it is read.- 19 headers and files redefine a global macro with a local meaning (the AFD domains'
ARMOR(x)andGUILD,NO_NEW_CHANNELS,TP,POSS,FIND,STD_HELP,__tmi__,OB_RESOLVER): each now has#undeffirst. The local value was already the one in effect.
- 2026-10-14: FluffOS 970d9f09 warns
Float value truncated to int(#1426). 6 sites in 4 files: assignments, initializers and returns now spell the truncationto_int(...)(same value). Changed objects loaded before and after in fresh drivers, no regressions (scripts/lpc_fix_float_trunc.py, KB 04 §6.10).
Inherit order (2026-10-14)
220 files never compiled with Illegal to inherit after defining global variables. FluffOS takes every inherit before the first global; the MudOS this lib ran on did not care. Two shapes: a global (int found;) above inherit ROOM; (the AFD Dark domain rooms, /d/Arctic/Sebastian monsters, _shout, _who2, obj/net/www), and #include <m_spells.h> above the inherit in every mage and priest spell and spell object (/d/class/wizard, /d/class/priest, /d/Ocean/Main/spells).
scripts/lpc_fix_inherit_order.pymoved each offending line below the lastinherit, then atoolkit_fix.shround cleared the warnings the newly loading files exposed (32 nosave functions, 27 bare returns, 103 unused locals, 2 varargs).include/m_spells.hdeclared its nine school tables (ABJURATION…UNIVERSAL) as global mappings.std/spell.lpcincludes it too, so every spell redeclared all nine. They are now#definemapping literals; the only reader,d/class/wizard/std/daemons/learn.old, still works.light,dark:check_magic_resist(TP, …)passed the caster object where the caster level goes; now 0, which makes the function use the caster's level.bladebarrier_obpassesmaster->query_level()instead ofmaster.d/Planar/Astral/monsters/githyanki.lpc:return 1;in avoid special_attack()becamereturn;.pool.lpcandsewer.lpc(cyberparty):int reset()→void reset()(as instd/room.lpc).vocalize_obdropped aDEBUGmacro identical to the one indebug.h.- 248 objects checked HEAD vs tree in fresh drivers: 190 newly loading, 0 regressions. Still failing, left alone: content missing from the archive (
SK_BSD,/d/Outer/Sigil,monster.h), and AFD attic copies of oldmonster.lpcand rooms callingadd_exit/set_value, which this lib'sstd/no longer has.
- 2026-10-14: 32 files did not compile with
Type of returned value doesn't match function return type ( int vs void ): a function declaredvoid(or with no type) returns a value, which the old driver passed to the caller. Functions whose value a caller reads (add_action handlers, commands, helpers) now carry the type they return:Prompt(),advance_level(),buy_func(),death(),die(),display_tag(),get_language2(),get_signature_item2(),hit_function(),populate(),receive_object(),scan_room()…. In driver applies whose value is ignored (init,reset,heart_beat,create) the return is now bare ({ ::init(); return; }keeps a call). HEAD vs tree in fresh drivers: 32 objects, 23 newly loading, 0 regressions (scripts/lpc_fix_void_return.py, KB 04 §6.3).
- 2026-10-14 (follow-up): functions retyped
intabove that still had a barereturn;nowreturn 0;(same value);die()overrides that the void-return fix had madeintarevoidagain, as instd/body/attack.lpc.cmds/object/_qc.lpc:default : "[!] ILLEGAL BONUS VALUE!";dropped the message; it now assignsmsg.
- 2026-10-14: 2 file(s) used
arrayon its own as a type (array names;), which this driver reads as a plain name (Illegal lvalue); nowmixed *, every declarator starred (scripts/lpc_fix_array_type.py) (KB 04 §6.1, §6.3). HEAD vs tree in fresh drivers: 2 objects, 1 newly loading, 0 regressions.
Removed predefine SAVE_EXTENSION (2026-10-18, KB 04 6.11)
Older MudOS predefined SAVE_EXTENSION (".o") in every LPC object; the FluffOS ChangeLog (COMPAT_BUSTER) removed it in favour of __SAVE_EXTENSION__. This lib never #defines it, so each file that names it was Undefined variable 'SAVE_EXTENSION' and the object never loaded. Now __SAVE_EXTENSION__ (scripts/lpc_fix_predefined_macro.py): adm/AFD/attic/save.lpc, adm/AFD/board/archive.lpc, adm/AFD/save.lpc, adm/daemons/network/netmail.lpc, adm/daemons/virtual/o_server.lpc, cmds/adm/_netmail.lpc, obj/shells/shsh.lpc. Loaded one by one with lpcc after the change: 6 pass, 1 still stop on another error (adm/daemons/network/netmail: syntax error, unexpected L_BASIC_TYPE, expecting L_ASSIGN or ';' or '(' or ',').
Room reset() raised on a missing monster file (2026-10-18)
std/room.lpc did living(ob) and ob->move() on the result of new() of a monster file the archive lacks: Bad argument 1 to living(), the room never loaded (24 rooms in the sample). Both lookups now continue on 0.
Base classes the lib did not ship, restored from sibling libs (2026-10-18, KB 04 6.15)
These base classes are not in the lib, so every object that inherits them (rooms, NPCs, items) failed to load (inherit XXX; with no macro, or inherit "/std/..."; with no file). A sibling lib carries the file and shares many files byte for byte with this one, so the file is copied as it stands: /d/Conf/voting_room <- tmi2, 1 object(s) inherit it, sample 1/1 load. Each base was loaded alone and a sample of its users after it; a base that did not load, or none of whose users did, was taken back (scripts/lpc_restore_bases.py).
Hand pass after the repair round (2026-10-19, KB 04 6.10)
std/attic/monster.lpc(retired; nothing inherits it): the prototypeprotected int create_ghost();named a function the file never defines and clashed withstd/body.lpc'sobject create_ghost()(does not match current function in return type): removed.mapping alias;redeclared the body'salias, andcreate()only writes([ ])into it: the declaration is removed so the body's variable is the one that gets set (the same line sat in esI'sstd/npc.lpc, KB 04).u/n/nsd/fair/room/mirrors10.lpc,moonwalk.lpc,u/t/tyran/spider/rooms/spcave20.lpc:int reset() { ...; return ::reset(); }over the room base'svoid reset(): nowvoid reset()with::reset();.u/z/zur/priv/AC1.0/:base.lpcinheritsmake_room.lpcand then includesac.hagain for its macros, soac.h'snosave string fname; nosave string file;were declared twice (Redeclaration of global variable).base.lpcdefinesAC_INHERITEDbefore the include andac.hskips the two declarations then (the pattern of xiyouji'sground.h).
The seven objects were loaded at HEAD and in the tree: no regression.
Punctuation the conversion lost, put back (2026-10-19, KB 03 4.6)
3 file(s) lost a semicolon, comma, quote or the closing } at the end of the file and did not load. The file stopped one or two } short (its last byte or two went missing); appended: /d/class/rogue/bandit/cmds/_pick.lpc, /u/i/islair/ddagger.lpc, /u/m/moltar/volcano/monsters/caverat.lpc. Loaded at HEAD and in the tree: all 3 were failing before, none regressed (scripts/lpc_restore_from_twin.py, lpc_fix_eaten_quote.py --comma, lpc_fix_eof_braces.py).
Unused locals in objects that load since their punctuation came back (2026-10-19, KB 04 6.10)
Objects that did not compile until the lost punctuation was put back still carried unused-local warnings: 2 file(s): /d/class/rogue/bandit/cmds/_pick.lpc, /u/m/moltar/volcano/monsters/caverat.lpc. The unused declarations are deleted; loaded at HEAD and in the tree, still loading, no warnings.
Old-driver type slack: missing arguments, untyped parameters, array initializers, class variables (2026-10-19, KB 04 6.19, 6.20)
MudOS filled the missing arguments of a call with 0, took an untyped parameter as mixed, never compared a declared type with its initializer and let a variable be named class; this driver refuses all of it at compile time and the whole object fails. 11 file(s) fixed: /adm/AFD/attic/body.lpc, /adm/AFD/attic/connection.lpc, /adm/AFD/attic/smartmonster.lpc, /adm/AFD/body.lpc, /adm/AFD/connection.lpc, /adm/AFD/old/body.lpc, /adm/AFD/old/connection.lpc, /adm/AFD/old/smartmonster.lpc ... (11 in all) (the repair rounds of scripts/lpc_round_files.py: zeros written out at the call, mixed for untyped parameters, string X = ({...}) given its star, void functions returning values, and class variables renamed pick_class). Loaded at HEAD and in the tree, per file: 9 of them failed before and load now, none regressed.
x == "a" || "b":后面的字符串常量永远为真(2026-10-19,KB 06 §7.281)
if (arg == "yuyou" || "yu you") 里的 "yu you" 是非空字符串,永远为真,整个条件对任何 arg 都成立;作者要的是每个字符串都和变量比较。在每个字符串前补上变量和运算符(scripts/lpc_fix_literal_or.py;forrest2.lpc 是 dir == "north" && "south",手工改成 (dir == "north" || dir == "south") && ...):/adm/AFD/Dark/Circus/rooms/c_3.lpc(2 处)、/adm/AFD/Dark/Keldros/rithik/obj/ledge.lpc(1 处)。
x != "a" || x != "b" and x == "a" && x == "b": comparison chains of one value that can never differ (2026-10-20, KB 06 7.282)
if (arg != "wan" || arg != "pill") return notify_fail(...) is true whatever arg is, dir == "south" && dir == "north" is never true: the guard under the test never opens, or never closes. The author meant "neither of these" (&&) or "either of these" (||). != chains joined by || became && (only where the statement under the test is a return, a refusal or a guard clause), == chains joined by && became || (scripts/lpc_fix_cmp_chain.py). Changed: /adm/AFD/Dark/Circus/mon/ugoat.lpc (1), /adm/AFD/Dark/Circus/rooms/c2.lpc (1).
A string that holds a number (2026-10-20, KB 04 6.22)
u/j/jyd/tool.lpc: string tmp holds an index (tmp >= sizeof(u) || tmp < 0): mixed tmp (scripts/lpc_fix_cast_cmp.py). A wizard tool; it still fails on the errors behind it (an object variable that holds an array, assign() and disp() defined nowhere).
Every call_out_info() entry has three elements (2026-10-20, KB 06 7.287)
The driver answers ({ object, function, delay }) without the call_out's arguments, so code that reads the fourth element info[i][3] (the callouts command, calloutd, the cod tool) died with Array index out of bounds on the first line of the list. Now (sizeof(info[i]) > 3 ? info[i][3] : 0) (scripts/lpc_fix_callout_info.py). Changed: /cmds/xtra/_callouts.lpc (1), /obj/tools/aclock.lpc (1), /obj/tools/cod.lpc (3).
if (x = CONST): a comparison written as an assignment (2026-10-20, KB 06 7.290)
if (arg = "button" || arg = "pill"), if (!arg || arg = "") return 0;, else if (check = 0 && tempB != ""): = was typed for ==, so the condition is the constant: the branch is always taken for a non-zero number or any string (even "") and never for 0, and the variable is overwritten; the driver does not warn. Every such = became == (scripts/lpc_fix_cond_assign.py, 2 terms). Changed: /adm/AFD/Dark/Keldros/rithik/rooms/fields.lpc (1), /adm/AFD/Dark/Prime/rooms/field.lpc (1).
socket / dumpsocket commands: dump_socket_status() is not in this driver (2026-10-20, KB 06 7.291)
These wizard commands do write(dump_socket_status());, a MudOS efun FluffOS lacks, so they did not compile. The simulated efun file now defines dump_socket_status(): the same table from socket_status() (a build without the sockets package, the WASM one, gets the header only; scripts/lpc_add_sefun.py dump_socket_status).
tail command: tail(file) is not in this driver (2026-10-20, KB 06 7.291)
cmds/*/tail.lpc calls the MudOS tail() efun, which FluffOS lacks, so the command did not compile. The simulated efun file now defines tail(file) (the compatible version of the Discworld family: the last 20 lines or so from a full line, 1 when the file exists; scripts/lpc_add_sefun.py tail).
深度功能测试(§10.7,2026-10-20)— TMI-2 的 show / boardcheck
mortremains 带着 TMI-2 的 cmds/wiz/_show.lpc 和 cmds/xtra/_boardcheck.lpc,与 tmi2 逐字节相同,缺的东西也相同(没有 dns_master,boardcheck 的 25 块留言板一块都不在);改法同 tmi2(KB 06 §7.293、§7.299)。cmds/std/attic/_party.lpc 是备份目录里的 party,不是指令,没动(它的 help() 与 tmi2 的同样有问题,KB 06 §7.292)。
A login password over 255 bytes: crypt() raises (2026-10-20, KB 06 7.294)
The driver's crypt() raises (Error in crypt(), check your salt) for a key over 255 bytes, where the old driver hashed any length. A password of 256 bytes or more typed at the login prompt threw an error in the password check and left the connection at a bare > prompt, so the login never completed (the old-password test of password the same); found by the sweep shape of 300 x. /adm/obj/simul_efun.lpc now defines a simulated efun crypt(key, salt) of the same name: only an over-long key is cut (to 255 characters, then character by character until sizeof(to_buffer(key)) <= 255: the limit is in bytes, a CJK character takes three) before it is handed to efun::crypt(); every ordinary password hashes as before and the saved hashes are untouched (scripts/lpc_add_sefun.py crypt).
存档里的 .c 文件名改成 .lpc(2026-10-21,KB 03 §4.2 第 9 项)
存档里写着 "/d/city/npc/wei.c" 这样的文件名;extract 时源文件已经全部改成 .lpc,而驱动对带扩展名的路径只认字面,new() / load_object() / move() 都找不到它,调用方多半把失败当成「没有这个东西」悄悄跳过。本库 1 个存档共 1 处按 scripts/lpc_fix_save_dotc.py 改成 .lpc(只改 .lpc 存在而 .c 不存在的字符串;玩家存档、备份、留言板、巫师目录和文件索引缓存不动)。改动:/data/adm/daemons/doc_d/Root.o(1 处)(共 1 个存档)。
std/body/attack.lpc 的 die():“坏死亡调用”的日志读了还没赋值的 killer(2026-10-21,KB 06 §7.348)
hit_points >= 0 时 die() 被误调用,日志要写 "bad death call by ..." 和 file_name(killer),但 killer 要到几行之后才赋值(last_attacker 或 previous_object())。这一处改成 file_name(previous_object())(调用者就是“坏调用”的来源),后面真正的死亡记录不动。改动:/std/body/attack.lpc。
add_action() registrations of functions nobody defines, second round: objects behind a macro path or that the static scan cannot settle (2026-10-21, KB 06 §7.349)
The first round's static scan could not read objects that inherit through a macro path (a #define ROOM STD_DIR "room/room" whose body starts with another macro) or an inherit it could not resolve, and skipped them as UNSURE; the second round fixed the resolver, rescanned, and asked function_exists() of those objects in a running driver. An add_action("fn", "verb") whose function is defined neither by the object, nor by its headers, nor by its parents, nor by any object derived from it (scripts/lpc_scan_callout_targets.py, descendants checked transitively, then function_exists() in a running driver, scripts/lpc_verify_handlers.py). The driver walks the actor's sentences from the newest one: when that sentence's function does not exist it raises *Function for verb 'drop' not found. and tries no other handler of the verb (an older dead sentence is passed over silently). Removed 7 registrations in 5 files (scripts/lpc_fix_dead_add_action.py), the verbs are left to their other handlers: /adm/obj/adm_staff.lpc (immunestaff), /adm/obj/summon_ob.lpc (rename), /d/Ocean/Main/room/beach.lpc (swim), /d/attic/Prime/Dagaard/dilligaf/obj/bone.lpc (extinguish, light_fatty), /d/attic/Prime/Dagaard/dilligaf/obj/bone2.lpc (extinguish, light_fatty). Most frequent: extinguish x2, light_fatty x2, immunestaff x1, rename x1, swim x1.
Combat probe with the seeded administrator (2026-10-22): clean
scripts/play_combat_probe.py mortremains --account ... --clone ...: the administrator cloned /d/Prime/Magehouse/monsters/mole12, /d/Prime/Sewers/generic/monsters/zombie, /d/Prime/Cyanide/monsters/slime, /d/attic/Outer/Sigil/monsters/ambar, /d/attic/Prime/Tormeid/mon/queen, /d/Ocean/Main/mon/siren into its room and attacked each for 20 s (1 fights read): no driver line and no error text on the screen. The first fight (zombie, 53 rounds) ended with the administrator in the death room; the other five files cloned there without a driver line and no further fight started. (KB 08 §10.16)
A macro parameter inside a string literal: MON(queen) named a file called x (2026-10-22, KB 06 §7.352)
The area headers of Mortal Remains (/d/attic/Prime/Tormeid/defs.h, /d/Prime/Norva, Petzoo, Cyanide, /d/Ancients/ancients.h, /d/attic/Outer/Sigil, 14 headers under /adm/AFD/, /std/buy_only_shop.lpc, /include/m_spells.h and p_spells.h) define their path macros the way a traditional K&R preprocessor reads them: #define MON(x) BASE + "mon/" + "x". The old MudOS replaced the parameter inside the string; FluffOS follows ANSI C and does not, so MON(queen) expanded to .../mon/x: every room exit, monster, weapon and shop item named through these macros pointed at a file called x. scripts/lpc_fix_macro_strparam.py rewrites them with the ANSI stringize operator ("x" -> #x, "/d/Ancients/rooms/x" -> "/d/Ancients/rooms/" #x): 79 macros in 24 files. Over the 836 rooms of the six areas, HEAD vs tree: rooms that load 750 -> 829, exits that name an existing file 543 of 1,638 -> 1,765 of 1,788 (the 23 left are rooms the archive lacks). Message-only macros (assert) were left.
new(MISSING)->move() / carry_object(MACRO(..))->wear() chains guarded (2026-10-22, KB 06 §7.73 addendum)
A statement new(PATH)->move(...); / carry_object(ARMOR_D("x"))->wear(); whose path names no file of this archive: new() answers 0 and ->move() on it raises Bad argument 1 to EFUN call_other(). In create() the object never loads, in reset() every spawn below the first missing one is skipped on every reset. 6 statement(s) in 6 file(s) (clone_object) are now { object new_ob = new(PATH); if (new_ob) new_ob->move(...); } (scripts/lpc_guard_carry_object.py --new: the path is resolved with the macros the file itself sees, an ambiguous macro or a virtual-object path is left). Load check of the 6 changed files, HEAD vs tree: 0 load at HEAD, 1 load now (1 newly loading, 0 regress).
log/ subdirectories nobody created: the logging write_file() / log_file() raises (2026-10-22, KB 06 §7.358)
The code of this lib writes logs into 2 subdirectories of log/: log/adm/harass (cmds/std/_harass.lpc), log/harass (adm/AFD/body.lpc). Neither the archive, a git clone nor the site pack has them (no local boot wrote into them, so gen_keep_dirs.py never recorded them), and write_file() raises *Wrong permissions for opening file /log/dlog/money for append. "No such file or directory" for a missing directory: the function that logs stops on that line and what came after it (the reward, the flag, the next step) never runs. This lib's log_file() is write_file(LOG_DIR + file, text) and raises the same error for a missing directory (measured). log/adm/harass, log/harass are now in scripts/wasm_keep_dirs.txt (scripts/lpc_keep_log_dirs.py; the site and the pristine tree create the empty directories from it). Proof (scripts/lpcc_eval.py, an eval object under /adm/daemons or similar writes /log/zzdirprobe/..., fresh processes): the measured table in KB 06 §7.358.
深度功能测试(2026-10-27,§7.209 负下标区间残渣)
[0..-N] / [1..-1] 等改为 [0..<N] / [1..<1]。