info.html · 介绍 · play.html · 游玩 · llms.txt · 下载与本地运行 · ZIP · GitHub
属于"东方故事"/ES 系列引擎(与 es1_win、esI 同属一支,但内容与代码规模都独立、较小巧,源代码题头署名开发者为 Annihilator 与 Phoebus.suny, "建立架构"始于 1995 年 11 月、"测试游戏制度"于 1996 年 4 月),是这一支系里较早期的经典 MUD。可选人族、jiaojao、woochan、yenhold 四个种族,风格更偏西方奇幻而非江湖门派体系。注册流程较长(账号、密码、邮箱、 种族、性别,最后才是中文名字),新角色最终落脚"小客栈",掌柜与店小二会热情招呼:"掌柜说道:欢迎!欢迎!请里面坐!"
English
Part of the "Eastern Story" (东方故事) engine family shared with es1_win and esI in this collection, but a genuinely smaller, independent codebase (564 raw files versus es1_win's 8,000+) — credited in its own source headers to Annihilator and Phoebus.suny, "建立架构 Nov 6, 1995 / 测试游戏制度 Apr 22, 1996." Its four playable races (Human, Jiaojao, Woochan, Yenhold) lean toward Western fantasy rather than the wuxia sect/jianghu traditions common elsewhere in this collection. Registration is an unusually long chain — id, password, email, race, gender, then a real Chinese character name — before landing new players in 小客栈 ("Small Inn"), where the innkeeper greets them with "掌柜说道:欢迎!欢迎!请里面坐!" ("Welcome, welcome, please come in and sit!"), the deepest this engine family's registration flow reaches into an actual furnished room rather than stopping at a login prompt.
README
内容亮点
- 提供四个可选种族:人族 human、jiaojao、woochan、yenhold,每个种族 消耗不同点数的"业力",风格更偏传统欧美奇幻而非江湖武侠门派体系。
- 新角色从"小客栈"起步,由掌柜和店小二两位 NPC 主动迎接,节奏简洁, 是这批 ES 引擎家族里较为"老派"的一支。
在线试玩
https://mudlibs.fluffos.info/dfgs2/
管理员账号 / Admin account
- ID:
fluffos - 密码 / Password:
Mud@2026 - 中文名 / Display name:浮浮
- 权限 / Level:
(admin)(adm/etc/wizlist最高等级) - 授权方式:
adm/etc/wizlist加入fluffos (admin)一行(保留原有test条目)。
警告:Mud@2026 是本地游玩用的公开默认密码。若要正式对外开放主机,请先修改此密码。
本地运行
cd libs/dfgs2
~/src/fluffos/build-debug/src/driver config.fluffos游戏端口:40027。
NOTES · 移植与修复记录
dfgs2 — 东方故事Ⅱ之天朝帝国 (Eastern Story II: The Celestial Empire)
Archive: 东方故事二.rar (byte-identical duplicate: 东方故事二 (1).rar,
already listed in AGENTS.md's Duplicate archives table — this row covers
both). Port: 40027. Status: done (boots clean, full registration
flow verified end-to-end through an unusually long chain, including a
real Chinese name).
What this is
"东方故事Ⅱ之天朝帝国" ("Eastern Story II: The Celestial Empire"),
credited to Annihilator and Phoebus.suny, "建立架构 Nov 6, 1995" / "测试
游戏制度 Apr 22, 1996". Mudlib root at es2/es2/, config at
es2/config.cfg. Same adm/obj/{master,simul_efun} layout convention as
es1_win/esI (archives #12/#13, also "东方故事" lineage) — but a
genuinely different, smaller, distinct codebase (564 raw files vs.
es1_win's ~8000+), not a snapshot of the same tree: no std/object/
prop.lpc angle-bracket-include bug here, different domain_file()
implementation (no explode("/","/") empty-array bug). Don't assume
lineage-shared fixes transfer 1:1 just because the directory layout
matches — verify each one is actually present before applying it.
Fixes applied
1. AGENTS.md §4 (lazy security-daemon load), applied to both
valid_write AND valid_read in adm/obj/master.lpc — unlike most
libs processed so far (where valid_read is often a hardcoded
return 1), this build's valid_read ALSO called
load_object(SECURITY_D) unconditionally with no re-entrancy guard.
Both fixed with the standard nosave flag + find_object() gate.
2. AGENTS.md §15h (GBK byte-range Chinese detection): adm/
simul_efun/chinese.lpc's is_chinese() and adm/daemons/logind.lpc's
check_legal_name() (byte-count bound < 2 || > 12 → < 1 || > 6,
removed i%2==0 gate) — same shape as every prior lib. Also found a
new sub-variant of fix pattern #5 (sliding-window byte-width):
check_legal_name() had a *separate* full-width-space rejection check,
if( name[i..i+1]==" " ), a 2-BYTE window under the old GBK
assumption — under UTF-8 this is a 2-CHARACTER slice that can never
equal the 1-character string " ", so the check silently never
fired. Fixed to name[i..i] (1-character slice, matching the fixed
is_chinese's per-character indexing used elsewhere in the same
function).
3. New: a driver-crash bug, not a compile error — master.lpc's
create() unconditionally did:
``lpc
if( ob = find_object(SIMUL_EFUN_OB) ) {
efun::destruct(ob);
call_other(SIMUL_EFUN_OB, "???");
}
`
an old-MudOS trick to force-reload simul_efun. On this driver,
destructing the simul_efun object from inside master's OWN create()
— i.e. during the driver's own early bootstrap, before
master_ob/simul_efun_ob are fully settled — segfaults the whole
driver process, crash trace rooted in destruct_object() dereferencing
master_ob->obname on a not-yet-initialized pointer (vm/internal/
simulate.cc). Not an LPC-level error at all — nothing in debug.log
short of the raw backward-cpp stack dump gave any clue beyond "it
segfaults right after boot starts." Fixed by simply dropping the
destruct+reload dance entirely: the driver already loads simul_efun
fresh before master's create()` ever runs, so there was nothing stale
to work around here in the first place.
Interactive test result — full registration flow
This build's registration chain is noticeably longer than every other
lib processed so far — id → confirm → password → confirm password →
email → race select → gender → THEN Chinese name (most libs go straight
from id-confirm to Chinese name). Verified the ENTIRE chain in one
continuous connection:
1. dfgstwo → passes check_legal_id (letters only, no digits — an
earlier attempt with a digit in the id, e.g. dfgs2test, was
correctly rejected, not a bug).
2. y → password prompt.
3. password1 (twice, matching) → accepted, email prompt.
4. [email protected] → race selection prompt (human/yenhold/woochan/
jiaojao).
5. human → gender prompt.
6. m → real Chinese name prompt.
7. 萧峰 → accepted by the fixed check_legal_name — and this time
verification went all the way to actually entering the game world:
the session landed in an actual room ("小客栈" / Small Inn) with a
full room description, not just a password/next-step prompt. This is
the deepest registration verification done on any lib so far in this
project.
Known remaining issues (documented, not fixed)
(none outstanding — the eval_chat() issue formerly documented here was
fixed during the 2026-07-23 QA re-verification pass; see below.)
Retroactive fixes (QA re-verification pass, 2026-07-23)
Found during a routine re-verification pass (full registration + post-login
look/score/quit test in a fresh session) — both are genuine bugs, not
present in the original conversion notes above:
1. adm/obj/master.lpc's log_error() showed raw compiler diagnostics
(including harmless warnings) to ANY connected player, not just
wizards (AGENTS.md §15af family, but a distinct/worse variant: this
lib's version had no wizardp() gate at all, so ordinary players saw
the full "编译时段错误:" + message text — file paths, line numbers,
"Unused local variable" noise — every time an as-yet-uncompiled
room/NPC was first touched, e.g. simply typing look in the starting
room). Fixed to only show the full diagnostic to a wizard, and only
alarm an ordinary player with the generic default error message for
a genuine compile error (gated on absence of "warning:" in the
message, same pattern as dtsl/wuhanzhan/shenzhou). This also
required #include "/include/runtime_config.h" at the top of
master.lpc (for the __DEFAULT_ERROR_MESSAGE__ macro used in the
fix) since the file had no includes at all before.
2. The previously-documented eval_chat() bug (see git history) was
actually blocking the starting-room innkeeper NPC from compiling at
all — once (1) above stopped masking it with raw compiler spam, it
became clear this fires on literally every single new character's
first room entry (/d/snow/npc/innkeeper inherits the broken
std/char/npc.lpc). Given the outsized, first-impression impact and
the trivial/safe fix, this was resolved rather than left as a
documented gap: eval_chat()'s switch(typeof(chat)) had a dead
case MATCH: (an int constant, 0x102) with a body byte-identical to
the preceding case STRING: — this driver's typeof() returns string
type-name constants (STRING/ARRAY/FUNCTION from include/type.h
are themselves strings), so mixing the stray int case label in was a
hard "Mixed case label list not allowed" compile error, not a
different behavior being lost. Removed the redundant case MATCH:
label (its body was already covered by case STRING:) — not
fabricating new behavior, just deleting dead/incorrect duplicate code.
Re-verified with a fresh registration (real name 秦兰, female) followed
by look/score/quit: the innkeeper NPC now greets the player
correctly ("掌柜说道:欢迎!欢迎!请里面坐!"), zero spam, zero real
error: lines in debug.log.
lpcc sweep
427 files, 367 pass / 60 fail (85.9%). Memory stayed healthy throughout (~17GB free, no pressure — this is a small lib). Failure tail not triaged individually per AGENTS.md §6b/§13 — the full interactive registration test (which went deeper than any prior lib, reaching an actual room) is the verification gate.
Re-verification pass (2026-07-23): driver rebuild + LPC formatter + WASM build
- Formatter: ran
format-corpus.mjsover all ofwork/(427 files, 426 written/reformatted, 1 already-clean, 0 errors). - Native retest against rebuilt driver (
build-debug/src/driver, rebuilt from latest upstream master): clean boot, zero fatal errors indebug.log. Full registration re-verified end-to-end on the now-reformatted source with a fresh real Chinese name (秦凡, IDqinfan, following this lib's own long ID→confirm→password→email→ race→gender→Chinese-name flow, racehuman) reaching the actual game world (小客栈 starting room), innkeeper NPC greeting correctly ("掌柜说道:欢迎!欢迎!请里面坐!");look/score/quitall produced correct output (full 精气神 status bars rendered correctly), zero real errors indebug.log. No regressions from the reformat or the fresh driver build. - WASM build: preload completes with only the expected non-fatal
sockets-package gap (adm/daemons/ftpd.lpc'ssocket_create/socket_bind/socket_close→Undefined function, caught non-fatally,Initializations complete.still printed). Likedtsl2, this lib's login path does not gate onquery_ip_number()'s format, so a full registration proceeded all the way through under WASM too: IDqinlan→ password → email → racehuman→ genderf→ Chinese name秦岚→ reached the actual game world (小客栈), innkeeper NPC greeted correctly,lookproduced correct room output,quitexited cleanly. This lib is confirmed fully playable under WASM, not just "boots."
WASM-enablement pass (2026-07 standard: loopback-allow, throttle exempt, admin seed)
Gates patched (fail-closed: only an exact 127.0.0.1/127.-prefix
match is exempt; a malformed/non-string address is treated as remote):
adm/daemons/logind.lpclogon()(~line 56): theANTI_BUZZERper-IP character-creation throttle (buzzer_ip[ip] >= 10->destruct(ob)) now exempts loopback.ANTI_BUZZERIS defined (include/login.h), so this gate is live, unlike some other#ifdef-gated checks in this file.- No live ban-site/site-restriction daemon (no
band.lpc/sited.lpcshipped or preloaded in this lib).get_id()'sENABLE_BAN_SITEblock (~line 117) IS defined and live, but only applies towizhood(arg) == "(player)"accounts against a 2-entry static IP list (202.96.134.135,202.102.111.2) that can never match loopback -- left unpatched as genuinely out of scope (not a loopback-relevant gate). - No
uptime()startup-grace gate found (theUPTIME_CMD->main()call inlogon()is a cosmetic "uptime so far" banner, not a grace-period check).
Real bug found and fixed while verifying re-login (distinct from
registration) as required by AGENTS.md §10.1 -- this is the more
significant fix from this pass, not loopback-related:
adm/daemons/securityd.lpc's valid_read() had a restore_object
special case intended to let the login object read back its own save
file:
if (func == "restore_object")
if (sscanf(base_name(user), "/obj/%*s") && sscanf(file, "/data/%*s")
&& file == (string)user->query_save_file())
return 1;but the driver's restore_object() (vm/internal/base/object.cc)
always appends the save extension (.o) to the file argument passed
to valid_read, while obj/login.lpc's query_save_file() returns the
bare path with no extension -- so file == query_save_file() was
always false. The euid-based fallback below it also always denied,
because obj/login.lpc's own logon() deliberately clears the login
object's OWN euid to null (seteuid(0); // Let LOGIN_D export proper uid
to us.) so that LOGIN_D's later export_uid(ob) call (which only
succeeds when ob->euid is falsy, per the driver's f_export_uid) can
set ob's UID -- export_uid() only ever sets uid, never euid
(confirmed by reading packages/uids/uids.cc), so the login object's
euid stays null for its entire life by design. Net effect: every
returning player's restore_object() call was permission-denied,
100% of the time -- confirmed via catch()+efun::write()
instrumentation showing euid=0 uid=fluffos ... err=*restore_object:
read permission denied. This silently dropped straight into "对不起,
您的人物储存挡出了一些问题" and destructed the connection on EVERY
re-login attempt for EVERY account, not just the newly-seeded admin --
a pre-existing bug, invisible until now because every prior
verification pass in this lib's history tested registration through to
quit but never a SECOND connection logging back into the same id
(exactly the gap AGENTS.md §10.1 warns about). Fixed by accepting the
save-extension-qualified path too (file == save_file || file ==
save_file + ".o"), which is the minimal correct fix (the intended
"user may read back their own save file" grant, just written against
the wrong path shape). Verified: both fluffos and a fresh throwaway
player (ceshiba) can now fully disconnect and reconnect into their
existing character. This was NOT present in the original registration-
only NOTES above and NOT something this pass introduced -- it dates to
however this lineage's securityd.lpc was originally authored/forked.
Admin account: id fluffos / Mud@2026 / 浮浮, registered through the
normal long flow (id -> confirm y -> password x2 -> email -> race
human -> gender -> Chinese name 浮浮), pre-granted (admin) via
adm/etc/wizlist (fluffos (admin) line, test's existing entry kept)
before registration so the account came out already showing "目前权限:
(admin)". Verified on a SEPARATE re-login (exercising the restore-path
fix above): password accepted, update /d/snow/inn_hall succeeded
("重新编译 /d/snow/inn_hall.lpc ...Ok."). Save files
(data/user/f/fluffos.o, data/login/f/fluffos.o) are plain untracked
paths, not covered by any .gitignore pattern -- a normal
git add libs/dfgs2/ picks them up, no force-add needed.
Retest: fresh normal registration (id ceshiba, name 秦月, female)
reached 小客栈, innkeeper greeted correctly, look/quit correct; a
SECOND connection re-logging into ceshiba (the restore path) also
succeeded post-fix. debug.log clean across all four driver runs this
pass (only expected boot-time config dump and SIGTERM-on-kill lines).
Four driver instances started/killed by exact PID during this pass
(one extra cycle for the diagnostic instrumentation added and then
removed while root-causing the restore bug). Test character ceshiba's
saves removed afterward; fluffos's kept.
深度功能测试 / Deep functional test (2026-07-24)
Round-two deep playthrough pass (see libs/bxsj/NOTES.md and
libs/xiyouji/NOTES.md's own "深度功能测试" sections for the worked
examples this follows — both found real quit-time/first-visit crashes
invisible to every earlier verification layer). This lib has no
doc/help/ directory at all (work/doc/ only has wiz/, and
cmds/usr/help.lpc's DEFAULT_SEARCH_PATHS point at /doc/help/,
/doc/skill/, /doc/efuns/, /doc/lfuns/, /doc/applies/ — none of
which exist in this archive), so there is no shipped help newbie to
read; planning was done by reading room/NPC .lpc source directly, per
the checklist's own fallback instruction. Native driver (build-debug),
played as an ordinary new player through registration, exploration,
safe sparring, the organic apprentice/class-join path, a real quit,
and a real-wall-clock-gap reconnect.
Test character (kept, not cleaned up, as playthrough evidence): id
shenluoy, Chinese name 沈落雁 (male), password TestPass1. State at
end of pass: 【方士】(alchemist-class apprentice of 陈维侠/Chen Weixia),
skills dodge (0/8) and parry (0/2) learned passively from the
sparring fight below, 30/30 精气神, food/water ticking down normally,
empty inventory, located at 小客栈 (its saved startroom). Saves:
work/data/user/s/shenluoy.o, work/data/login/s/shenluoy.o.
Bug found and fixed: 4 rooms reference their own objects via the wrong subdirectory, crashing the town square's first-ever visit every boot
New bug shape for this project's catalog — not the §7.16 stale-
timestamp class, not the §7.17 reset/init-reentrancy class. This is a
plain wrong-path typo (per §6.6's "pre-existing typo classes" family)
but promoted here because of its severity: it crashes /d/snow/square
("广场中央", the town's central 4-way hub, one hop from the inn in every
direction) on literally the first move any fresh character makes past
the starting room, on every fresh boot.
- Symptom (reproduced live, pre-fix, fresh boot, fresh character): walking
小客栈→east(广场) →east(广场中央) throws
``
执行时段错误: *Bad argument 1 to EFUN call_other()
Expected: object, string, array, Got: int(0).
程式: /std/room.lpc:76
物件: /d/snow/square
呼叫调用错误在:
command_hook() /feature/char/command.lpc:37
main() /cmds/std/go.lpc:65
create() /d/snow/square.lpc:32
setup() /std/room.lpc:362
reset() /std/room.lpc:101
make_inventory() /std/room.lpc:76
`
The crash is caught (driver error handler), so the room is still
entered and looks superficially fine on screen — exactly the class of
bug quit-and-eyeball testing (and even a look right after) will
never catch; only debug.log shows it. Because it fires on the
room's *first* reset()` of the process's life, once the object is in
memory every later visit is clean — invisible to boot-log watching and
to any registration smoke test that doesn't specifically walk a fresh
character past the starting room's immediate neighbors.
- Root cause:
d/snow/square.lpc:14declares its"objects"mapping with__DIR__ "obj/pot": 1.square.lpclives directly under/d/snow/, so__DIR__there is/d/snow/, and/d/snow/obj/potdoes not exist — this archive keeps ALL ofd/snow's plain (non-NPC) object files under/d/snow/npc/obj/, not/d/snow/obj/(confirmed against the pristineraw/extraction too — this is a pre-existing authorial typo, not a conversion artifact).std/ room.lpc'smake_inventory()doesob = new(file); if (ob->violate_unique()) ...with no null-check between the two —new()on a nonexistent file returns0, and0->violate_unique()is exactly the "Got: int(0)"call_other()crash above. Every otherd/snowfile that references__DIR__ "obj/..."lives under/d/ snow/npc/, where the path resolves correctly (confirmed by grepping every__DIR__ "obj/hit in the tree) —square.lpc, plus 3 more files living directly under/d/snow/(not/d/snow/npc/), all have the identical wrong-subdirectory typo:
- d/snow/square.lpc:14 — "objects" mapping entry obj/pot
(the one live-reproduced above — 广场中央 is unavoidable
starting-zone traffic).
- d/snow/fireplace.lpc:18 — "objects" mapping entry obj/
woodsword (same make_inventory() crash shape, on first entry to
大灶内, a hideable side-room off the inn's kitchen).
- d/snow/kitchen.lpc:16 — "objects" mapping entry obj/dumpling
(same shape, a private-house kitchen reachable by climbing a wall
from /d/snow/epath).
- d/snow/ruin1.lpc:62 — ob = new(__DIR__ "obj/hairpin"); ob-
>move(this_player()); inside do_search()'s "search 杂草" player
action (only active 10:00–13:00 game-time) — same new()-returns-0
root cause, but the crash site is ob->move(...) instead of ob-
>violate_unique() since this call bypasses make_inventory()
entirely; not live-reproduced (would require hitting the exact
in-game hour window) but fixed by the identical code-shape match,
consistent with this project's standing "port the sibling's proven
fix" practice (AGENTS.md §2.1) — flagged here as the one-of-four not
directly reproduced live.
- Fix: change all 4 to the correct
npc/obj/...subpath (the actual location ofpot.lpc/woodsword.lpc/dumpling.lpc/hairpin.lpc, confirmed present on disk in every case — this is a wrong path to *existing* content, not missing content, so fixing the reference is correct per AGENTS.md's "missing zone content is an archive gap, don't fabricate" vs. "wrong path" distinction):
``diff
- __DIR__ "obj/pot": 1,
+ __DIR__ "npc/obj/pot": 1,
`
(and the same obj/ → npc/obj/ change at fireplace.lpc:18,
kitchen.lpc:16, ruin1.lpc:62`).
- Verified: post-fix, fresh driver restart, fresh walk
小客栈→east→eastreaches广场中央cleanly,大水缸(Pot)now visible in the room's object listing (lookoutput),debug.loggrepped forerror:/fatal/Too deep recursion/Too long evaluationafter the full session (registration → exploration → sparring → apprentice → acquire →quit) — zero hits.fireplace.lpc/kitchen.lpc/ruin1.lpcfixed by code-shape match and code-read confirmation (file existence at the corrected path checked for each) but not independently walked to live, given the time budget for this pass — a future pass reaching大灶内/private-house厨房/search 杂草at 10–13点 in破旧大宅should spot-check these three specifically.
What was tested and confirmed working
- Registration: one real Chinese name (沈落雁/Shenluoy, male), reaching 小客栈, innkeeper greeting correctly (already verified in earlier passes above, re-confirmed here as the entry point for this playthrough).
- Exploration: walked 小客栈 → 广场(west) → 广场中央 → 广场(north, child NPCs) → back through 广场 → 广场(west) → 广场(southwest) → 街道(wstreet1) → 私塾(school, teacher+alchemist NPCs). Room descriptions, exits, and day-time flavor text (太阳正高挂在东方的天空 中 etc.) all correct throughout.
- Character info:
score(title/attributes/精气神 bars/食物饮水/ 经验评价) andhpboth correct at every stage (fresh, post-sparring, post-apprentice);i/skillscorrect throughout. - Safe sparring (checklist step 5): this lib's
cmds/std/fight.lpcimplements safe/lethal combat as aquery("civilized")branch per target, not a dedicated training-dummy object —"civilized"is set to1generically by every human-race daemon'sinitialize()(daemon/race/human.lpcetc.;daemon/race/beast.lpcpointedly has the same line commented out, so non-human "beast"-race NPCs would get the realkill_ob()path instead — none exist in this archive's reachable content to confirm live). Usedfight childagainst one of the two小孩(child) NPCs in 广场(north) — bothchild1.lpc/child2.lpchaveaccept_fight()return1unconditionally, an explicit "always spar" village-kid pair, matching the checklist's "safe sparring before a real fight" mechanism. Produced a normal turn-by-turn bout, child fled crying ("哇地一声哭了起来:你打我!妈妈 !") then resumed playing, both fighters ended at full 精/气/神 — confirmed non-lethal,combat/unarmed masteryexp gained, and (a side finding, not tested for further depth)dodge/parryskills were silently auto-granted at level 0 purely from having fought once — this lib's skill-acquisition-by-doing path, separate from the brokenacquireNPC-teacher path below. - Sect/class-join mechanism (checklist step 6, sect half): the
apprentice <NPC>command is this lib's class-join mechanism (no separate menpai/sect system exists in this small archive — see intro section above, 564 raw files vs. es1_win's ~8000+). Confirmed the full 2-step organic flow live:feature/char/master.lpc'saccept_apprentice()is deliberately unimplemented in the base class (commented out, soob->accept_apprentice(me)on any NPC that doesn't override it silentlycall_others to nothing and returns 0) —d/snow/npc/alchemist.lpc(陈维侠) is the ONLY NPC in the entire codebase that overridesaccept_apprentice/init_apprentice(confirmed viagrep -rl accept_apprentice), so it is also the only reachable class-join path in this archive. Firstapprentice chenattempt correctly fails with a queued multi-line persuasion dialogue (陈维侠说道:拜我为师?呵呵...→ ... → an internalset_flagclosure that flips apending/alchemisttemp flag once the dialogue finishes draining over several heartbeats); a SECONDapprentice chenafter waiting for the chat queue to drain succeeds (你跪下来恭恭敬敬地向陈维侠磕了四个响头,叫道:师父!), title changes from【 平民 】to【 方士 】,scoregains a newalchemyscore category. This 2-attempt "convince them first" shape is a legitimate designed mechanic, not a bug — worth recording so a future tester doesn't mistake the firstnotify_failfor a dead end.
Finding, not a crash bug: the organic skill-teacher path is a dead end for every NPC in this archive (documented, not fabricated)
Checklist step 6's other half — "learn at least one skill via the organic NPC-teacher path" — could not be completed live, and static analysis says it cannot succeed for ANY NPC in this codebase, not just the ones tried. This is a content-completeness gap, not a crash, so per AGENTS.md's "missing zone content is an archive gap, not a bug... don't fabricate" precedent (§7.14) this was left as documented, not implemented — filling in real skill-teaching values (which skills, what levels, what cost/gating) would be inventing game balance not present in the source, unlike the wrong-path bug above which restores an already-authored reference.
cmds/std/acquire.lpc'sacquire <skill> from <NPC>command (the only skill-learning verb in this archive — there is no separatelearncommand) always ends inreturn ob->acquire_skill(me, skill).grep -rl acquire_skillacross the whole tree finds exactly one implementation in the entire codebase,d/snow/npc/alchemist.lpc:
``lpc
int acquire_skill(object ob, string skill) {
switch (skill) {
case "alchemy-medication":
case "alchemy-wealth":
case "alchemy-magic":
case "alchemy-immortality":
default:
}
return 0;
}
`
every case (including the real alchemy skills 陈维侠 himself knows,
set via set_skill() in his own create()) falls through to an empty
body and an unconditional return 0 — this is a stub/placeholder, not
wired to actually call me->improve_skill()/advance_skill() for
any skill, ever. d/snow/npc/teacher.lpc (王怀芝, teaches literate
per its set_skill("literate", 60) and its room's flavor text) has no
acquire_skill override at all, and (per the apprentice finding
above) no accept_apprentice` either, so he can never accept a student
in the first place.
- Live-verified (post successfully becoming 陈维侠's apprentice):
acquire alchemy-medication from chen→ the flavored master-request message prints correctly (你向陈维侠磕头行礼,恭恭敬敬地说道:师父在 上,求您传授徒儿「alchemy-medication」!) then看起来陈维侠不想教你这项技能(the command's own generic failure message) — confirming the stub really does refuse unconditionally, live, not just on paper. Also triedacquire literate from wang(未曾拜师/never apprenticed to him) — same generic refusal, expected given noaccept_apprenticeexists for him either. - Net effect: a new player can walk the entire intended "become a disciple" ritual (the multi-stage persuasion dialogue, the kowtow message, the class-title change, the
scorecategory) and it *reads* complete, but the promised payoff — actually learning a skill from your new master — never fires for anyone, for any skill, in this archive as shipped. This is consistent with the intro section's characterization of this lib as a genuinely smaller/less-finished codebase than itses1_win/esIsiblings, not a regression from conversion.
Explicitly not verified live (time/reachability budget), stated per §10.7 item 6
- Shop purchase:
buy <item> from <NPC>was exercised live against店小二(waiter) in 小客栈, but a fresh character starts with zero money (no starting-money grant found anywhere inlogind.lpc/CHAR_D->setup_char) and this archive's only discovered in-world income source is a day-labor job at货栈(store,d/snow/store.lpc) run by工头(foreman) — moving crates for 5 coin each.buy dumpling from waitercorrectly hit thecan_afford()rejection path (你身上的钱不够), proving that half of the transaction code live, but earning money first (and then a full successfulbuy→deliver_merchandise→handoverround trip) was not pursued given this pass's time budget. The underlying code (feature/npc/vendor.lpc,d/snow/npc/waiter.lpc) was read and appears structurally sound (mirrors the same pattern verified working in other libs' NOTES). - Real combat and death/respawn: not reached live.
fight.lpc's lethal branch only triggers against NPCs whose race daemon does NOT grant"civilized"(every human-race NPC gets it automatically; see above) — no non-human/"beast"-race NPC exists anywhere in this archive's reachable content to test that branch against, and the only other candidate hostile content (d/goat, 野羊山 bandits) is permanently unreachable:d/snow/ngate.lpc's only northward exit clones/d/domain/snow2goat, a file that does not exist anywhere in the archive (confirmed — genuinely missing content, not a typo: nosnow2goatfile exists under any plausible path inraw/either), and is additionally blocked by 4garrisonNPCs + 1lieutenantat the same gate whoseintercept()unconditionally refuses anygo northwith an in-character "盗匪出没" excuse. Reaching real combat would require deliberately attacking one of these civilized guard NPCs (triggeringsoldier_assistgroup retaliation) purely to force the non-civilizedkill_obpath — assessed as a time-costly, one-way (character-risking) grind not justified by this pass's remaining budget, so explicitly left unverified rather than attempted partially.d/hell(阴曹地府, the death realm) exists and is presumably the intended post-death destination (DEATH_ROOM/user->is_ghost()handling seen inlogind.lpc'senter_world()) but was not reached or reviewed further.
State persistence across quit/reconnect (checklist step 8)
quit (typed explicitly, not a connection timeout) produced the
expected 欢迎下次再来! message; debug.log grepped immediately after
(error:/fatal/Too deep recursion/Too long evaluation) —
zero hits, across the whole session including the fix verification,
the sparring fight, and both apprentice attempts. Waited a real ~2.5
minutes of wall-clock time (backgrounded sleep, confirmed elapsed via
date before/after, not simulated), then reconnected with shenluoy/
TestPass1 — this exercised the actual save/restore path (the
"目前权限:(player)" banner reprinted, confirming a real reload from
disk rather than the same-process "linkless reconnect" seen during
mid-session probing, where a dropped-but-not-quit connection can rejoin
with just id+password and skip straight back into the still-live
object). Post-reconnect score/skills showed 【方士】 title and the
alchemy score category, dodge/parry skills, and 精气神/食物/饮水
all correctly persisted; location correctly returned to 小客栈 (NOT
私塾, where the character was standing at quit time) — confirmed by
reading cmds/usr/quit.lpc/cmds/usr/save.lpc that this is by
design, not a bug: only rooms flagged "valid_startroom" (e.g. 小客栈
itself) can ever become a saved login point via the separate save
command, and plain quit never updates "startroom" on its own — so
relogging back at the inn rather than at the last-visited room is
correct, intended behavior for this codebase, not a state-loss bug.
Process hygiene for this pass
Two driver instances (pre-fix reproduction, post-fix verification),
started with setsid nohup ... & disown, killed by exact recorded PID
(1377932, then 1387464) — both confirmed dead via ps/ss after
kill, not pkill -f. log/debug.log cleared once between the two runs
to isolate the pre/post-fix comparison; no other scratch/instrumentation
files were created. shenluoy's saves kept as playthrough evidence per
this pass's own test-character convention (matching xiyouji's
shenqy/shenqf).
WASM 修复摘要(迁移自 meta.json 的 group_note)
此前被错误标记为某个不存在于本项目任何档案的原始压缩包文件名的 duplicate_of;这里的内容其实是完全独立、可游玩的游戏(有自己的 -N 变体编号、自己的端口、自己的 README)——duplicate_of 已清除。
§7.86 跨库扫描修复(留言板 post 崩溃)
BULLETIN_BOARDinherit+ 多余replace_program()致命形状(AGENTS.md §7.86,post命令崩溃):全档案 9 处命中,已删除多余的replace_program(...)调用(保留inherit),逐文件保留原有行尾格式(CRLF/LF 按文件原样)。本次为跨库 §7.86 扫描修复(触发原因:该 bug 已在 6+ 个互不相关的血统家族独立确认,属于近乎普遍的拷贝粘贴模式),仅做编译检查(驱动干净启动、端口正常监听),未做完整 §10.7 深度游玩测试。
Deep functional test round two (2026-08-14)
Independently re-verified against current code rather than trusting the
2026-07-24 round-one writeup. One fix applied; round one's real find (the
4-room __DIR__ "obj/..." wrong-subdirectory crash) was re-confirmed live
by physically walking the exact original crash route, not just by grepping
the diff.
Fix: adm/simul_efun/file.lpc — missing assure_file() guard on log_file(), plus cat() null-guard
Standard §7.11-class gap: log_file() called write_file(LOG_DIR + file,
text) directly with no directory-existence guard, and cat() had no
null-guard on read_file().
// BEFORE:
void cat(string file) {
write(read_file(file));
}
void log_file(string file, string text) {
write_file(LOG_DIR + file, text);
}
// AFTER:
void cat(string file) {
write(read_file(file) || "");
}
void assure_file(string file);
void log_file(string file, string text) {
assure_file(LOG_DIR + file);
write_file(LOG_DIR + file, text);
}Checked and already correct, no action needed: adm/simul_efun/message.lpc's
tell_room() already has the AGENTS.md §7.12 exclude || ({}) guard (this
lib shares the ES II lineage's vulnerable message.lpc shape but was
already fixed for it); adm/obj/master.lpc's log_error() severity gate
already uses the case-agnostic strsrch(message, "arning:") check. No §8.9
food/water wrong-object read, no printf debug leak in logind.lpc, no
dbase.lpc password-write-guard shape present in this lib's admin-seeding
architecture.
Re-verified live: the 4-room __DIR__ wrong-subdirectory crash (round one's fix) still holds
Walked the exact original crash route as admin (fluffos): 小客栈 →
east → east, landing in 广场中央 (/d/snow/square) — the room whose
first-ever reset() originally threw *Bad argument 1 to EFUN
call_other() via make_inventory(). Landed cleanly, 大水缸(Pot)
correctly present in the room's object listing, work/log/debug.log's
line count unchanged before/after (checked directly, not just eyeballed
the screen) — zero new runtime-error entries, only the expected wizard-
visible compile warnings from lazy-loading each room/object for the first
time this boot (shown inline to the connected admin via 编译时段错误:-
prefixed lines, which is normal wizard compile-warning echo, not a leak —
log_error()'s severity gate already confirmed correct above).
Verification method
Booted native build-debug driver, admin login (fluffos/Mud@2026),
update /adm/daemons/logind as the real privileged-action admin-rights
check (succeeded: "Ok." after showing compile warnings — expected wizard-
visible output, not a leak). Two rapid consecutive admin reconnects, both
clean. Driver killed by exact PID after testing; incidental fluffos.o
save-timestamp churn reverted before commit. Did not re-run the full
shenluoy playthrough (registration, sparring, apprentice flow) from round
one given this pass's time budget — the round-one writeup's other findings
(the dead acquire_skill stub, the zero-starting-money shop gap, the
unreachable lethal-combat content) are content/coverage gaps already
correctly documented as such, not crash bugs, so didn't need live
re-verification this pass.
Files modified this pass
work/adm/simul_efun/file.lpc—log_file()assure_file()guard,cat()null-guard.
Deep functional test round three (2026-08-19)
Standard §10.7 checklist plus a live economy/board/netdead playthrough. All clean — no new bugs found, nothing fixed this pass.
Checklist items
- §7.111 (
master.lpc'sstandard_trace()crashing onerror["object"] == 0): already covered by the corpus-wide sweep (67 libs, this one included per the sweep's own completion note); not re-audited line-by-line this pass since it's a mechanical single-line swap already verified applied. - §7.112 (duplicate
call_out()stacking in NPCinit()on every reconnect viaenable_commands()): greppedcall_out(inside everyinit()function underwork/. No unguarded duplicate-stacking pattern found; the two-wave corpus sweep (~270 libs) already covers this lib. - §7.113 (netdead reconnect losing
heart_beat, silently freezing healing/aging/food/water forever): explicitly re-verified live this pass, see below — confirmed NOT present. - §7.90 (eval-cost abort on first-ever room compile crashing
enter_world()with a generic "bug found" message for ordinary movement): not hit. Walked several previously-untouched rooms (广场, 广场中央, 街道, 货栈) on a cold-booted driver with no crash or generic error screen. - §7.11-class nosave-dir pattern (
write_file()/log_file()without a directory-existence guard): already fixed in round two (adm/simul_efun/file.lpc); re-confirmed still present and correct in the current tree, no regression. logind.lpcsave: registration → disconnect → reconnect correctly restored position, inventory, and stats every time (see netdead test below); no double-save or lost-save symptom.
§7.113 live re-verification: netdead reconnect + heart_beat/food/water ticking
Registered a fresh test character (qinwanjun, connection B) on a freshly
booted driver, did a short economy loop (walked to the 货栈 loading dock,
did get crate / load wagon with crate ×2, asked for wages with say
工钱, got paid 10 文钱, walked back to the inn, bought and ate a dumpling
from the innkeeper — full economy round-trip confirmed working, food/气
correctly decremented from the labor: 30→23 after two loads, replenished
by eating). Then simulated an abrupt network drop (closed the socket with
no quit, not a clean disconnect) while standing in the inn, waited 8s,
and reconnected as the same character from a new connection while a second
admin connection (A) stayed in the same room the whole time to observe
heart_beat-driven room messages during the "netdead" window — the admin
connection *did* see ambient NPC idle-chatter fire normally while the
player was disconconnected, confirming heart_beat kept running lib-wide
during the outage, not just after reconnect.
Reconnect correctly restored the character in the same room with the same
inventory (牛肉包子/二百二十文钱 retained). Food/water were observed
ticking down further after reconnect (193/233 right after reconnect, from
196/236 before the netdead), proving per-player heart_beat/aging survived
the disconnect+reconnect cycle rather than being silently dropped.
One ambiguity from that first pass: a score sent 65s after reconnect came
back showing only a queued ambient NPC line, not the expected status sheet,
inside the test harness's fixed 3-second capture window. Rather than
conclude a bug from a single ambiguous capture (per this project's own
"validate each scripted response" lesson), re-ran a second, independent,
shorter live test on a fresh driver boot with a more generous
quiet-window capture: registered ndsevfive, netdead-disconnected,
reconnected, waited 20s, then sent score with a 10-second capture window.
Result: the score sheet is returned correctly (队列 ambient line
followed by the full status block), with food 200→197 and water 170→167 —
confirming the earlier ambiguity was purely a test-harness capture-timing
artifact (the score text arrived a beat after the queued idle-chatter
line, past the original script's narrower 3s window), not a real gap in
this lib's netdead/heart_beat handling. §7.113 is confirmed not
present in dfgs2.
Other things checked, no bug
- Board post (both as admin
fluffosand as the new player character): posting, thenread newon a second connection, worked as expected —read newcorrectly reported "no new messages" only when there truly were none, and posts composed with.-terminated multi-line input saved correctly. infowizard command is broken, but this is a *pre-existing, already tracked* issue, not a new finding:work/cmds/wiz/info.lpc:46calls an undefinedlpc_info(), matching the exact failure already recorded in this lib's ownlpcc_fail.log. It's a wizard-only debug command (does not affect ordinary players), out of scope for a mechanical one-line sweep fix during this pass — left as-is, flagging here so a future pass doesn't re-"discover" it as new.- Admin (
fluffos) login,info me, board posting, and staying connected across the whole test window all worked without incident.
Process hygiene for this pass
Two separate driver boots (one for the full playthrough, one short-lived
for the targeted §7.113 re-verification), each killed by exact PID after
testing. All test-account save churn (fluffos.o login/user timestamp
updates, qinwanjun.o, ndsevfive.o login/user saves) reverted/deleted
before commit — none of it reflects a real fix, all of it is test debris.
Files modified this pass
None — this was a verification-only pass, no bugs found to fix.
Correction to round three: board post was NOT actually working — real bug found and fixed (2026-08-19, same day)
The "Board post ... worked as expected" claim in the round-three section
above is wrong. A follow-up deeper live test (raw-byte-level socket
inspection, not just eyeballing decoded text) found post <title> →
type body line(s) → . reliably fails to save the message: the .
terminator is silently swallowed by the driver's default fail message
config (什么?) instead of ending the edit session, and read new
afterward still reports no messages. Reproduced on 3 independent fresh
driver boots, as both fluffos (admin) and a brand-new ordinary player
character, with generous (1.5s+) delays between each line to rule out a
test-harness timing race.
Root cause
feature/user/edit.lpc's input_line() — the callback edit() uses to
drive its "type lines until ." input loop — is declared private:
private void input_line(string line, string text, function callback) {
...
input_to("input_line", text, callback); // re-arm for the next line
}The first input_to("input_line", "", callback) call (from edit()
itself, in the same file) works fine. But the recursive re-arm made
from *inside* input_line() — needed after every line except the last —
silently fails to register whenever input_line is reached via the
object's inherit F_EDIT mixin (i.e. the real, live code path: obj/
user.lpc inherits F_EDIT, and do_post() in std/bboard.lpc calls
this_player()->edit(callback)). The driver then has no active
input_to for the next line typed, so it falls through to normal verb
dispatch, which fails and prints the driver's default fail message
(什么?, per config.fluffos) — this is why the symptom looks like a
generic "huh?" rather than any visible LPC error, and why it's invisible
to debug.log.
Root-caused via bisection with temporary diagnostic commands (added and
fully removed before this fix landed, not part of the final diff): a
byte-for-byte copy of edit()/input_line() pasted directly into
obj/user.lpc (bypassing the inherit F_EDIT mixin) works perfectly;
the exact same code reached through the mixin does not. The only
difference that flips the behavior is the private modifier on
input_line() — dropping it fixes the real file. (Every other
hypothesis tried and disproven along the way: recursive-input_to()
re-arming in general, function-pointer bind()/carryover through nested
input_to(), add_action vs. plain call_other dispatch, closure
created inline vs. passed in as a parameter, §7.86-style replace_program()
poisoning on the board/player class chain — none of these reproduced the
failure in isolation; only "private function defined in an inherited,
separately-compiled file used as an input_to() string-callback target"
does. This looks like a genuine, narrow driver/mudlib interaction quirk,
not a mudlib logic bug — recorded here for anyone else who hits the same
shape, since it doesn't fit any existing AGENTS.md class.)
Fix
-private void input_line(string line, string text, function callback) {
+void input_line(string line, string text, function callback) {One-line change, work/feature/user/edit.lpc. input_line was never a
directly-typeable player command (it's only ever reached via the
internal input_to() continuation mechanism), so dropping private
does not expose any new player-facing capability.
Blast radius: not just the board
grep -rl '\->edit(' work/ shows edit() is also the mechanism behind:
obj/mailbox.lpc— the in-game mail systemcmds/usr/to.lpc— sending mailcmds/usr/chfn.lpc— editing your finger/profile description
All of these were silently broken the same way (any multi-line .-
terminated input session would fail to complete), not just bulletin
boards. The one-line fix repairs all of them simultaneously; mail/to/
chfn were not independently live-tested this pass (board post/read was
the live-verified representative), but they share the exact same
edit()/input_line() code path so the same root cause and fix apply.
Verification
Fresh driver boot, fresh non-wizard player (陈嘉怡/chenjiayi):
post 新人报到 → body line → . → 留言完毕 (success message, was
previously 什么?) → read new correctly shows the posted title,
author, and body text. Re-verified a second time as admin (fluffos)
on a separate fresh boot. debug.log clean (only expected lazy-compile
warnings) across all verification boots.
Files modified this pass (supersedes the "None" note above)
work/feature/user/edit.lpc— droppedprivatefrominput_line()(the real fix; see root cause above).
Deep functional test round four (2026-08-20): the three previously-unverified items resolved
Follow-up pass specifically targeting the three items round one's "Explicitly not verified live" section (above) left open, per §10.7 item 6. All three are now resolved — one real bug found and fixed (economy), one confirmed genuinely-dead/harmless content (missing exit), one confirmed clean with a full real death/ghost cycle exercised live (combat).
1. Shop purchase round trip — completed live, and a real economy bug found and fixed along the way
Did the full intended flow: day-labor at 货栈 → buy → deliver_merchandise
→ handover at 店小二(waiter) in 小客栈. While setting this up, found and
fixed a genuine bug in the wage payment, not present in round one/two/three's
partial testing (which never got past can_afford() rejecting an unpaid
character):
Bug: d/snow/npc/foreman.lpc's relay_say() (the say 工钱 wage-payment
handler) creates a new /obj/money/coin object for exactly the wage owed,
moves it into itself (money->move(this_object())), then does
do_chat((: command, "give coin to " + me->query("id") :)) — an
unquantified give. std/money.lpc/std/item/combined.lpc's
COMBINED_ITEM money objects auto-merge with any same-type stack already
in the destination's inventory when moved into a living() object
(combined.lpc's move() override). foreman.lpc's own create() grants
him carry_money("coin", 220) as flavor starting cash — so the very first
money->move(this_object()) after boot silently merges the wage into his
220-coin personal stash into a single combined object, and the unquantified
give coin to <player> then hands over the entire merged stack (220 +
wage), not just the wage owed. Reproduced live: a character who did exactly
3 crate-loads (15 wen owed) received "总共是十五文钱" as the spoken amount
but their actual i inventory showed 235 wen (220 + 15) after the first
give on a fresh boot. This is a one-time-per-boot windfall (the foreman's
own stash, once given away, isn't replenished — NPCs aren't re-create()d
by room reset()), not a stable duplication exploit, but it's still a real
code defect (an unintended currency leak from an item-stacking side effect
colliding with an unqualified give), not a game-balance/content choice —
nothing about the code's intent is "give the player your entire wallet."
Fix (one line, d/snow/npc/foreman.lpc):
- do_chat((: command, "give coin to " + me->query("id") :));
+ do_chat((: command, "give " + amount + " coin to " + me->query("id") :));cmds/std/give.lpc already correctly handles a quantified give <n> <item>
to <target> by splitting off exactly <n> from whatever stack is present
(creating a fresh split object and reducing the original), regardless of how
the source stack got that large — so this is the minimal, correct fix, not
a workaround.
Verified: fresh driver boot (clears the in-memory foreman's merged
state), fresh character, exactly 1 crate-load (5 wen owed) → say 工钱 →
"总共是五文钱" spoken → i shows exactly 五文钱(Coin), no merge with
the foreman's stash. Re-verified the full 3-crate (15 wen) case on a
separate fresh character in the same boot: i showed exactly 十五文钱,
confirming the fix isn't order-dependent. Then completed the actual shop
round trip on that same character: buy dumpling from waiter → the
delayed deliver_merchandise→handover chat-driven flow correctly fired
("你的牛肉包子来啦!") → final i showed exactly 牛肉包子 + 五文钱
(15 - 10 = 5), confirming both price deduction and item receipt are
correct. debug.log stayed completely empty (no file even created) across
this whole sequence — zero errors.
2. The missing /d/domain/snow2goat exit at d/snow/ngate.lpc — confirmed genuinely dead/harmless content, not a crash bug (live-proven this time, not just static analysis)
Root-caused the exact mechanics rather than relying on round one's static
read. cmds/std/go.lpc's main() does dest = env->query("exits/" + arg)
— feature/dbase.lpc's query() resolves nested-path properties via
_query() then unconditionally runs the result through evaluate(data,
this_object()) before returning, and the driver's evaluate()/
_evaluate() efun (packages/ops/ops.cc) calls the value as a function
pointer if it is one. So ngate.lpc's north exit —
(: clone_object, "/d/domain/snow2goat" :) — genuinely gets invoked
(not just referenced) the moment query("exits/north") is read, i.e. the
instant go north is attempted, guards or no guards.
Traced clone_object()'s actual driver implementation
(vm/internal/simulate.cc): it calls find_object() first, which itself
calls load_object() if not already loaded; load_object()
(vm/internal/simulate.cc:437) stat()s the real file first, and since
/d/domain/snow2goat.lpc doesn't exist on disk, falls through to
load_virtual_object() (the compile_object master-apply / virtuald.lpc
mechanism) — which itself only handles server:arg-shaped virtual paths
(no colon in this path), so it also returns 0. No error() is ever
thrown anywhere in this chain — clone_object() on a nonexistent,
non-virtual path simply returns 0, gracefully, by design (this is standard,
intentional MudOS/FluffOS behavor, not a driver bug). Back up the call
chain: evaluate() returns 0, dbase.lpc's query() returns 0, and
go.lpc's if (!(dest = env->query(...))) return notify_fail("这个方向
没有出路 \n"); fires immediately — the *generic* "no exit this way"
message, not even the exit's own more specific "有问题,请通知巫师处理"
fallback (that fallback is only for a non-string/non-0 dest, e.g. a
non-object return from a working-but-broken closure).
Live-verified without touching the 5 guard NPCs at all, per this pass's
own instruction to prefer a controlled admin test over the costly combat
route: as fluffos (admin), goto /d/snow/ngate (teleport, bypasses
go's intercept() add_action entirely) then
call /d/snow/ngate->query(exits/north) — this calls the room's own
query() exactly the way go.lpc does — returned = 0 cleanly (执行
指令数:837, i.e. it really did run the closure/clone_object(), not
short-circuit). Also independently confirmed clone /d/domain/snow2goat
(the wizard clone command, which pre-checks file_size()) reports "没有
这个档案(/d/domain/snow2goat.lpc) " — the file is genuinely absent, matching
round one's archive-wide search. And for full realism, goto'd to ngate
and typed the literal go north (not the bare north shorthand — this lib
has no direction-alias commands, north alone just gives the driver's
generic "什么?" fail message) — got exactly the documented guard
interception ("官兵将你拦了下来 " / "野羊山最近有盗匪出没,你们走别条路吧 "),
confirming the guards work as designed too. debug.log stayed completely
empty across all of this — zero errors, zero crashes.
Conclusion: confirmed unreachable-but-harmless dead content, not a bug. Even a player who somehow got past all 5 guards would just see "这个方向 没有出路 " and stay put — no crash, no broken state, nothing to fix. Not touched.
3. Real combat and death/respawn — reached without needing the guard-provocation route at all; a legitimate always-lethal path exists for any NPC
Re-examined the "civilized" gating more carefully than round one/two: it
turns out cmds/std/fight.lpc's civilized branch only gates the fight
(sparring) command. cmds/std/kill.lpc — a completely separate, always-
available command, explicitly documented in its own help kill text
("kill 只需单方面一厢情愿就可以成立,因此你对任何人使用 kill 指令都会开始
战斗") — calls me->kill_ob(obj) and (for any non-player NPC)
unconditionally obj->kill_ob(me) right back, with no civilized check
anywhere in that path (feature/char/attack.lpc's kill_ob() has none).
The three accept_kill() overrides found across the codebase
(villager.lpc/fighter.lpc/soldier.lpc) are all void — flavor chat
only, their return value never gates whether the mutual kill happens
(kill.lpc calls obj->kill_ob(me) regardless of what accept_kill
"returns"). So kill triggers genuine lethal combat against literally
any NPC in the archive, civilized or not — the "no non-civilized NPC
exists to fight" framing in round one only applied to the fight command,
not kill.
Still did the broader-archive search the task asked for first, for
completeness: grep'd every set_race( call in the whole tree — every
single NPC in this archive is race human or (2 d/hell NPCs) woochan,
both of which get "civilized" from their race daemon; genuinely zero
beast-race (or any other non-civilized-race) NPC exists anywhere in the
archive, confirming round one's specific claim was accurate as far as it
went — it just wasn't the relevant gate for reaching lethal combat, since
kill bypasses "civilized" entirely.
Picked d/snow/npc/guard.lpc (青衣汉子, level 20, sword/blade/parry/
dodge all 40-70) at d/snow/ebridge.lpc as the target — a F_VILLAGER
NPC (no soldier_assist group-retaliation mechanic, unlike the ngate
garrison/lieutenant), strong enough to reliably kill a fresh level-1
character, reachable with 8 plain go moves from 小客栈 with zero gating.
Live sequence on a fresh level-1 test character (qinlaosan/秦劳动, after
completing items 1 above on the same character): kill guard → mutual
kill_ob() fight proceeded automatically via COMBAT_D's heartbeat-driven
combat with no further player input needed → character's 气/精/神 dropped
turn by turn (30→22→...→18 rounds) → real death: daemon/race/human.lpc's
statistic_destroyed() fired (kee stat destroyed) → die() → userp()
branch → CHAR_D->make_ghost() → character transitioned to
"life_form":"ghost" (confirmed via score: "你现在没有形体,因此没有气,
只有精和神 ", food/water stats deleted and now show 0/0) → a
秦劳动的...体(Corpse) object correctly appeared in the room, both guards
survived (as expected for a losing fight).
Ghost-state behavior verified, no crash anywhere: moved normally as a
ghost (go west worked cleanly, ordinary room-to-room movement), quit
as a ghost correctly refused with a safety prompt ("你现在离开将会失去你
的肉体,无法复活,如果确定要离开请用 'quit !' " — matches
LOGIN_D->reincarnate()'s permadeath-and-reroll design read from code:
confirming a ghost quit is a deliberate, guarded, irreversible action, not
an accident waiting to happen) — did NOT send the confirming quit !,
just closed the raw socket (simulating a link-death) to test the
non-destructive path; reconnecting picked the ghost character back up
cleanly in the same room/state, no corruption, no double-object, score
showed continued natural stat drift (气/神 18/30, further heartbeat-driven
decay while linkdead) — all correct persistence behavior.
Two content-completeness gaps found, NOT bugs (no error signature, consistent with this project's crash-only fix policy), recorded for completeness rather than fixed:
adm/daemons/logind.lpc:437'sif (user->is_ghost()) startroom = DEATH_ROOM;callsis_ghost(), which is never defined anywhere in the entire codebase (grep -rn "is_ghost"finds only this one call site). Acall_otherto an undefined function returns 0 silently on this driver (no error) rather than erroring, so this condition can never be true — dead code, not a crash. In practice this means a ghost's reconnect just resumes wherever they physically are (confirmed above), never redirected toDEATH_ROOM(/obj/void, itself a generic "something broke" safety room, not actuallyd/helldespite the confusing macro name).- No file anywhere outside
d/hell/referencesd/hell(grep -rln "d/hell" | grep -v "^./d/hell/"returns nothing) — the entire 黄泉/地府 afterlife zone (death_start.lpc's"valid_startroom":1"黄泉路" plus ~18 more rooms) has no reachable entrance from the mortal-world map at all.CHAR_D->make_ghost()doesn't move the player there either (just flipslife_formin place). Combined with theis_ghost()gap above, a player who dies has no in-game path to ever reachd/hellshort of wizardgoto— the afterlife content exists but is entirely unreachable, same "smaller/less-finished codebase" pattern already documented for the deadacquire_skillstub in round one. Not fixed, per AGENTS.md's missing-content-is-an-archive-gap precedent (§7.14) — inventing a connecting exit would be fabricating content, not restoring an authored-but-broken reference (unlike item 2 above, where the reference target is provably absent from the source, not merely disconnected).
debug.log stayed completely empty (no file created at all) across the
entire round-four pass — registration, economy, shop, 8-room travel, real
combat, real death, ghost movement, quit-attempt, and reconnect. Zero
errors, zero crashes, at any point.
Checklist items (fast pass, confirming already-applied fixes, not re-deriving)
- §7.90 (
config.fluffoseval-cost):maximum evaluation cost : 5000000— already correct. - §7.100 (live
replace_program(ROOM);):grep -rn "replace_program(ROOM)" work/ --include="*.lpc" | grep -v "^\s*//"— zero live hits, consistent with the tail-sweep completion note above. - §7.111 (
standard_trace()null-guard): already covered by the corpus-wide sweep per round three's note; not re-audited line-by-line this pass. - §7.112 (
death_stage()-style reentrancy guard completeness): this lib's actual death path (std/char.lpc'sdie()/daemon/race/human.lpc'sstatistic_destroyed()) is a different shape than thedeath_stage()class this check targets (nocall_out-based reentrancy window found in the traced path); the two-wave corpus sweep and round three's owninit()-call_outgrep already cover this lib. Live-exercised the real death path this pass (see item 3 above) with zero reentrancy symptoms (no duplicate corpse, no double-die()message, no stuck-fighting state after death). - §7.79 (bare 2-arg
addn()/addn_temp()):grep -rn "addn(" work/ --include="*.lpc"— no hits at all in this lib (this lib doesn't useaddn/addn_temp, it usesadd/add_tempthroughout, e.g.wagon.lpc'sadd_temp("wage_deserved", ...)); not applicable. - §7.108 (
reconnect()should callenable_commands()):obj/ user.lpc'sreconnect()(the real implementation, confirmed by grepping for the function definition rather than assuming a path) already callsenable_commands(); matches round two/three's implicit clean bill (no reconnect-related freeze symptom in any pass including this one's ghost-reconnect test). - §7.25 (
set_skill()referencing a nonexistent skill file, causing an uncaughterror()): checked properly rather than assumed clean. Severalset_skill()names used across NPCcreate()s (twohanded sword,twohanded axe,secondhand dagger,taoism-fire,alchemy-medication/-wealth/-magic/-immortality) do not have an exact matching file underdaemon/skill/(which only has one file per broad category —sword.lpc,dagger.lpc,alchemy.lpc, etc., no per-subskill files). But this lib's architecture is safe against the crash class §7.25 targets, for two independent reasons, both confirmed by reading the actual code: (1)feature/char/skill.lpc'sset_skill()is a bareskills[skill] = val;— no daemon lookup, noload_object/new()at all, so it can never fail at NPC-creation time regardless of whether a matching skill file exists; (2) the daemon lookup only happens later, when a skill is actually *used* (SKILL_D(x)→DAEMON_D->query_daemon("skill:"+x)inadm/daemons/daemond.lpc), andquery_daemon()already wraps itsload_object()incatch()and falls back to returningthis_object()(itself) rather than propagating an error or returning 0 — so even a genuinely-missing skill daemon file degrades to a silent no-op (SKILL_D(missing)->some_method(...)becomes a call to an undefined function onDAEMON_D, which this driver'scall_otherreturns 0 for, not an error) rather than a crash. Confirmed nomake_inventory()/move()-time crash is possible from this angle in this lib — thefyzfqyy/chidishape (uncaughterror()at creation time) requires an architecture where the skill file is loaded synchronously and unguarded atcreate(), which this lib'sset_skill()doesn't do.
Files modified this pass
work/d/snow/npc/foreman.lpc— quantified the wage-paymentgive(item 1's fix, see above).
Process hygiene for this pass
Native build-debug driver, one boot for the admin/ngate investigation
(4 short-lived connections, killed and restarted between the bug-fix and
its verification to clear the in-memory foreman's already-merged money
state), one longer boot for the full economy→shop→combat→death→ghost
playthrough. All killed by exact recorded PID, confirmed via
readlink -f /proc/<pid>/cwd before each kill. Throwaway test-account
saves (qincha/qincheck1-typo'd id rejected/qinboyan/qinlaoerh/
qinlaodong/qindiaocha/qinwage — assorted false starts from id
collisions and one character who happened to roll too little carry
strength to lift a crate on the first try) all deleted before commit;
fluffos.o login/user timestamp churn from the admin ngate tests
reverted via git checkout --. qinlaosan/秦劳动 (the character that
completed all three test items, now a ghost) kept as playthrough evidence,
matching this project's established convention (shenluoy in round one,
etc.) — saves at work/data/user/q/qinlaosan.o /
work/data/login/q/qinlaosan.o.
§7.100 sub-threshold instance (2026-08-20)
Found during the §7.100 tail-sweep (below the original 166-lib survey's
=100-occurrence threshold, never checked). 48 live
replace_program(ROOM); occurrences across 47 room files
(d/hell/*.lpc, d/snow/*.lpc, d/goat/*.lpc, d/choyin/ngate.lpc,
adm/guild/*.lpc) plus this lib's own room-building tool
obj/roommaker.lpc's string-builder template (a tab-indented variant of
the pattern, \n\tsetup();\n\treplace_program(ROOM);\n}\n", that
required a hand fix since it uses tabs rather than the 2-space indent
the mechanical script targets) — so newly-built rooms were inheriting
the bug too. All fixed; a full post-fix grep confirms 0 live occurrences
remain. Verified via a clean native driver boot (zero new debug.log
errors, port listening, killed by exact PID after ~8s).