info.html · 介绍 · play.html · 游玩 · llms.txt · 下载与本地运行 · ZIP · GitHub
A classic, from-scratch English-language MudOS mudlib whose own commit history dates to 1998 -- the oldest-vintage lib onboarded into this collection so far. A full game world, not a bare engine: character creation drops new arrivals into the "Hall of Creation," where a one-time `become <race>` command permanently picks one of 12 playable races (Drow, Duergar, Dwarf, Elf, Gnome, Goblin, Half-Elf, Half-Orc, Halfling, Human, Lizard-Man, Orc) before sending them to that race's own dedicated newbie zone -- real per-race taverns and shops, not a shared generic starting room. Five character guilds (Cleric, Fighter, Mage, Rogue, Warrior) round out the RPG progression, with the Mage guild alone branching into nine D&D-style specialist schools (Abjurer, Conjurer, Diviner, Enchanter, Illusionist, Invoker, Necromancer, Transmuter, and a general path), plus a full wizard-building toolset for creators.
README
Final Realms (source: <https://github.com/quixadhal/fr>) is a classic, from-scratch English-language MudOS mudlib -- the last commit in its own history dates to 1998, making it the oldest-vintage lib onboarded into this collection so far. It's a full game mudlib, not just an engine: race selection (12 playable races -- Drow, Duergar, Dwarf, Elf, Half-Elf, Gnome, Goblin, Halfling, Human, Lizard-Man, Orc, Half-Orc), a real starting-zone "newbie" world per race, guilds, shops, taverns, and a full wizard toolset.
Highlights
- Being MudOS-native (not a CD-driver codebase like some other libs in this collection), the compat gap with FluffOS was narrower in *kind* -- no
&operator(...)/@composition syntax, no arbitrary uid:euid string-pair model -- but the 1998-era archive itself carried more incidental bitrot: a legacystatustype keyword (an old LPmud alias forint) used throughout with no FluffOS equivalent, anadd_item()/add_feel()pair declared narrower (string) than 1000+ live call sites actually pass (arrays), a missing base case for a::-chained save-data accessor quartet used by every weapon/armour object, and several newbie zones missing directory-local path macros or per-domain error-log directories (the latter was silently masking hundreds of *other* real errors behind a secondary "can't open the log to report this" crash). SeeNOTES.mdfor the full list. - The single most severe bug found: this driver build was compiled with the older, single-efun
OLD_EDeditor interface, but this codebase's editor glue (global/new_ed.lpc,#include-fragmented directly into the player object) was written against FluffOS's newered_start()/ed_cmd()/query_ed_mode()API, none of which exist in this driver binary. Left unfixed, this would have broken compilation of the *entire player object* -- no one, wizard or mortal, could have connected at all. Rewritten against the realed()efun (with aquery_ed_mode()simul_efun approximated via thein_edit()efun this build does have). SeeNOTES.md\S2. - A second, quieter but mud-wide bug:
secure/gods.lpc/secure/lords.lpcnever actually defined thequery_gods()/query_lords()functionssecure/master.lpccalls on every boot to build its core admin- permission list -- silently breakingquery_lord()/high_programmer()for every account except the archive's own pre-blessedgodcharacter (which carries admin status directly in its save file, independent of this bug). Fixed; seeNOTES.md.
Registration flow
name -> confirm (y/n) -> password (real validation, hit live during
testing) -> confirm password -> gender (male/female) -> drops into the
"Hall of Creation" -> become <race> (one-time, permanent) -> the actual
game world. Name rules: 3-11 characters, letters only (no digits).
Admin account
- id:
fluffos - password:
FluffOS2026Admin - rank: god (added to
secure/gods.lpc'squery_gods()list, then promoted in-game withpromote fluffosrun as the archive's own pre-existinggodaccount)
Registered through the normal registration flow first (as a mortal, so a
real player save file exists), then promoted via the in-game promote
command (which itself only works because of the gods.lpc fix above).
Verified live: update /secure/simul_efun (the canonical ACL check, since
it exercises both read and compile permission) succeeds for fluffos.
The archive's own original admin account, god / password god (per the
game's own first-boot banner), also still works and was used to perform
the promotion above -- both accounts have god-level access.
Status
Boots clean: zero compile errors and zero uncaught runtime errors in
log/errors/*.err / the per-domain d/*/log/debug.err files across
repeated fresh-driver boots. Full registration (a brand-new character all
the way through race selection into the actual game world), look/
score/inventory/quit, re-login as an existing character, and wizard-
level update/promote all verified end-to-end with a real driver
session and a raw socket client.
A large compile-sweep tail remains beyond the fixes above (see NOTES.md
\S3 for the full breakdown) -- the overwhelming majority is either
genuinely dead/unreferenced legacy content shipped in the archive
(confirmed via repo-wide reference search before excluding, not guessed),
template files read as raw text rather than compiled, #include-fragment
files that only compile as part of their aggregator, or a repeatable
lpcc-batch-harness-only artifact (a per-monster call_out nesting
counter that only accumulates because lpcc --batch loads hundreds of
objects with no event-loop yielding between them -- confirmed absent on a
real driver boot). None of it blocks a real boot or blocks play.
WASM status: playable. Login as god, look, and score are
verified under the shared WASM driver (NOTES.md WASM status update).
Play: https://mudlibs.fluffos.info/finalrealms/
Local run
cd libs/finalrealms
~/src/fluffos/build-debug/src/driver config.fluffosGame port: 40216.
NOTES · 移植与修复记录
Final Realms -- porting notes
Source: git clone https://github.com/quixadhal/fr (commit 16308bd, cloned
2026-08-25). Slug finalrealms, number 168, port 40216. Mudlib root is the
clone's mudlib/ subdirectory (mudos/ alongside it is a bundled v21.7b21
MudOS driver source tree, ignored -- this project uses its own driver).
Original repo history stops in 1998 (git log -1 on the clone shows a
1998-11-10 commit date), making this the oldest-vintage lib onboarded this
session -- a genuine late-generation classic MudOS mudlib (not a CD-driver
codebase like genesis), so the compat gap with FluffOS was narrower than
genesis's in kind (no &operator/@ composition, no uid:euid string-pair
model) but the archive itself had more incidental bitrot/lost-content gaps.
1. Conversion
scripts/convert_lib.sh on raw/mudlib -> work: all-ASCII source (0
lossy conversions except 4 pre-existing corrupted bytes in two duplicate
ave8.c files and two already-empty intermud.o save files), 3273 files
renamed .c->.lpc, 2933 literal .c" references fixed, 6 local
angle-bracket includes converted to quotes, 7 files static->nosave.
2. Compile-time driver-compat fixes
switchwith onlydefault:(AGENTS.md §6.3):secure/simul_efun/ mud_long_name.lpc'sswitch (mud_name()) { default: ... }-- collapsed to a plainreturn.statusas a legacy type keyword: this MudOS-era codebase usesstatus(an old LPmud alias forint, signalling boolean intent) as a return/parameter type throughout -- FluffOS has no such type at all, so every use was a hard "unexpected L_IDENTIFIER" parse error. Swept\bstatus\b->intacross the 16 files that used it as a genuine type (verified none of the ~90 otherstatushits repo-wide are type positions -- the rest are English prose in comments/strings/identifier substrings like"ansi-status", left untouched).add_item()/add_feel()declaredstring, called with an array:std/room.lpc'sadd_item(string str, string desc)andstd/senses.lpc'sadd_feel(string feel, string feel_desc)were both declared narrower than the type their own bodies actually branch on (setup_item()/feels[]handling already usedpointerp()/mixedinternally) -- 1200+ and 190+ call sites respectively pass({"a","b"})-style arrays (the documented "give many items the same description" idiom). Widened both tomixed.add_feel()specifically had a pre-existing internal inconsistency: its own forward declaration already saidmixed, only the definition saidstring.- Missing base case for the
int_query_static_auto_load/query_dynamic_auto_load/init_static_arg/init_dynamic_arg::-chain:obj/weapon.lpc,obj/armour.lpc,std/chest.lpc,obj/label.lpceach define these and call::-qualified versions of themselves to walk up to a parent's saved state, but no ancestor anywhere in this codebase ever defined a base case -- "Unable to find the inherited function". Added no-op/empty-mapping terminators tostd/item.lpc(the common ancestor). condition::create()calling a function that doesn't exist:obj/weapon.lpc/obj/armour.lpc'screate()both callcondition::create(), butstd/basic/condition.lpcnever defined one. Added a no-opcreate()there.- 2-arg integer
exp(base, exp)power function:obj/weapon.lpc'sset_value()callsexp(2, enchant-1)expecting this codebase's own historical integer-power helper (std/basic.old/misc.lpc'sint exp(int x, int y), "added by dank Feb 23, 93") -- but that file is in the dead.oldtree, not part of the live inherit chain, and this driver's realexp()efun is the 1-arg float natural exponential. Added a smallprivate int int_pow(int x, int y)helper directly inobj/weapon.lpc(the only live call site) rather than reviving the dead file. lowest_condused but never declared:obj/weapon.lpc's andobj/armour.lpc'squery_dynamic_auto_load()/init_dynamic_arg()both read/write alowest_condglobal that was never declared anywhere in either file -- "Undefined variable". Declaredint lowest_cond;in both (342 downstream call sites across 133 weapon/armour-derived files were affected by theobj/weapon.lpchalf alone).- Missing
ROOM/other path macros in several newbie-zonepath.hheaders:d/newbie/grads/rooms/path.hand itscave1/sibling never definedROOM(every other zone'spath.hdoes, e.g.d/newbie/elf/ rooms/path.h) even thoughadd_exit(...)calls in that zone's rooms use it;d/newbie/new_halfelf/rooms/path.hhadROOMSbut notROOM(guild.lpc/shop.lpcuse the singular).d/newbie/grads/npcs/andd/newbie/grads/temp/had nopath.hat all ("Cannot #include path.h"), as didd/newbie/newelf/(top level) andd/newbie/newelf/npcs/. Added/ extendedpath.hin each, mirroring the sibling that already had one;grads/npcs/wizard.lpc'sOBJ+"fr_arrow_learnable.lpc"clone target doesn't exist anywhere in this archive (lost content, not this port's doing) --OBJitself still needed a definition to compile, so it's pointed at this zone's own (currently nonexistent)obj/directory, matching the convention other zones' path.h files use; the clone simply fails gracefully at runtime exactly as it would have upstream. - Missing per-domain
log/directories:secure/master/error_handler. lpccomputes"/d/"+domain+"/log/debug.err"for any uncaught error inside domain code, but NONE of the four domains (mudlib,vehicle,newbie,heaven) shipped alog/directory (newbiehad alogs/, plural, which doesn't match) -- every domain-code runtime error crashed the error handler ITSELF while trying to log the original error ("Wrong permissions for opening file ... for append", "No such file or directory"), masking the real error behind a secondary one. This is a previously-documented "lpcc-vs-live-driver artifact" pattern for this project, but it also affects a REAL boot (any domain-code runtime error, not just lpcc), so it's a genuine structural gap, not just a sweep artifact. Createdd/{mudlib,vehicle,newbie,heaven}/log/(with.gitkeep). - **
d/heaven/heaven/*rooms inherit/std/roomdirectly instead of/std/shield_room**: every admin-zone room in this directory (ave1throughave16,admin2,admin3,entry,ntower1,palace1,palace2) callsshield_it(SHIELD, ...)ininit(), butshield_it()is only defined in/std/shield_room.lpc(itself a thin wrapper around/std/room) -- a pre-existing content bug (wrong base class), not something this port introduced, but compile-blocking regardless. Swapped theinheritin the 19 live files (room/admin/heaven.nothere/*has an identical-looking but entirely unreferenced duplicate set -- see \S4 -- left untouched). std/newbieguild.lpc'sreset()crashing every newbie-guild room:clone_object("/obj/misc/board")can return 0 (confirmed underlpcc's isolated single-object test harness, a previously-documented artifact of that harness lacking a real preload/euid context), and the followingboard->set_datafile(...)then hard-errors oncall_other()againstint(0), taking down every/std/newbieguild-derived room'screate()(~20 live guild rooms across the newbie zones). Guarded withif (board).std/pub.lpc'screate()crashing every tavern: unconditionally callsBAR_TRACKER->add_bar(...)whereBAR_TRACKERis/d/aprior/ guilds/bard/tools/bar_tracker.lpc-- the "aprior" bard-guild domain never shipped anywhere in this archive (lost content), so this crashedcreate()for all 20+ live/std/pub-derived taverns with "call_other() couldn't find object". Wrapped incatch()so the (already-lost) songster-quest bar tracking silently no-ops.protected private(multiple access modifiers):secure/crerem/ remote.lpc'seventProcess()declared bothprotectedandprivatetogether -- every sibling function in the same file usesprotectedalone; dropped the redundantprivate. This file IS in the live preload list (master.o's savedpreloadarray includes/secure/ crerem/remote), so this one mattered for a real boot, unlike most of the daemon-directory syntax errors found during the sweep (see \S4).query_ed_mode()/ed_start()/ed_cmd()not available in this driver build: this codebase's editor glue (global/prompt.lpc,global/ process_input.lpc,global/new_ed.lpc) was written against FluffOS's "new" ed API (ed_start/ed_cmd/query_ed_mode, declared inpackages/ core/core.spec's#elsebranch), but this project's compiled driver binary was built withOLD_EDdefined (confirmed vianmon the driver:f_ed()is present,f_ed_start()/f_ed_cmd()/f_query_ed_mode()are not) -- meaning only the older single-efuned()API is actually available. This broke compilation ofglobal/player.lpcitself (new_ed.lpc/process_input.lpcare#include-fragments into it, not separate objects), which would have meant no player could connect at all -- easily the most severe single gap found in this port. Fixed in three parts:
- secure/simul_efun/query_ed_mode.lpc (new): a simul_efun using the
in_edit(object) efun (which IS available in this build) to
approximate query_ed_mode() -- returns 0 while genuinely editing,
-1 otherwise. Loses the driver's finer sub-states (the -2 "more"
pagination prompt, per-line insert-mode prompts) but preserves the one
behavior actually load-bearing across the codebase: the != -1 gate
that routes input to the editor instead of normal commands.
- global/new_ed.lpc's begin_editing() rewritten against the real
ed(fname, exitfn_string, restricted) efun. ed()'s exit-function
argument is a plain string (a function *name*), not a closure, so a
new __ed_exit_dispatch() entry point was added as that name, which
then invokes the actual function closure callers already pass
(preserving every existing caller's API, e.g. global/line_ed.lpc's
(: editor_finish_ed :)). One convenience lost: the original used
ed_cmd("$a") right after starting to drop mortals/empty-file edits
straight into insert mode; there's no LPC-level way to feed the
just-started real ed() session a synthetic command, so editing now
always starts at ed's normal command prompt. Low-severity: ed is
documented in global/line_ed.lpc as an explicit opt-in advanced
editor ("Please do not set your editor to ed unless you know what you
are doing"), not the default player editing experience (that's a
separate, unaffected pure-LPC "menu"/"command" editor in the same
file).
- global/process_input.lpc's ed_cmd() call site (the branch that
forwarded raw input to an in-progress ed session) neutralized to a
defensive error message -- under the real ed() efun the driver
handles an entire editing session internally (this apply isn't even
invoked while genuinely editing), so this branch can't actually fire
in practice, but it still needed to compile.
secure/gods.lpc/secure/lords.lpcmissingquery_gods()/query_lords():secure/master.lpc'screate2()calls"/secure/gods.lpc"->query_gods() + "/secure/lords.lpc"->query_lords()on every boot to build thegodsarray thatquery_lord()/high_programmer()(the mud's core admin-permission checks) test against -- but neither file ever defined those functions (only an unrelated per-name flavour-text lookup,query_boo()). This silently broke everyquery_lord()/high_programmer()check mud-wide (acall_other()to an undefined function returns 0, not an array, sogodsnever gained any entries). The boot banner itself instructs new installs to log in asgod/godas the first admin, and the archive's shippedplayers/g/god.oalready carriescreator 1and inherits/global/god.lpc(a real, working god-rank player object) independent of this bug, which is why testing asgoddidn't immediately surface it -- butpromote,update all to <domain>, and everything else gated onquery_lord()for accounts *other* than the pre-blessedgodwere all silently broken. Addedquery_gods() { return ({ "god", "fluffos" }); }(this project's admin account, see the README) andquery_lords() { return ({ }); }(no lord names survive anywhere in this archive -- the commented-out example inlords.lpcis template text, not lost content).secure/master.o's savedpreload/call_out_preloadarrays used literal.cpaths:/global/thane.c,/global/patron.c,/global/ god.c,/net/intermud3/intermud.c,/global/do_chat.c,/net/ who_server.c--convert_lib.sh's literal-.c-reference fixup only scans.lpc/.h*source*, not.osave-file *data*, so these six preload entries (saved player/master state, not code) were missed and failed to load every boot ("call_other() couldn't find object") -- caught bymaster.lpc's owncatch()around eachpreload()call, so non-fatal, but meant the thane/patron/god persona objects, Intermud-3, do_chat, and the who-server never actually preloaded. Stripped the stray.csuffix from all six entries directly insecure/master.o. A handful of individual shop rooms' own save files (d/newbie/*/rooms/ shop.oand similar, 6 files found viagrep -rlfor a quoted.cstring in*.o) have the same class of stale reference in their saved-inventory item paths -- much lower-impact (per-room shop stock, not the boot-critical preload chain) and left unfixed given the time budget; flagged here for anyone doing deeper content testing later.
3. Compile-sweep summary
scripts/lpcc_check.sh against the full 3273-file tree: started at 0/3273
(master/simul_efun themselves failed to compile -- the status-type and
switch-only-default bugs above), reached 2400/3273 (73%) passing after the
fixes above. The remaining ~870 failures break down as:
- Dead/unreferenced legacy content (verified via repo-wide
grepfor any live reference before excluding, same methodology asgenesis's \S10):releasefiles/(a ~60-file backup snapshot ofsecure/,d/ mudlib,d/newbie,d/heaven,w/duplicating already-live content byte-for-byte where diffed, referenced from nowhere live); every*.olddirectory and*/old/*path (std/basic.old,cmds/creator.old,net/ old,std/room/old, etc.);std/living/{dankequip,divstuff,baldy}/(three complete, mutually-exclusive alternate equip/skills implementations -- the live one is the extensionlessstd/living/ living.lpc, confirmed viaobj/monster.lpc's actualinherit, and zero files outside these three directories reference any of them);std/adnd/(an alternate D&D-stats race system, zero live references);net/daemon/(gopher/www/mail-queue/remote-post internet daemons -- absent from bothpreloadandcall_out_preloadinmaster.o, and zero live references outsidenet/old/udp/, itself already dead);room/admin/heaven.nothere/(ad/heaven/heaven/-shaped duplicate, the "nothere" name matching its actual status -- zero live references). - Template/fragment files, not standalone compilation units (same category
genesis's \S10 documented):std/creator/workroom{,2}.lpcandstd/dom/{master,common,loader}.lpcareread_file()d as raw text bysecure/master/create_dom_creator.lpcand written out per-new- wizard/domain with a#defineprepended -- never compiled at their own path (std/dom/{com,domain_mas}.lpcare unreferenced older variants of the same idea, dead per the point above). 95 files across 17 aggregators (secure/master.lpc,secure/simul_efun.lpc,global/player.lpc,net/intermud3/services.lpc, and others) are#included as text fragments into their aggregator and compile fine as part of it but lack standalone context in a flat per-file sweep. - A repeatable
lpcc-harness-only artifact, not a real bug:obj/ monster.lpc'sset_level()->init_command("init_race")->call_out("do_command", 0, ...)->command("init_race")chain issues a delay-0call_outon every single monster'screate(). A real driver's event loop actually executes each pendingcall_outbetween object loads (unwinding the nesting depth);lpcc --batchloads hundreds of monster objects back-to-back in one continuous process with no event-loop yielding, so the nesting depth accumulates *across unrelated objects* until it trips the driver's hard 1000-level nesting cap ("Nesting call_out(0) level limit exceeded"). Confirmed via the per-domaind/newbie/log/debug.errthis session's \S2 log-directory fix made visible for the first time -- every affected NPC shows the identical 7-frame trace, and the live driver boot (\S5) shows zero such errors across the full registration/play test. This is the same documented "lpcc-vs-live-driver artifact" class as \S2's log-directory point andgenesis's \S10 -- explains the large majority of thed/newbie/*/npcs/*and*/monsters/*"Fail to load object" entries with no visible compile error. - A handful of genuinely broken string literals:
d/newbie/half-elf/ newroomss/{F5,v5}.lpcand a couple of siblings have literal stray\characters outside any string ("Illegal character '\\'") -- looks like archive-era authoring/transcription corruption in this one small subdirectory (~10 files), not something introduced by this port. Left as a documented content gap rather than hand-reconstructing each string. - The remainder is ordinary individual room/NPC content bugs (a broken
int as[NUM_AS]array-size declaration in the already-deadstd/adnd/, isolatedUndefined functions for guild-specific verbs likeadd_main_skill/ChangeAbilityScorein the dead skills variants, etc.) -- logged here as scope, not fixed exhaustively, per this project's standing "programming bugs only, not content" rule; deeper content testing can revisitlibs/finalrealms/lpcc_fail.logif desired.
4. Boot and play test
Booted ~/src/fluffos/build-debug/src/driver config.fluffos (memory-capped
per this project's own past-incident precaution) repeatedly; zero compile
errors on every boot after the fixes above, Initializations complete. and
accepting connections on 40216 every time.
Verified via a raw Python socket client (this project's established method), end to end:
- Existing account (
god/god, shipped in the archive withcreator 1already saved): logs in, lands in/room/entryroom,look/score/quitall produce correct output, clean save on quit. - Brand-new character registration (name -> confirm -> password -> confirm password -> gender -> race selection): a fresh name registers all the way through
become humaninto the actual game world (the "Entry Hall"),look/score/inventory/quitall correct. Name validation is real and was hit live during testing (max 11 chars, min 3, letters only -- no digits). - Wizard command access:
promote <name>(run asgod, itself only possible after the \S2gods.lpcfix) andupdate <path>(the canonical ACL check, exercising both read and compile permission) both verified working for the project's admin account.
Zero uncaught runtime errors logged anywhere (log/errors/*.err,
d/{mudlib,vehicle,newbie,heaven}/log/debug.err) across this whole
session's boots and play-testing.
WASM status update (2026-08-25, another session)
Promoted wasm_status from "" to playable. Hit a genuine, real
(non-test-artifact) bug: secure/login.lpc's logon() has a hardcoded
if(uptime() < 20) { ... refuse connection ... } startup-grace gate.
This project's own WASM deployment boots a fresh in-browser instance
per visitor, so EVERY real player would hit this on EVERY page load
(not just an admin reconnecting moments after a manual reboot, which
is what the check is actually for) -- per AGENTS.md's standing policy
on legacy connection-time gates, bypassed for loopback:
query_ip_number(this_object()) != "127.0.0.1" && uptime() < 20
(current WASM builds correctly report 127.0.0.1, confirmed via
AGENTS.md's own IP-format section).
Once past that, booted and played clean: login as god/god, look
and score both producing correct output matching native testing.
Also found (documented, not fixed -- non-blocking, same class as
ds386's optional network tools): /net/identd.lpc (an ident-protocol
lookup daemon) fails to compile on connection due to socket_error()/
socket_address() being undefined (no sockets package on this
driver build) -- caught gracefully, game continues normally, not on
the boot/login/play path. quit wasn't recaptured in this WASM
transcript but is already verified clean under native testing above
and untouched by either fix.
5. Deep functional test (round two, 2026-08-27)
First full §10.7 round-two pass on this lib (confirmed via grep: no prior
深度功能测试/dated round-two heading existed). One continuous session,
English-named test characters per this lib's own naming convention
(quillfrost, brackenfell, plus throwaway testerbrave/brytewind
during earlier diagnosis -- all four deleted from players/ before this
commit, keeping only the seeded god account). Booted
~/src/fluffos/build-debug/src/driver config.fluffos repeatedly via a raw
Python socket client; killed each instance by exact PID when done.
Newbie help read first: help advance (skill-training syntax) and
help fighter (guild locations/requirements) -- confirmed guild training
(advance <skill>) is gated on physically being inside a guild hall, not
available from the open newbie zone, matching the help text's own
"Location: various" framing; not a bug, just meant travel wasn't budgeted
this pass (see "not reached" list at the end).
Six real, confirmed, and fixed bugs, found in this order:
5.1 room/raceroom.lpc: every new character of every race was funneled into a generic admin/coding-school hub instead of their own race's fully-built starting room, and permanently got zero starting equipment
do_become()'s success branch had startplace = "/room/entryroom.lpc";
(with the ORIGINAL, correct me->move("/room/start/"+race); commented out
immediately above it) followed unconditionally by
startplace->add_equipment();. /room/entryroom.lpc (an
FR:Illumitech-branded admin/mudlib-coding-instruction hub, exits only to a
meeting-room and post office) never defines add_equipment(), so that
call_other silently no-op'd -- and since entryroom.lpc has NO exit into
any of the 12 fully-built d/newbie/<race>/ zones this project's own
onboarding notes describe, EVERY new character was also permanently cut
off from all of that content. room/start/<race>.lpc (all 12 races have
one) is a real, still-fully-implemented starting room with its own
add_equipment() (or, for drow/duergar, add_clone() calls directly in
setup()) AND a real exit into that race's own newbie zone -- confirmed
this predates the port (byte-identical in raw/mudlib/room/raceroom.c),
not something this project's conversion introduced. Fix: restored
startplace = "/room/start/"+race; (this project's usual
scope: a hardcoded override + wrong call_other target defeating an
already-fully-implemented feature, not a content/design call -- the
original commented-out line proves the intended behavior). Verified live:
a fresh human character now lands in "Human entry room" (a real cottage
room with leave/south/west exits, leave reaching
d/newbie/human/rooms/t05.lpc) instead of the generic hall, look
correctly shows "Torch. Cloak. Dagger." lying on the floor, and get
all/i correctly picks them up ("Carrying: Dagger. Cloak. Two Torches."
-- the extra torch is leftover from repeat testing in the same shared
room instance, not a bug).
5.2 obj/misc/torch.lpc: a broken comment (/ where /* was meant) hard-failed this file's compile, silently breaking every race's starting-equipment torch (and any other torch clone) project-wide
Line 43: / Will try add_action (missing the second *) -- a genuine
pre-existing archive bug (confirmed present in raw/mudlib), not a
porting artifact; it was already a known FAIL in this lib's own
lpcc_fail.log from onboarding but wasn't reachable/prioritized then.
This is exactly why 5.1's add_equipment() fix alone wasn't enough: even
with routing fixed, clone_object("/baseobs/misc/torch") (called from
EVERY race's add_equipment()) returned 0, throwing "Bad argument 1 to
EFUN call_other()" the moment it tried boo->move(this_object()) on the
failed clone. Fix: restored the missing *. Same missing-star typo
also found and fixed in obj/misc/book.lpc (6 sites, an otherwise
unreferenced/dead file, fixed for completeness), d/heaven/heaven/
meeting.lpc, and room/admin/site_control.lpc (reachable via
d/heaven/heaven/admin3.lpc) -- a corpus-wide grep -n '); / [a-z]'
sweep found no further live instances. Added as new AGENTS.md §7.146
(a genuinely new bug class, not covered by any existing entry). Verified
live: update /baseobs/misc/torch now compiles clean; the full 5.1
equipment flow above is the live end-to-end proof.
5.3 global/player.lpc heart_beat(): an abrupt disconnect crashed with an uncaught runtime error on every single heartbeat tick, forever (or until a delayed self-quit eventually finished) -- AGENTS.md §7.130, new confirmed instance with an added nuance
The old inline idle-kick else block had been commented out at some point
in this codebase's history, but the comment's own boundaries left
last_command = time() - query_idle(this_object()); stranded INSIDE the
if (!interactive(this_object())) branch instead of removing/relocating
it -- an interactive-only efun called unconditionally on a path already
known to be non-interactive. Reproduced live via a genuine abrupt TCP
close (no quit sent): log/errors/no_object.err immediately began
accumulating "*Bad argument 1 to interactive() Expected: object Got: 0."
entries (see 5.3a below for why it's interactive(), not query_idle(),
in the final fixed version) roughly once per heart_beat tick, indefinitely
-- this project's own scattered leftover test-character disconnects from
earlier in this exact session had already been silently spamming this
error the whole time before it was noticed. Fix: guard with
interactive(this_object()), matching AGENTS.md §7.130's established
pattern exactly.
5.3a -- second-order crash the naive §7.130 fix exposed: guarding with
only interactive(this_object()) was not sufficient here. This lib's
quit() calls really_quit() SYNCHRONOUSLY (immediate dest_me()) for a
non-fighting player, so the vulnerable line can be reached in the SAME
heart_beat() tick as the object's own destruction -- and this_object()
reads back as literal int 0 once already destructed, so interactive(0)
itself THROWS rather than returning false, reproducing an equivalent
crash one line downstream. Fix: added an objectp() guard ahead of
interactive(). Verified live: a fresh abrupt-disconnect repro, left
running across a 20+ second wait (multiple heartbeat ticks), produced
zero further errors, and the netdead body was confirmed reaped (who
correctly shows only currently-connected players, the disconnected body
no longer lingers). Extended AGENTS.md §7.130 with this nuance.
5.4 baseobs/monsters/healer.lpc / ~20 race raiserooms: the entire death/resurrection ("raise") mechanic was silently broken for every race except the one using this NPC base's own default name -- new AGENTS.md §7.144
healer.lpc's setup() unconditionally self-named via set_name("james")
at clone time; /obj/monster.lpc's set_name() is a one-shot setter
(no-ops once name is already non-default). ~20 per-race raiseroom files
clone this SAME base object once as a persistent "nurse" NPC and then try
helper->set_name(<real name>) in their own reset() (e.g.
d/newbie/human/rooms/v05.lpc renames it to "june") -- every such rename
was a complete no-op, so find_match(nurs, this_object())/find_living()
lookups keyed on the intended name permanently failed, even though the
NPC was visibly present under that display name (set_short() isn't
gated the same way). Root-caused live via debug_message() tracing
(log_file() would have needed elevated euid this room's object doesn't
have -- see AGENTS.md §7.129's own note on this exact gotcha). Confirmed
via grep: 20 raiserooms across the whole newbie-zone corpus clone
baseobs/monsters/healer.lpc; at least 12 rename it to something other
than "james" (d/newbie/newliz/rooms/raiserm.lpc, newelf/rooms/
raiseroom.lpc, newken/rooms/raiseroom.lpc, elf/rooms/raiseroom.lpc,
human/rooms/v05.lpc, half-elf/rooms/castle/d1.lpc, dwarf/{newrooms/
raiserm28,rooms/raiserm25}.lpc, new_halfelf/rooms/h1.lpc, guests/
rooms/raiseroom.lpc, lizard/rooms/raiserm.lpc, halfling/rooms/
raiseroom.lpc) and were all affected. Fix: removed the self-naming
set_name("james") call from healer.lpc's own setup(); the one caller
that relies on the "james" default without ever renaming it
(std/raiseroom.lpc's do_raise(), which clones a disposable, never-
looked-up-by-name temporary priest) now explicitly calls
priest->set_name("james") itself. Verified live end-to-end on the human
zone: a test character (quillfrost) was killed by the "june" NPC, walked
(as a ghost) to the raiseroom, and raise me now correctly runs the full
sequence -- "The healer raises his hands...", "You reappear in a more
solid form.", "Saving...", the full nurse/priest dialogue -- and a
subsequent score shows the character alive again with real HP. Before
the fix, raise me produced zero output and left the character
permanently dead.
5.5 Five race-zone shops: buy/sell were completely dead (silent no-ops) because a broken "is the shopkeeper here" pre-check was wired as a direct verb override instead of through the base class's own dedicated hook -- new AGENTS.md §7.145
std/shop.lpc (the shared shop base) exposes set_open_condition(mixed)
specifically so a subclass can gate buy()/sell()/list()/value()/
browse() (all of which check it internally via test_open()) on a
custom precondition. d/newbie/human/rooms/v02.lpc (and 4 siblings:
newelf/rooms/shop.lpc, newliz/rooms/shop.lpc, newken/rooms/
shop.lpc, halfling/rooms/shop.lpc) each wrote a do_check() matching
that exact 0/1 contract, with the correct set_open_condition("do_check")
call sitting right there in setup() -- but COMMENTED OUT, replaced with
add_action("do_check","buy"); add_action("do_check","sell"); in
init(). Since a subclass's own init() registrations are tried before
an inherited base class's same-verb registration, do_check() always won
and, since it return(1)s unconditionally whenever the shopkeeper is
merely present, completely swallowed every buy/sell (silent success,
nothing exchanged) -- std/shop.lpc's real buy()/sell() never ran at
all. d/newbie/half-elf/rooms/town/b3.lpc has the identical broken shape
but already fully commented out (dead, unreachable) -- left untouched.
Root-caused live via debug_message() tracing after noticing list
worked (not registered via the broken do_check at all) while buy/
sell/value all produced zero output. Fix: deleted the shadowing
add_action pair, restored the commented-out set_open_condition
("do_check") call, in all 5 live-affected files.
5.5a -- second, independent bug this one was hiding behind:
newelf/rooms/shop.lpc's do_check() checked present("Old woman"), but
this room's actual shopkeeper NPC (chars/geldon.lpc) is named "geldon"
-- a copy-paste leftover from the generic "Old woman" shop template used
elsewhere. Fixed the string to match. newliz/rooms/shop.lpc and
newken/rooms/shop.lpc clone/reference a shopkeeper NPC that doesn't
actually exist in this archive at all (d/newbie/newliz/npcs/woman.lpc
is missing; newken/rooms/shop.lpc never clones anyone) -- a genuine,
pre-existing missing-NPC CONTENT gap, left untouched per this project's
scope (documented with a code comment at each site); both shops will now
correctly and honestly report "shopkeeper not present" instead of
silently doing nothing, which is the right outcome for a real content
gap.
5.5b -- a second, ALSO independent case-sensitivity bug, found only
after 5.5's fix made it newly visible: v02.lpc's (and the other
"Old woman" shops') present("Old woman") used the NPC's capitalized
DISPLAY name (set_short()), not its actual lowercase id()/name
(set_name("old woman")) -- id() on this driver
(std/basic/id.lpc) is a plain case-sensitive == compare, so even after
5.5's dispatch fix, EVERY shop command (including list/value/browse,
which don't go through do_check directly but do share the test_open()
gate) reported "shop closed" until this string was lowercased to match.
Fixed in v02.lpc and halfling/rooms/shop.lpc (the two live shops using
the real "old woman" NPC). Verified live, full end-to-end transaction on
d/newbie/human/rooms/v02.lpc (admin-granted test funds via call
adjust_money(50,"silver") @brackenfell): list shows real stock,
buy torch -> "You buy a Torch for 7 copper coins." with correct
inventory/purse updates, value torch -> "The Torch is valued at 5 copper
coins.", sell torch -> "You sell a Torch for 5 copper coins." with
correct purse update. Before either fix: all four silently did nothing.
Observed, NOT fixed -- native add_action-registered command failures are silently swallowed somewhere in this lib's custom command-queue dispatch, distinct from and not fully root-caused
While diagnosing 5.4/5.5, repeatedly observed that when a command
registered via a NATIVE add_action() call (not a cmds/-directory file
dispatched through CMD_HANDLER) fails its own internal check and calls
notify_fail("...") before return 0 (e.g. raise bob with no such
target, value/sell for an item not carried), NEITHER that
notify_fail() message NOR the driver's generic default fail message
displays at all -- total silence, no crash, nothing in any error log.
Meanwhile notify_fail()-based failures on cmds/-directory commands
(cmds/player/kill.lpc's "Alas, your etherealness has little effect..."
when dead) display correctly. Traced as far as confirming
std/living/action_queue.lpc's custom per-tick dispatcher (command(
curr_act), called from aq_add()/action_check() rather than the
driver's own top-level process_user_command()) is the architecture
involved, and that this driver's own C source (parse_command() /
user_parser() / notify_no_command()) SHOULD still display either
message given how command_giver is threaded through save_command_giver()/
restore_command_giver() around the raw command() efun -- but could not
pin down why it empirically doesn't, within this pass's time budget.
Does not block real functionality: every SUCCESS path through this
same native-add_action mechanism (5.4/5.5's fixes, kill's actual
attack) displays its own output correctly; this is specifically about
missing FEEDBACK TEXT on certain failure paths, not a functional
blocker. Documented here rather than guessed at or "fixed" blind, per
this project's own standing policy -- flagged for whoever next does a
deep dive on this lib (or a driver-level command()/command_giver
investigation) to pick up.
Standing cross-cutting patterns checked systematically (grep + targeted live tests)
- §7.121 (float arithmetic in a declared-
inteconomy function): the entire money system (obj/handlers/money_handler.lpc,std/shop.lpc'sscaled_value()/PAY_RATES) is pure integer arithmetic throughout -- confirmed clean, no float anywhere in the currency path. - §8.3a (
privatecommand-dispatch/callback function silently demoted once inherited): a script cross-referencing everyadd_action("fn", "verb")target against aprivate-declared function of the same name in the same file found zero matches -- confirmed clean (this codebase doesn't use a single centralprivate nomask command_hookidiom at all). - §7.112 (NPC
init()unconditionally scheduling acall_out()chain with no re-entry guard): found 18 files withinit()+call_out, all are simple one-shot "greeting" messages (a duplicate on reconnect would at most double-print a greeting line), none matches the death/reincarnation multi-stage state-corruption shape this pattern is about -- confirmed not applicable here (this lib's own death flow uses a completely different, NPC-command-drivenraise/do_raising()mechanism, see 5.4). - §7.118 (
.c->.lpcfilename-slice arithmetic): grepped for both the hardcoded-offset-slice shape and the+".c"literal-concatenation variant -- zero hits, confirmed clean. - §7.122 (autoload/class-item duplication on reconnect): grepped for
query_auto_load/auto_load/compute_autoload_array-style mechanisms -- none exist in this archive at all (no TMI-2/Nightmare- style reload-on-login system); confirmed not applicable. - §7.123 (bare file-scope
IDENT = (...)statement): grepped for the shape corpus-wide -- zero real instances (the one syntactic match,net/intermud3/cmds/tell.lpc'smorse(), is a normal local-variable assignment inside a function, not file scope). - §7.126 (stale pre-
.c-to-.lpcextension in.osave data): this project's own onboarding NOTES.md (§2) already flagged this exact gap in a handful of individual shop.osave files as a known, deliberately-deferred, low-impact item -- re-confirmed still present and still low-impact (per-room shop inventory, not the boot path); not re-investigated further this pass, no new instances found beyond what onboarding already documented. - §7.129 (
tell_room()wrapper forwarding omittedexcludeas literal0):secure/simul_efun/modified_efuns.lpc'stell_room()usesevent()(acall_other-based dispatcher), not the strictmessage()efun -- confirmed a structurally different, safe mechanism;event_say()'s ownpointerp(avoid)/avoid == this_object()guard already handles a bare0correctly. - §7.130: confirmed instance, see 5.3/5.3a above.
- §7.131 (
find_living()/find_player()needingset_living_name()registration): confirmed ALREADY correctly called, twice (global/player.lpc'sstart_player()andset_name()) -- not the bug shape here (though a structurally adjacent bug, §7.144, was found and is new). - §7.132 (
map()-over-mapping wrong-argument binding): grepped for the shape -- zero hits, this codebase doesn't appear to usemap()over mappings in a way that would trigger this. - §7.133 (classic-driver
remove_interactive()never bridged to this driver's realnet_dead()apply): this lib's disconnect handling is inline inheart_beat()(the §7.130 shape, not a separatenet_dead()/remove_interactive()split) -- confirmed not applicable, no deadremove_interactive()doc-comment/mechanism found anywhere. - §7.134/§7.135 (lazy-init/empty-accumulator guard missing): confirmed instance, see the new AGENTS.md §7.134 addendum (
obj/handlers/align_tracker.lpc). - §7.136 (command-soul stripping with no re-grant path): not applicable -- this codebase's command architecture (native
add_actioneverywhere,soul_commands()etc. called unconditionally fromstart_player()) doesn't use a race-content-gated soul-granting design at all. - §7.141 (MudOS-era
replace_program()fold crashing on a pending- replace closure creation): FOUND a live instance of the fold itself (std/room.lpc'screate(), identicalreplaceable()+inherit_list()+sizeof==1shape) -- but traced its ENTIRE class hierarchy (std/basic/{light,property,cute_look,desc}.lpc,std/senses.lpc,std/add_clone.lpc) and found zero closure/function- pointer creation anywhere in it, unlike thedsIprecedent'seventHearTalk()filter closure. Live-tested directly:say/askin a genuinely trivial (inherit "/std/room"only) room within the first ~160 seconds of a fresh boot (squarely inside the vulnerable window) produced zero "pending replace_program()" errors. Left the fold in place -- confirmed-clean, not a live bug in this codebase, per this project's "verify empirically before fixing a pattern-matched bug" policy; documented here rather than removed speculatively. - §7.143 (
force_me()+add_actionNPC self-dispatch failing for room-reset()-spawned NPCs):force_me()is never called anywhere in this entire archive -- confirmed not applicable, the mechanism this pattern depends on doesn't exist here.
Combat, quit/reconnect, shop -- what was and wasn't reached live
- Combat: no dedicated safe-sparring mechanism exists in this archive (no "dummy"/"practice" NPC found anywhere) -- used a genuinely weak wild newbie-zone monster instead (
d/newbie/human/monsters/ cat.lpc, level 3-6,set_wimpy(10)) per the methodology's documented fallback. A level-0 character (1 max HP) died to it almost immediately -- confirmed this is intended difficulty (help text explicitly frames level 0 as needing guild training before real combat), not a bug. A second combat test (kill june, an NPC with real HP) played out a full multi-round fight to death and back through a full resurrection (5.4). - Skill/guild training:
advance <skill>confirmed gated on physical guild-hall presence (matcheshelp advance/help fighter); not reached live this pass (would require travel this session's time budget didn't cover) -- documented here as explicitly unverified-live rather than silently skipped. - Quit / debug.log / reconnect: this lib has no
debug.logat all (its own error handler,secure/master/error_handler.lpc, routes everything tolog/errors/<domain>.errinstead -- thedebug.logwrite path is explicitly commented out there); checkedlog/errors/*after everyquitthroughout this pass, not just once.quitwhile dead (a ghost, mid-testing) was confirmed to correctly persist across a real reconnect after a wall-clock gap -- reconnecting showed the same disembodied-spirit state rather than silently reviving or corrupting the character. - Shop/economy: fully reached and live-tested end-to-end, see 5.5.
- Death/respawn: fully reached and live-tested end-to-end, see 5.4.
Six files' fixes verified via targeted lpcc single-file/room compiles
plus full native driver boots (repeated ~13 times across this session,
each killed by exact PID); zero compile regressions, zero new runtime
errors in log/errors/* across the final clean boot's full test replay.
Throwaway test characters (testerbrave, brytewind, quillfrost,
brackenfell) deleted from players/ before commit; only the seeded
god account's own incidental save-state drift (last-login timestamp,
playtime) and normal daemon/shop aggregate-stat drift (save/
timekeeper.o, d/newbie/human/rooms/v02.o's sales counters, a
freshly-created save/death.o from this session's first-ever kills)
were left as ordinary collateral of live testing, matching this
project's usual convention.
深度功能测试(§10.7,2026-10-04)— negative range ends
scripts/lpc_fix_negative_ranges.py: 3 line(s) in 3 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)— 主控 domain_file("/")
驱动的 mudlib_stats 包在主控载入时调用一次 domain_file("/"),要求返回字符串。本库 secure/master/creator_file.lpc 的 domain_file() 在 creator_file() 没有答案时返回 0,"/" 正是这种情况,所以每次启动都打印 domain_file() in the master file does not work, using bb_ui as fallback 一行,网页版每位访客的终端里都看得到(author_file(<主控文件>) 本来就返回 root uid,不报警)。现在 domain_file() 对 "/" 直接返回 get_bb_uid(),正是驱动缺省时用的值。其他路径不变:创作者的 ls 和 ftpd 直接调用 master()->domain_file(),返回 0 时显示 "Root",所以没有把 0 一律改成字符串(KB 06 §7.222)。用 lpcc config.fluffos /nonexistent.lpc 前后对比,主控载入段只少了这一行。
深度功能测试(§10.7,2026-10-06)— 技能等级每次登录都丢失(KB 06 §7.233)
std/skills.lpc(技能控制器)声明了存档变量 skills(技能树,create() 里由 STD_SKILLS 生成),std/living/skills.lpc 继承它,又声明了自己的存档变量 skills(玩家的技能等级)。于是玩家存档里有两行 skills;restore_object() 按变量名找第一个匹配的变量(继承链最深最早的那个),两行都写进控制器的技能树,玩家自己的技能等级被清成 0。实测(只含被跟踪文件的树,god 登录):exec 给 str 加 5 级,query_skill("str") 为 5,存档第二行 skills ({"str",5,0,({}),}),退出再登录后为 0。控制器的三个变量(skills、reg_skills、stat_bonus,都在 create() 里重建,没有任何代码保存它们)改为 nosave:存档里只剩玩家的技能等级,旧存档的两行都写进等级变量、后一行为准。复测:dex 加 3 级,退出再登录后仍为 3。档案里所有玩家存档的技能等级都是空的,与此相符。
深度功能测试(§10.7,2026-10-06)— 编译警告
命令文件在第一次使用时才编译,编译警告直接打印到玩家屏幕上(如 look.lpc 的 Number of arguments to 'cmd' disagrees),所以警告就是玩家看得到的问题。
1. scripts/lpc_warnings.py finalrealms --fix:13 个 nosave 函数改 protected,补 213 处 varargs(包括 192 个两参数 cmd() 覆盖三参数基类的情形),4 处缺返回值的 return;,删 454 个未用局部变量;lpc_audit_removed_locals.py 复核 725 个,0 个需要处理。
2. 同名变量(KB 04 §6.10,都是 nosave,不涉及存档):global/more_file.lpc 与 more_string.lpc 各有一套分页状态(fsize、topl、fname、stat_line、last_search、finish_func),前者改 mf_ 前缀;std/living/equip.lpc 的私有 worn_ac 只被置 0(查询走 wear.lpc 的那份),改名 equip_worn_ac;技能控制器的树改名 skill_tree(上一节已改为 nosave);global/wiz_communicate.lpc 未使用的 my_file_name 改名;global/pweath.lpc(个人天气)的昼夜标志 light 与 std/basic/light 的光照值同名,改名 daylight。
3. net/daemon/http 的 protected private 改为 private(FluffOS 不允许两个访问修饰),现在可以编译。global/events.lpc 注释掉的旧 fix_string 结尾行里的 /* 去掉(只留关闭的 */)。
4. 启动警告 202 → 0,错误 0。改动的 .lpc 在只含被跟踪文件的树里 HEAD 与新版各编一次:352 → 353 通过,无回退。实测 god 登录,look、score、i、who 屏幕上不再出现编译警告,技能等级退出再登录后保留。本库未经 §9 格式化,本次也不格式化。
5. std/index.lpc(手册索引)与 net/daemon/remotepost.lpc 里还有 12 处 private protected(早期把 static 一律换成 protected 时,private static 变成了两个访问修饰),整个文件编译不过:函数改为 private,变量改为 private nosave(即原来 private static 的意思)。remotepost 现在可以编译;std/index 编译通过但载入时另有问题,未处理。
深度功能测试(§10.7,2026-10-06)— 巫师 man 命令
巫师命令 man 一直回答「It appears that the manual page displayer is broken.」,三处原因:
1. std/index(文档索引,obj/handlers/man 继承它)和 obj/menu(翻页显示器)都 inherit "/std/basic/wildcard.lpc",而原始存档里这个文件只在 std/basic.old/ 下。两个对象都载入失败。改为继承 /std/basic.old/wildcard.lpc。
2. 索引在 create() 后用 call_out("sort_manual_dirs", 2) 分批建立,而 sort_manual_dirs() 是 private:继承它的 obj/handlers/man 里看不见,驱动拒绝这个 call_out(apply() with insufficient permission ... sort_manual_dirs ... needs: private, has: hidden,KB 07 §8.3),索引永远停在「the system is currently loading」。改 protected。
3. obj/menu 一个没用的局部变量 is_directory:警告会在第一次 man 时直接打到巫师屏幕上,删去。
验证(临时副本,新进程):fluffos 登录,man write_file 显示 efuns/filesystem/write_file 的说明。登录时 secure/login 对 global/creator 调 set_name() 被拒(该函数是 protected,原 MudOS 版是 static,同样不能被外部调用;名字随后由存档恢复时设置),是原样行为,未改。
深度功能测试(§10.7,2026-10-08)— .lpc 后缀的定宽切片
.c 改名 .lpc 时只改了字符串没改切片宽度:f[<2..] == ".lpc" 拿两个字符比四个字符,永远不成立(!= 永远成立),按后缀筛文件、去后缀的代码因此从不运行或截成 x.l(KB 06 §7.238)。scripts/lpc_lpc_suffix_slice.py --apply 改了 2 处比较、2 处去后缀(如 std/remote.lpc);改动的文件 HEAD 与工作树分别加载(新进程),无回退。
深度功能测试(§10.7,2026-10-10)— 基础类里的重复全局变量
分块扫描器重新整库扫描,还有 1565 条 Redeclaration of global variable(634 个文件),根源在四个基础类。这些警告在物件第一次被复制时打印(武器、护甲、怪物都不在预载里,所以启动时看不到)。
obj/weapon.lpc、obj/armour.lpc继承了std/item,又直接继承一次std/basic/condition,而std/item已经继承了它:每件武器、护甲带着两份cond/max_cond,外加五个inherited from both警告。删去第二次继承;两处condition::cond_string()改为item::cond_string()(经 item 找到同一个函数;item.lpc里那个返回 0 的cond_string()其实在/* Bah. ... */注释里,从未生效),两处空的condition::create()调用删去。obj/armour.lpc自己声明了两次armour_ac(第 40 行的nosave那份在所有函数之前就被第二份遮住,从未被用到),armour_type又与std/item.lpc的同名:删去前者,armour_type改用 item 的那份(护甲上没有任何地方调用set_armour_type(),行为不变)。obj/monster.lpc的level与std/living/living.lpc的同名(living 里只声明不用):怪物改用 living 的那份。std/living/mon_actions.lpc的触发表spells与global/spells.lpc的法术表同名:前者改名trigger_spells(只在本文件里用)。
四个基础类加上继承它们的 458 个物件,HEAD 与工作树各载入一次:408 -> 408 通过,无回退;这批文件里的重复声明警告 1164 -> 22。剩下的 22 条来自三个叶子文件:d/newbie/gnome/npcs/clones/cave_rat*.lpc 为了 death() 回调又继承了 /std/actions(它继承 /std/room),而注册回调的 add_triggered_action() 早已被注释成空函数;npcs/king*.lpc 继承的 respond_give 有自己的 value。未改。
- 2026-10-10(续):
scripts/lpc_fix_return_types.py改正 4 处返回类型/原型与基类或定义不符(int reset()/init()改void等);改动的 5 个文件 HEAD 与工作树载入 2 -> 2,无回退,警告 7 -> 2。
深度功能测试(§10.7,2026-10-11):“无副作用表达式”警告复查
obj/weapons/ranged/shoot.lpc、throw.lpc、throwold.lpc:me, him, ammo = 0;是逗号表达式,只清掉最后一个变量,me/him一直留着上一次的射手和目标;create()里的me, him = ({ })还把数组赋给 object 变量。改成me = him = ammo = 0;(共 15 处)。std/living/dankequip/stats.lpcset_extra_bonus():switch 缺break,设攻击加成时会顺带覆盖防御加成;default : 0;是空语句。补break。d/newbie/human/monsters/bandicoo.lpc:":snarls."那一行写在战斗喊话数组外面(落成无副作用的表达式),挪回数组里。- 5 个对象 HEAD 与工作区都能载入,无回退。
std/actions.lpc(NPC 回调混入:death()/do_pick_up()/pub_brawl())自己inherit "/std/room"。两只地底大老鼠inherit "/obj/monster"; inherit "/std/actions";,后继承者胜出,于是create/init/id/short/dest_me/add_alias全用了房间的版本:cave_rat载入时在房间的 create 流程里出错,cave_rat2的add_alias("rat")撞上房间两参数的出口别名,连编译都过不去。混入只用previous_object()和 efun,删掉对房间的继承(KB 06 §7.239 变体)。d/newbie/gnome/npcs/clones/cave_rat2.lpc是矮人区同名 NPC 的拷贝,MISC + "bread.lpc"在地精区指向不存在的文件;改成矮人区真实的/d/newbie/dwarf/misc/bread.lpc。修后两只老鼠 clone 出来都是 living、等级 4、create/init 来自/obj/monster。
Deep test (§10.7, 2026-10-12): compile warnings, final pass
Warnings in files that compile: 59 -> 0 (lpc_warnings.py rescan; 2461 -> 2469 files compile, and no file gained an error). Part of this was ported back from ninetears (lpc_twin_port.py adopt ninetears finalrealms, 12 twin files: #undef before the standard.h macros redefined in net/ daemons and gopher.h/newudp.h, stalker.lpc's start_player, nameserver.lpc, quest_handler.lpc comment, unused locals).
Real bugs:
- Insects had human bodies.
baseobs/races/insect.lpcredeclaredrace_size,limbsandbits. Itssetup()wroterace_size = 1and the six-leggedbitstable into the leaf's own copies, whilequery_race_size()andquery_bits()instd/races/standard.lpcread the base copies: the race reported size 4 and the standard humanoid body parts. The leaf declarations are gone.strange.lpchad the same duplicatebits, but it overrides every accessor, so removing it changes nothing. - The Cathedral of Lawful Evil never loaded.
room/sacrifice/le_grimbrand5.lpcinherited both/std/raiseroom(room throughcollect) and/std/sacroom(room). With two copies of/std/room, the compile failed withIllegal to redefine 'nomask' function 'query_dest_dir'. Its::init()could only reach raiseroom'sinit(), and it never callsset_god_here(). Everything in the room (the plaque,raise, the account commands,heal) belongs to the raise service, so thesacroominherit was dropped and the room now loads (KB 06 §7.240 shape). In ninetears the same two rooms also fail on other errors, so they were left alone there. d/vehicle/airplane.lpcused a globalnameas scratch space for the pilot's name. That shadowedstd/object'sname. It is renamedpilot_name.cmds/creator/dest.lpcredeclaredwiz_cmd_base'sdiscard_obj/dest_obj/objn/majd. The basedest()and the command's ownask_dest()kept separate state. Both now use the base copy.
Behaviour-preserving cleanups: void overrides where the parent is void and no caller reads the value (monster set_race/set_guild, weapon set_name, six newbie shop/pub dest_me, katuran heart_beat, dankequip attack_by); string query_short_help() in four locate/rwho commands; a duplicate object board; in the duergar guild room; equiptest.lpc's worn_ac renamed equip_worn_ac as equip.lpc already does; pendant.lpc's bitwise | replaced by ||; #undef before W_BASE (armory.h after weapon.h), DEAD (tree.h), LEVEL, and PACIFY_*; a /* inside comments in inetd.lpc, testsoul.lpc and grads/npcs/path.h, rewritten so each comment still covers the same text.
listcheck: 100 objects (changed files, includers of changed headers, inheritors of changed bases), HEAD 86 -> tree 87 PASS. No load regressions; le_grimbrand5 loads newly. The deleted-declaration audit has 0 entries to review.
- 2026-10-14: FluffOS 970d9f09 warns
Float value truncated to int(#1426). 52 sites in 12 files: assignments, initializers and returns now spell the truncationto_int(...)(same value);x op= <float>converted the float before the operation (zeroingxfor a factor below 1) and is nowx = to_int(x op ...):/baseobs/races/shadows/malik_sh.lpc(i *=),/obj/handlers/death.lpc(totaldif +=),/obj/handlers/money_track.lpc(vals[0] +=),/obj/handlers/money_track.lpc(vals[1] +=),/obj/handlers/money_track.lpc(vals[2] +=),/obj/handlers/money_track.lpc(vals[3] +=),/obj/handlers/money_track.lpc(vals[4] +=),/obj/handlers/money_track.lpc(vals[5] +=),/obj/weapons/ranged/ammo.lpc(dam *=),/obj/weapons/ranged/throw.lpc(dam *=),/obj/weapons/ranged/throwold.lpc(dam *=). Changed objects loaded before and after in fresh drivers, no regressions (scripts/lpc_fix_float_trunc.py, KB 04 §6.10). - 2026-10-14: the silver bonus in
baseobs/races/shadows/malik_sh.lpc(i *= 1.5) and the ranged-weapon bonus inobj/weapons/ranged/{ammo,throw,throwold}.lpc(dam *= 1.3) multiplied by 1: the driver turns the float into an int before the*=. Nowx = to_int(x * 1.5).
- 2026-10-14: 18 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:add_note(),begin_worship(),clean_up(),current_stat(),delete_note(),do_crawl(),do_death(),do_deposit(),do_dig(),do_help(),do_open(),do_pass()…. 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: 23 objects, 14 newly loading, 0 regressions (scripts/lpc_fix_void_return.py, KB 04 §6.3).
- 2026-10-14:
obj/handlers/money.lpcupdate_money()indexedpool[dom][type]beforepool[dom]existed (every NPC with money raisedValue being indexed is zero, caught inadjust_money()); the domain entry is now created first.std/raiseroom.lpcclean_up()isintreturning 0 (what itsvoidreturned). Bare returns left in retyped functions arereturn 0;.
- 2026-10-14: 4 files did not compile with
Illegal to inherit after defining global variables: FluffOS takes everyinheritbefore the first global, MudOS did not care.scripts/lpc_fix_inherit_order.pymoved the globals (and#includes of headers that declare globals) written above theinheritto just below the last one (KB 04 §6.3).
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. 7 file(s) fixed: /net/daemon/chars/in_finger_demon.lpc, /net/daemon/chars/tell_demon.lpc, /obj/misc/board_tale.lpc, /obj/misc/stat_book.lpc, /room/sacrifice/grim_sac.lpc, /secure/permissions.lpc, /std/vaults/vault_room.lpc (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: 4 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") && ...):/d/newbie/goblin/rooms/ngob15.lpc(1 处)、/d/newbie/goblin/rooms/ngob20.lpc(1 处)、/d/newbie/goblin/rooms/ngob23.lpc(1 处)、/d/newbie/goblin/rooms/ngob36.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). What it was: push lever always answered "You must push an existing lever." (str == "levers" && str == "lever"). Changed: /obj/handlers/weather_room.lpc (1), /std/environ/weather_room.lpc (1).
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, 10 terms). Changed: /d/newbie/duergar/rooms/pr.lpc (2), /d/newbie/half-elf/rooms/castle/b5.lpc (1), /d/newbie/half-elf/rooms/secret/c4.lpc (1), /doc/driver/efuns/objects/sand.lpc (1), /global/more_file.lpc (1), /std/living/baldy/equip.lpc (1), /std/living/dankequip/equip.dank.lpc (1), /std/living/dankequip/equip.lpc (1), /std/memcode.lpc (1).
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. /secure/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).
apply() with insufficient permission at login (2026-10-20, known, harmless)
Creating a character, secure/login.lpc calls set_name on global/player, which is protected: the console prints apply() with insufficient permission ... function: set_name. The name is set by set_name(bong) inside player.lpc anyway (who shows it), so the line is a dead call from upstream and was left alone.
wer and whoami did not compile: efun::tell_object() outside the allowed directories (2026-10-20, KB 06 7.311)
/cmds/player/wer.lpc:226:9: error: Invalid simulated efunction override (twice) every time wer was typed, and the same in whoami: the master's valid_override() lets only secure/, std/, obj/, simul_efun, global/ and a few more call efun:: for a function the simul_efun object overrides, and a command file is none of them. Both commands now write through the player's do_efun_write(), as the Discworld mudlib they descend from does. dump_socket_status() (a MudOS efun this driver lacks) is a sefun built from socket_status(); a driver without the sockets package (the WASM build) prints the header of the table only.
每次登录控制台一条 apply() with insufficient permission ... set_name(2026-10-21,KB 06 §7.322)
secure/login.lpc 和 secure/new_login.lpc 的 begin2() 里 new_copy -> set_name(name); 调用的 global/player.lpc 的 set_name() 是 protected(原版 MudOS 里是 static,call_other 到不了它,调用一直是空操作);这个驱动对这种调用在控制台打印 apply() with insufficient permission: cob: secure/login#2, ob: global/player#8, function: set_name, origin: call_other, needs: public, has: protected,每个访客登录一次。名字其实由 move_player_to_start() 里本地的 set_name(bong) 设置(who 里是 Xqaab),所以删掉这两处死调用(留一行注释),行为不变。ninetears 的同名函数是 nomask(公开),没有这个问题。扫描时这条错误记在了字母序第一个命令 allinv 上:登录时的控制台输出被第一个命令吃掉,见 KB 08 §10.16。实测:登录后控制台没有这一行,who、score 正常。
存档里的 .c 文件名改成 .lpc(2026-10-21,KB 03 §4.2 第 9 项)
存档里写着 "/d/city/npc/wei.c" 这样的文件名;extract 时源文件已经全部改成 .lpc,而驱动对带扩展名的路径只认字面,new() / load_object() / move() 都找不到它,调用方多半把失败当成「没有这个东西」悄悄跳过。本库 6 个存档共 15 处按 scripts/lpc_fix_save_dotc.py 改成 .lpc(只改 .lpc 存在而 .c 不存在的字符串;玩家存档、备份、留言板、巫师目录和文件索引缓存不动)。改动:/d/newbie/dwarf/newrooms/shop.o(2 处)、/d/newbie/dwarf/rooms/shop.o(2 处)、/d/newbie/lizard/rooms/shop.o(2 处)、/d/newbie/lizard/rooms/store.o(2 处)(等共 6 个存档)。
add_action() registrations of functions nobody defines (2026-10-21, KB 06 §7.349)
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 22 registrations in 12 files (scripts/lpc_fix_dead_add_action.py), the verbs are left to their other handlers: /d/newbie/elf/rooms/rasrmbkup.lpc (do_raising), /d/newbie/gnome/items/parchment.lpc (give_me, put_me), /d/newbie/gnome/rooms/gnome18.lpc (do_sell), /d/newbie/orc/items/misc/corpse.lpc (get_me), /d/newbie/orc/items/misc/parchment.lpc (give_me, put_me), /global/communicate.lpc (do_echo, do_echo_to, do_emote_all), /obj/misc/board_mas4.lpc (subjects), /std/bank.lpc (close_account), /std/living/dankequip/living.lpc (hold, remove, wear, wield), /std/living/dankequip/living_creator.lpc (hold, remove, wear, wield) and 2 more files (12 files). Most frequent: remove x4, give_me x2, put_me x2, hold x2, wear x2. Renamed: /obj/misc/board_mas2.lpc new -> newx (the file defines newx), /scripts/board_mas2.lpc new -> newx (the file defines newx). Left alone (read): /global/wiz_backup/wiz_file_comm.lpc indent (the object does not compile; the registration waits until it loads).
add_action() registrations of a base whose descendants define the handler: the base itself raises when loaded on its own (2026-10-21, KB 06 §7.349)
A base class (a room, an NPC, a board) registers add_action("fn", "verb") in init() for a function only its descendants define; when the base object itself is loaded (a room one can walk into, an NPC that is cloned) its visitors get *Function for verb 'x' not found. (the first scan passed these over as alive because a descendant defines the handler; scripts/lpc_verify_handlers.py on the base object showed function_exists() MISSING). The registration now stands behind if (function_exists("fn", this_object(), 1)) (flag 1: function_exists() treats protected / private functions as not defined by default); descendants register as before. 8 statements in 1 files (scripts/lpc_fix_dead_add_action.py --guard): /global/wiz_backup/wiz_communicate.lpc (do_channels).
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. 12 statement(s) in 8 file(s) (clone_object, new) 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 8 changed files, HEAD vs tree: 0 load at HEAD, 7 load now (7 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 1 subdirectory of log/: log/save/board (global/line_ed.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. In the eval probe this lib's log_file() wrote nothing (the probe object has no write access and the sefun does not make the directory itself), so what a real caller gets was not measured. log/save/board 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,§6.25 one-arg tell_object 残渣)
elf smith / smithr:tell_object("Eldevan…") → tell_object(this_player(), …)。