info.html · 介绍 · play.html · 游玩 · llms.txt · 下载与本地运行 · ZIP · GitHub
A long-running, still-live English-language fantasy LPMud built on a heavily customized Nightmare/TMI-lineage mudlib base. Source cloned from `git clone https://github.com/MudRen/SunderingShadows` (a fork of `SuS-Tsera/SunderingShadows`). Slug `sunshadow`, number 930, port 40232.
English
A long-running, still-live English-language fantasy LPMud built on a heavily customized Nightmare/TMI-lineage mudlib base -- 43,147 files (~47MB) that the live game auto-commits every 30 minutes. New characters emerge through a full command-driven, 15-stage creation wizard (class, gender, race, subrace, template, age, point-buy stat allocation, body type, hair/eye color, bonus languages, alignment, deity, class-special) before a scripted basic-commands tutorial drops them into the world proper. The std/races/ library lists 43 playable strains: the familiar human, elf, dwarf, gnome, halfling, half-elf, half-orc, and drow sit alongside centaurs, minotaurs, satyrs, dragonkin, wemics, kitsune, saurians, yuan-ti, firbolgs, gnolls, bugbears, and troll-kin, with a further tier -- devils, imps, devas, dragonkin, soulforged, strix -- locked behind account-level approval rather than open to every newcomer. Base classes offered at creation (fighter, thief, mage, cleric, druid, ranger, paladin, bard, barbarian, sorcerer, monk, warlock, psion, inquisitor, magus, cavalier, and more) are only the entry point: dozens of prestige classes are earned in play, from archmage, assassin, eldritch knight, and shadowdancer to hexblade, warmind, hierophant, and arcane trickster. The setting runs its own homegrown ten-deity pantheon -- Jarmila, Callamir, Kreysneothosies, Ryorik, Nilith, Lord Shadow, The Faceless One, Kismet, Lysara, and Nimnavanon, each with a distinct portfolio and alignment restrictions that gate which deities and class-special options a given character can take. The persistent world spans dozens of named zones built up over decades of individual-wizard development: the newbie cities of Tabor and Muuldaan, the city of Shadow with its own coliseum sparring grounds, an Underdark drow-and-derro tunnel network, a Ravenloft-flavored gothic-horror domain, a Mesoamerican-styled Atoyatl/Tecqumin jungle region with its own tabaxi denizens, the Dagger forest and marsh territories, plus dedicated guild halls, a magic college with its own demiplane, and player-run housing and shops.
README
The world
A real, substantial archive: 43,147 files (~47MB) that the live game
auto-commits every 30 minutes, per the repo's own README. The
std/races/ library lists 43 playable races: humans, elves, dwarves,
gnomes, halflings, half-elves, half-orcs, and drow sit alongside
centaurs, minotaurs, satyrs, dragonkin, wemics, kitsune, saurians,
yuan-ti, firbolgs, gnolls, bugbears, and troll-kin, with a further tier
(devils, imps, devas, soulforged, strix) locked behind account-level
approval rather than open to every newcomer. Base classes offered at
creation (fighter, thief, mage, cleric, druid, ranger, paladin, bard,
barbarian, sorcerer, monk, warlock, psion, inquisitor, magus, cavalier,
and more) are only the entry point -- dozens of prestige classes are
earned in play, from archmage, assassin, eldritch knight, and
shadowdancer to hexblade, warmind, hierophant, and arcane trickster. The
setting runs its own homegrown ten-deity pantheon -- Jarmila, Callamir,
Kreysneothosies, Ryorik, Nilith, Lord Shadow, The Faceless One, Kismet,
Lysara, and Nimnavanon -- each with a distinct portfolio and alignment
restrictions that gate which deities and class-special options a given
character can take. The persistent world spans dozens of named zones
built up over decades of individual-wizard development: the newbie
cities of Tabor and Muuldaan, the city of Shadow with its own coliseum
sparring grounds, an Underdark drow-and-derro tunnel network, a
Ravenloft-flavored gothic-horror domain, a Mesoamerican-styled
Atoyatl/Tecqumin jungle region with its own tabaxi denizens, the Dagger
forest and marsh territories, plus dedicated guild halls, a magic
college with its own demiplane, and player-run housing and shops.
Registration flow
name -> "Do you really wish NAME to be your name? (y/n)" -> password ->
confirm password -> ANSI color test (y/n) -> screen reader (y/n) -> email ->
lands in a void room; press button spawns the character-creation object
and starts the 15-stage wizard (select OPTION / select random per
stage -- except the stats stage, which instead uses
recommended/add N to STAT/reroll/done; sheet to review, reset
STAGE to redo a stage, finalize once all stages are answered) -> a
short scripted tutorial (skip to bypass) -> the real game world.
Status
Boots clean on both the native FluffOS driver and under WASM. Verified
end-to-end with a raw socket client (native) and wasm_client.js
(WASM): full registration, the complete 15-stage creation wizard,
finalize, tutorial skip, and look/score/who/say/quit all
producing correct output post-login on both, with no mudlib-side changes
needed for the WASM pass. A character genuinely persists to disk only
once creation completes (query_level() >= 1), matching this codebase's
own intended save-gating, not a bug.
Known gaps (documented, not invented): /cmds/soul/atmos.lpc (the
emote/soul-verb fallback daemon) is referenced but entirely missing from
the archive, so emote-only input that doesn't match any real command will
fail. A handful of content files reached only via non-core background
daemons (a mage spellbook's magic-school helper, two tower/marsh-zone NPCs)
still fail to compile and were left for a future pass -- none of them sit
on the registration or basic-play path. adm/daemon/ipc.lpc (an
unreferenced admin chat-relay daemon using raw sockets) fails to compile
under WASM, where the sockets package doesn't exist -- harmless, since
nothing else in the archive calls it. See NOTES.md for the full
porting and deep-functional-test log.
Local run
cd libs/sunshadow
~/src/fluffos/build-debug/src/driver config.fluffosGame port: 40232.
NOTES · 移植与修复记录
Sundering Shadows -- porting notes
Source: git clone https://github.com/MudRen/SunderingShadows (a fork of
SuS-Tsera/SunderingShadows, commit 0fb3102f9058f7d756be6c81f88a6bee04a5c8ff,
cloned 2026-08-26). The repo's own README says it best: "This repository is
synced with the mud every 30 minutes, so any changes made in game will be
reflected here." Real, currently-live English-language content: 43,147 files,
~47MB, a Nightmare/TMI-lineage mudlib heavily customized over what looks like
decades of active development. Slug sunshadow, number 930, port 40232.
1. Conversion
Pure ASCII archive -- no encoding-normalization work needed. convert_lib.sh
renamed every .c to .lpc (roughly 41,000 files) and ran the usual
static->nosave/protected sweep with no collisions found. All headers
resolve through /adm/include/ (a handful of files also use local
#include "dieties.h"-style relative includes, which resolved fine as-is).
2. The pervasive same-file forward-reference gap
This driver compiles each file in a single top-to-bottom pass, unlike the
two-pass compiler the original archive was clearly written against: calling
a function defined *later* in the same file, with no forward declaration,
is a hard compile error here. A corpus-wide scan (scan_forward_calls.py)
found 3,212 files with at least one such call (8,453 sites total); a
de-duplication pass found the archive's own authors had already added the
*correct* forward declaration in ~65% of cases (5,465 of ~8,438 inserted
lines were exact duplicates of an existing declaration elsewhere in the same
file) -- removed those duplicates, leaving ~2,973 genuine fixes applied
mechanically.
3. map_delete() dialect: void/in-place here, not return-the-mapping
Classic MudOS/LDMud-vintage code assumes map_delete() returns the
resulting mapping; on this driver it's void and mutates in place. Found
live via daemon/multi_d.lpc:169's whoWhen = map_delete(whoWhen,who);
breaking that whole file's compile -- which mattered immediately, because
login.lpc's get_name() calls MULTI_D->is_allowed(...) uncaught, so a
broken multi_d.lpc silently aborted every new registration mid-flow with
no visible symptom. Swept the whole corpus: 245 files renamed off the
m_delete() MudOS alias, plus 9 files (20 sites) assigning map_delete()'s
result to a variable.
4. The set_short()/set_long() closure gap -- blocked EVERY new registration
The single most impactful bug in this port, and the reason no new character
could ever get past the very first room. std/Object.lpc's
set_short(string str)/set_long(string str) only accepted plain strings,
but 291 files (set_long) and 20 files (set_short) call them with
old-style closures for dynamic descriptions, e.g.
set_long((:TO, "my_long":));. That exact call is in
d/shadowgate/setter.lpc -- ROOM_SETTER, the literal first room every
brand-new character is moved into by std/user/setup(). Since the room
object simply failed to compile, move() into it silently failed with zero
driver-level crash, leaving new connections commandless with no error
anywhere obvious.
Fixed by widening both setters to mixed and teaching query_short()/
query_long() to evaluate a functionp() case:
if (tmd = query_property("function_long")) {
if (!tmd = call_other(TO, tmd)) {
tmd = "A function messed up in query_long() in Object.lpc";
}
}else if (functionp(ob_data["long"])) {
tmd = (string)(*ob_data["long"])();
}else {
tmd = ob_data["long"];
}This exact pattern already existed elsewhere in the same file (the
"cap_name" derivation), confirming it's the established calling convention
for this codebase, not a new invention. Also had to widen four dependent
override files that had their own stale string-typed set_short/
set_long wrappers: std/room.lpc, std/tobj.lpc, cmds/mortal/room.lpc,
and three duplicate Object.lpc copies under dead/failsafe test paths.
5. The command-dispatch filename-slice bug -- blocked EVERY ordinary command
The second silent, total blocker, and the hardest to find: after fixing
\S4, new characters could finish the full 15-stage creation wizard and land
in the world, but look, score, say, inventory -- every single
ordinary player command -- failed with the driver's generic default fail
message ("Why?") and *zero* log signature. Verb-specific add_action
calls (character-creation's select/press/finalize, room-specific
quit/bypass) kept working throughout, which is what made this so
confusing: the wildcard catch-all itself (std/living.lpc's
add_action("cmd_hook", "", 1)) was clearly registering and running (added
debug_message()/tell_object() instrumentation to confirm), but every
lookup through it came back empty.
Root cause: daemon/command.lpc (a Nightmare-style command-dispatch
daemon) scans each command directory for _*.lpc files and derives the
verb name with:
choses[j] = choses[j][1..strlen(choses[j])-3];This strips a leading _ (1 char) and a trailing extension of exactly 2
characters -- correct for the archive's *original* _look.c naming, where
strlen("_look.c")-3 lands exactly on "look". This port's mechanical
.c->.lpc rename lengthened every extension by two characters, so the
same arithmetic on _look.lpc (a 4-character extension) sliced off
"look.l" instead of "look" -- silently corrupting the command-name key
for literally every command in every scanned directory, while the
*directory* itself still got recorded as "already processed" so it was
never retried. Fixed with an extension-agnostic replacement:
choses[j] = explode(choses[j][1..], ".")[0];This is the same *class* of bug already seen in another lib onboarded this
session (a fixed-width filename-slice tied to the .c/.lpc rename), but
a distinct instance/formula, found independently here.
6. Missing runtime directories silently aborting callers
Same write_file()/save_object()-into-a-missing-directory pattern
documented elsewhere in this project's AGENTS.md: a failing write throws an
error that, if the caller isn't wrapped in catch(), silently aborts the
*entire* calling function with no driver-level crash. Found via the
mudlib's own separate error log (work/log/debug.log/debug.heavy,
written by adm/daemon/error_handler_d.lpc, distinct from the driver's
native log). Created:
log/adm/jail,log/errors,log/living,log/pkill,log/player,log/reports,log/tshlog/{avatar,player,wizard},log/watch,d/antioch/antioch2/logs-- abortedenter_email()/registration logging.daemon/save/saved_descriptions/<a-z>-- aborteddaemon/description_d.lpc's per-character description save duringfinalize().d/save/quests-- aborted the high/mid/low random-quest daemons' periodic saves (background, non-blocking, but a real recurring error).
daemon/treesave_d.lpc, daemon/letter.lpc, daemon/localpost.lpc, and
daemon/yuck_d.lpc all self-heal their own save directories with
mkdir()/dir_exists() checks already, so those were left alone.
7. Missing ansi_str() simul_efun
Referenced by cmds/mortal/_look.lpc, _glance.lpc, _spy.lpc, and one
NPC file, but implemented nowhere in the entire 43,000-file archive -- a
genuine gap, not something the port broke. This driver already
auto-translates this mudlib's %^COLOR%^ markup to real ANSI escapes at
the write()/tell_object()/more() layer (confirmed: plenty of other
output renders color correctly with no explicit ansi_str() call), so
added a minimal passthrough simul_efun (adm/simul_efun/ansi_str.lpc)
rather than inventing real behavior for a function whose original
implementation is simply lost.
8. Smaller one-off compile bugs
All the usual "stray or missing type" classes seen elsewhere in this session's work, collected here rather than given their own section:
daemon/filters_d.lpc:int *is_living(object ob)declared to return an array but returns a plain0/1-- stray*, matching this archive's recurringTYPE *adeclaration-binding typo class. This one mattered: it broke the whole file, which is called every heartbeat fromstd/user.lpc's passive-perception check, spamming the log continuously.daemon/chat.lpc:ipc_chat()'s call toformat_chat()was missing its finalemoteargument (driver requires exact arg counts for non-varargs params); added a0default.daemon/guilds_d.lpc:query_guild()returned({ })(array) from amapping-returning function;delete_player()(void) had a strayreturn 1;.d/shadowgate/void.lpc,d/common/obj/daily/compiler.lpc,d/atoyatl/tecqumin/tabaxi/obj/tabaxi_battler.lpc(5 sites): calledget_eval_cost(); this driver's efun iseval_cost()(noget_prefix).daemon/bonus_d.lpc:varargs effective_ac(object who)andvarargs ac_bonus(...)omitted their return type entirely -- this driver treats a barevarargs FUNC(...)as an unparseable/unknownreturn type rather than defaulting tomixed. Swept the same pattern corpus-wide:receive_objects()(5 files),init_treasure_for_area()(1 file, 2 sites),cmd_gossip()(1 file) all had the same gap.adm/daemon/advance_d.lpc,adm/daemon/titles.lpc,cmds/creator/_scan.lpc(5 sites total):return notify_fail(...)used insidestring/object-returning functions. This driver declaresnotify_fail()void(not the classic MudOS int-returning idiom), so the pattern only compiles insideint/voidfunctions; fixed the 5 sites that live in other return types.d/magic/symbols/holy_symbol_inherit.lpc(cleric/paladin holy-symbol logic): avoidfunction with a strayreturn 1;; astring tmpreused for array-building (tmp = ({})) in one function and assigned anint(in_mind[x]) in another -- widened tomixed; and a bare(int)CLERIC_CALCS->query_max_spell_array(...)cast truncating an array-returning call that's immediately indexed afterward -- fixed to(int*).daemon/language_d.lpc:string word = ({});infakeWord()-- the empty-array-literal-defaults-wrong-type class; fixed to"".cmds/mortal/_save.lpc:if (!TP->query(...) != file)-- a stray!making the comparison always-true (int vs string, flagged as a hard compile error, not just a logic bug); removed the errant!.cmds/mortal/_inventory.lpc:sort_by_name()'s swap temp was declaredobject tmpwhile sorting astring*array; fixed tostring tmp.d/common/obj/potion/advanced_heal.lpc:void new_do_effect(...)had two strayreturn 1;sites.d/dagger/marsh/tower/mon/knight.lpc:void set_paralyzed(...)had a strayreturn 1;.d/magic/spellbook.lpc(mage class starting item): a(string)cast on an array-returning call (should be(string*)), astring tmpassigned({})and otherwise unused (removed), and threevoidfunctions (flip(),set_owner()) with strayreturn 1;sites.
9. Verification
Booted the real FluffOS driver (~/src/fluffos/build-debug/src/driver)
against config.fluffos and drove it end-to-end with a raw Python socket
client (no mudclient.py/tmux_mud.sh shortcuts): fresh registration
(name/password/ANSI test/screen-reader/email) through the full 15-stage
ROLL_CHAIN creation wizard (class, gender, race, subrace, template, age,
stat rolling via recommended+done, body type, hair/eye color, language,
alignment, deity), finalize, the scripted basic-commands tutorial
(skip), and then look, score, who, say hello world, and a clean
quit -- all producing correct, fully-rendered output. Confirmed the
character actually persists (adm/save/users/<letter>/<name>.o written
only once query_level() >= 1, i.e. only after creation genuinely
completes -- intentional, not a bug) and that daemon/save/saved_descriptions
saves cleanly post-\S6 fix. debug.log is clean of errors on this full
path after all fixes above; a background random-quest daemon and the
bonus_d.lpc/filters_d.lpc heartbeat spam are the only things that
appeared during testing, both fixed.
10. Known gaps -- documented, not invented or exhaustively chased
/cmds/soul/atmos.lpc(SOUL_D, the emote/soul-verb fallback used when no real command matches) is referenced bystd/living.lpc's command dispatcher but does not exist anywhere in the 43,147-file archive. Not invented; emote-only input (not matching any real command or the chat daemon) will fail until a real implementation is written or restored.- A handful of specific content files encountered via non-core background daemons still fail to compile and were left for a future pass rather than chased indefinitely, since none of them block registration or ordinary play:
daemon/magic_d.lpc(several return-type/cast mismatches, pulled in transitively by the mage spellbook), and two NPC files ind/dagger/marsh/tower/(bishimon.lpc,avenger.lpc) andd/attaya/mon/reached viaknight.lpc's inherit chain. - This is a live, actively-developed 43k-file mudlib; a full corpus-wide
lpcc_check.sh --batchsweep was not run to completion (an earlier partial run reached ~19,800/43,147 files at 14,041 PASS / 5,753 FAIL before being proactively stopped per this session's memory-safety protocol). The fixes above target every bug that surfaced on the actual registration/character-creation/basic-command path, not a full-corpus clean compile.
深度功能测试 / Deep functional test (round two, AGENTS.md §10.7)
First genuine hands-on *playthrough* pass on the real driver (the earlier
"Verification" section above only exercised registration through the
15-stage creation wizard, tutorial-skip, and look/score/who/say/
quit -- never a continuous session with real movement, combat, skill
acquisition, or economy). One continuous session on
~/src/fluffos/build-debug/src/driver, a raw Python socket client
throughout (a persistent background process reading commands from a
named pipe, since a single Python invocation per command would have lost
in-progress creation-wizard/input_to() state across reconnects), plus a
second connection for a fluffos/Mud@2026 admin account (registered
fresh through the normal flow -- this lib had never been seeded before;
granted via directly editing the freshly-registered save file's
position field to "Admin", matching the exact string std/user.lpc's
enable_wizard() gate checks, since the in-game set_position() path to
grant a non-mortal position itself requires calling from an existing
wizard's own _xmote/_avmaker command -- a chicken-and-egg the data-edit
sidesteps, per AGENTS.md §1.5's own guidance to prefer editing the save
data over the code).
Newbie path: help newbie and the community-authored
doc/help/user/newbie_tutorial (a step-by-step guide to a wood-elf
fighter, explicitly recommending sweepingblow/impale as the first
feats to buy) gave a clear, well-signposted test path: register, land in
the Offestry newbie tutorial, skip it, reach the town square, then
travel west/northwest to "the keep" to fight xvarts for early XP.
Test character: Testwind, a wood-elf fighter (soldier style),
password Test@2026 -- kept as a representative playthrough character.
A second throwaway character (Feattest, a random saurian fighter) was
registered specifically to verify the feat-acquisition fix on a clean
slate, uncontaminated by the counter-desync the live bug left on
Testwind's save (see Bug 3 below); both are left in place as evidence.
Bug 1: a bare top-level IDENT = (mapping-or-array-literal); statement outside any function is not a valid initializer on this driver -- broke look in every outdoor room, game-wide, plus two spells and the rumors command (new AGENTS.md §7.123)
adm/daemon/astronomy_d.lpc declared nosave mapping moons; (and
siblings moonstate/moonphase/moonorbit, plus two globals never even
declared at all, moonvisibility/moonillumination) and then assigned
their real values via ordinary-looking top-level statements later in the
same file, outside create() -- a MudOS/LDMud-era convention this
driver does not support: a bare IDENT = EXPR; outside a function is
parsed as a type-less attempt to *redeclare* the global, producing
error: Type mismatch ( unknown vs mapping ) when initializing moons
and leaving the whole file uncompiled. Since
adm/simul_efun/total_light.lpc's total_light() -- called from every
single look/room-entry via describe_current_room() ->
light_blind() -> light_blind_remote() -- calls
ASTRONOMY_D->query_eclipse() unconditionally and uncaught, every
outdoor look in the entire game silently printed nothing past the
point of failure (no room description, no exits, no items) from the very
first fresh boot. Reproduced live: a fresh character's very first look
at the Offestry town square (an outdoor room) printed only a blank
-> prompt.
Fixed by merging the bare declaration and its later top-level assignment
into a single proper declaration-with-initializer inside create()
(this file already had one), and adding the two missing declarations:
nosave mapping moons;
nosave mapping moonstate, moonphase, moonorbit, moonvisibility, moonillumination;
nosave string* moonphases;
int in_eclipse;
void create()
{
seteuid(getuid());
moons = (["sera" : "...", "tyrannos" : "..."]);
moonphase = ([...]);
moonvisibility = ([...]);
moonillumination = (["sera":1, "tyrannos":0]);
moonphases = ({ "new", ... });
}Fixing this then exposed a second, previously-masked compile error in
the same file (int query_moon_illumination(moon){ -- a parameter with
no declared type, which this driver rejects outright), fixed to
string moon.
Three more independent instances of the exact same shape, found the
same session (see new AGENTS.md §7.123 for the general pattern and
detection heuristic): cmds/mortal/_rumors.lpc (arealist/areas --
broke the rumors command referenced by the game's own
doc/help/user/newbie text, "<rumors> will tell you where people are
currently congregating"), and two mage/magus spells,
cmds/spells/e/_elemental_aura.lpc and _elemental_body_i.lpc
(shortmap/colormap/elementmap). All fixed the same way (merge
declaration+initializer into one statement). A targeted grep
(^[a-zA-Z_][a-zA-Z0-9_]* = (\(\[|\(\{|\(:) at column 0) found 18
candidate files corpus-wide; 14 were false positives (unindented code
inside a function, or inside a large /* ... */ block of example
output/pseudocode) -- each hand-verified against its actual brace
context before deciding. A full corpus-wide sweep for this exact shape
was NOT attempted given the archive's 43,147-file size and this
project's standing memory-safety caution around full lpcc_check.sh
batch runs on this lib (see "Known gaps" below); the 4 confirmed
instances were all found via this one targeted grep pass plus whatever
the live playthrough itself happened to touch.
Verified live, before/after: before the fix, look in the Offestry town
square printed nothing past the prompt; after, it renders the full room
description, exits, and NPC list correctly. rumors now lists nearby
areas by level range instead of failing outright.
Bug 2: daemon/help.lpc called an undefined topic_dir() (a typo for its own topics_dir()) -- broke help completely, for every player, from the very first boot
daemon/help.lpc's query_topics() has one case per help category,
all calling the same local helper topics_dir(DIR) -- except
case "*bloodlines", which calls topic_dir(DIR_BLOODLINES_HELP+"/")
(missing the s), an undefined function. This is a hard compile error
on this driver (error: Undefined function topic_dir), so
daemon/help.lpc itself never compiled, and cmds/mortal/_help.lpc
(the help command) failed every single invocation with
*No program in object '/daemon/help'! -- reproduced live: bare help,
help newbie, help spar, help combat all printed nothing at all,
game-wide, for every player, from the very first boot. Fixed the typo
(topic_dir -> topics_dir). While in the same function, also fixed an
adjacent if(sizeof(statdata) && statdata[0] = myhold[i]) (a stray =
where == was clearly intended, in the *spells/*feats category's
file-validity filter -- an always-true condition that silently made the
filter a no-op, listing every directory entry rather than only the
stat-verified ones).
Verified live: help now renders the full category/topic menu; help
*bloodlines lists its 12 sub-topics; help feats renders its full
help text.
Bug 3: daemon/feat_d.lpc's directory scan used the exact §7.118 filename-slice bug already documented for this lib's own daemon/command.lpc -- silently disabled the ENTIRE feat/combat-technique system for every class in the game
The single most severe finding this session (see the extended
AGENTS.md §7.118 writeup for the full mechanism). build_feat_list()
scans every category folder under the feats directory and decides
whether each filename ends in .lpc with
strsrch(files[y], ".lpc") != (strlen(files[y]) - 2) -- correct only
for a 2-character .c extension, and since this port's mechanical
rename lengthened every extension to .lpc (4 characters), this check
is now false for every single file, so continue (skip) fires on
literally every feat file in the archive. __ALL_FEATS was permanently
empty, is_feat() returned false for every feat name in the game, and
add_feat() silently no-op'd on every feats add/feats <type> <feat>
call -- while the confirmation handler (confirm_add_type() in
cmds/mortal/_feats.lpc) prints "Congratulations, you have successfully
added..." UNCONDITIONALLY, never checking whether the daemon call
actually succeeded, so the player sees a convincing fake-success message
every time. Downstream, std/living.lpc's central command dispatcher
gates every /cmds/feats/* verb behind FEATS_D->usable_feat(), so
every feat-granting command (including the game's own tutorial-
recommended sweepingblow) failed with the driver's generic "Why?"
default-fail message, indistinguishable from a nonexistent command. This
hits EVERY new character immediately: the automatic class-granted
feats every fighter/etc. receives at creation (light/medium/heavy armor
proficiency, shield/simple/martial weapon proficiency) were also
silently invisible to feats known/has_feat()/usable_feat(), even
though the "Adding fighter class levelling feat: ..." messages print
correctly during character creation.
Root-caused live via temporary debug_message() instrumentation in
add_feat() (note: this driver's debug_message() writes to the
driver's own stdout/stderr, NOT debug.log -- a real trap during
tracing) after ruling out has_feat(), __FEAT_DATA storage-key
mapping, and a first (wrong) theory that query_player_feats() was
missing a "martial" bucket read (it wasn't -- "martial"-type feats
are intentionally aliased to the pre-existing "bonus" bucket via
get_feats()/set_feats()'s own case "martial": ... query_bonus_feats()
mapping, already correctly read back; a would-be fix was drafted then
reverted once the real root cause -- is_feat() returning false because
build_feat_list() never populated __ALL_FEATS at all -- was found).
Fixed identically to §7.118's established extension-agnostic pattern:
if (strlen(files[y]) < 4 || files[y][strlen(files[y]) - 4..] != ".lpc") {
continue;
}Fixing the scan then made build_feat_list() actually attempt to
compile all ~130 feat files under cmds/feats/ for the first time ever
-- surfacing a second wave of ~25 files with their own independent,
ordinary compile bugs that had simply never been reached before. All
fixed, same session (grouped by shape, not per-file since most repeat):
- Undefined
m_indices()(the MudOS alias forkeys(), not an efun on this driver):cmds/feats/a/_animal_companion.lpc,_arcane_bond.lpc,p/_pact_bond.lpc,p/_primal_companion.lpc(4 files, identicalimplode(m_indices(valid_types), ",")site) ->keys(). - **
TYPE *a, b/TYPE a, *bdeclaration-binding typos** (this archive's recurring class, already documented in this file's §8):a/_augment_arrow.lpc(object weapons;used as an array throughout ->object *weapons),d/_defenders_presence.lpcandr/_radiant_aura.lpc(object* party=({}), healed=({});--healedsilently scalar ->*healed),j/_judgement.lpc(object inv;used as an array ->object *inv),s/_shapeshift.lpc(string myrace="",subrace="",oksubraces=({});->*oksubraces),s/_swipe.lpc(object wielded;->object *wielded, plus a real logic bug found in the same line, see below),obj/inspiration.lpc(object * diffout, diffin;->*diffout, *diffin). mappingdeclared but assigned an array (keys()'s return):c/_counterspell.lpcandp/_primal_scar.lpc(mapping available;used withpointerp/sizeof/available[i]throughout ->string *available).- Stray
return N;inside avoid-declared function (the same class already fixed repeatedly during this lib's original onboarding, §8 below):d/_dazzling_display.lpc(2 sites),e/_enchant.lpc(6 sites, one revealed only after the first 5 were fixed and the function's tail became reachable),s/_scribe.lpc(7 sites, same reveal-in-waves pattern),s/_shield_of_whirling_steel.lpc(2),s/_spinning_kick.lpc,w/_whirl.lpc,obj/flee.lpc(return remove();insidevoid execute_attack()->{ remove(); return; }),r/_resilient_arcana.lpc(return ::remove();inside avoid remove()override -- fixed to bare::remove();, matching the exact convention already used by two sibling overrides in this same codebase,obj/judgement.lpcandobj/inspiration.lpc, both of which already call::remove();as a bare statement). - One-off logic/argument bugs, each independently confirmed:
c/_channel.lpc(attackers = attackers[i];inside aforloop clearly meant to assign the singularattackervariable -- a real, distinct variable already declared two lines above -- fixed toattacker = attackers[i];; left unfixed this would have destructively overwritten the loop's own iteration array on every pass);d/ _dirty_trick.lpc(int bonus, effect, sickened;wheresickenedis assignedload_object(...)and called via->-- anobject, not anint-- split into its ownobject sickened;);g/_greater_rage.lpc(set_required_for("mighty rage");passing a bare string where the function requiresstring*-- wrapped({ "mighty rage" }));i/ _impale.lpc(strsrch(type, "pierc" == -1)-- a misplaced parenthesis comparing the literal"pierc"to-1instead of comparingstrsrch(...)'s result, permanently mis-scoring weapon damage type -- fixed tostrsrch(type, "pierc") == -1);s/_swipe.lpc(beyond the array-declaration fix above,if (!sizeof(wielded) == 1)-- operator-precedence bug parsing as(!sizeof(wielded)) == 1, so the "you must be wielding a single one-handed weapon" gate only fired when wielding NOTHING, never when dual-wielding or empty-handed as intended -- fixed tosizeof(wielded) != 1);obj/inspiration.lpc(3 calls toinspire_ally(ally, dir)omitting the function's required 3rdchangeflagargument -- this driver enforces exact non-varargsarg counts -- added the0default at each call site, matching this same lib's own establisheddaemon/chat.lpcfix for the identical shape).
Verified live end-to-end on a fresh throwaway character (Feattest,
uncontaminated by Testwind's pre-fix state): feats known
immediately after creation correctly lists all six automatic class
proficiencies; feats martial sweepingblow (yes to confirm) reports
success AND the sweepingblow command itself now actually runs (reaches
its own two-handed-weapon gameplay check, instead of the driver's
generic "Why?"); feats known afterward correctly shows Sweepingblow
under TwoHandedWeapons. Testwind's own two attempts to add
sweepingblow (one before the fix, one shortly after, before this was
traced) both consumed the character's one-time "2 free martial bonus
feats" allowance via the bonus_feats_gained counter (which increments
unconditionally in add_my_feat(), independent of whether the feat
actually persisted) without ever storing the feat -- Testwind is left
in this state intentionally, as evidence of the pre-fix bug's exact
failure mode (a permanently fake-successful, resource-consuming, no-op
grant); a live wizard could restore the two consumed slots via
ob->set_bonus_feats_gained(0) if this character is ever used for
further testing, but that's a data-repair decision left to a human, not
made here.
Bug 4: d/laerad/mon/anti.lpc (a level-22 mid-game boss NPC) failed to load at all, cascading from a stray (string) cast and several void-declared functions with stray return 1;/return N;, transitively taking down two dependent item files with it
Found while spot-checking a daemon/quests_mid background quest-daemon
reference in debug.log (*No program in object
'/d/laerad/mon/anti'!) during otherwise-unrelated testing --
init()'s aggro check cast query_level()'s int return to (string)
before comparing it against int literals ((string)TP->query_level() >
18), a hard type-mismatch error on this driver; heart_beat() and
bolt() were both declared void with several return 1; sites.
Compiling this one NPC file transitively attempted to load everything
its own create() clones (new("/d/laerad/obj/lstealer"), etc.),
surfacing two more independent bugs in the same load chain:
d/laerad/obj/lstealer.lpc's void save_me(string file){return 1;} (a
deliberate no-op override to prevent this unique weapon from
auto-loading on restore, per its own header comment -- but declared
void while the base std/Object.lpc's real save_me() returns int
and IS consumed by callers elsewhere, e.g. std/bag_logic.lpc's
j = inv[x]->save_me(fname); -- fixed by widening to int save_me(...),
preserving the intended no-op behavior exactly). Fixed the cast (removed
the stray (string)), the stray returns (changed to bare return; in
the two void functions), and the lstealer.lpc return type. This NPC
and its unique weapon are now reachable/loadable, though not personally
fought live this session (a level-22 boss well above a level-1 newbie
test character's reach) -- fix verified via a clean lpcc --batch
single-file compile only, not a live encounter.
Bug 5: daemon/virtual_room_d.lpc's player-room save/registration function was declared void but its ONE caller (cmds/mortal/_save.lpc) captures its return value -- broke the save command's virtual/player-built-room persistence path
register_virtual_room() computes and returns a save_name string,
but was declared varargs void register_virtual_room(...) -- the
identical bug already present (and left commented-out, itself with the
same void+return save_name; mismatch) in an EARLIER, abandoned
version of the same function directly above it in the file, suggesting
this was never actually fixed across a "recode" the file's own comment
describes ("attempting to recode to account for moving/linked virtual
rooms - Saide, April 2017"). cmds/mortal/_save.lpc's only call site,
file = "/daemon/virtual_room_d.lpc"->register_virtual_room(...),
needs that return value. Fixed by widening the declared return type to
string (matching the function's own return save_name;) and changing
its one early bare return; (for an invalid-object guard) to return 0;
to satisfy the new non-void signature.
Bug 6: the general store's stock list was silently empty ("I have nothing on hand.") because two unrelated shared item classes it stocks failed to compile -- masked behind a generic, plausible-sounding shop message
Reached via the newbie tutorial's own recommended first stop ("s;w from
offestry center to get to the general store"). list at the Offestry
general store (Odds and Ends) printed only "I have nothing on hand." --
a message that reads as ordinary content (an understocked shop), not an
error, and would have been easy to accept as intentional. debug.log
told a different story: d/common/obj/misc/book.lpc and d/common/obj/
misc/canvas.lpc (two shared "writable item" classes this shop stocks,
alongside sacks/lanterns/tools/rope) both failed to compile, so the
shop's stock-loading silently skipped every reference to either.
book.lpc: fourvoid-declared functions (set_stuff(),set_author(), and thecase "title":branch of a switch inside another) had strayreturn 1;sites, fixed to barereturn;; a local variable-declaration linestring *chap_keys=({}),*page_keys=({}), new_keys=({}),tmp;had the sameTYPE *a, bbinding typo as several feat files above (new_keyssilently scalar despite being assignedkeys(...)and indexed as an array) -- fixed to*new_keys.canvas.lpc:end_long()was called at line 73 but defined (with no forward declaration, and with NO declared return type at all -- a bareend_long(object tp, string long) { ... }) at line 86 -- this driver's single-pass-per-file compiler treats that as calling an undefined function. Added avoid end_long(object tp, string long);forward declaration and gave the definition itself an explicitvoidreturn type. This then surfaced a second bug: two SAME-OBJECT direct calls toquery_long()with zero arguments, but the inheritedstd/Object.lpc'squery_long(string str)requires exactly one -- fine when called via->call_otherelsewhere in this same codebase (loosely checked), but a hard compile error for a same-object direct call (statically checked). Fixed both sites toquery_long(""), matching this exact codebase's own established convention for the "give me the whole description" case (std/bag_logic.lpc'scontainer::query_long("");).
While tracing this, also found and fixed the SAME already-known,
already-partially-fixed potion bug class from this lib's own original
§8 "Smaller one-off compile bugs" list (void new_do_effect(...) with
stray return 1; -- previously fixed only in
d/common/obj/potion/advanced_heal.lpc, apparently never swept across
its siblings): healing.lpc, newbie_healing.lpc, epic_heal.lpc,
full_heal.lpc, legendary_heal.lpc, extra_heal.lpc (6 more files,
identical shape, all fixed the same way). These weren't stock items in
this particular shop, but were caught by a quick sibling-file grep
(awk '/^void new_do_effect/,/^}/' file | grep 'return 1;') run
immediately after finding the first instance, per this project's
standing "a bug found once has usually been copy-pasted" heuristic.
Verified live: the Offestry general store's list now shows all 16
real stock items (belt pouches, lanterns, rope, tools, the canvas, the
book, etc.) instead of "I have nothing on hand." A further, unrelated
*No program in object '/d/common/obj/sheath/sheath_inherit'! surfaced
in debug.log from a DIFFERENT background subsystem (a weapon-sheath
randomizer) while re-testing the shop -- confirmed not to block the shop
list itself, and left unfixed as a deliberate stopping point: this
43,147-file live archive has effectively unbounded further
transitively-reachable compile bugs in its shared item library once
lpcc actually starts reaching previously-dead code paths (the same
mechanism as Bug 3's "second wave"), and chasing every one of them is
out of scope for a single §10.7 pass — flagged here for whoever next
works this lib, not chased further.
What was tested and confirmed working (Testwind's playthrough)
- Registration + full 15-stage creation wizard (class, gender, race, subrace, template, age, stat rolling via
recommended, body type, hair/eye color, language, alignment, deity, class special), tutorialskip, landing in Offestry town square. look/score/iat every major state change: after register (post-Bug-1-fix, renders correctly), after first move, after gaining a feat, after combat, after quit/relogin.- Combat: no dedicated PvP-safe sparring/training-dummy mechanism reachable from the newbie zone within this session's travel budget (the game's real training-dummy room,
d/shadow/coliseum/, is in a different city,Shadow, not the newbie townOffestry-- reaching it would have required either significant travel or admingoto, which this lib's freshly-seeded admin account was used for other verification instead). Used the newbie-tutorial-recommended "keep" area's xvarts instead (level ~1, low threat) -- fought and killed two separate xvarts across two sessions with normal hit/miss/damage/death/ corpse mechanics and correct XP gain (58, then another for a running total of 290), nodebug.logerrors during either fight. - Skill/feat acquisition:
feats allowed/feats known/feats martial <feat>(with itsyesconfirmation flow) all now function correctly post-Bug-3-fix, verified on a completely fresh character (see Bug 3 above for the full before/after). quit,debug.loggrep, then reconnect after a real wall-clock gap:Testwindwas quit and reconnected across 4 separate real driver-restart-and-reconnect cycles over the course of this session (each time a fix required a fresh boot to test); state (level, exp, HP, feats-known-counter, room position) persisted correctly across every cycle;debug.logwas clean of error-level output at everyquit(only routine compile warnings from unrelated lazily-loaded objects).- Economy: reached the Offestry general store and confirmed its full stock list renders (see Bug 6); did not complete an actual purchase live since
Testwindnever accumulated any gold during this session's limited combat.
Known gaps -- honestly unverified, not invented or guessed at
- Death/resurrection: NOT reached live this session.
Testwindnever dropped below ~50% HP fighting single newbie-zone xvarts, and deliberately seeking out a lethal fight (or using an adminsmash/die()to force it) was not attempted given the time already spent on the six compile-bug chains above. Left as explicitly unverified rather than silently skipped. - A full corpus-wide sweep for the §7.123 top-level-bare-assignment bug class (Bug 1) was NOT attempted (see Bug 1's own note on why) -- the four instances found are everything this session's targeted grep plus live playthrough happened to surface, not a proof of completeness.
Node.jsis not installed in this environment, so the required §9 LPC formatter pass (format-corpus.mjs) could not be run on any of the ~35 files touched this session. All edits were made by hand, matching each file's existing indentation/brace style as closely as possible; a formatter pass (plus the three documented post-format blind-spot checks) should be run on these files by whoever next hasnodeavailable, before considering this pass's formatting fully compliant with the project's own §9 requirement.
WASM verification (wasm_status audit)
First WASM pass for this lib (wasm_status had been left "" since the
original port -- see AGENTS.md §1.4's triage playbook). Ran
scripts/wasm_client.js against ~/src/fluffos/build-wasm/src with a
full fresh registration (name/password/ANSI/screen-reader/email) through
the entire 15-stage ROLL_CHAIN creation wizard -- select fighter,
select male, select dwarf chosen explicitly to keep the path
deterministic (dwarf isn't one of the two is_statmod_race() races
[human, half-elf], so the stat_mod stage auto-skips reliably;
fighter avoids caster-class complications), select random for the
remaining option-list stages, and recommended + done for the stats
stage specifically (it's the one ROLL_CHAIN stage that rejects the
generic select/select random verb outright -- select_stats()
always returns 0 and just reprints the stat-allocation help; confirmed
this live on a first attempt that sent select random into it and
watched the wizard stall repeating the same screen) -- then finalize,
tutorial skip, and look/score/who/say hello world/quit.
Result: clean end-to-end with zero mudlib-side changes needed. The
full chargen produced a real character (fighter/male/shield dwarf,
svelte body, silver eyes, gray hair, chaotic neutral, The Faceless One,
thug fighter-style), landed correctly in the tutorial room, skip
reached the rules room, and score/who/say/quit all rendered
correct output with no debug.log-equivalent errors, no No program in
object, and no driver-level crash anywhere in the transcript.
The only compile-time error seen during boot was the pre-existing,
already-documented adm/daemon/ipc.lpc (an admin chat-relay daemon using
raw socket_create()/socket_bind()/socket_listen(), which don't
exist in the sockets-less WASM build) -- confirmed via a full-corpus
grep that this file is referenced from nowhere else in the archive (not
preloaded, no IPC_D-style macro, no caller), so its compile failure is
inert noise, not a WASM blocker; left unguarded rather than adding a
speculative #ifdef __PACKAGE_SOCKETS__ to a file nothing calls. The
only other socket-using file in the whole archive,
cmds/creator/_netstat.lpc, is a creator-only diagnostic command never
exercised by ordinary registration/play and wasn't touched either. No
uptime() startup-grace gates, no pcre/regexp() calls in
simul_efun/master, and no IP-parsing bans in logind's equivalent
(adm/obj/login.lpc's BANISH_D/MULTI_D chain: both start with empty
allow/deny lists on a fresh archive and have no IP-format-sensitive
logic that could reject a loopback WASM connection) -- none of the
usual §1.3 bug classes applied here. std/user.lpc's setup() already
hardcodes ip = "127.0.0.1"; for every logged-in player regardless of
driver mode, which incidentally makes this lib immune to any future
query_ip_number() regression on this specific field (unrelated to the
still-live query_ip_number() efun call itself, which was never an
issue here since nothing in the login/registration path parses it).
Set wasm_status: "playable" in meta.json. Also took this pass as the
opportunity (per the site's ongoing description-quality sweep) to
rewrite meta.json's english_description and README.md's intro
section, which had drifted into porting-bug narrative instead of game
content -- replaced with real specifics pulled from std/races/ (43
race files, several is_restricted()-gated), std/class/ (base vs.
is_prestige_class() roster), d/shadowgate/dieties.h's ten-deity
NEWPANTHEON, and the d/ zone tree (Tabor/Muuldaan, the city of Shadow
and its coliseum, Underdark, a Ravenloft-flavored domain, the
Atoyatl/Tecqumin jungle region, Dagger). Also stripped a stray
40232 markdown-bold artifact from the README's "Local run" section
(the site's parse_readme() doesn't strip markdown, so it would have
rendered as literal asterisks on the site card).
深度功能测试(§10.7,2026-10-04)— negative range ends
scripts/lpc_fix_negative_ranges.py: 1 line(s) in 1 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-04)— <../x.h> includes resolve
3 files in old areas (d/dragon/old/sewer/fall1.lpc, d/dagger/derro/old/mon/elemental.lpc,
d/islands/common/eldebaro/eldebaro/ob/runestone.lpc) include a header named relative to themselves in angle brackets; the
driver refuses .. in a <...> name. adm/obj/master.lpc now answers include_file() with "./" + path (KB 04 §6.1) and
runestone.lpc loads; the other two include correctly now and fail for their own reasons (old code). The lib's warnings pass has
not been done (unused locals in the master and std/room/terrain.lpc).
深度功能测试(§10.7,2026-10-05)— 主控 author_file()/domain_file()
驱动的 mudlib_stats 包在主控载入时各调用一次 author_file(<主控文件>) 和 domain_file("/"),要求返回字符串。本库这两个 apply 只为 domains / realms 目录下的文件返回名字,其余返回 0,所以每次启动都打印 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 两行,网页版每位访客的终端里都看得到。adm/obj/master.lpc 改成与 fluffos 组织维护的 Dead Souls 上游(deadsouls_fluffos)相同的写法:其余文件的域返回 "BACKBONE",作者返回 "NONAME"。域等于 backbone 域时驱动让对象沿用创建者的域,与返回 0 时相同;作者统计(author_stats())多出一行 NONAME,汇总 realms 目录以外的对象(KB 06 §7.222)。用 lpcc config.fluffos /nonexistent.lpc 前后对比,主控载入段只少了这两行。
2026-10-06 — protected private
adm/daemon/services.lpc 的 build_mudlist() 声明为 protected private(早期把 static 换成 protected 时留下的两个访问修饰),FluffOS 报 Multiple access modifiers;改为 private。该文件仍因缺少 NETWORK_D(本驱动没有网络包下的互联服务)无法编译。
Deep test (§10.7, 2026-10-07) — messages cut by a stray ;
std/class/shadowdancer.lpc prerequisite text: a stray ; ended the string early, so "10 Ranks spent in Athletics Skill" was never shown. Joined.
Changed files loaded at HEAD and in the tree in fresh processes: no regression.
深度功能测试(§10.7,2026-10-08)— .lpc 后缀的定宽切片
.c 改名 .lpc 时只改了字符串没改切片宽度:f[<2..] == ".lpc" 拿两个字符比四个字符,永远不成立(!= 永远成立),按后缀筛文件、去后缀的代码因此从不运行或截成 x.l(KB 06 §7.238)。scripts/lpc_lpc_suffix_slice.py --apply 改了 1 处比较、0 处去后缀(如 cmds/system/_update.lpc);改动的文件 HEAD 与工作树分别加载(新进程),无回退。
Deep test (§10.7, 2026-10-10) -- compile warnings, chunked rescan
Re-run with the chunked scanner (LPCW_CHUNK=600): the earlier pass predates it and its coverage could not be confirmed. scripts/lpc_warnings.py sunshadow --fix plus scripts/lpc_fix_no_effect.py --apply changed 3273 .lpc and 3 .h files (mostly unused locals). Loaded at HEAD and in the tree in fresh processes: 1781 -> 1782 PASS of 3265 changed files; the one "regression" is the d/deku/keep/rooms/jail1 / dungeon4 pair, which swap PASS/FAIL between batch runs (each loads alone). The 76 includers of the changed headers: 59 -> 59. Audit flags in files that do not compile at HEAD were read: all are calls (east_limit(path), uptime()) or names kept in the rebuilt declaration. Boot warnings 50 -> 0.
By hand: adm/include/conf.h and config.h both define MUDLIB, MUDLIB_VERSION, MUD_IS_LOCKED, GMT_OFFSET, MAX_LOG_SIZE and MORTAL_POSITIONS with different values, and files include them in both orders (Macro redefined); each define is now preceded by #undef, so the header included last still wins exactly as before. std/daemon.lpc clean_up() is now int (returns 0, which is what the void meant to the driver) because 7 daemons override it as int and return 1 to stay loaded; the 19 daemons that overrode it as void are now int too, with return 0; (same value). Login on a fresh driver: same as HEAD.
深度功能测试(§10.7,2026-10-10)— 站点树缺 access.db;帝国卫兵的装备
- 上游自带的
work/.gitignore忽略了adm/db/access.db、groups.db、privs.db,仓库里只有.example。本地工作树有这三个文件(从.example复制而来),而按 git 建出的树(站点 zip、pristine_tree.sh)没有。主控的load_access()对read_file()返回的 0 做explode()而出错,每次check_access()都失败;连错误处理守护进程写/log/debug.log时也要过这一关,于是原始错误被吞掉。结果是大部分 NPC 无法载入:/d/azha/mon/的 40 个 NPC 在 git 树里 38 个载入失败。三个文件与.example逐字节相同,已用git add -f纳入版本库(shadowgate 本来就跟踪了它们)。之前用 git 树做的整库扫描(编译警告整理、§7.238)中,NPC 一侧两边同样失败,所以“无回退”的结论仍然成立,只是覆盖面比数字显示的小。 d/azha/mon/impguard.lpc、s-impguard.lpc:随机三选一的装备里,/d/tsarven/equip/ghalberd.lpc和gshield.lpc在存档里从来不存在,new()出错使create()中断,三分之二的卫兵载入失败;switch的三个case也没有break,持戟的卫兵会把三套武器都拿上。改用现有的同系装备/d/tsarven/equip/ihalberd.lpc(Tsarvani halberd)和/d/azha/equip/ishield.lpc(Tsarvani medium shield),并补上break。有 access.db 的树里,两个卫兵连续 4 次载入都通过(之前 HEAD 3 次全失败)。
- 2026-10-11:编译警告第二轮(按驱动报告逐条修):按位 & 改逻辑运算 0 处,返回类型/原型 140 处,NPC+F_UNIQUE 的 init() 0 个,带初始化的未用局部变量 11 个,只在 #if 分支用的声明移进分支 2 个文件;改动 116 个文件,HEAD 与工作树各载入一次无回退(116 objects; HEAD PASS 54, tree PASS 54; regress 0; newly loading 0),这些文件的警告 392 -> 252。
- 2026-10-11:编译警告第二轮(按驱动报告逐条修):按位 & 改逻辑运算 0 处,返回类型/原型 19 处,NPC+F_UNIQUE 的 init() 0 个,带初始化的未用局部变量 0 个,只在 #if 分支用的声明移进分支 0 个文件;改动 17 个文件,HEAD 与工作树各载入一次无回退(17 objects; HEAD PASS 4, tree PASS 4; regress 0; newly loading 0),这些文件的警告 71 -> 91。
深度功能测试(§10.7,2026-10-11):“无副作用表达式”警告复查
把 shadowgate 提交 6ae0f9a959f 的 72 处 Expression has no side effects 修正移植过来:25 个与 shadowgate 代码逐词相同的文件用 scripts/lpc_twin_port.py adopt 原样套用,其余 33 个文件按 sunshadow 自己的文本手工移植或修正(共改动 58 个 .lpc)。lpc_twin_port.py audit shadowgate sunshadow --pre 6ae0f9a959f~1 复查后,只剩与本次无关的旧差异(sunshadow 的 _heal 已用 rnd 计算伤害;quests_high/mid 少一个 test_quests() 原型)。
- 影响玩法的:
d/laerad/lgnolls的bloodfang、gnollhunter(及原样套用的 gnollbarb/priest/warrior、worg_tooth)do_damage("torso",(2,20)+20)改roll_dice(…),原先只取逗号右值、固定伤害;d/deku/monster/bheur的四行list - …改-=,瞬移时才会排除禁入房间、当前房间和 fire_tunnel;daemon/search_d的is_item_locked()(if (locked > 0); return 1;对任何已登记物品都答“已锁定”)改为按标志返回;std/tobj的do_trap()去掉if(...);的分号;daemon/rumours_d三个来源后缀改为fb += …;std/battle.lpc与测试副本d/deku/hhouse/inherits/test/battle.lpc的reset_scrambles()改iteration = sizeof(combatants)(原先循环一次不跑);tecqumin/rooms/spell的prof = FULL_EFFECT;tabaxi/obj/cauldron的desc += ", ";nymph_dress_in的yy = random(10)+1(高魅力描述原先从不出现);archer的for(i=0;level, i<level;i++);两份river.lpc的direction += (ob, PORT)改标量赋值(enter()无人调用);d/azha/daemon/overseer的resetMons = ({ })及add_timed_monster()里从未赋值的ob。 - 无可见变化的:6 个任务 NPC 的
step3 = 1(ghostking、ghostshaman、imp、sirith、grakulan、ulfgar;step3从未被读取);maze_builder2/3的room_number = prev_room_number;std/vendor、std/assvendor的enchant = enchant ? enchant : 1(在 if 内);std/races/orc的default: return 0;;_sweepingblow的num = -1;spell_research删去游离的keyword_index[words[j]];;demon_footman删去游离的属性数组;daemon/multi_d、daemon/unique_d只含注释的 if 并入注释。 - sunshadow 独有的:
std/magic/spell.lpc的define_clevel()里一个空的if(spell_type == "psion") { },删去;std/effects/effect.lpc的if(!prof) prof == FULL_EFFECT;改=(与rooms/spell.lpc同形;该基类目前无人继承)。std/assvendor.lpc见上。
HEAD 与工作树新进程分别载入改动的 58 个对象:regress 0。未通过的 25 个(_freeze、两份 river、rooms/spell、cauldron、豺狼人、rumours_d、search_d、unique_d 等)在 HEAD 同样无法载入,是本库原有的其它编译错误。bheur 在第一次批量载入里失败一次:其 create() 有一半机会 new() 出 /d/deku/weapons/winters_chill.lpc,后者 void 函数 return charges; 编译失败(shadowgate 同样),单独重载 3 次均通过,属随机伪影。改后这些文件的“无副作用”警告为 0。58 个对象载入无回退。
- 2026-10-11 补记:
d/deku/weapons/winters_chill.lpc(冬寒法杖)query_charges()声明为 void 却return charges;,failure()为 void 却return 1;;sunshadow 下整份不编译,d/deku/monster/bheur.lpc一半几率 clone 它而载入失败。改为int query_charges()、return;(调用方不取返回值)。
深度功能测试(§10.7,2026-10-12):编译警告收尾
能编译的文件中的警告从 27853 条(4003 个文件)降到约 100 条(1 个文件,d/magic/obj/rebirther.lpc,见下)。
本库与 shadowgate 是同一代码,先用 scripts/lpc_twin_port.py adopt shadowgate sunshadow --pre a9c8e7aeead 照搬 258 个代码相同的文件,余下 185 个有差异的文件中 134 个直接套用 shadowgate 的补丁,其余按同样的根源手改(详见 libs/shadowgate/NOTES.md 同日一节):adm/include/priest.h 不再 inherit SPELL;body.lpc 的 ac/size、room.lpc 的 flag、townsman/caravan 的 path/speed 改名;巨魔七种的属性改在 create() 里赋值(此前每只巨魔都没有类型和描述);tsh 的命令历史不再被 pushd 栈顶掉;road10 去掉会把房间整个清掉的 DAEMON;法术叶子不再重复声明 spell.lpc 的 caster/place 等,自身的时长 duration 改名 spell_time(本库另有 17 个);字符串函数指针的消费者(search/items/read、Object::set())认得真正的函数指针;重复继承与两个完整类的拼合;d_robe 改为 PARMOUR、ring 自带 artifact 的成员等。
本库自己的部分:
- 返回类型与基类不符(约 220 条,shadowgate 不报这一类):叶子把基类的
void help()、void remove()、void execute_feat()、void init_disease()、void die()等写成int。没有调用者使用返回值的改为void、return 1;改为return;、return ::remove();改为{ ::remove(); return; }(约 218 个函数),残留的旧原型一并改正;基类为 string、叶子为 int 的query_broken()、get_serial_number()改为mixed;body.lpc返回武器数组的query_wielded()与ammo.lpc返回数组的query_wc()改为mixed。错的重复原型删去:combat.lpc的doProtection、_note/_pcnote的format_notes、government.lpc的query_offices、tunnels/rooms/library的set_long。 - 真错误:
d/guilds/order/下 84 个守卫/兵士的object *query_party()覆盖了 living.lpc 返回队伍名的同名函数,改名query_guard_party()(无调用者;shadowgate 同改)。std/plate.lpc的wearit()是 void:body.lpc 在穿戴函数答 0 时拒绝穿上,所有用基类wearit的板甲都穿不上,改为返回 1(shadowgate 同改)。d/common/mons/questor.lpc的string thanklist存的是数组,本库严格类型下整份不能编译,导致newtharis/rooms/north5的任务房间(现在 rquestroom 的 create 才真正执行)载入失败;改为string *(shadowgate 同改)。std/magic/spell.lpc的反射段里两个嵌套的/*。 ?:两支类型不同(_date、_pcnote、jailer的分钟补零,_mmap的出口数):数字一支加"" +,输出不变。verdant_hammer的set_wield/set_unwield/set_hit会立即求值指针取得处理函数名,改为本库通用的(: TO, "name" :)写法。d/magic/obj/jaunter.lpc删掉#include <user.h>,自己补query_invis()/set_invis()(它只用到这两个);assassinhall去掉从未用到的 assassin(VAULT)继承;graez/rooms/battle/86的CORE已在 scrubland 里;newuser.lpc重复的more继承与diety/retinue声明;graez/obj/sword声明了两次room。
遗留:d/magic/obj/rebirther.lpc 同 shadowgate(从 user.lpc 抄来的死亡代码作用在它自己身上,去掉 user.h 就无定义,未动)。
验证:改动的 719 个文件加包含改动头文件的文件,共 3250 个对象在 HEAD 与工作树的新进程中分别载入:回退 5 个,逐个核对:newtharis/rooms/north5 因 questor 编译错误,已修;deku/hhouse/rooms/ffr7、sfr3/5/6 随机生成的 good_host/monster_feat_d 本身就编译失败(文件未改动),单独重载 sfr3/5/6 通过、ffr7 视随机结果而定。其余 3245 个对象载入无回退。lpc_audit_removed_locals.py 列出的 85 条中 83 条是巨魔叶子改成赋值的声明,另两条核对无误。
- 2026-10-14: FluffOS 970d9f09 warns
Float value truncated to int(#1426). 11 sites in 9 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 ...):/daemon/combat_d.lpc(defend *=),/daemon/world_events_d.lpc(mod *=),/daemon/world_events_d.lpc(exp +=),/daemon/world_events_d.lpc(exp -=). Changed objects loaded before and after in fresh drivers, no regressions (scripts/lpc_fix_float_trunc.py, KB 04 §6.10). - 2026-10-14 (same rescan, by hand):
d/atoyatl/tecqumin/test/guardcham.lpc(an old draft ofrooms/guardcham.lpc) declared its ownint enteredon top of the one inzig_int_base.lpc. The base never reads its own copy, so the leaf's copy is dropped and the leaf now uses the base's.d/magic/obj/rebirther.lpcstays as noted above: naming winners for its 60 overlapping functions would not remove the variable redeclarations. The 10 changed objects load at HEAD and in the tree, with no regressions.
- 2026-10-14: 41 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). The toolkit round then cleared the warnings the newly loading files showed (0 nosave functions, 746 bare returns, 79 unused locals). HEAD vs tree in fresh drivers: 1664 objects, 1333 newly loading, 0 regressions.
- 2026-10-14: 1315 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:CloseDoor(),GoJungle(),GoSwamp(),OpenDoor(),Query(),__Help(),action(),add_base_class(),add_horse_name(),add_option(),add_stuff(),add_title()…. 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: 1673 objects, 1344 newly loading, 0 regressions (scripts/lpc_fix_void_return.py, KB 04 §6.3).
- 2026-10-14: this config has no
old type behavior : 1(shadowgate's does), so the type errors that config hides stopped 1,294 files outright. Fixed:voidfunctions returning a value (add_action handlers, specials,query_*hooks) take the type they return, or a barereturn;in applies.intfunctions returning strings takestring: the signet rings'query_seal(),query_symbol(),query_mysize()and others, 269 files.voidoverrides ofvoidbases (set_paralyzed,spell_effect,set_tripped,set_bound,catch_say) stayvoid. The toolkit round's HEAD-vs-tree check: 1,673 objects, 1,344 newly loading, 0 regressions. - Base signatures narrower than their callers:
add_id()instd/Object.lpc,std/tobj.lpcandstd/living.lpctook astring, whileadd()accepts an array and 55 items calladd_id(({...})); it now takesmixed.std/room/items.lpchad two contradictingremove_item()prototypes (string / string array), so 38 rooms passing one name did not compile; it now takesmixed, and the single-name branch returns instead of falling into the array loop over the string's characters. Instd/psionportal.lpc,add_location()declared the destinationintwhile every caller passes a room path; it is nowstring. A list check of the 113 callers: 84 newly loading, 0 regressions. - 12 monsters called
set_new_exp("boss", 70)with the arguments swapped (the signature isset_new_exp(int level, string perc)): the level fell back toquery_level()and the percentile to"normal", so bosses gave normal experience. They now readset_new_exp(70, "boss"). The eldebaronpc_creator.lpcpassed"35"as a string; it now passes35. - Left failing on purpose: the newseneca tattoos (each file pasted several times) and
tecqumin/obj/table.lpc(class diamond, see above). Making them compile only trades the error for dozens of redeclarations.
- 2026-10-14: some monsters call
set_mlevel(39)with only a level. A 2016 override instd/monster.lpcmeant to handle that (the comment says so) was left commented out, because itsstringparameter made theintp()test impossible. It is back with amixedparameter: a bare level goes to the monster's first class (or fighter), and calls with a class name are unchanged. 9 of the 11 callers newly load; 80 random monsters load the same at HEAD and in the tree.
Restored base-class macros (2026-10-18, KB 04 6.13)
inherit IDENT; named a macro that no header of this lib defines (syntax error, unexpected L_IDENTIFIER, expecting L_STRING or '('), so the object never loaded; the base file is in the lib and other libs map the macro to it. Added to the global header adm/include/std.h: ALLEY -> /d/tharis/alley (23 file(s)) (scripts/lpc_restore_macros.py). Loaded one by one with lpcc afterwards: 23 of 23 pass.
Exits to rooms that do not exist, repaired from the room on the other side (2026-10-18, KB 06 7.257)
A room exit named a file the lib does not have, so the player was stuck (a typo, a renamed room, a numbered room the archive lacks). The room at the other end exists and its opposite exit points back, so that is where the exit was meant to go: /d/attaya/crafters/desert/des201 southeast: /d/attaya/crafters/desert/des222.lpc -> /d/attaya/crafters/desert/des223; /d/attaya/crafters/desert/des268 northeast: /d/attaya/crafters/desert/des291 -> /d/attaya/crafters/desert/des246; /d/khalian/stresa/cicatrice/room115 northeast: /d/khalian/stresa/cicatrice/room/108 -> /d/khalian/stresa/cicatrice/room116; /d/khalian/stresa/cicatrice/room134 west: /d/khalian/stresa/cicatraice/room133 -> /d/khalian/stresa/cicatrice/room133; /d/khalian/stresa/cicatrice/room85 south: /d/khalian/stresa/cicatrirce/room100 -> /d/khalian/stresa/cicatrice/room100; /d/khalian/stresa/cicatrice/room92 west: /d/khalian/stresa/cicatraice/room91 -> /d/khalian/stresa/cicatrice/room91; /d/magic/temples/cevahir/archery south: /d/magic/temples/cevhair/cevahir_training -> /d/magic/temples/cevahir/cevahir_training; /d/tharis/Tharis/market5 south: /d/tharis/palace/mountain/m32 -> /d/tharis/Tharis/market8. Every repaired exit now resolves to a file of the lib (scripts/lpc_fix_dead_exits.py; libs whose virtual rooms load anyway are left alone).
Macros the global header lost (2026-10-19, KB 04 6.16)
/adm/include/std.h did not define MOON_DESC; the objects that use them (1 on the list) stopped at Undefined variable/Undefined function, and since the compiler stops at the first undefined name the errors and warnings behind it stayed hidden. The sibling libs of the same lineage carry these lines in their headers: the copy of the sibling with the most files in common was added after the last #define of /adm/include/std.h; every path in a macro body exists in this lib. the listed objects have other errors as well and still do not load. (scripts/lpc_restore_defines.py)
Headers next to the including file, quoted (2026-10-19, KB 04 6.17)
23 files wrote #include <area.h> for a header that sits in their own directory (area_stuff.h, boathouse.h, strongholds.h ...). The old driver searched the includer's directory for an angle-bracket name; this one searches only the include path, so each stopped at Cannot #include .... Rewritten to #include "area.h" (resolved against the includer everywhere: preload, lpcc, run-time compiles). (scripts/lpc_fix_local_include.py)
Calls to an inherited function no parent has (2026-10-19, KB 06 7.259)
/d/antioch/valley/val.lpc, /d/dagger/aketon/rooms/tree1/1one5.lpc (2 objects) call ::create();/::init(); where the inherited bases have no such function. The old driver took such a call for a no-op returning 0; this one stops with Unable to find the inherited function 'init'.. These objects had only this error (it was uncovered once the others cleared); each call is deleted (return ::fn(...) becomes return 0;). (scripts/lpc_fix_dangling_super.py)
Object paths with two leading slashes (2026-10-19, KB 06 §7.276)
5 file(s), 5 site(s) wrote an object path as //… (like carry_object("//clone/cloth/hic_cloth")): /d/azha/town/market2.lpc, /d/azha/town/market3.lpc, /d/azha/town/market4.lpc, /d/azha/town/market5.lpc, /d/azha/town/market6.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.
The rebirther object: a second copy of the player class and a player it never touched (2026-10-19, KB 06 7.278)
d/magic/obj/rebirther.lpcis the same object as shadowgate's (see its NOTES):<user.h>(the player's inherit list) plusinherit OBJECT;, 100+ warnings, and death and rebirth code that calledsave_player()and the money functions on itself instead of the dying player. The same edit (no<user.h>, the calls go toreborn,set_invis()as theset_hidden/set_magic_hiddentoggle,query_invis()kept); it loads with no warning (HEAD: 100+) anddo_rebirth()/do_death()run to the end against a fake player in the shadowgate copy of the same code.
Standard repair round (2026-10-19, scripts/toolkit_fix.sh)
Rescanned with the current driver; mechanical fixes: 53 files with values returned from void functions. Every changed object was loaded at HEAD and in the tree: no regression. 1 further float sites need a hand edit (listed by lpc_fix_float_hooks.py).
Type slack of the old driver: core files and mechanical layers (2026-10-19, KB 04 6.19)
After the standard round 2,984 files still failed; the ones other files inherit were ranked by dependents and fixed by hand, the rest went through scripts/lpc_round_files.py (compile only the failing objects, run the fixers, repeat). 230 files changed, 146 objects that never loaded now load (loaded at HEAD and in the tree: no regression), the real warnings of the compiling files are gone.
- Core files:
d/deku/inherits/foresttraili.lpc(238 rooms:ob = "/d/magic/obj/treant"; ob->set_hp(...)moved and fought the blueprint, nownew(...)), the dinosaur islandjungle.lpc/lakefront.lpc(114 rooms:(mapping)TO->query_traps() == ({})is always false, so no jungle trap was ever set; now!sizeof(TO->query_traps())),d/mine2/mine.lpc(29 rooms: a local namedclass),d/dagger/Torm/mon/dayperson.lpc/dayvendor.lpc(20 NPCs:int timeholding "twilight"),d/common/obj/sheath/sheath_inherit.lpc(16 items:return 1+write(...),string inv = all_inventory(TO)),d/tharis/barrow/mound_inherit.lpcandrooms/mound1-6(object here = RPATH "inner1.lpc"is a path used as a call_other and message target:string),daemon/monster_feat_d.lpc(string *tmpholding the level keys of a mapping,(int)featOb->query_feat_type() == "instant"never true: now(string)),d/common/obj/misc/pipenew.lpc((string)cast on an array),d/laerad/obj/wand_para.lpc(!TP->query_name() == "hansoth": the wand now asks to be wielded by everyone but Hansoth, as its own comment says),daemon/crayon_d.lpc(string COLORS = ({...}), andstr + S;is nowstr += S;). - Mechanical: 116 calls with the missing arguments written out (
query_long()->query_long(0), 26 rooms and every spell that calls it), 39 untyped parameters typedmixed(andobject room, object, victimrepaired), 29+ array initializers given their star, void-return and prototype repairs, nineint remove();prototypes abovevoid remove()definitions. - Kept as the old driver ran it: seven
roll_dice((5,10+50), 0)calls (serakiiecho.lpc/sliver.lpc, tecqumin spells) pass ONE argument (the comma operator) and a missing second one, so the damage is flat 60 / 110 / 120 / 210; the pointless first operand is dropped, the intendedroll_dice(5,10)+50is a person's call. - Left (2,837 files with errors): 1,149 placeholder files named
x(archive loss), the./../../newbie.h/area_stuff.h/undead.h/castle.h/eldebaro_in.hfamilies inold/directories (headers the archive lacks), 36cmds/priest/godspells/*spells calling the oldset_spell_level(3)(the base takes a class mapping),d/shadowgate/masters/masters.lpc(KNIGHTALIGN / RANGERALIGN / DRUIDS lost), the pasted-togetheralzar_*tattoos,d/atoyatl/tecqumin/obj/table.lpc(two class stacks, 40 diamond warnings).
Old-driver type slack: missing arguments, untyped parameters, array initializers, class variables (2026-10-19, KB 04 6.19, 6.20)
MudOS filled the missing arguments of a call with 0, took an untyped parameter as mixed, never compared a declared type with its initializer and let a variable be named class; this driver refuses all of it at compile time and the whole object fails. 22 file(s) fixed: /cmds/skills/bak/_boost.lpc, /cmds/skills/bak/_cure.lpc, /cmds/skills/bak/_detect.lpc, /cmds/skills/bak/_feed.lpc, /cmds/skills/bak/_heal.lpc, /cmds/skills/bak/_lore.lpc, /cmds/skills/bak/_mend.lpc, /cmds/skills/bak/_offer.lpc ... (22 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: 15 of them failed before and load now, none regressed.
One stray backslash after an include (2026-10-19, KB 04 6.1)
d/antioch/valley/rooms/v6.lpc began #include <std.h>\: the backslash joined the next line to the include directive, so #include "../valley.h" was swallowed and inherit VALLEY; was an undefined macro (the same file in shadowgate has it too; its siblings v1..v5 have none). The backslash is removed: the room fails at HEAD and loads in the tree, no warning, no regression (its sibling v5 loads in both).
An unused local in a file that loads since the type-slack rounds (2026-10-19, KB 04 6.10)
d/underdark/mines/spell/emgem.lpc: object obj; in die() was never used, deleted (the final rescan found it; no other real warning is left in the compiling files except the two whole-class diamonds below the cap, d/laerad/dojo/obj/shield.lpc and d/mine2/rooms/mine22.lpc: two full classes inherited on purpose, see the shadowgate NOTES).
x == "a" || "b":后面的字符串常量永远为真(2026-10-19,KB 06 §7.281)
if (arg == "yuyou" || "yu you") 里的 "yu you" 是非空字符串,永远为真,整个条件对任何 arg 都成立;作者要的是每个字符串都和变量比较。具体是:whisper 的语言参数永远被改成 wizish。在每个字符串前补上变量和运算符(scripts/lpc_fix_literal_or.py;forrest2.lpc 是 dir == "north" && "south",手工改成 (dir == "north" || dir == "south") && ...):/cmds/mortal/_whisper.lpc(1 处)、/d/dagger/phederia/rooms/v1.lpc(2 处)、/d/dagger/phederia/rooms/v15.lpc(2 处)、/d/darkwood/yntala/mon/centaur.lpc(5 处)、/d/darkwood/yntala/mon/centaurcl.lpc(4 处)、/d/darkwood/yntala/mon/centaurg.lpc(3 处)、/d/deku/hhouse/study.lpc(1 处)、/d/islands/giant/hill/rooms/r13.lpc(1 处)、/d/shadow/city/room/moffice.lpc(1 处)、/d/shadow/room/githyanki/27,17.desert.lpc(1 处)、/d/shadow/room/githyanki/rooms/0_0.lpc(1 处)、/d/shadow/virtual/desert/27,17.desert.lpc(1 处)。
x != "a" || x != "b" and x == "a" && x == "b": comparison chains of one value that can never differ (2026-10-20, KB 06 7.282)
if (arg != "wan" || arg != "pill") return notify_fail(...) is true whatever arg is, dir == "south" && dir == "north" is never true: the guard under the test never opens, or never closes. The author meant "neither of these" (&&) or "either of these" (||). != chains joined by || became && (only where the statement under the test is a return, a refusal or a guard clause), == chains joined by && became || (scripts/lpc_fix_cmp_chain.py). Changed: /d/antioch/ruins/rooms/graveyard.lpc (2), /d/islands/dreams/rooms/fire.lpc (1). Left as written (the body under the test does something else, == and || may have been meant): /cmds/mortal/_abandon.lpc (1), /d/darkwood/forest/village/priv1.lpc (1), /d/darkwood/forest/village/priv2.lpc (1), /d/darkwood/forest/village/priv3.lpc (1), /d/darkwood/forest/village/priv4.lpc (1), /d/darkwood/forest/village/priv5.lpc (1), /d/darkwood/forest/village/priv6.lpc (1), /d/darkwood/forest/village/priv7.lpc (1), /d/darkwood/forest/village/room1.lpc (1), /d/darkwood/forest/village/room2.lpc (1), /d/darkwood/forest/village/room3.lpc (1), /d/darkwood/forest/village/room4.lpc (1), /d/darkwood/forest/village/room5.lpc (1), /d/darkwood/forest/village/room6.lpc (1), /d/darkwood/forest/village/room7.lpc (1), /d/darkwood/forest/village/well1.lpc (1), /d/darkwood/forest/village/well2.lpc (1), /std/weapon.lpc (1).
Type slack, second layer, and (string) casts of numbers (2026-10-20, KB 04 6.21, 6.22)
scripts/lpc_round_files.py sunshadow --failing ... with the three new fixers: lpc_fix_bad_assign.py (a variable that was handed a number, a string or an object it was not declared for is declared mixed), lpc_fix_void_return.py widened to every declared type (a function whose return contradicts its declared type becomes mixed; the string *query_guild() { return "Iron Fist"; } of the 45 Fist troopers is string query_guild() by hand), and lpc_fix_cast_cmp.py ((string)TP->query_level() > 12 is (int), string light_level that holds test_dark() is mixed). 314 files changed; HEAD loads 2 of them, the tree loads 203 (201 new), no regression, and no real warning in the objects that load. The 45 troopers (d/guilds/fist/oldfist/troopers/*.lpc; shadowgate has the same ones) compile now, but each create() calls ->set_property() on new("/realms/bane/closed/bracers5") or /realms/tristan/healing, wizard-directory items the archive does not have, so none of them loads (KB 06 7.269).
By hand, in the objects that load since: cmds/spells/f/_fell_flight.lpc redeclared heritage, which the SPELL base declares (the spell now sets the base's, so the base's help text shows the pact); d/magic/store_comp.lpc and d/magic/obj/store_comp.lpc declared int set_name over the base's void; d/islands/elf/mon/guard.lpc had ?: with an int and a string branch; d/atoyatl/bridge_monitor_d.lpc bp = to_int(bp + 0.5) (the conversion the driver does anyway, said out loud); d/laerad/obj/ragnarok.lpc holder holds the owner's name (string holder); d/tharis/obj/trunk.lpc base_name(inven[i] == "...") is base_name(inven[i]) == "..." (the misplaced parenthesis handed the result of a comparison to base_name(), so the check for the tree thing never worked; shadowgate has the same line).
Left: of the 2,829 objects that failed, 2,628 still do not compile (a dry round over the whole list): the 1,149 placeholder files named x, the missing ./../../newbie.h (249), ./../inherits/area_stuff.h (98), undead.h (69), castle.h (58), eldebaro_in.h (46), drownew.h (33), areadefs.h (25), the 119 files that hold an FTP 421 Timeout line, and the 36 priest spells under cmds/priest/godspells and old/weapons that call the old set_spell_level(3) with an int where the base takes a mapping (keys(levels) has nothing to work on): archive loss and dead code, not slack.
member_array('/', str) became strsrch(str, "/"): the character-literal member_array is a byte search (2026-10-20, KB 06 7.283)
'/' in member_array('/', str) is an int, so the driver searches the UTF-8 bytes and answers a byte offset (a third argument is a byte offset too). FluffOS 923cdbc0 warns once per call site (Warning: member_array(int, string) searches BYTES) when the string holds non-ASCII text before the match, and str[0..idx-1] counts characters, so a non-ASCII string was cut in the wrong place. Now strsrch(str, "/") (-1 when absent, a character index; scripts/lpc_fix_member_array_char.py, the JSON encoders and a few other sites by hand). Changed: /adm/simul_efun/json.lpc (11).
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, 31 terms). Changed: /cmds/bard/_lore.lpc (1), /cmds/spells/a/_animate_object.lpc (1), /d/atoyatl/tecqumin/city/rooms/balcony.lpc (1), /d/atoyatl/tecqumin/rooms/spell.lpc (1), /d/common/obj/clothing/magerobe.lpc (2), /d/dagger/exchange/horse/storage/horse_master.lpc (1), /d/dagger/islands/wailing_isle/mon/mutt.lpc (1), /d/darkwood/watermargin/pator/H2Osword1.lpc (3), /d/darkwood/yntala/rooms/spassage2.lpc (1), /d/deku/fortress/throne.lpc (1), /d/deku/misc/pet.lpc (3), /d/islands/dreams/rooms/good2.lpc (1) and 11 more (23 files).
A login password over 255 bytes: crypt() raises (2026-10-20, KB 06 7.294)
The driver's crypt() raises (Error in crypt(), check your salt) for a key over 255 bytes, where the old driver hashed any length. A password of 256 bytes or more typed at the login prompt threw an error in the password check and left the connection at a bare > prompt, so the login never completed (the old-password test of password the same); found by the sweep shape of 300 x. /adm/obj/simul_efun.lpc now defines a simulated efun crypt(key, salt) of the same name: only an over-long key is cut (to 255 characters, then character by character until sizeof(to_buffer(key)) <= 255: the limit is in bytes, a CJK character takes three) before it is handed to efun::crypt(); every ordinary password hashes as before and the saved hashes are untouched (scripts/lpc_add_sefun.py crypt).
darkrobe keeps a name in an object variable (2026-10-20, KB 06 7.305)
d/attaya/senund/obj/darkrobe.lpc declares object OWNER; but stores the player's name (a string) in it and compares against it: error: != always true because of incompatible types ( string vs object ), the robe (and the NPC that carries it) never loaded. The declaration is string OWNER; now.
玩家命令 /cmds/mortal 与 refs_d:旧驱动的类型松弛、前向调用、保留字(2026-10-20, KB 04 6.21, 6.19, 6.14, 6.24)
load 检查在 cmds/、adm/、daemon/、std/ 下找到 252 个失败对象,其中 136 个是真对象;这是第一优先组(玩家每天打的命令)。HEAD 上 24 个文件全部加载失败,现在 23 个加载,没有 warning;cmds/mortal/make/m_monster.lpc 因存档损坏仍失败(见末节)。MudOS 对声明类型、前向调用和 (object) 强转都不检查,这个驱动全部拒绝,整个命令对所有访客都不存在。
(object)TP->query_attackers() != ({})(数组与强转成 object 的值比较,编译器看来永远为真,旧驱动里靠共享的空数组碰巧成立)改成sizeof(TP->query_attackers()):_eavesdrop(1)、_forage(2)、_heal(1)、_hunt(2)、_study(1)。_bury的object attacker其实装数组,改成object *attacker+sizeof(attacker)。return 1 + write(...)(write() 现在是 void)改成write(...); return 1;:_blindfold(2)、_gag(2)。_gag给自己戴口塞的分支没有return 1(_blindfold的对应分支有),命令会返回 0 再多打一句 "What?",补上return 1;。- 声明类型与实际内容不符:
_actions的mapping* itemmap, * searchmap装的是query_items()/query_searches()的映射,keys()拒绝数组,改成mapping;_buff的string special装parse_special()的 0 / -1 / 字符串,special == -1永远假,改成mixed special;_skin的string what装present()的对象,改成object what;obj/enchant_ob的string bonuses是映射,改成mapping bonuses;obj/buffob四处(object)强转的是法术对象数组,改成(object *)。 _follower:string *retinue_keys_string = ([])再retinue_keys_string[i] = ...,旧驱动里它其实是个整数键的映射,调用方用sizeof()和[i]当数组读;现在是真数组({}),用+= ({ ... })追加。- 没有类型的旧式函数定义写在第一次调用之后,这个驱动对前向调用报
Undefined function:_passwd的adm_pass()/adm_confirm_pass()/adm_pw_entry()/adm_pw_verify()加上类型(passwd参数是string),再加一行void adm_pass();原型;_gossip的report_wrong_type(int, int, ob)改成void report_wrong_type(int s_type, int rum_type, object ob),原型补进头部的原型表。 _craft:原型varargs void help(string str);与varargs int help();互相矛盾,命令里 6 处return help();把 void 当 int 返回;现在help()答 1(varargs int help(string str)),三个 void 函数里的 4 处return help();改成help(); return;,return 1;(2 处)改成return;;list_crafted的num先装stat()的大小再装文件名后面的数字串,改成mixed num;remake_item的mymap是映射;两处 int 函数里的裸return;写成return 0;(旧驱动给的就是 0);删掉没用的局部变量file、short_desc。adm/daemon/refs_d(_craft、_usercall、_call的基类):ref是保留字(KB 04 6.14),参数和局部变量ref改名rname(6 处)。cmds/mortal/make/m_armour、m_room、m_weapon、m_monster:#include "/cmds/creator/make/..."指向旧位置,头文件其实在cmds/mortal/make/(KB 04 6.24),改成绝对路径/cmds/mortal/make/...。m_weapon另有wvalue()/wclass()的int arg(input_to交来的是字符串,其余函数都是string arg)改成string arg,局部变量class改名pick_class(KB 04 6.20)。obj/spellbook:keys((mixed *)MAGIC_D->query_mage_index()),magic_d里没有query_mage_index(),同目录的兄弟d/magic/spellbook.lpc用keys(MAGIC_D->query_index("mage")),照改;d/magic/spellbookx.lpc是同一行,一并改(addall是 avatar 命令,原来一调用就keys(0))。
验证:lpc_listcheck.sh 对这组文件 HEAD 0 个加载,树 23 个加载(只有 m_monster 仍失败),lpc_load_diff.py regress 0。scripts/play_boot.sh 起的真驱动里(play-sunshadow-xq,机器人 xqaab)实际打了:actions、blindfold(对自己、不带目标)、buff(无参、list、diff、add magic missile)、gag、heal(无参、对自己)、hunt、study(无参、study sword)、forage(无参、for herbs、for food)、bury(corpse)、eavesdrop、spells mage、spells cleric、cast(无参、cast fighter x、cast mage ...)、craft(crafted、items weaponsmith)、follower(无参、list)、skin、passwd(再输一个错的旧密码,答 Permission denied.)、gossip:每个命令都有自己的回答,驱动控制台和 log/debug.log 都没有新错误(下面两条除外)。spells mage 是新启动后第一次用 magic_d,它要加载880 个法术文件建索引,调试驱动里 32 秒才答,之后几秒;索引存进 /daemon/save/magic_d.o,以后的启动不再重建(lpcc 里同样的首次加载 28 秒)。skin <活物>(例如 skin xqaab)会走到 do_live(),这个函数只有一行原型 int do_live(object corpse);,shadowgate 也一样,哪里都没有定义,log/debug.log 是 Undefined function called: do_live;没有这个函数的原文,没有改。
法术文件 cmds/spells:类型松弛(2026-10-20, KB 04 6.21)
8 个文件,HEAD 全部失败,现在全部加载,没有 warning:a/_aura_of_healing(全局 flag; 没有类型,flag = 1 是 Bad assignment ( unknown vs int ),改成 int flag;);b/_blasphemy、h/_holy_orb、h/_holy_word、u/_unholy_orb(object limb 装 return_target_limb() 的字符串,damage_targ(object, string, int, string) 拒绝,改成 string limb);d/_dictum、w/_word_of_chaos(string align 装 query_true_align() 的整数,再和 1 / 2 / 3、7 / 8 / 9 比较,永远假,改成 int align);d/_dissolving_touch(基类的 set_immunities(string* arr) 要数组,其余法术都写 ({ "acid" }),这一个传了字符串,改成 ({ "acid" }))。没有改的:_word_of_chaos 的 preSpell() 里 align == 7 || align == 8 || align == 8,第三项看起来是 9(十行之后的过滤器用的是 7、8、9),是一个不影响加载的手误,留给人判断。
守护进程 /daemon、/adm/daemon:类型松弛、缺少的函数、第一次启动没有存档(2026-10-20, KB 04 6.21, 6.19, 7.259, 7.298)
HEAD 失败、现在加载(没有 warning)的 15 个:magic_d、bboard_d、party_d、screen_d、crayon_d、destinations_d、item_level_d、letter、localpost、map_maker_d、news_d、rumours_d、save_area_lists_d、templefunds_d、treesave_d;另有 monster_d(编译一直通过,启动预加载时报错,见下)。
magic_d:mapping *query_global_index()与filter_global_index()返回的是spellIndex(映射),调用方_spells、d/magic/spellbook用filter_mapping()和speccache[name]["sphere"]读它,改成mapping。bboard_d:mapping *query_posts()在没有存档时return ([]),调用方(std/bboard、_bboard)当数组用,改成return ({});另有两处?:一支是整数一支是字符串(warning),整数那支写成""+...。party_d:calculate_exp()里manage_party(my_party)传的是成员数组,函数要组名;旧驱动里party[数组]查不到,等于什么也没做。改成manage_party(group)(其余 12 处调用都传组名,推测是笔误;效果是计算经验时顺手清掉已被销毁的成员)。screen_d:redeem_user()的日志里把整个bad_user映射拼进字符串,改成刚被删除的lower_case(str)(推测)。crayon_d:color_word()里int ..., *colors, color装的是颜色代码字符串,改成string *colors, color;sizeof(keys(scheme) != 4)括号放错,是sizeof(keys(scheme)) != 4(原来scheme的检查从来不起作用)。destinations_d:generate_maze_waystations()调用的add_maze_waystations()在哪里都不存在(shadowgate 也没有),这个函数没有调用方,而且dest_room是字符串,if (!objectp(dest_room)) return;在循环之前就返回了,是没写完的generate_waystations()副本。只加一行原型让守护进程加载(map、lmap、pathfinder_d都靠它),调用本身没动。item_level_d:get_bonus_value(string bonus_name, string num, ...)里num * 8,调用方传的都是数字,改成int num。letter:string tmp装get_dir(),改成string *tmp。localpost:notify_online()、remote_post()没有类型,定义在第一次调用之后,改成protected void并补原型。map_maker_d:generate_new_exit_map(rm)的参数是房间文件名,rm是对象,旧驱动里每次都走 "roomname passed is not a string" 分支,返回空映射;改成generate_new_exit_map(base_name(rm))(推测,键rooms[base_name(rm)]就是这个名字)。news_d、std/news/rn:unguarded()是 Nightmare 的 sefun,这个库没有,news_d的save_me()直接save_object(SAVE_FILE);rn.lpc的exit()里unguarded(...)后面紧跟着同样内容的普通write_file(...),删掉前一行。rn.lpc另有show_article()的string tmp(数组)和没有类型的syntax_error()。rumours_d:get_next_num_from_array(int nums)遍历数组,改成int *nums;set_feedback(subject, fb, st, inst, adj)把传闻发起人的数组传给要名字的参数(之后give_feedback()用ob->isKnown(sub_name),唯一的调用方_gossip的数组是({instigator, instigator, instigator})),改成sizeof(inst) ? inst[0] : "no-one"(推测;"no-one" 是这个函数里没有发起人时的默认值)。save_area_lists_d:send_error()永远答 0,却被string、mapping、string *函数return send_error(...),同一个文件里别的查询函数早就是mixed;把send_error()声明成mixed(两个原型和定义)。templefunds_d:create()调clear_data(),它对第一次启动还不存在的pDonations/tAccounts(存档/daemon/save/templefunds.o没有入库)做keys(),整个守护进程加载失败;先把两个映射初始化(其余访问函数本来就是这样做的)。treesave_d:error(-1)八处,error()要字符串,改成error("-1")(旧驱动里这个调用本来也只会抛错)。monster_d(编译通过,每次启动预加载时报Bad argument 1 to explode(),log/debug.log里是第一行):load_mons_db()读运行时才产生的/daemon/save/monsters_unique.db、monsters_cloned.db,文件不存在时read_file()答 0;在explode()之前加file_size(fromfile) < 0就返回空映射(文件存在但是空的,仍走作者原来的提示)。
验证:这 16 个对象 HEAD 失败(monster_d 是 HEAD 通过、预加载报错),树加载,regress 0;重新启动 play-sunshadow-xq,log/debug.log 里没有 monster_d 那一行。
其余命令与管理对象(2026-10-20, KB 04 6.21, 6.14, 6.20, 7.287)
- avatar:
_addnwp(string nwps装keys(),改成string *nwps);_alterobj(enter_loreskill(int myval, ...)收到的是输入的文字,却和""比较;改成收string input,int myval = to_int(input)。"lore difficulty" 在别处都是数字,旧代码存进去的是字符串);_goto(mixed *rooms装query_rem_rooms()的映射,改成mixed rooms;ob ? ob : str一支对象一支字符串,对象那支写成(mixed)ob);item、item2(check_stat_add(int mybonus, ...)的mybonus是input_to交来的文字,又和"0"比较,改成string mybonus);valid_weapons.h(两处static mapping,static已不是修饰符,改成nosave mapping);avitem(11 个菜单数组声明成string,改成string *;hl()答字符串;60 个input_to回调声明成void却return 1,改成int,里面的裸return;写成return 0;;str = bnum把整数赋给字符串,改成str = "" + bnum,行为不变(0 仍然重新提问);删掉没用的rs);avsay(hl()答字符串,7 个回调改int,删掉没用的tmp、tmp_msg、rs、set_str、unset_str、default_str、de)。 - creator:
_acheck(string wear_string, unwear_string装query_wear()的字符串或整数,改成mixed;functionp()分支是死的,set_wear()存的是函数值调用后的结果);_callout(call_out_info()的行只有三个元素,KB 06 7.287:sizeof(element) < 3才算异常,参数取element[3]只在有的时候,"Size problem" 那行里把数组拼进字符串,改成identify(element))。 - system / over:
_clog(参数ref改名idx,删掉没用的tmp);_rgrep2(regexp(".+/"+exp, all_files)参数顺序反了,是regexp(all_files, ".+/"+exp));_call、over/_usercall(只是refs_d的继承者,基类修好后加载,各删掉两个没用的局部变量:temp、f/temp、i)。 - adm:
_dir(neww("/cmds/system/_"+cmd)是new(...),一次全局改名把变量new改成neww时连 efun 一起改了);_whoprisoned(each()已不是这个驱动的 efun,改成foreach (string prisoner in keys(prison_list)))。 - adm/obj、adm/simul_efun:
listener(变量名buffer是类型名,改成msg_buffer);pacct(set_password()里的query_name()在这个对象里不存在,是this_object()->query_name(),save_pacct()自己也这样写);sum_array(sizoef手误)。 - 德鲁伊:
d/priest/abilities/is_druid.lpc({ who = TP}少一个分号,是cmds/druid/druids/*的共同基类);dlevel.lpc(is_allowed_wizard(who)的参数是玩家名,改成(string)who->query_true_name());cmds/druid/druids/_druids(全局string class,保留字,改成int pick_class,它装的是等级整数;删掉 5 组没用的局部变量);_spell_list(删掉没用的局部变量)。std/class/druid.lpc的命令搜索路径是/cmds/druids("temporary"),cmds/druid/druids/*不在任何命令路径上。 std/adt/iqueue:inherit LIST_ADT;的宏从来没定义过,adt_defs.h里的列表 ADT 是ILIST_ADT(iqueue用的insert、set_tail、prev、query_tail、set_next、value都在ilist里),改成inherit ILIST_ADT;(推测,没有调用方)。std/user/retinue.lpc(编译通过):打follower时init_retinue()抛Bad argument 2 to foreach:mkdir("/d/save/retainers/<名字>/")的父目录/d/save/retainers/在新检出的树里不存在(/d/save整个在.gitignore里),mkdir()不递归,失败后get_dir()答 0。先mkdir("/d/save/retainers/")。没有改的:同一个函数里的校验循环写成next_slot_to_fill < sizeof(next_slot_to_fill)(sizeof一个整数,循环从不执行,作者要的大概是sizeof(retinue)),改它会让一段从没跑过的代码开始跑。
验证:这些文件 HEAD 失败,树加载,没有 warning,regress 0。refs_d 修好之后,九个 old/ 目录里继承 REFS_D 的对象也能加载了(cmds/adm/old/_cref、cmds/creator/old/_unmove、_unref、cmds/system/old/_dref、_eref、_move、_ref、_refs、_tref),按规矩没有动它们;其中 _unmove、_unref、_move 各有一个没用的局部变量的 warning。
最后一次全量 lpc_listcheck.sh(cmds、adm、daemon、std 下的全部 2934 个对象):HEAD 2682 个加载 / 252 个失败,树 2759 / 175,lpc_load_diff.py regress 0,新加载 77 个(todo 里的 68 个,加上上面九个 old/ 对象)。
留下的对象(2026-10-20)
第一次 todo 的 136 个对象里还有 68 个没有加载,靠改类型修不了,或者本来就没有问题:
- 存档没有的头文件或实现:
/realms/grendel/grendel.h(adm/daemon/dm_npc_d、avatar 的_aclist_alignchange_newrp_peo_poses_rolld、cmds/creator/_channels、cmds/law/_jailbirds、cmds/over/_chkalign_guilds_talk);/d/shadowgate/diety.h(cmds/druid/druids/_dieties,宏DIETYS哪里也没有);definitions.h(std/adt/istack);/adm/simul_efun/exclude_array.lpc、idle.lpc、distinct_array.lpc(adm/include/simul_efun.lpc是一份旧的 sefun 清单,真正的清单是/adm/obj/simul_efun.lpc);NETWORK_D(Intermud-2 网络守护进程从没随库发布:adm/daemon/services、cmds/adm/_netstats)。 - 这个驱动没有的 efun / sefun:
opcprof()(cmds/adm/_opcprof)、cat()与extract()(cmds/system/_less;cmds/guild/philosopher/_cmd还cat一个/realms/descartes的文件)。 - 没写完、没有任何引用的开发者半成品:
daemon/charisma_d(fnord、缺分号)、daemon/pk_d(save_pk_log()从没存在过)、daemon/player_skills_d与daemon/tax_d(从别的库粘来的经验税片段:_IRS、LEVEL_LIMIT、init_IRS()、query_lowest_level())、adm/daemon/titles(MALE_MAGE/FEMALE_MAGE在任何头文件里都没有,真正的称号来自ADVANCE_D->get_new_title())。 - 存档损坏:
cmds/mortal/make/m_monster.lpc第 741 行case "se: do_query("chat"); break;,随后是 6 段被截断重复的casee spells"...残片(约 741-890 行),上游文件就是这样(raw/里的原文件和 shadowgate 的副本相同),缺的文字只能靠原作者;include 路径已经改对。 - 老法术接口(
set_spell_level(3)传整数):cmds/druid/druids/_monster_summoning_1至_3、cmds/wizard/holding/*(14 个,其中_detect_invis、_taunt还有语法错误)。 adm/failsafe/*(22 个):std 文件的应急副本,static private等修饰符是语法错误,后面是整套没改过的老类型松弛;不会被启动,没有动。cmds/mortal/room.lpc:/std/room的一份游离副本,继承/realms/saide/traps/container.lpc。d/priest/mistletoe.lpc、obj/d_amulet、books/book_ranks、docs/book_ranks(1997 年的德鲁伊圣徽和书,用到is_druid.lpc):原来死在is_druid.lpc的语法错误上,现在越过了它,各自还有自己的类型松弛和一个class变量(mistletoe.lpc:251);不在 todo 里,只有创作者工具d/priest/abilities/fix.lpc会创建它们,没有动。- 加载检查的假失败,对象本身没有问题:
cmds/feats/r/_rending_blows(全局初始化FEATTIMER调用FEATS_D,feat_d的create()又把每个 feat 都加载一遍,在一个全新的 VM 里第一个加载它会递归进自己并被销毁;feat_d先加载就正常);cmds/druid/druids/_compare(文件级string name = TP->query_name()需要玩家);std/caravan(create()里TO->remove(),故意自毁);std/newuser(没人引用的另一份玩家身体,create()调用存档里没有的/daemon/userchain/useruc_d)。
真驱动里碰到、但编译正常因此没有动的问题(2026-10-20)
cmds/mortal/_map.lpc:12:不带参数的map走if (!stringp(str)) { TP->force_me("map"); return 1; },强迫玩家再打一次不带参数的map,无限递归,控制台是Too deep recursion.(std/user/tsh.lpc:333),访客看到Error: you should probably contact a dev.。shadowgate 的副本相同;该显示帮助还是地图列表是作者的决定。cmds/mortal/_skin.lpc:76:skin <活物>调用的do_live()只有原型,没有定义(见上)。std/user/retinue.lpc:161:校验循环的sizeof(next_slot_to_fill)(见上)。cmds/spells/w/_word_of_chaos.lpc:70:align == 8 || align == 8(见上)。magic_d在每次全新启动后的第一次spells/cast要建法术索引(880 个法术文件),调试驱动里 30 多秒;之后/daemon/save/magic_d.o在,不再重建。
daemon/letter:if 没有花括号(2026-10-21,KB 06 §7.298)
create() 的循环 if(file_size(DIR_LETTERS+"/"+i) == -2) y += ...; 后面还有 if(y > 9999) break; 和 __LettersDir += get_dir(DIR_LETTERS+"/"+i+"/");,缩进像在 if 里面,实际总会执行。/secure/save/letters/N/ 目录不存在时 get_dir() 答 0,+= 0 抛 Bad argument 2 to (void)+=。sunshadow 的检查树带这些目录所以没有出现,shadowgate 的新树没有,邮件守护进程加载失败。补上花括号,目录都在时行为不变。
map(不带参数)无限自我强制,skin <活物> 调用没有函数体的 do_live()(2026-10-21,KB 06 §7.333)
cmds/mortal/_map.lpc:if (!stringp(str)) { TP->force_me("map"); return 1; }:不带参数的 map 强迫玩家再敲一次不带参数的 map,一直到 Too deep recursion,玩家收到 "contact a dev"(实测)。下面 switch 的 default: 分支对不认识的参数调用 help(),所以不带参数也走 help()(<map view> 等用法说明)。cmds/mortal/_skin.lpc:skin <活物> 走 return do_live(corpse);,do_live() 只有一条原型,归档里没有函数体,调用会出错;活物和其他剥不了皮的东西一样回答 You can't skin that.,原型删掉。sunshadow 和 shadowgate 同一处,实测:map 打出用法,map list 正常,控制台没有错误。
Word of Chaos:混乱阵营的判断重复了 8(2026-10-21)
cmds/spells/w/_word_of_chaos.lpc 的 preSpell() 写 if(align == 7 || align == 8 || align == 8) 来拒绝混乱阵营的目标("Word of Chaos only affects non-chaotic targets!"):阵营按 3×3 编号(1-3 守序、4-6 中立、7-9 混乱),混乱是 7、8、9,同系的 _dictum(守序之言)写的是 1 || 2 || 3。最后一个 8 是 9 的笔误,混乱邪恶(9)的目标没有被拒绝。改成 align == 9。shadowgate 的这个法术是另一份实现,没有这一段。
战斗探针:五个怪物、Stormbringer 和卓尔面具编译不了,Freloc 死在巫师目录的匕首上(2026-10-21,KB 04 §6.22,KB 06 §7.305 / §7.269)
战斗探针登录时的控制台报 d/dagger/marsh/tower/mon/grimmy.lpc:311: != always true because of incompatible types ( string vs object ):if((string)TO->query_current_attacker() != targ)。本库的基类把 query_current_attacker() 声明为 object,所以这个 (string) 转换在五个怪物里是编译错误(d/common/assassins/dryzil.lpc、freloc.lpc,d/dagger/drow/mon/shadow.lpc,d/dagger/marsh/tower/mon/grimmy.lpc,d/darkwood/mon/malgoroth.lpc),作者要的是 (object)。d/dagger/marsh/tower/obj/storm.lpc 把 object owned; 用来存 TPQN(一个名字):改 string owned;。卓尔 shadow 和 Grimmy 带的 d/dagger/drow/obj/mask.lpc 的 scry_func() 里有 this_player(realName(target) != 0),调用了文件里没有定义的函数(Undefined function realName):删掉多出来的这一半条件,前面的 (string)this_player()->realName(target) != "" 才是条件。freloc 在 create() 里两次克隆 /realms/grazzt/assassins/dagger.lpc,这个巫师目录档案里没有,new() 答 0,0->move() 让 NPC 加载就中止:克隆和两条 wield 放在 file_size(...) > 0 后面。这 7 个对象 HEAD 加载 0 个,工作树 7 个。改动:上面列出的文件。
study room <东西>:obj 没有赋值(2026-10-21,KB 06 §7.346)
cmds/mortal/_study.lpc 在检查完房间里确有这件东西之后读 obj->query_obvious_short(),obj 从没赋过值,所以 study room <东西> 一律抛 Bad argument 1 to call_other()。shadowgate 的原版只打印 what;这里把重写过的两段消息换回原版的两句(tell_object(TP, "You carefully begin studying the " + what + " here.") 和对房间的那句)。
兽人小队、树屋平台和扑克荷官:三个拼错的局部变量(KB 06 §7.348,KB 06 §7.350,2026-10-21)
orog.lpc 的 swing_in() 问的是没赋值的 critter(循环变量是 critters[i]);t_house_base.lpc 的 fly_platform() 问 rooms->query_spiked(),对象是 room;扑克荷官 deal() 对离开的玩家播报 absentee->QCN,应是 player(同一个文件里 sizeof(hand !=2) 还让它根本编译不了,见下)。改动:/d/atoyatl/tecqumin/mons/orog.lpc、/d/atoyatl/tecqumin/treehouse/rooms/t_house_base.lpc、/d/shadow/mon/dealer.lpc。
魔怪 fiend.lpc:a != b != c 的比较链编译不了(KB 06 §7.282,2026-10-21)
心跳里的 if (base_name(att[i]) != base_name(TO) != base_name("klauth")) 先比出一个 int 再和字符串比,类型检查报 != always true because of incompatible types ( int vs string ),这个怪物整个加载不了;改成两个比较用 && 连接。改动:/d/islands/common/mon/fiend.lpc。
sizeof(x == n):括号里放了比较(KB 06 §7.350,2026-10-21)
sizeof(tmp != 2)、sizeof(casualties ==1)、sizeof(file_list == 0) 先算比较再取 0 / 1 的 sizeof(本驱动里 sizeof(1) 是 0),守卫或提示的分支永远不执行;扑克荷官的 hand 是 string *,类型检查直接报错,荷官根本加载不了。改成 sizeof(x) == n。改动:/d/atoyatl/tecqumin/tabaxi/obj/catapult.lpc、/d/atoyatl/vis_climb_d.lpc、/d/shadow/mon/dealer.lpc、/d/token_hunt/token_vendor.lpc。
账号登录和 IPC 监视:两个拼错的调用目标(KB 06 §7.349,2026-10-21)
pacct_login.lpc 的 input_to("get__pacct_password", ..)(双下划线)名字对不上 get_pacct_password,输入的密码被吞掉;ipc.lpc 的 call_out("ip_monitor_self", ..) 对应的函数是 ipc_monitor_self。改动:/adm/obj/pacct_login.lpc。
注册到不存在函数的 add_action(2026-10-21,KB 06 §7.349)
add_action("函数", "动词") 绑定的函数,在对象本身、它包含的头文件、它继承的父类和继承它的子类里都没有定义(scripts/lpc_scan_callout_targets.py 静态解析,子类用传递闭包复核,scripts/lpc_verify_handlers.py 运行时 function_exists() 复核)。驱动从玩家最新的一条动作往回找,遇到这样的句子就报 *Function for verb 'drop' not found.,同一个动词的其他处理函数不再尝试(不是最新的一条就悄悄跳过)。删除了 82 条注册(54 个文件,scripts/lpc_fix_dead_add_action.py),动词留给别的处理函数:/d/antioch/greaterantioch/mon/zathon.lpc(ask_em)、/d/antioch/wiz_hall/wizard_rm1.lpc(peruse_titles)、/d/atoyatl/tecqumin/base_files/ropebridge_base.lpc(jump_off)、/d/atoyatl/tecqumin/city/rooms/balcony.lpc(address_city)、/d/atoyatl/tecqumin/jungle/rooms/ropebridge_base.lpc(jump_off)、/d/atoyatl/tecqumin/rooms/ropebridge_base.lpc(jump_off)、/d/atoyatl/tecqumin/rooms/t_house_middle.lpc(climb、fly_platform、shoot_rope、throw_rope)、/d/atoyatl/tecqumin/rooms/treetop_base.lpc(swing)、/d/atoyatl/tecqumin/rooms/zigzenith.lpc(release)、/d/atoyatl/tecqumin/tabaxi/rooms/battlement.lpc(kick_em) 等共 54 个文件。最多的函数名:no_kill 16 处、dive_func 5 处、ask_em 3 处、jump_off 3 处、throw_rope 3 处。没有动(读过):/cmds/guild/philosopher/_cmd.lpc 的 relsoul(对象本身编译不过,登记留着,等对象能加载再处理)、/d/antioch/antioch2/d_animal.lpc 的 get_off(对象本身编译不过,登记留着,等对象能加载再处理)、/d/antioch/greaterantioch/obj/healring.lpc 的 use_ring(对象本身编译不过,登记留着,等对象能加载再处理)、/d/atoyatl/tecqumin/obj/jung_maze.lpc 的 place(对象本身编译不过,登记留着,等对象能加载再处理)、/d/atoyatl/tecqumin/obj/tec_maze.lpc 的 clear_maze(对象本身编译不过,登记留着,等对象能加载再处理)、/d/atoyatl/tecqumin/obj/tec_maze.lpc 的 place(对象本身编译不过,登记留着,等对象能加载再处理)、/d/attaya/tunnels/rooms/ugstream.lpc 的 mine_crystals(对象本身编译不过,登记留着,等对象能加载再处理)、/d/magic/new_book.lpc 的 copy(对象本身编译不过,登记留着,等对象能加载再处理)等共 11 处。
基类的 add_action 登记:处理函数只在后代里定义,基类自己被加载时报错(2026-10-21,KB 06 §7.349)
房间、NPC、公告板这类基类,init() 里登记的 add_action("函数", "动词") 的函数只由继承它的后代定义;基类对象自己被直接加载(房间可以走进去、NPC 可以克隆)时,访客输入这个动词,驱动报 *Function for verb 'x' not found.(第一轮扫描把这些当成“有后代定义,是活的”放过了,scripts/lpc_verify_handlers.py 在基类对象上跑 function_exists() 才看出来)。登记前加 if (function_exists("函数", this_object(), 1))(第三个参数 1:默认 function_exists() 把 protected / private 函数当作没有定义),后代照常登记。共 3 处、3 个文件(scripts/lpc_fix_dead_add_action.py --guard):/d/antioch/valley/forest.lpc(observe_em)、/d/shadow/room/pepitom/bar.lpc(read)、/d/shadow/voting.lpc(me_vote)。
注册到不存在函数的 add_action,第二轮:宏路径父类和静态解析不了的对象(2026-10-21,KB 06 §7.349)
第一轮的静态解析读不了经宏路径继承父类的对象(#define ROOM STD_DIR "room/room" 这类定义的正文以另一个宏开头)和它解析不了的继承,这些对象当作 UNSURE 没有扫;第二轮修了解析器,重扫并对这些对象在运行时问 function_exists()。add_action("函数", "动词") 绑定的函数,在对象本身、它包含的头文件、它继承的父类和继承它的子类里都没有定义(scripts/lpc_scan_callout_targets.py 静态解析,子类用传递闭包复核,scripts/lpc_verify_handlers.py 运行时 function_exists() 复核)。驱动从玩家最新的一条动作往回找,遇到这样的句子就报 *Function for verb 'drop' not found.,同一个动词的其他处理函数不再尝试(不是最新的一条就悄悄跳过)。删除了 1 条注册(1 个文件,scripts/lpc_fix_dead_add_action.py),动词留给别的处理函数:/d/atoyatl/tecqumin/obj/amulet_of_seeking.lpc(find_all)。最多的函数名:find_all 1 处。
缺失物品的 new() / carry_object() 调用链加了保护(2026-10-22,KB 06 §7.73 补充)
new(路径)->move(...); / carry_object(宏("x"))->wear(); 这类语句,路径指向的文件在这个压缩包里没有:new() 返回 0,->move() 在 0 上报 Bad argument 1 to EFUN call_other()。在 create() 里这个对象永远加载不了,在 reset() 里第一个缺失物品之后的刷新每次 reset 都被跳过。319 个文件里 636 处(new)改成 { object new_ob = new(路径); if (new_ob) new_ob->move(...); }(scripts/lpc_guard_carry_object.py --new:路径按这个文件自己看到的宏解析,宏名冲突、虚拟对象路径一律跳过)。 对这 319 个改动文件做加载检查(HEAD 与工作区对比):HEAD 能加载 139 个,现在 209 个(新增可加载 70 个,回归 0 个)。
/d/magic/scroll.lpc 从来没有编译成功:卷轴和所有刷卷轴的房间 / NPC 都加载不了(2026-10-22)
魔法卷轴 /d/magic/scroll.lpc 是 safe_scroll / newbie_scroll / scribed_scroll / mscroll 的父类,132 个文件(房间、怪物、图书馆)new("/d/magic/scroll") 它或者继承它,它在这个库里编译失败:Undefined function set_clevel(3 处)和 Bad type for argument 1 of set_spell_name ( string vs string * )。原因两个:(1) set_clevel(int l) 是不写返回类型的老式定义,写在用到它的 void init() / varargs void set_av_spell() 后面,这两个函数声明了返回类型,驱动对声明了返回类型的函数里还没见过的函数名报 Undefined function(KB 04 §6.5 前向声明;四个版本对比:只补原型就消掉这 3 处,只给定义补 void 不行);(2) 文件开头的原型清单声明了 int set_spell_name(string str),set_av_spell() 却把一个声明成 string * 的变量传给它(rclass 取自 SCRL_CLASSES[random(...)],是字符串;rspell 是 MAGIC_D->query_random_spell() 的返回)。改法:原型清单补 void set_clevel(int l); void set_spell_level(int l);,两个定义加 void,string * rclass; string * rspell; 改成 string rclass; mixed rspell;。证明(scripts/lpcc_eval.py,HEAD 与工作区各一个新进程):HEAD 上 new("/d/magic/scroll") 报 No program in object;改后连续创建的卷轴都有法术名、法术等级和价格(spell turning 7 级 4900、greater heal 7 级 4900、monster summoning iv 4 级 1600 ……),newbie_scroll、safe_scroll、scribed_scroll、mscroll 和 5 个用到卷轴的房间 / 怪物(/d/deku/sanctuary/tower/library、/d/newbie/rooms/keep/old/floor8、/d/dagger/aketon/rooms/tree3/3two13、/d/islands/argentrock/mon/fey_ri、ambusher)全部能加载。上一节调用链保护里 sunshadow 加载检查的几条随机分支 REGRESS(!random(3) 刷 new("/d/magic/scroll") 的怪物)就是这个文件不能编译造成的,不是回归。
不带前导斜杠的物品路径:缺失物品的 new() / carry_object() 调用链加了保护(2026-10-22,KB 06 §7.73 补充)
carry_object("d/city/obj/jiasha")->wear(); 这类路径不带前导斜杠的写法在这个驱动里按根目录解析(new("d/city/obj/changjian") 能加载 /d/city/obj/changjian),所以和带斜杠的一样:文件不在压缩包里时 new() 返回 0,->wear() 在 0 上报 Bad argument 1 to EFUN call_other(),整个 NPC 加载不了。此前的保护工具把这种写法当作「相对当前目录,没法判断」跳过了。2 个文件里 2 处(d/standard/obj/weapon/orc_slayer、realms/general/weapons/dragonclaw)改成带保护的块(scripts/lpc_guard_carry_object.py)。 对这 2 个改动文件做加载检查(HEAD 与工作区对比):HEAD 能加载 0 个,现在 1 个(新增可加载 1 个,回归 0 个)。
log/ 下的日志子目录没有人建:写日志的 write_file() / log_file() 报错(2026-10-22,KB 06 §7.358)
这个库的代码往 log/ 的 2 个子目录写日志:log/archive(daemon/voting.lpc)、log/magic(cmds/adm/_bestow.lpc)。压缩包、git clone 和站点包里都没有这些目录(本地启动没有写过它们,gen_keep_dirs.py 没记),write_file() 对不存在的目录抛 *Wrong permissions for opening file /log/dlog/money for append. "No such file or directory",调用它的函数在写日志那一行中断,后面的奖励、标志、步骤都没有执行。本库的 log_file() 是 write_file(LOG_DIR + file, text),目录不存在就抛同样的错误(实测)。把 log/archive、log/magic 记进 scripts/wasm_keep_dirs.txt(scripts/lpc_keep_log_dirs.py;站点和 pristine 树按它建空目录)。证明(scripts/lpcc_eval.py,在 /adm/daemons 这类目录下的求值对象里写 /log/zzdirprobe/...,新进程)见 KB 06 §7.358 的实测表。
写死的 "/路径"->函数() 指向库里没有的文件:改成现存的同名文件(2026-10-22,KB 06 §7.359)
6 处语句级的 call_other 字面量路径,库里没有这个文件(区域改了名 / 大小写不对 / 差一个字母 / 文件挪了地方):"/realms/lujke/daemons/destinations_d.lpc"->add_waystations()(/d/atoyatl/tecqumin/obj/far_scanner3.lpc、/d/atoyatl/tecqumin/rooms/destination_room.lpc、/d/atoyatl/tecqumin/rooms/destination_room2.lpc)→ "/daemon/destinations_d.lpc"。call_other() 对不存在的对象抛 couldn't find object,所在函数当场中断。改成库里现存、并且定义了被调用函数的文件(scripts/lpc_fix_call_path.py;巫师目录、备份目录、名字不同的兄弟文件都不算候选)。
深度功能测试(2026-10-27,§6.25 one-arg tell_object 残渣)
与 shadowgate 同形:gossip / rumours / cape / smithy / bloodlust / lair / map / spyglass / snakestomach。
深度功能测试(2026-10-27,§6.25 tell_object 字符串目标)
crushing_hand:tell_object("target", …)→tell_object(target, …)。
深度功能测试(2026-10-27,§6.25 one-arg tell_room)
tell_room("…") 补房间参数(this_object() / ETO / place / environment(this_object()) 等)。