info.html · 介绍 · play.html · 游玩 · llms.txt · 下载与本地运行 · ZIP · GitHub
QuestMud (source: <https://github.com/vlehtola/questmud>) is a real, English-language LPMud restored from a 2012 backup of a game whose "golden age" ran 1996-2000, per its own upstream README: up to 35 concurrent players and over a thousand individual registered accounts at its peak. It's a full game mudlib set in "The Isles of Deleria" -- 20 playable races (from Human and Dwarf through Golem, Lich, Demon, and Spirit rebirth races), 9 base guilds (Fighters, Mages, Clerics, Necromancers, Psionicists, Warlocks, Martial Artists, Abjurers, plus a handful of multiguild combinations like Paladin and Battlemage), a stronghold/ housing system, a mail system, and years of individual wizards' own personal build areas.
README
The port: LDMud, not MudOS
Every other classic-LPMud lib in this collection so far has been
MudOS-lineage, close enough to this project's FluffOS target that the
compatibility gaps were mostly small dialect differences. QuestMud is
different: its original driver was LDMud (bin/ldmud 3000 per the
archive's own StartMud script), a genuinely different LPC dialect --
closures (#'name, lambda/unbound_lambda/bind_lambda), a
set_driver_hook()-based master-object architecture instead of
FluffOS's hardcoded apply conventions, wide/multi-value mappings
((["key": v0; v1; v2]) with map[key, N] column indexing, no
FluffOS equivalent at all), and several LDMud-only efuns
(cat(), tail(), strstr(), to_string(), object_name(),
unshadow()) that simply don't exist on this driver. Porting it was a
substantially different job from every other lib here -- see NOTES.md
for the full technical writeup.
Severe bugs found
Several genuinely severe, totally-blocking bugs were found and fixed, each of which alone would have made this lib permanently unbootable:
master::valid_read()never had a case for"load_object"/"include"-- FluffOS routes every single object compile and every#includeresolution throughvalid_read()with these call_fun values, which this LDMud-derived master had never needed to handle (LDMud doesn't route compilation through valid_read at all). Without it, *every* object load and *every*#includein the entire 12,500+ file mudlib was silently denied -- the single highest-impact fix in this whole port.master::creator_file()'s "no special uid" case returned a bare int (LDMud's own sentinel for "ordinary, non-wizard object"), but this driver's uid system requires a real string return always, and silently destructed the object being loaded if it didn't get one. This broke loading almost every non-wizard-owned file in the mudlib.- A
creator()simul_efun used as a "reject if this looks like a wizard's ad-hoc clone" guard inobj/player.lpc's ownreset()became permanently true for every legitimate connection oncecreator_file()above was fixed to always return a real uid string, self-destructing every single newly-registered character immediately afterclone_object(). Root-caused by tracing all the way down to a raw uid-vs-backbone-uid comparison; seeNOTES.md\S2. - This driver's
move_object()efun is 1-arg (movesthis_object()only); this whole session's corpus-wide fix for that (A->move_object(B)) turns out to silently no-op wheneverAisn't alreadythis_object(), becausecall_other()never falls back to an efun of the same name. This affected roughly ten thousand call sites across the archive and is only partially remediated here (a realmove_object(dest)method added toobj/living.lpc, covering players and monsters) -- seeNOTES.md\S2 for the full severity assessment and what's still open. efun::name(...)calls anywhere outsidesecure/simul_efun.lpcitself requiremaster::valid_override(), which this master never defined -- everyefun::override compiled fine insidesimul_efun.lpc(which loads beforemaster_obis even set) but failed everywhere else with "Invalid simulated efunction override."- FluffOS's
reset()is scheduled/lazy, not synchronous-on-load like LDMud -- several daemons whosereset()populates state their own other functions depend on (daemons/guildrank_d.lpc,daemons/multiguild_d.lpc,daemons/race_stats.lpc,world/special/traits.lpc,guilds/guilds/guild_init.lpc) crashed the very first character's own registration flow with "Value being indexed is zero" before theirreset()had ever run. Fixed with an explicitcreate() { reset(0); }(or a direct global initializer) on each one -- confirmed to be a real, repeatable, and *not yet corpus-wide-audited* class; seeNOTES.md\S2.
Registration flow
c (create) -> character name (2-10 letters, no digits) -> password
(4+ characters) -> confirm password -> race selection room (select
<race>, then continue) -> optional special traits (list, select
<trait>, continue) -> guild selection (select <guild>) -> the real
game world. g logs in as a guest; w lists who's online; q quits
from the login menu.
Status
Boots clean under the native build-debug driver with a
"lazy resets : 1" config override (see NOTES.md \S1 for why). Full
registration verified live and end-to-end with a raw socket client: a
brand-new character all the way through race selection, trait
selection, and guild selection into the real game world, followed by
working look (real room description, "No obvious exits.") and score
(a complete, correctly-formatted character sheet -- level, race, guild,
stats, HP/SP/EP, bank balance, alignment, age) and a clean quit that
saves the character file.
A large compile-sweep tail remains: of the archive's 12,565 .lpc
files, 5,829 compile clean and 6,736 do not. The overwhelming majority
(6,021 of 6,736, 89%) are inside individual wizards' own personal
sandbox directories under wizards/ -- each wizard kept full private
copies of core files like player.lpc/living.lpc to experiment on,
never loaded by the live game (the canonical obj/player.lpc/
obj/living.lpc etc. are the only copies actually referenced). Of the
715 failures outside wizards/, a large fraction are
doc/examples/* tutorial/template content and a handful are the
stronghold/ housing subsystem -- none block registration or the
verified play session above. See NOTES.md \S3 for the fix patterns
that would close most of this tail, if picked up as future work.
WASM status: playable. Shared WASM driver. Full new-character
registration (c / wasmqm / password / select human / continue /
continue / select fighter) into the Fighter guild of Duranghom
(five exits, guildmaster visible), plus look / score ("level 1
Human", "Primary guild: Fighters") / quit ("Saving Wasmqm."),
verified with scripts/wasm_client.js. The compile-sweep tail (LDMud
leftover syntax in wizard/cmd files) still prints at boot and on
quit's drop-all path; none of it blocked the login or play session.
Play: https://mudlibs.fluffos.info/questmud/
Local run
cd libs/questmud
~/src/fluffos/build-debug/src/driver config.fluffosGame port: 40247.
NOTES · 移植与修复记录
QuestMud -- porting notes
Source: git clone https://github.com/vlehtola/questmud (cloned
2026-08-28). Slug questmud, number 945, port 40247. Mudlib root is the
clone's lib/ subdirectory (bin/, ftpd/, share/ alongside it are the
archive's own bundled driver/tools, ignored -- this project uses its own
FluffOS driver). Confirmed via lib/secure/master.c + lib/secure/
simul_efun.c before cloning; what that early check didn't catch is that
this is an LDMud mudlib, not MudOS-lineage like every other classic
LPMud in this collection so far -- a materially different porting job. MIT
licensed. 22,198 files in the raw clone, 12,565 .c files under lib/.
1. Conversion
scripts/convert_lib.sh on raw/lib -> work: source is English-language
and already ~99% ASCII/UTF-8 (735 lossy-conversion files out of ~21,000,
mostly Finnish wizard-name comments with accented characters -- the
original wizard team, per names throughout the archive like Ahma, Piikki,
Nalle, Rag, Siki, was Finnish), 12,565 files renamed .c->.lpc, 11,041
literal .c" references fixed, 6 local angle-bracket includes converted to
quotes, 231 files static->nosave.
2. The LDMud-vs-MudOS/FluffOS architecture gap
This is the first LDMud archive onboarded into this collection. LDMud and
MudOS/FluffOS share LPC syntax at a surface level but diverge sharply
underneath. Closure usage (#'name, lambda/unbound_lambda/
bind_lambda) turned out to be narrow in *scope* (36 files total, mostly
secure/master.c and a handful of scattered wizard sandbox files) but
every one of the driver-architecture-level gaps below was broad in
*impact*, since they sit in secure/master.lpc/secure/simul_efun.lpc
or otherwise get exercised by every single connection.
2.1 secure/master.lpc: set_driver_hook() doesn't exist
The entire original create()/inaugurate_master() chain existed to
install set_driver_hook()-based behavior for object creation,
uid-loading, and command dispatch -- none of which exists on FluffOS.
Replaced with a no-op stub, since FluffOS already hardcodes the equivalent
behavior directly:
H_CREATE_SUPER/H_CREATE_OB/H_CREATE_CLONE->"create"/"reset": FluffOS always callscreate()on load/clone andreset()on the normal reset cycle, unconditionally -- no hook needed.H_LOAD_UIDS/H_CLONE_UIDS->creator_file()/get_bb_uid(): FluffOS callsmaster::creator_file()(a real apply, kept) for exactly this purpose, and inherits euid from the creating object otherwise.H_MOVE_OBJECT0: FluffOS'smove_object()efun already has the init()-calling behavior this hook existed to install, built in natively (see \S2.4 for the *actual* move_object gap, which is different).H_MODIFY_COMMAND(_FNAME)/H_NOTIFY_FAIL/H_INCLUDE_DIRS: cosmetic (direction abbreviations, fail message, include search path) -- covered byconfig.fluffos's "default fail message"/"include directories" instead.
2.2 master::valid_read() never had a case for "load_object"/"include" -- the single highest-impact fix in this whole port
FluffOS's vm/internal/simulate.cc (load_object()/clone_object()) and
compiler/internal/lexer_utils.cc (#include resolution) both route
through master::valid_read(path, eff_user, call_fun, caller) with
call_fun values "load_object"/"recompile_object"/"include" that
this LDMud-derived master's valid_read() switch had never needed a case
for (LDMud doesn't route object compilation through valid_read() at
all). Without a matching case, the switch falls through to its own
default (deny), so every single object load and every single
#include in the entire mudlib was silently denied from the very first
boot -- every preload line failed with a bare "Read access denied", and
every file's very first #include <ansi.h>/"log.h" failed with
"Cannot #include ...", cascading into hundreds of secondary "Undefined
variable"/syntax errors from the missing macros with no indication the
real problem was access control, not the files themselves. Fixed by
adding both cases to the existing permissive read policy (this lib's
valid_read() already allows read_file/read_bytes/file_size to
everyone):
case "load_object":
case "recompile_object":
case "include":
case "restore_object": return 1;2.3 master::creator_file()'s "no special uid" case returned a bare int -- broke loading nearly every non-wizard file
FluffOS's uid system (packages/uids/uids.cc's set_uid()) hard-requires
creator_file() to always return a real uid string; any other return
type -- including the bare 1/0 LDMud used as its "no special
creator, ordinary system object" sentinel -- fatally destructs the object
being loaded with "Illegal object to load: return value of
master::creator_file() was not a string", *even though the object had
compiled cleanly moments earlier*. This hit essentially every
non-wizards/-owned file the instant it got past its own compile errors
(confirmed live on obj/timer, obj/wizlist, room/house_shop_d,
daemons/mastery_d, and would have hit every other ordinary content file
in the mudlib). Fixed by returning get_bb_uid() (the existing
"Backbone" sentinel) everywhere the original returned a bare int or fell
off the end of the function with no return at all.
2.4 move_object() is 1-arg on this driver -- a corpus-wide dialect fix that only partially works
FluffOS's move_object() efun takes one argument and moves
this_object() only (void move_object(object | string)); the archive's
LDMud/older-MudOS convention is 2-arg (move_object(item, dest), moving
an arbitrary object). This project's established mechanical fix for this
dialect gap is move_object(A, B) -> A->move_object(B), applied
corpus-wide here too (9,873 sites/182 files via a paren-aware script, not
a flat regex, so nested-call arguments were handled correctly).
This fix is only reliable when A is already this_object() at the
call site. call_other() never falls back to an efun of the same name
when the target object has no matching LPC function of its own -- it
just returns 0 silently, no error, no exception. Confirmed live: a
brand-new character's own myself->move_object("/obj/race_selection")
(inside obj/player.lpc's own registration flow, where myself ==
this_object()) *appeared* to work -- no exception, normal control flow
continued, the write()s after it printed fine -- but
environment(myself) was still 0 immediately afterward. A bare
move_object(dest) call from the same code worked correctly. This was
diagnosed by literally inlining a debug_message() with a catch() around
the call and comparing find_object()/environment() before and after.
Fixed for the by-far-most-common case: obj/living.lpc (inherited by
every player and monster via living.h) now defines a real
move_object(dest) method:
mixed move_object(mixed dest) {
return efun::move_object(dest);
}This makes X->move_object(dest) a real, correctly-resolving function
call for any living (player, monster, NPC) target -- covering the
overwhelming majority of ->move_object( call sites in this specific
mudlib (players/monsters moving themselves or being moved by daemons).
efun::move_object (not a bare recursive call) is required specifically
*because* the new function shares the efun's own name; using efun::
here needed its own separate fix, see \S2.5.
Not fixed: any ->move_object( call site whose *target* is not a
living (money, weapons, corpses, and other clonable items that don't
inherit obj/living.lpc) still silently no-ops. This is a real,
confirmed-live, corpus-wide gap that a full future pass should audit --
grep the whole corpus for ->move_object( and, for each target class
that isn't already living-derived, either add the same real
move_object(dest) wrapper to that class's own base file, or rewrite the
call site to run from inside the target's own execution context. Left
as documented, unfixed scope for this session given the size of the
corpus (12,565 files) and that the verified registration/play path above
doesn't depend on any of the remaining cases.
2.5 efun::name(...) outside secure/simul_efun.lpc requires master::valid_override()
The compiler's efun:: override syntax (compiler/internal/
grammar_rules_exprs.cc's rule_efun_override()) gates every use through
master::valid_override(calling_file, identifier, main_file); with no
valid_override() defined at all, this silently *denies* every use once
master_ob is set (a NULL master apply return -> master_approved()
returns 0), which happens the moment secure/master.lpc itself finishes
loading. secure/simul_efun.lpc's own many efun:: calls (efun::
map_delete, efun::file_name, efun::command, etc., used throughout
this file's compat shims) never hit this because simul_efun.lpc loads
and compiles *before* master_ob is set (the "no master yet" bootstrap
allow case in master_approved()) -- but any efun:: call anywhere else
in the mudlib, compiled lazily later during actual play, hit "Invalid
simulated efunction override" and failed to compile. First surfaced by
obj/living.lpc's new move_object() wrapper (\S2.4) needing
efun::move_object. Fixed with a permissive valid_override():
int valid_override(string calling_file, string identifier, string caller) {
return 1;
}2.6 Wide/multi-value mappings -- no FluffOS equivalent at all
LDMud mappings can hold multiple values per key
((["key": v0; v1; v2]), accessed via map[key, N] column indexing).
FluffOS mappings are always single-value. Every instance found this
session was converted to a single array value instead
((["key": ({v0, v1, v2})]), read back as map[key][N]):
obj/player.lpc'sability_train(3 columns: current exp, exp target, training-destination guild).room/banish_d.lpc'snames/sitesban-tracking mappings (4/3 columns).daemons/guildrank_d.lpc'sleaders(2 columns: score, leader name).daemons/emote_d.lpc/daemons/emote_data.lpc'se_map(9 columns per emote verb -- ~190 individual emote entries converted via a quote-aware script, plus 2 entries that had wrapped onto a second physical line and needed hand-fixing after the automated pass).cmds/std/_mail.lpc'smail(5 columns: sender, timestamp, subject, body, unread-flag).guilds/guilds/guild_init.lpc'sguilds(5 columns: display name, room path, description path, exp threshold, flag) andmultiguilds(2 columns: two base guild names).
daemons/leader_d.lpc's leaders mapping turned out to be a false
positive for this pattern: it's genuinely single-valued
(leaders[race] = name), but was *read* with leaders[race, 0]
LDMud-style column-0 indexing anyway (valid LDMud syntax for a
width-1 mapping, equivalent to plain leaders[race]) -- fixed by
dropping the , 0 rather than wrapping the write side in an array.
Worth checking for this exact false-positive shape (write side is
single-value, read side still uses [key, 0]) before assuming every
map[key, N] site needs the full array-value conversion.
2.7 FluffOS's reset() is scheduled/lazy, not synchronous-on-load
LDMud calls reset() synchronously right after create() for every new
object (this mudlib's original master.c explicitly documented this:
"1. reset() will be called first... 4. The game will enter multiuser
mode"). FluffOS's reset() only fires on the normal reset-timer cycle
("time to reset", default here 3600s) unless "lazy resets" is
enabled in config.fluffos, in which case it fires on the object's
*first* apply_low()/move_object() touch instead -- but even with
lazy resets : 1 set, several daemons still had their reset()-
populated state read before reset() had run, crashing with *Value
being indexed is zero on a raw-LPC-default (int 0) mapping. Confirmed
live, repeatedly, as the actual blocker for the very first character's
own registration flow (each fix below was found by tracing one specific
crash during a real registration playthrough, not by static analysis):
secure/simul_efun.lpc'sliving_name_m/name_living_m(thefind_living()/find_player()name-index mappings) -- these were never going to get initialized viareset()at all, since they were originally populated bystart_simul_efun(), itself only ever called frommaster::get_simul_efun()-- an LDMud-only master apply not in FluffOS's applies table, so never invoked. Fixed by initializing both directly at declaration (mapping living_name_m = ([]), name_living_m = ([]);) instead of relying on a function nothing calls.daemons/guildrank_d.lpc'sleaders-- fixed with a realcreate() { reset(0); }plus, belt-and-suspenders, initializingleaderswith the same real 10-guild starting value directly at declaration (the first live repro showedcreate()->reset()alone still weren't enough the very first time this exact daemon was touched -- not fully root-caused given time constraints; the direct initializer is what actually fixed it).daemons/multiguild_d.lpc,daemons/race_stats.lpc,world/special/traits.lpc,guilds/guilds/guild_init.lpc-- all fixed with a straightforwardcreate() { reset(0); }.daemons/xp_rate_d.lpc-- deliberately *not* given thecreate()->reset()treatment: itsreset()chains intoprocess_tune()->add_race_xp()->race_stats.lpc'squery_race_stat(), which itself assumes a real connected player context (this_player()-> query_guest(), unguarded) that doesn't exist yet during a synchronous boot-timecreate(). Forcing the full chain surfaced a *new* crash (this_player()was0) instead of fixing the original one. Fixedrace_stats.lpc's missing null-guard as a genuine, narrow pre-existing bug (this_player() && this_player()->query_guest()) since that daemon-context call shape is legitimate, but leftxp_rate_d.lpcitself with just direct mapping initializers rather than a forcedreset(), to avoid trading one lazy-reset bug for a different "assumed player context" bug.
This class is confirmed real and repeatable but almost certainly not
exhaustively found -- every instance above was discovered by literally
playing through registration and reacting to each new crash, not by a
systematic corpus grep. A future pass should grep for every mapping/
array global that a file's own reset() populates and cross-check
whether anything in that same file (or callable from elsewhere) reads it
before reset() could plausibly have run.
2.8 Missing LDMud-only efuns, added as secure/simul_efun.lpc shims
cat(file, start, num)-- prints a chunk of a file's lines, returns line count printed or 0 on failure. Genuinely load-bearing: 582 call sites including the very first login banner (obj/player.lpcprinting/WELCOME) and the "more"-style pager (even_more()/cat_file()). Implemented viaread_file()+explode()/slicing.tail(file)-- print the last ~10 lines of a file. Already a cataloged pattern in this project's own AGENTS.md \S6.2 for a different lib; reimplemented the same way here.to_string(mixed)-- int/float->string conversion, 64 call sites. This driver's+operator already coerces int/float onto a string (confirmed elsewhere in this same mudlib), soreturn "" + x;is a safe, simple implementation.strstr(haystack, needle, start)-- this driver'sstrsrch()has the identical(string, string|int, int start)contract and -1-on- not-found return, so a direct passthrough alias was sufficient (24 call sites).unshadow()(bare, "remove every shadow layer onthis_object()") -- used by this mudlib's real spell/status-effect system (haste, shield, heal_resist, stat_boost allshadow()the target for the effect's duration). Implemented by walking the shadow chain (shadow(this_object(), 0)) anddestruct()-ing each layer, matching howsecure/master.lpc's ownprepare_destruct()already tears down a shadow chain on this driver.object_name(object)-- this driver's real efun is the older MudOS namefile_name()instead;object_name()doesn't exist here at all (confirmed: zero hits anywhere in this whole project's corpus/AGENTS.md before this lib, unlike every dialect gap already cataloged from the MudOS-lineage libs). The mudlib's own pre-existingfile_name()simul_efun had it backwards (aliasedfile_name->object_name, assumingobject_namewas the real one) -- flipped to aliasobject_name->efun::file_nameinstead, so the handful of LDMud-nativeobject_name()call sites (secure/master.lpc,secure/ simul_efun.lpc's owngeteuid(), one wizard test file) work without breaking every other file'sfile_name()calls.creator()/domain()-- see \S2.9, a related but distinct bug from simply "missing efun".set_modify_command(ob)-- registers the (nonexistent, \S2.1)H_MODIFY_COMMAND_FNAMEhook; stubbed as a no-op (cosmetic, direction abbreviations only).member(what, elem)-- LDMud's overloaded array-index-or-mapping- presence builtin (~70 call sites), reimplemented viamapp()+efun::member_array().member_array()itself needed its own fix: the mudlib's existingmember_array(elem, arr) { return member(arr,elem); }wrapper called the (missing)member()builtin with reversed argument order; replaced with a directefun::member_array(elem, arr)passthrough (FluffOS already has a realmember_arrayefun with the exact contract wanted).m_delete(mapping, key)-- on LDMud a real, non-mutating built-in efun (returns a *new* mapping with the key removed); this mudlib's own compat version of it shipped commented out insecure/ simul_efun.lpc(since on LDMud it was never needed -- the driver already had a realm_delete). This driver has nom_deleteefun at all, onlymap_delete()(void/in-place, the classic MudOS/LDMudmap_delete()dialect split already cataloged in this project's AGENTS.md) -- re-enabled the mudlib's own commented-out shim, fixed to copy via mapping+(nocopy_mapping()on this driver either) before callingefun::map_delete()on the copy. 35 files across the mudlib callm_delete(), includingobj/player.lpcand several daemons.m_indices/m_values->keys/values: already a cataloged MudOS/LDMud dialect gap in this project's AGENTS.md; swept corpus-wide (229 files/709 sites).
2.9 creator()/domain(): the *other* self-destructing-registration bug
Once \S2.3's creator_file() fix was in place, obj/player.lpc's own
reset() had a second, independent bug that also self-destructed every
newly-registered character: if (creator(this_object())) { ...
destruct(this_object()); } is a "reject this if it looks like a
wizard's own ad-hoc clone of player.lpc, not a normal system-created
character" guard, and the mudlib's own creator() simul_efun was a bare
getuid() passthrough. Under real euid tracking every object always has
*some* uid (never LDMud's original int-0/1 "no special uid" sentinel,
per \S2.3), so creator() became truthy for literally every object,
including perfectly ordinary clone_object("obj/player") calls from
master::connect(). Fixed by filtering out the backbone/system uid
specifically:
string creator(object ob) {
string uid = (string)getuid(ob);
return (uid && uid != (string)MASTER->get_bb_uid()) ? uid : 0;
}3. Ordinary compile-time bugs (same class as every other lib in this
collection, just discovered via an LDMud lens)
Once the architectural gaps above were fixed, the remaining errors were the same familiar patterns already cataloged in this project's top-level AGENTS.md, just numerous because this is a 12,565-file archive:
- Untyped functions/parameters (old-LPC implicit
int/mixed, strict "unknown" on this driver): swept mechanically across the whole corpus with two scripts -- one adding an explicitmixedreturn type to every barename(args) {definition (63,952 sites/many files), one addingmixedto every untyped parameter in the same definitions (15,171 sites). Both are strictly additive (they only add type information the compiler was missing;mixedis compatible with everything, so neither changes runtime behavior). - Declared-scalar-but-actually-array (
object ob;thenob = all_inventory(...);string s;thens = ({...}); etc.): the single most common individual bug hit while chasing the registration path live -- dozens of instances acrossobj/living.lpc,obj/player.lpc,room/room.lpc(property, shared by every room in the mudlib via inheritance -- a single one-line fix here likely resolved a large fraction of the wider compile-sweep tail),daemons/race_stats.lpc,daemons/guildrank_d.lpc,guilds/guilds/guild.lpc,obj/new_money.lpc,obj/timer.lpc, and more. No corpus-wide mechanical fix attempted (each needs a human read of how the variable is actually used to know whether it's genuinely array-typed or the assignment itself is the real bug). - Same-file forward references (this driver compiles one pass per file; a function called before its own textual definition needs a forward declaration first) -- extremely widespread, expected for a file this size (
obj/player.lpcalone needed ~60 forward declarations). Fixed with a purpose-built script (parses out brace-depth-0 function definitions including multi-line signatures, finds bare calls preceding their own definition line, emitsmixed name();prototypes) rather than by hand. - Base-class calls a function only the derived class defines (
obj/living.lpccallingadd_hunger()/add_thirst()/check_condis()/query_real_name()/add_hunter_list()/query_hunter_list()/wiz_commands()/wiz_commands2(), all of which only exist inobj/player.lpc;guilds/guilds/guild.lpccallingguild_name()/guild_max_level()/skill_list(), which only exist in each individual derived guild room) -- this driver compiles each file standalone, so a base class genuinely cannot see a subclass-only symbol at compile time even though it works fine at runtime once a real subclass instance is what's actually calling it. Forward-declared each; documented as narrow, real, pre-existing gaps in the original mudlib's own class design (a bareobj/monster.lpcinstance reachingadd_hunger()would still hit a genuine runtime "Undefined function") rather than invented around. - Arg-count mismatches (a function declared with N required params, called elsewhere with fewer) -- this driver treats these as hard compile errors unless the function is
varargs; ~15 individual functions acrossobj/player.lpc,obj/living.lpc,daemons/guildrank_d.lpc,daemons/race_stats.lpc,room/room.lpc,room/bank_d.lpcmadevarargsto match their real, pre-existing call-site shapes (never invented a new default value beyond what the function body's own logic already treated a missing arg as, e.g. falsy/zero). statusas a legacy type keyword (an old LPmud alias forint): swept corpus-wide (504 files/1,097 sites), same pattern as every other MudOS/LDMud-lineage lib in this project's AGENTS.md \S4.3.- A single genuine copy-paste bug, unrelated to porting:
daemons/guildrank_d.lpc'squery_rank()hadguild[i](indexing a scalar string parameter, giving a stray character code) where every sibling call in the same branch used plainguild-- a leftover from copy-pasting the function's own later array-loop version. Fixed to match the surrounding code.
4. Compile-sweep results
scripts/lpcc_check.sh: 12,565 files, 5,829 pass, 6,736 fail. Of the
failures, 6,021 (89%) are inside individual wizards' own personal
wizards/<name>/ sandboxes -- each wizard kept a full private copy of
core files like player.lpc/living.lpc/guildrank_d.lpc to
experiment on, confirmed via grep to never be referenced from the live
game (only the canonical obj/player.lpc, obj/living.lpc, etc. are
actually loaded) -- the same "wizard-sandbox clutter" shape this
project's AGENTS.md already documents for several other libs, just at a
larger absolute scale given this archive's size. Of the remaining 715
failures outside wizards/, the largest identifiable groups are
doc/examples/* (114 files, tutorial/template content, several
genuinely incomplete even in the original archive per their own
comments) and the stronghold/ player-housing subsystem (5 files). None
of the 6,736 failures block the registration/play path verified live
this session (see README.md). The fix patterns in \S2/\S3 above would
likely close a large fraction of this tail if picked up as a future
sweep -- the declared-scalar-but-actually-array pattern and the
forward-reference script in particular are corpus-wide, mechanical, and
already proven safe on the files fixed this session.
5. Known limitations / deliberately left for future work
- \S2.4's
move_object()gap is only fixed for living-derived targets. - \S2.7's lazy-reset class is confirmed real but not exhaustively audited -- only the specific daemons that blocked the verified registration path were fixed.
- The 6,736-file compile-sweep tail (\S4) was triaged by category, not fixed file-by-file.
- WASM pass not attempted this session.
§10.7 deep functional test (2026-08-31, round two)
Full continuous playthrough against ~/src/fluffos/build-debug/src/
driver config.fluffos (the shared driver, no dedicated worktree
needed), several fresh throwaway registrations, following AGENTS.md's
own note that \S2.7's lazy-reset class was "confirmed real but not
exhaustively audited." It wasn't -- this pass found the single most
severe, most far-reaching bug in this whole port, well beyond what
\S2/\S7.158 already catalogued. Full technical writeup: new AGENTS.md
\S7.192. Summary here.
SEVERE finding: room/room.lpc and obj/monster.lpc were both still missing the \S2.7 lazy-reset create() bridge -- this made essentially the ENTIRE explorable game world unreachable
Registering a fresh character (Bragoth, human Fighter) through the
full chargen flow and joining the Fighter guild landed in a room with
no description at all and "No obvious exits." -- a hard, silent,
total dead end immediately after character creation, for every single
new player, every time. Root-caused via vm/internal/base/object.cc's
call_create(): it arms next_reset to an hour in the future BEFORE
create() even runs, so try_reset()'s lazy-resets check is already
false the instant any object finishes loading -- meaning reset()
(which is what actually populates short_desc/long_desc/exits via
add_exit(), per room/room.lpc's own header comment) never fires
until a full hour after that specific room's first load, REGARDLESS of
lazy resets : 1. room/room.lpc (inherited by 5,368+ files
corpus-wide) and obj/monster.lpc (the base class for every NPC/
monster clone in the game) both had no create() of their own. Fixed
both with the same pattern AGENTS.md \S7.177 already established for
"universal base class, subclasses each override reset()" (a bare
reset(0) can silently no-op on this driver -- route through
call_other() instead):
void create() {
call_other(this_object(), "reset", 0);
}The reset() gap cascaded into a whole chain of previously-unreachable, independent compile/type bugs -- fixed one link at a time via live reproduction, not static analysis
Once reset() actually started running for the first time ever on
this driver, every bug hiding inside it (permanently dead code before
the fix) became a real, live, uncaught crash that aborts the calling
room's reset() PARTWAY THROUGH -- silently skipping whatever comes
textually after the crash point, including the room's own
add_exit()/description setup that always follows its initial
"populate my furniture/NPCs" clone_object() calls. Traced and fixed
via repeated live reproduction (join guild -> crash -> read trace ->
fix -> reboot -> repeat), not a static sweep:
obj/monster.lpc: a dozen+ same-file forward-reference bugs (functions called before their own definition --random_move,short,query_invisible,test_match,check_al_aggr,pick_any_obj,follow,random_resists,set_skills,heart_beat,set_skill_chance,query_race,set_short,query_hunter_list,set_random_pick,random_combat_tactic,set_resists,query_resists,set_skill,set_spell_chance,query_animal-- all forward-declared, same mechanical pattern as \S3's original sweep, just not caught for this specific file);move_away()called but never defined anywhere in the mudlib (forward-declared only, left as a genuine pre-existing gap per \S3's own precedent); several arg-count mismatches needingvarargs(query_limb,check_al_aggr,query_hunter_list,set_skill,random_resists,set_skills-- each verified falsy-safe against its own body before addingvarargs); declared-scalar-but-actually- array globals (skill_chance,use_skill,spell_words,talk_func/talk_match/talk_type-- the last three literally commented "Vector of ..." in the original source) and locals (second_life()'smembers,set_antiobjects()'sob); a missing[i]index inset_skill_chance()'s own array-copy loop (copying the whole old array into one slot instead of one element, same shape asroom/room.lpc's own already-correctadd_exit()copy loop); plus its own missingcreate()(above).obj/living.lpc:death(mixed force)neededvarargs--obj/monster.lpccalls it with 0 args in two places, andforceis already used with a falsy-safe!forcecheck in the body.obj/monster_data.lpc:get_hp()'s localcostdeclaredintbut assignedallocate(21)and indexed throughout --int *cost.obj/base_object.lpc:sizedeclaredstringalongside the file's other string fields, but every real call site (set_size(2)/set_size(30)/set_size(5)) and this file's ownget_size(int)(0-100 numeric size code) treat it asint-- fixed the declaration, plus a same-file forward-reference onget_size()itself.wizards/siki/base_drink.lpc-- a file this project's usual "wizards/ = personal sandbox, never referenced from the live game" assumption does NOT hold for:/world/objects/fountain.lpc(the real, shipped fountain at the game's own Central Square) inherits it directly. A forward-reference (get_drinks_left) and a genuinely undefinedquery_name()(forward-declared only, per \S3's "base class calls a subclass-only function" precedent -- the one live caller, the fountain, always setsliquid_typeand never reaches that branch) were both blocking Central Square's ownreset().cmds/std/_environment.lpc: a forward-reference (return_color) plus a false-positive instance of \S2.6's wide-mapping pattern --lis a genuinely single-valued mapping but read with LDMud-stylel[key, 0]column indexing in three places, fixed by dropping the, 0(same remedy \S2.6 already documented fordaemons/ leader_d.lpc's identical false-positive shape).secure/master.lpc:valid_read()had no case for call_fun"stat"-- this driver'sget_dir()/stat()efuns BOTH route throughvalid_read(path, eff_user, "stat", caller)(packages/core/file.cc), a different call_fun value than the"get_dir"case this master's switch already had (dead code on this driver -- never actually passed). Silently deniedget_dir()return an int0instead of an array, which fell all the way through the race-selection room's ownsizeof(str)guard with zero error -- "Available races:" printed as a genuinely empty list on every single registration. Fixed by adding"stat"to the existing permissive read policy alongsideread_file/read_bytes/file_size.- 267-file corpus sweep:
string chat_str;declared as a scalar but always assignedallocate(N)and indexed viachat_str[0..N] = ...; load_chat(N, chat_str);-- a copy-pasted NPC idle-chat idiom used across the ENTIRE archive (guild masters, city guards, wizard-realm NPCs, wild monsters). A quote-aware Python sweep found 294 candidate files, confirmed array usage (chat_str[orchat_str = allocate() in 267 of them before fixing (27 correctly skipped as genuine single-string scalars, e.g.world/misc/chat.lpc).
Verified live, end to end, after all of the above
Full continuous session, fresh registration each time a fix needed a
reboot: name -> password -> race selection (Available races: now
lists all 22 real races, not empty) -> special traits -> guild
selection (select fighter) -> the Fighter guild room now shows its
real description AND all 5 exits -> east onto Gold street (a real
citizen and city guard NPC both visible and correctly described, no
crash) -> south into Central Square (the game's own central hub
-- also showing its real description, all 4 exits, and a patrolling
guard NPC) -> north back. quit produced a clean disconnect with
log/debug.log never created (no uncaught errors) both before and
after every fix in this chain. A ~220s idle long-sit boot watch (real
driver stdout capture, not debug.log alone) produced zero new error
lines beyond the already-catalogued, unrelated 66-line compile-sweep
tail from \S4/\S5.
What was NOT reached, flagged honestly rather than silently skipped
- Combat:
kill citizenon an ordinary street NPC produced no visible combat exchange at all (no damage either direction over several heartbeat ticks) -- most likely this specific NPC/room is intentionally non-hostile "flavor" content (the newbie help text explicitly directs new players to dedicated zones instead: "City slums: 2s,2e,2s,1e", "Ant mines: 16s,11e,3s,6e,2n" from Central Square), not a bug; not chased further given the travel distance and time budget. No dedicated safe-sparring dummy/mechanism exists in this archive (grepped for theaccept_fightpattern this project's own AGENTS.md documents as the thing to look for -- zero hits anywhere outsidewizards/). - Skill training past the automatic background-advancement phase, and admin-shortcut guild assignment: not reached -- the organic
select <guild>path (verified above) is the only guild-join mechanism found in the explorable game; no separate admin/newbie- gift shortcut exists to cross-check against. - Shop/economy: not reached, time-budgeted out in favor of the severe crash-chain fix above.
- Death/respawn: not reached;
help newbie's own "Death and logging in to the game" section describes a ghost/resurrection flow (ask a cleric-guild player, or accept a costly ress from an NPC called Khaland) that was read but not live-triggered. - Restore/reconnect path: attempted, but this lib's own
save_me()deliberately no-ops (if (level < 2 && age < 3600 || guest) return 1;inobj/player.lpc) for any character below internal level 2 and under 1 hour old -- a design choice from the original 1996-2000-era archive (not something this session's fixes touched or should touch per scope discipline), most likely intended to avoid persisting throwaway newbie registrations. The "Saving <name>." message prints unconditionally regardless of whethersave_me()actually persisted anything, which is genuinely misleading but was left alone as an in-scope-adjacent messaging quirk of an intentional gate, not fixed. Practical effect for testing: a normal single-session§10.7playthrough cannot exercise the restore path for this lib (every test character quits well under both thresholds) -- a future pass would need either a wizard-seeded high-level test account or a genuine 1-hour real-time session to verify reconnect/restore. - Minor, unresolved cosmetic observation: a room's
lookoutput occasionally printed a stray bare0on its own line (once directly, once astwo times 0viadaemons/string_stack_d.lpc's item- stacking formatter), implying some item'sshort()resolves to the literal string"0"rather than a real description in at least one placed object. Did not block navigation or any other functionality; not root-caused given time constraints -- flagged for a future pass rather than guessed at.
Files modified this pass
room/room.lpc, obj/monster.lpc, obj/living.lpc,
obj/monster_data.lpc, obj/base_object.lpc,
wizards/siki/base_drink.lpc, cmds/std/_environment.lpc,
secure/master.lpc, plus 267 files (guild masters, city NPCs, and
wizard-realm monsters/NPCs archive-wide) for the chat_str array-type
sweep -- see AGENTS.md §7.192 for the full technical writeup.
WASM measurement (2026-09-03)
meta.json was already playable from the 2026-08-31 deploy-unblock;
the README still said "not attempted." Cold-boot under the shared
~/src/fluffos/build-wasm succeeded with no new mudlib-side compile
fix. work/ is ~378MB and copies into MEMFS without OOM. Verified
with scripts/wasm_client.js (--timeout 300 --idle 0.4): c →
wasmqm → password → select human → continue → continue →
select fighter. Landed in the Fighter guild of Duranghom with the
real room description, five exits, and Anrax the guildmaster visible.
look reprinted that room; score showed "Wasmqm. You are a level 1
Human" / "Primary guild: Fighters" / HP 86(141); quit printed
"Saving Wasmqm." The already-catalogued \S4 compile-sweep tail
(LDMud leftover #' closures, No program in object on
/guilds/obj/skillfun, /cmds/std/_drop, etc.) still prints at
preload and on quit's drop-all path; none of it blocked the login
or play session. Shop/combat/death were not exercised this pass
(same gaps as the native §10.7 writeup). The MEMFS copy does not
write player saves back to the host, so wasmqm left no work-tree
debris.
深度功能测试(§10.7,2026-10-04)— negative range ends
scripts/lpc_fix_negative_ranges.py: 4 line(s) in 4 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)— 主控 author_file()/domain_file()
驱动的 mudlib_stats 包在主控载入时各调用一次 author_file(<主控文件>) 和 domain_file("/")。本库主控没有这两个 apply,所以每次启动都打印 author_file() in the master file does not work, using root_uid as fallback 和 domain_file() in the master file does not work, using bb_ui as fallback 两行,网页版每位访客的终端里都看得到。secure/master.lpc 补上了这两个 apply:作者返回 get_root_uid(),域返回 get_bb_uid(),正是驱动缺少它们时所用的值。域名等于 backbone 域时,驱动让对象沿用创建者的域,与没有这个 apply 时相同(KB 06 §7.222)。本库保留上游代码风格(几乎所有文件都未经 §9 格式化),所以只插入代码,不格式化。用 lpcc config.fluffos /nonexistent.lpc 前后对比,主控载入段只少了这两行。
深度功能测试(§10.7,2026-10-05)— 启动预载全部能编译;武器、护甲、频道、角色创建、旧存档
启动时 55 条编译错误 → 0。整树扫描编译/载入失败的文件 5643 → 约 2850(剩下的多在巫师个人目录)。
1. 启动预载(obj/init_file)里的核心对象大多编译不过。
- obj/weapon、obj/armour:用了保留字 class 作变量名,调用在定义之前的函数(LDMud 不需要原型),armour 的 value/weight 声明成字符串却存整数。591 件武器、784 件护甲继承它们,全部无法载入。
- daemons/channel_d(聊天频道)、room/virtual_map_d、daemons/news_d、daemons/area_entry_d:用 LDMud 多列映射(([k: a; b; c])、m[k, N]),改成数组值(m[k][N]),与当年移植 ability_train、邮件、封禁名单的做法一致;virtual_map_d 的第二列(最后使用时间)拆成平行映射 rooms_time。
- daemons/lotto 的 m - ([ key ]) 改 map_delete;std/user_meter 用 new 作变量名;obj/wizlist、daemons/explist、daemons/questlist、room/house_shop_d、guilds/obj/skillfun 的声明类型与实际存放的值不符(LDMud 不检查),按实际改正。
2. LDMud 在对象载入时调用 reset(0),本驱动只调用 create()(KB 06 §7.228)。 房间、怪物的基类早有 create() { call_other(this_object(), "reset", 0); },但武器、护甲、玩家对象和 113 个不继承任何东西的核心对象没有:新克隆的武器 short() 出错(start() 从未运行),频道守护没有频道,新角色跳过性别提问、也不会加入 inform/mud/newbie 频道。config.fluffos 的 lazy resets 注释说会补上,实际不会(驱动只在 next_reset 到期后才调用)。按 §7.177 的写法补上这些 create()。
3. 整个库被当成 GB18030 转码,而原文是 Latin-1(芬兰语,KB 03)。 一对高字节变成一个汉字,落单的高字节连同后面的 ASCII 字节(空格、句点、右引号)一起被吞掉:ensimmäinen paikka on tyhjä 变成 ensimm鋓nen paikka on tyhj,玩家存档的别名 "ä" 吞掉右引号后整个存档无法读回。scripts/latin1_misdecode_repair.py 按 raw/ 原文逐行修复 814 个文件、约 5600 行(只改除了被吞字节外与原文一致的行,之后改动过的行不动)。
4. 旧存档读不回。 玩家存档的 ability_train、notes(LDMud 宽度为 0 的映射)、邮件、封禁名单、道路、新闻、留言板、错误报告、投票都是 LDMud 映射写法。scripts/ldmud_wide_save_convert.py 改成数组值 / key:1 / ([]);逐值调用 restore_variable() 检查,253,085 个值全部可读(原有 2 处损坏按 raw/ 手工修复)。笔记本命令 _note 同时改掉 note += ([ arg ]) 这种写法。
5. 全库机械修复:调用在定义之前的函数补 varargs 原型(scripts/lpc_forward_undefined.py);声明类型与用法不符的变量改 mixed(scripts/lpc_loosen_types.py,运行时行为不变);room/room.lpc 的 string items; 改 string *items;(2400 个房间因此编译失败);参数不足的调用给被调用的那个定义加 varargs;#'func 闭包改 (: func :)。
6. 小问题:channel_d 的 last 只有一条消息时显示 0 条;area_entry_d 道路键有空号时取到 0;_ignore 的列表 [0] 存等级,声明成字符串数组。
7. 验证(只含被跟踪文件的树):启动 0 错误;新建角色(问性别 → 选人类 → 战士公会)、newbie hello all 显示 Qatestw [newbie]: hello all、channels 列出 11 个频道并显示已加入的三个、走动、ignore、quit(Nothing dropped.、[inform]: Qatestw left the game.),驱动无运行时错误。低于 2 级且不满一小时的新角色不存档,是原设计。
8. 仍未处理:留言板(mudboard/wizboard/warlockboard/vboard/bull_board)、bug_d、据点 stronghold/daemons/sh_main_d 的多列映射代码(存档已转换),world/misc/trace 的 transfer(),以及巫师目录里约 800 个有编译错误的文件。
深度功能测试(§10.7,2026-10-06)— 冒险者大厅、留言板、存档里的换行和芬兰文
1. 非生物对象的 X->move_object(dest) 全部无效。 移植时把 LDMud 的 move_object(A, B) 改成 A->move_object(B),而本驱动的 move_object() 只移动 this_object(),call_other() 也不会回落到同名外部函数。当年只给 obj/living.lpc 补了同名函数,所以留言板、奖牌、怪物身上的物品等一切非生物对象都“移动成功”却留在原地(世界目录里有 452 处这种调用)。现在把这个函数放进全局 include /include/empty.h,每个对象都有,obj/living.lpc 里的那份删掉。
2. 冒险者大厅(城里的休息回复点)进不去。 投票板 daemons/vboard 用 LDMud 多列映射编译失败,大厅 reset() 克隆它时出错,west 从旅店进大厅毫无反应。投票板和三块留言板(mudboard、wizboard、warlockboard)改成数组值(m[k][N]);m -= ([ k ]) 改 map_delete;new 是保留字,留言板的 new 改名 new_thread;wizboard 缺 headers() 原型。LDMud 读不存在的键返回 0,数组写法读已删除的主题会出错,所以列表和回复扫描先判断 m_map[n]/r_map[i] 是否存在。连带能载入的还有战士巫师公会 guilds/warlock/warlock、obj/warlock、room/job_room。
3. daemons/bug_d(玩家 bug 命令)同样改成数组值,#'> 排序改 sort_array(x, 1);obj/bull_board 的 add_action("new"); add_verb("note"); 合成一句,new 改名 new_note。
4. 表情列表是倒序的,第三人称代词固定为第一个使用者的。 早先把 sort_array(x, #'>) 改成了 -1(降序),而 LDMud 在 a > b 时交换,是升序,现改 1。daemons/emote_data 在载入时读 this_player()->query_possessive() 拼进 shakes his head 之类的文字:没有玩家时载入出错,有玩家时所有人都用第一个人的性别。改成占位符 $POSS$,由 emote_d 的 third_person() 在每次表情时替换。
5. log_file() 用 LDMud 的 get_dir(f, 2)(旧版表示取文件大小)判断日志大小,本驱动返回文件名,字符串与整数比较报错,新角色注册时写 NEWPLAYER 日志就出错。改用 file_size()。巫师命令 ls/cls 的 get_dir(path, 0x21/2/4) 改 get_dir(path, -1),并补 dir_filter2 原型(两个命令以前都编译不过)。
6. 存档里的芬兰文仍是乱码。 10-05 的逐行修复跳过了已转换多列映射的存档行(和其他被改过的行)。按 raw/ 原文重建:精确模拟当年的错误解码(GB18030 双字节;无效的一对连同后一个字节一起丢掉),工作树的行与模拟结果完全相同才换成正确的 Latin-1 解码,再做同样的映射转换;被吞掉换行而合并的行按原文分组处理。147 个文件、443 行(玩家存档、信件、留言板、日志)。新增的非 ASCII 字符几乎全是 ä/ö/å。剩 AREA_OHJEET 和 log/SHOUT 两个文本文件有后来改动过的行,没动。
7. LDMud 存档里的换行读回来是字母 n。 LDMud 把字符串中的换行存成 \n 两个字符,本驱动存成一个 CR 字节,读档时把 \X 还原成 X。留言板内容显示为 nn Sehän on…!!!n。新脚本 scripts/ldmud_save_newlines.py 把引号内的 \n 改成 CR、\t 改成制表符:1162 个存档、41,888 处(KB 03)。\r(击杀统计里怪物名末尾带的回车)无法表示,保留。
8. 验证(临时副本,新进程):新角色从战士公会走到大厅,看到留言板、金色奖牌、投票板;board list 跳过已删除的 3 号主题;board read 2/4 显示正确的芬兰文和换行;投票板 list/info Mudcon 读出旧票数;look at plaque 显示排行;shake/snap 正常,emotes 按字母升序;bug here … 写入数组格式的存档。非巫师目录 1683 个文件编译:失败数 297 → 284,无新失败。
9. 据点(stronghold)的代码全部编译不过。 sh_main_d(每个据点的数据守护)和 sh_base_room(据点房间基类)的多列映射改成数组值(room_data、hired_guards、door_data、chest_data、guard_data、雇佣守卫列表 mapp[k][N]);add_door 的原型少了第三个参数 flag;daemons/copier 用保留字 in 作参数名,改 src;base/sh_guard 的 LDMud 单参数 remove_action(verb) 改 remove_action("block_move", verb)。stronghold/ 下 9 个文件现在全部编译通过。读这些映射的地方都遍历 keys(),不会读到不存在的键。据点存档在运行时生成,库里还没有,所以只做了编译检查。
10. obj/monster 和 obj/living 各声明了一个 spell_dam(重复声明警告;两边都没有读它,怪物法术按咒语命令施放),删掉 monster 的那个。
11. 巫师工具 world/misc/trace、trace2 编译通过:LDMud 的 transfer(ob, dest)(成功返回 0)改成 catch(ob->move_object(dest));保留字 in 作函数名,改 trace_in(它注册的动作是 "In",大小写不同,原本就没有绑定);函数上的 nosave 改 protected;man 的 string manuals 改 string *manuals。
12. 未做:本库的 2700 多条编译警告。发帖需要 10 级、投票需要 40 级,这两条写入路径只做了编译检查。
深度功能测试(§10.7,2026-10-06)— 编译警告整理
scripts/lpc_warnings.py questmud --fix(无用局部变量、转义、nosave 函数等机械类):1242 个文件改动。改动过的 1240 个 .lpc 在 HEAD 与工作树分别加载(新进程):925 -> 925 通过,无回退;scripts/lpc_audit_removed_locals.py HEAD:3504 个被删声明,0 条需要看。启动时的警告 71 -> 0。手工修的三处:
daemons/area_entry_d.lpcadd_road():VMAP->set_special_mark(new_[0,0], new_[0,1], new_[0,2])是 LDMud 宽映射的写法,而new_的键是m_sizeof(roads)不是 0,取到的都是 0;改为直接传x, y, mark。secure/master.lpcprepare_destruct():errors、i只在#ifdef COMPAT_FLAG里用,声明移进该分支。obj/player.lpc:LDMud 的(object) ("/cmds/std/_say");(装载对象)在这里只是一个无作用的表达式,下一行的call_other本来就会装载,删去。
验证:新进程启动,以访客(g)登录,look、score、quit(存档)正常。剩下 2867 条诊断几乎都是本来就不能编译的文件里的错误(LDMud 专用语法与缺失的头文件),留待逐个阅读。
Deep test (§10.7, 2026-10-07) — messages cut by a stray ;
Messages cut by a stray ;: joined in wizards/terror/goblin/roomit/g1.lpc and wizards/daoloth/mansion/path2.lpc (the orphan adds a sentence); deleted in wizards/torspo/areat/ogre/room4.lpc, terror/goblin/roomit/g5.lpc, rimber/cave/room4.lpc (a fragment or copy of the line above). daemons/ability_d.lpc is an LDMud ([ k: v1; v2 ]) table, not a cut message.
Changed files loaded at HEAD and in the tree in fresh processes: no regression.
- 2026-10-10:编译警告第二轮(按驱动报告逐条修):按位 & 改逻辑运算 1 处,返回类型/原型 12 处,NPC+F_UNIQUE 的 init() 0 个,带初始化的未用局部变量 1 个,只在 #if 分支用的声明移进分支 5 个文件;改动 19 个文件,HEAD 与工作树各载入一次无回退(19 objects; HEAD PASS 14, tree PASS 15; regress 0; newly loading 1),这些文件的警告 28 -> 9。
深度功能测试(§10.7,2026-10-11):“无副作用表达式”警告复查
逐条复查了能编译的文件里 156 条 Expression has no side effects / Value of conditional expression is unused 警告,156 条全部修掉。
LDMud 宽映射读法漏改(116 条,最主要的一类)。 移植时把宽映射改成了“值是数组”的 m[k][N],但不少读点还是 LDMud 写法 m[k, N]。在 FluffOS 里这是逗号表达式,等于 m[N],静悄悄读错了键(通常读出 0)。按写入方的实际布局改正:
cmds/std/_lite.lpc(及wizards/kaappi/cmds、wizards/kaappi/fix、wizards/nalle三份副本):玩家的colors映射值是颜色名字符串(LDMud 宽度 1),l[k,0]改l[k]。原来give_color()总返回空,lite设的 tell/emote/频道自定义颜色从来不生效,lite列表也显示 0。daemons/multiguild_d.lpc:multiguilds[k,0]、guilds[x,0/1/2]改[k][N](guild_init 里两张表的值都是数组)。原来get_starter_guild()对 battlemage/paladin/abjurer 一律返回 0,复合公会的背景公会比较、join_multiguild()的公会文件全部读错。world/special/slct_gui.lpcinfo:query_guilds()[str,2/3/4]改[str][N];只在复合公会表里的名字按 §7.234 加了guilds[str] &&守卫(LDMud 缺键读 0,原来走“No info”分支);复合公会表只有两列,[str,4]改成先看长度。cmds/std/_fixme.lpc:guilds[tmp,0/1]改[tmp][N],缺键时报错返回。daemons/bak.emote_d.lpc、daemons/backup.emote_d.lpc、wizards/nalle/emote_d.lpc(在用的daemons/emote_d.lpc之前已改好):e_map[verb,N]共 123 处改e_map[verb][N],和在用版本一致。wizards/{cendor/leader,moonstar,moonstar/test}/leader_d.lpc:leaders[k,0]改leaders[k](值是名字字符串,和在用的daemons/leader_d.lpc一致)。wizards/astarel/workroom2.lpc读bunny_d的五列计分;wizards/astarel/events/b_hunt/bunny_d.lpc本身还是 LDMud 的([ k: 0;0;0;0;0 ])语法,一并改成数组值并给未报名的玩家补默认行(它仍因别的语法问题不能载入,未处理)。
其他形状(40 条):
(object) (X);空转型语句 6 条(_look、_wwb、三份channeltest_d、guild.lpc),同样的行另有 8 份副本(_tell、_look2、公会备份等)一并删掉;下一行都会 call_other 同一对象,行为不变。- 空的
if (guild_level >= N) { }6 条(artists/base_guild2、channel/appr_cleric及 jenny 副本、mage/adv_mages、mage/apprmage)和siki/weather_d的空 if:删除,行为不变。 belar/dwarf/rooms/*8 个房间的if(!monster);(共 9 条):空 if,删除,行为不变。- 被
;截断的房间描述 6 条:guilds/fighter/warrior.lpc、terror/goblin/roomit/g8.lpc后半句承接上句,接回;torspo/areat/ogre/room15.lpc三行是旧措辞残留(以 “heart of the fortress” 半句开头),删掉;walla/void{,2,3}.lpc少了long_desc =,补上。 default: "too many";3 条(及_peer等 4 份副本):改enumb = "too many";,超过 20 个出口时不再拼出 0。if(move_ep > effect);多余分号(guilds/spells/abjuration/_kyo_zzt_zas.lpc,及moonstar/abjubisnes/ep.lpc):去掉分号;同一文件里ob->query_ep() + (move_ep);丢了外层调用,目标从来没加上体力,改ob->add_ep(move_ep);(living.lpc 的 add_ep 自带上限)。法术提示 “You force some of your endurance” 现在真的生效。==写成赋值的 4 条(bulut/event/pkobj2、proge/bazaar1、rag/elf/monsters/angdalgender == "it"改为默认代词pron = "it",原来对中性对象打印 “throws 0 down”)。return str == "event_sign","sign";改||(原来只认 "sign");light_source的{ normal_light; }改赋值;siki/void_pocket的short()补return。
复查中顺带发现的两处编译失败(警告扫描看不到):
cmds/std/_tell.lpc(玩家的tell命令):check_ignored()把ignore_list声明成string,capitalize(ignore_list[i])编译报错,整个 tell 命令自移植以来载入失败。改成mixed *(第 0 格是等级数字),循环加sizeof上限。cmds/std/_peer.lpc(及 garath、astarel、siki 三份副本):局部变量叫buffer(FluffOS 的类型名),语法错误。改名buf。
验证:改动 64 个对象,HEAD 与工作树各在新进程里载入一次:64 objects; HEAD PASS 50, tree PASS 55; regress 0; newly loading 5(_tell、_peer 及三份 peer 副本)。仍载入失败的 9 个都是 HEAD 就失败的旧副本/草稿(_look2、公会备份、bunny_d、kaappi/fix/tell、nalle/ng、rag/guild、yodingui),以及 g8.lpc(它克隆的 ghunter.lpc 运行时出错,与本次无关)。临时探针对象对比 HEAD 与工作树:get_starter_guild("battlemage") 0 -> "mage",give_color(tell) 0 -> 红色转义码,bak.emote_d->emote("ack") 输出 “You 0.” -> “You ack.”。proge/bazaar.lpc 还剩一条 Redeclaration of global variable 'items',未处理。
深度功能测试(§10.7,2026-10-12):编译警告收尾
能编译的文件里的编译警告 167 -> 0(scripts/toolkit_fix.sh 去掉 5 个多余的 inherit(wizards/manta/eq/damulet2.lpc 两次 inherit "obj/armour" 一项就占 54 条)、4 个未用局部变量,其余逐条手改,共约 90 个文件;有编译错误的文件不在范围内)。
真实的编程错误:
cmds/std/_cal.lpc(玩家的cal命令,及wizards/cendor/_cal.lpc副本):两处注释结尾*/被拆成*换行/,从 88 行到 118 行全成了注释,fixtab()(闰年修正)和weekday()(每月第一天是星期几)两个函数都不存在;this_object()->fixtab(y)/->weekday(m, y)对不存在的函数返回 0,日历从不处理闰年、每个月都从星期日开始。接回*/。wizards/siki/ent/echo_room.lpc:同时继承room/room和echo,后继承的 echo 的init()取代了房间的init()(加出口命令的那个),房间里输入east没有反应。叶子里定义init()依次调用两者(KB 06 §7.214)。- 一个文件里同一全局变量声明两次(驱动把第二个当成另一个变量):
castle/obj/castle_code*.lpc与wizards/astarel/castle/castle_code.lpc的castledir、wizards/grathlek五个房间的monster、wizards/bulut/catacombs/guardian.lpc的ob、wizards/duncan/sharkisland/rooms/ferry/deck2.lpc的rod_found:只留一份。 room/shop_d.lpc、guilds/library/book_d.lpc与room/room.lpc各有一份相同的create()(调用reset(0)),30 个商店/图书馆房间因此出现菱形继承;用到这两个文件的房间都同时继承room/room,两个 mixin 的create()删掉,行为不变。
行为不变的整理: 叶子变量与基类同名而含义不同时改叶子一侧的名字(walker03 的 dead→mom_dead——基类的 dead 表示“已死、不能被攻击”;beldor 三个怪物的 string *short_desc/long_desc→short_descs/long_descs;mummy 的 limbs、lemming 的 name(颜色)、hissi 电梯的 direction、thief/tutor 的 object money、wizards/siki/bow.lpc 自己的 short_desc/long_desc/weight);没用到的 direction(三个 ferry、potup)、bow 的 slot/wielded_by、bazaar 的全局 items 删掉。wizards/daoloth/mansion 等 13 个用 EXTRA_RESET 调 extra_reset() 的房间加原型(调用在宏生成的 reset() 里,早于定义)。necro 的 scythes.lpc/shields.lpc 注释结尾 ~/ 改 */(只吞了注释)。
验证:改动的文件和继承 shop_d/book_d/echo/bow 的对象共 90 个,HEAD 与工作树各在新进程载入(90 objects; HEAD PASS 72, tree PASS 73; regress 0; newly loading 1),72 个对象载入无回退;这些对象的警告 159 -> 0;全库重扫后无编译错误的文件里 0 条警告(10475 个文件能编译)。
- 2026-10-12:
wizards/yodin/vankila.lpc(监狱房间的 “HAVE A NICE DAY” 笑脸)在 f5a5b6ea210 的转义整理里丢了图形里的反斜杠:--fix当时把\\|的后一半当作未知转义\|删掉,\-;-/等也被删。按原始文本把这五行画面里的每个反斜杠写成\\,现在照原样显示(脚本已修,KB 04 转义一行)。 - 2026-10-14: 6 save files (player saves
players/bix.o,james.o,veron.o, maildata/mail/walla.o, posts inroom/post_dir/) still held Latin-1 bytes (Finnish text: ä, ö), sorestore_object()stopped withInvalid utf8 stringand the player or mailbox loaded empty.scripts/lpc_save_encoding.pytranscoded them from Latin-1 and kept a file only when the result parses as a save (KB 03 §4).
- 2026-10-14: FluffOS 970d9f09 warns
Float value truncated to int(#1426). 1 sites in 1 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).
- 2026-10-14: 7 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).
- 2026-10-14: 3 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:cmd_diff(),stop_skill(),use_ok(). HEAD vs tree in fresh drivers: 10 objects, 5 newly loading, 0 regressions (scripts/lpc_fix_void_return.py, KB 04 §6.3).
- 2026-10-14: 1 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); 1 file(s) declared a functionint(orvoid) that returns a string or object (int query_seal() { return "…"; }), which the old driver passed to the caller; it now has the type it returns (scripts/lpc_fix_void_return.py) (KB 04 §6.1, §6.3). Warnings the newly loading files showed are fixed (unused locals removed, bare returns in value functionsreturn 0;). HEAD vs tree in fresh drivers: 10 objects, 5 newly loading, 0 regressions.
Object paths with two leading slashes (2026-10-19, KB 06 §7.276)
1 file(s), 2 site(s) wrote an object path as //… (like carry_object("//clone/cloth/hic_cloth")): /wizards/yodin/areas/weird_land/vl1.lpc. The driver these games ran on did not mind (the path was normalised first); this one hands the string to the master's valid_read() as written, securityd looks up //clone, the NPC's euid has no entry and the answer is Read access denied.: the NPC does not load. One slash now.
Standard repair round (2026-10-19, scripts/toolkit_fix.sh)
Rescanned with the current driver; mechanical fixes: 1 functions made varargs (callers pass fewer arguments). Every changed object was loaded at HEAD and in the tree: no regression.
Punctuation the conversion lost, put back (2026-10-19, KB 03 4.6)
2 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: /wizards/torspo/stuff/flagon.lpc, /wizards/walla/testi/huone.lpc. Loaded at HEAD and in the tree: all 2 were failing before, none regressed (scripts/lpc_restore_from_twin.py, lpc_fix_eaten_quote.py --comma, lpc_fix_eof_braces.py).
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. 39 file(s) fixed: /cmds/std/_cast.lpc, /guilds/abjurer/prot_resolve.lpc, /guilds/obj/bak.skillfun.lpc, /guilds/obj/bak2.skillfun.lpc, /guilds/obj/preMoonstar.skillfun.lpc, /guilds/obj/skillfunbeforeMtweaking.lpc, /guilds/obj/spell.lpc, /wizards/astarel/castle/castle_code_with_doors.lpc ... (39 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: 11 of them failed before and load now, none regressed.
string variables and arrays that hold numbers (2026-10-20, KB 04 6.22)
daemons/auctioneer.lpc (and the wizards/siki/ copy): num, price and money (stored by sscanf("%d")) and the arrays last_bid, auction_time, how_long (auction_time[i] - time()) are declared mixed: the auction daemon compiles and loads (the siki copy then stops on save_object() permission, a wizard directory). room/map_daemon.lpc, map_daemon2002.lpc and the wizard copies: light_level = this_player()->test_dark() is a number, declared mixed; the daemon still stops at check_rooms(), which only the virtual_map.lpc files of the wizard areas define (it is their base) (scripts/lpc_fix_cast_cmp.py and by hand).
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).
who did not compile: invis_color() is called before its definition (2026-10-20, KB 04 6.5)
cmds/std/_who.lpc:249:99: error: Undefined function invis_color: the command calls invis_color() (the yellow (I2) of an invisible wizard) 49 lines above its definition, which LDMud allowed; this driver wants a declaration first, so the who command never loaded (the login survey printed it as a play-time console error). The file already had prototypes for bold_color and give_bcolor with the comment Called before their definitions (LDMud allowed that); invis_color was the one it missed (scripts/lpc_forward_undefined.py questmud --compile /cmds/std/_who.lpc --apply). HEAD 0 PASS, tree 1 PASS.
get、eq、ntell、pwho 编译不过:局部变量类型(2026-10-20,KB 04 §6.21)
cmds/std/_get.lpc:object item_o 装 EQ_FILTER 返回的对象数组,再交给 get_all(object *, ...);string *livings 里放的是 all_inventory() 的对象。_eq.lpc:string slots 其实是数组(valid_slot(string slots, ...) 的参数也是)。_ntell.lpc:两处 string ignore_list 装的是 query_ignored() 的名字数组。_pwho.lpc:object *members 先接 query_members() 的映射、再接 keys() 的结果,改 mixed。各按真实内容改类型。加载检查:四个都是 HEAD FAIL → tree PASS。没有动的:_calc(浮点和字符串混着传给 potenssi())、_look2(get_env_color 哪里都没有)、_sshout(缺花括号、变量没声明、tail(),整个文件是残片)、daemons/warlock_d.lpc(is_demon、invite 从没声明)。
copy_file(string in, string out):参数名 in 是保留字(2026-10-20,KB 04 §6.26)
巫师的 cp、rn、mv 系列文件(cmds/wiz/_cp.lpc、_rn.lpc,castle/kk.lpc、castle/sh/daemons/copier.lpc,wizards/*/ 下的几份)把 string in 当参数名:syntax error, unexpected L_IN。scripts/lpc_fix_in_ident.py 改名为 in_var。加载检查 11/11,HEAD FAIL → tree PASS。
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 2 registrations in 2 files (scripts/lpc_fix_dead_add_action.py), the verbs are left to their other handlers: /castle/obj/shopkeeper.lpc (countries), /obj/player.lpc (in_room). Most frequent: countries x1, in_room x1. Left alone (read): /castle/obj/masterkey.lpc owner (the object does not compile; the registration waits until it loads), /guilds/spell_obj/auctioneer.lpc bid (the object does not compile; the registration waits until it loads), /guilds/spell_obj/auctioneer.lpc claim (the object does not compile; the registration waits until it loads), /guilds/spell_obj/auctioneer.lpc collect (the object does not compile; the registration waits until it loads), /world/voting/voting_d.lpc list (the object does not compile; the registration waits until it loads).
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. 4 statement(s) in 4 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 4 changed files, HEAD vs tree: 3 load at HEAD, 3 load now (0 newly loading, 0 regress).
Object paths without a leading slash: guarded chains for a missing item (2026-10-22, KB 06 §7.73 addendum)
A path written without the leading slash (carry_object("d/city/obj/jiasha")->wear();) is root-relative in this driver (new("d/city/obj/changjian") loads /d/city/obj/changjian), so it behaves like the absolute spelling: when the file is not in the archive new() answers 0 and ->wear() on it raises Bad argument 1 to EFUN call_other(), and the whole object does not load. The earlier guard pass skipped these paths as "relative to the cwd at run time: cannot tell". 1 statement(s) in 1 file(s) (world/mage/mag/ld_preventer) are now guarded blocks (scripts/lpc_guard_carry_object.py). Load check of the 1 changed files, HEAD vs tree: 1 load at HEAD, 1 load now (0 newly loading, 0 regress).
glued inherit entrance + ball(2026-10-27,KB 04 §6.25 Also)
wizards/walla/area/extension/entrance 的 inherit "room/room"、wizards/yodin/ball 的 inherit "obj/armour" /*…*/ 均缺分号。entrance 的房间描述字符串原先是无副作用表达式,改为 long_desc =。证明:HEAD 失败;改后两件均加载。
深度功能测试(2026-10-27,§6.25 question_d 截断)
daemons/question_d 的 ask_questions 在 if(question / tell_object(ob, question 处截断。按 input_data 的 mapping 用法补完(m_sizeof + random 出题)。证明:HEAD 失败;改后加载。
深度功能测试(2026-10-28,sort_array 方向闭包)
剩余 LDMud sort_array 的 #'> / #'< 改为 1 / -1(KB 06 7.234)。