info.html · 介绍 · play.html · 游玩 · llms.txt · 下载与本地运行 · ZIP · GitHub
自称"书剑飘零Ⅱ",英文名 "Stray Book & Sword",由"飞白工作室"制作,主题是离家闯荡江湖、白手谋生的中文武侠 MUD;角色创建时要选择一种"出生状况"(书香门第/商贾之家/贫寒农家/武力世家),不同出身带来不同的初始属性倾向和专属指令——商贾之家自带"交易"指令,贫寒农家自带"乞讨"指令,比单纯的门派养成多了一层身世设定;死亡/复活场景(如"义庄")与角色真正的固定家园是分开记录的两套数据,暂居死地不会覆盖玩家选定的家。虽然名字里同样带"书剑"二字,但与 `shujian2008`/`sjtx2` 是完全不同、互不相关的两套代码;不过它与同一批档案里的 `sjplii`(书劍飄零II)其实是同一件事的两份快照——两者约 13,073/13,080 个档案路径相同,其中 98.2% 逐字节一致,应理解为"飞白工作室"同一次发布打包成 zip、rar 两种形式的结果,而非各自独立开发的游戏。
English
Self-titled "Stray Book & Sword" (书剑飘零II), made by Feibai Studio, on the theme of leaving home to make one's own way in the jianghu. Character creation asks for a "birth circumstance" — a scholarly household, a merchant family, a poor farming family, or a martial family — each shaping starting stats and unlocking its own dedicated command (a merchant background comes with a trade command, a poor-farmer background with a begging command), layering a backstory choice on top of the usual sect progression. Death/revival locations (e.g. the mortuary) are tracked separately from a player's actual saved home, so a temporary death-site never overwrites it. Despite sharing the words "Book and Sword" in its name, it is a completely separate, unrelated codebase from shujian2008 and sjtx2 elsewhere in this archive — but it is NOT separate from sibling sjplii (書劍飄零II) also in this archive, which carries the identical Chinese title: a file-level comparison found 13,073 of ~13,080 files at matching paths, of which 98.2% are byte-identical, meaning the two are best understood as two snapshots/mirrors of the same Feibai-Studio archive (one shipped as a .zip, one as a .rar) rather than independent games, with only small bugfix-era divergences (e.g. differing log-directory paths) separating them.
README
在线试玩
https://mudlibs.fluffos.info/sjpl2/
管理员账号 / Admin account
- ID:
fluffos - 密码:
Mud@2026 - 中文名: 浮浮
- 权限:
(admin)(本 mudlib 巫师等级表的最高级别) - 权限授予方式:
/adm/etc/wizlist数据文件末尾追加fluffos (admin)。 - 小提示:
update命令不能用来更新它自己(update /cmds/wiz/update.lpc会在执行中把自己 destruct 掉、无任何输出—— 这是原始代码的固有行为),更新其他文件一切正常。
警告:对外公开架设前请务必修改此密码。
本地运行
cd libs/sjpl2
~/src/fluffos/build-debug/src/driver config.fluffos游戏端口:40031。
NOTES · 移植与修复记录
sjpl2 — 「书剑飘零」Ⅱ ("Stray Book & Sword" II)
Archive: 书剑飘零II .zip. Port: 40031. Status: done (boots clean,
full registration flow verified end-to-end including a real Chinese
name).
What this is
"【书剑飘零Ⅱ】", English tagline "Stray Book & Sword", credited to
"飞白工作室" (Feibai Studio), http://sjpl.sq.sh.cn. Mudlib root at the
archive top level (config.wd at the root, not nested in a subfolder).
Uses adm/obj/{master,simul_efun} — despite the similarly-named "书剑"
title, this is a genuinely different, unrelated codebase from
shujian2008/sjtx2 (archives #35/#36, adm/single/ layout):
different directory convention, no named.c daemon, and a securityd.c
with different internal logic. Don't assume shared lineage from a
similar Chinese title alone — always verify via layout/file diff.
~13,022 raw files, 12,349 after .c→.lpc rename.
Fixes applied
1. AGENTS.md §15h (GBK byte-range Chinese detection), standard
shape: adm/simul_efun/chinese.lpc's is_chinese() (GBK lead-byte
range → CJK codepoint range) and adm/daemons/logind.lpc's
check_legal_name() (byte-count bound < 2 || > 12 → character-count
bound < 1 || > 6, removed the i%2==0 even-byte-offset gate). The
name[i..<0] slice syntax was already a clean single-character-to-end
slice — no change needed there, unlike some other libs that used a
multi-byte suffix relying on is_chinese reading only the first 2
bytes.
2. Proactively added get_include_path() to master.lpc (the
standard §15o/§8d shape) as cheap insurance, even though this specific
symptom was never actually observed — the lib booted clean and the
full registration flow worked without it being strictly proven
necessary. Left in as prophylactic protection for any future
mid-connection lazy compile.
3. Confirmed NOT needed, verified via source reading rather than by
hitting and diagnosing a crash:
- §4 (lazy security-daemon load): master.lpc's
valid_write/valid_read already gate via find_object(SECURITY_D)
only (no unconditional load_object).
- §15n (custom-securityd func-discrimination gap): this lib's
securityd.c's valid_read does NOT override the user parameter
with this_player() (unlike shujian2008's), so compile-time reads
(which the driver attributes to master_ob, per
check_valid_path(..., master_ob, "load_object"/"include", ...))
see master_ob's own euid — presumably Root — and hit this
securityd's early if (euid==ROOT_UID) return 1; check before any
directory-based ACL is even consulted. Confirmed by reading the
source rather than needing to reproduce the crash first, per the
lesson learned on shujian2008.
Re-verification pass (2026-07-23) — found and fixed a §15w-class bug
The original pass above never tested a post-login command. Re-testing the
full flow through look/score/quit found adm/obj/master.lpc's
log_error() unconditionally broadcasting every compile *warning* (not
just real errors) to the connected player — wizards saw 编译时段错误:...,
and ordinary players saw an even vaguer but equally alarming
你发现事情不大对了,但是又说不上来。("something's wrong, but I can't
say what") on literally every lazily-compiled file's first warning,
which in practice fired repeatedly right after registration and on every
look/score in a fresh session (AGENTS.md §15w). Fixed by gating both
branches on the message not containing "warning:" (still always logged
to file). Also checked, not a live bug: feature/command_new.lpc has
the exact §15ae private nomask int command_hook shape, but it's dead
code — nothing in the whole lib references command_new by path; the
actually-inherited command feature (F_COMMAND = feature/command.lpc,
confirmed via std/char.lpc) already declares command_hook as plain
nomask, no private. Verified via 2 full registration sessions
(id sjplhh/sjplii, real Chinese names 秦风十四/秦风十五) that
look/score/quit all produce correct real output with zero spurious
"something's wrong" messages after the fix. debug.log clean of
denied/cannot/undefined function/error in error handler in both
sessions.
Interactive test result — full registration flow (original pass)
Verified the complete registration path in one continuous connection:
1. sjplbtest → passes check_legal_id, reaches the "确定吗(y/n)?"
confirmation.
2. y → reaches the Chinese-name prompt.
3. Real Chinese name 秦风 → accepted (no rejection message),
proceeds straight to "请设定您的密码:" — the actual proof the §15h
fix works. No novel-character-name restriction found in this lib's
banned_name list (unlike shujian2008) — just game-title words
("书剑", "书剑飘零", "飘零", "飞白") and pronouns.
lpcc sweep
12,349 files, 11,772 pass / 577 fail (95.3%). Failure tail is the usual
shape (a large syntax-error cluster, missing MONEY_D/prepare_skill
globals, a handful of type-mismatch returns) — not triaged individually
per AGENTS.md §6b/§13, boot + full interactive registration test is the
verification gate. Memory stayed healthy throughout (~15GB free
consistently, no pressure).
Rebuilt-driver / formatter / WASM re-verification pass (2026-07-23)
1. LPC formatter applied across all 12,349 .lpc files in work/:
{"total":12349,"written":12291,"wouldChange":0,"unchanged":4,
"errors":54}. Verified post-format that feature/command.lpc's
command_hook is still plain nomask and master.lpc's §15w
strsrch(message, "warning:") == -1 guard is intact.
2. Native re-test against the rebuilt build-debug/src/driver:
booted clean (zero fatal errors). Full registration verified
end-to-end via mudclient.py: id sjplfmtc → confirm → real Chinese
name 秦风廿二 → password ×2 → email [email protected] → gender m
→ birth-family choice 0 (书香门第) → entered the game world at
民居, look displayed the room, score showed a correct character
sheet (才智/体质/... matching a 书香门第 background), quit dropped
items and printed "欢迎下次再来!". debug.log: zero error in error
handler/denied/undefined function/bad argument lines. (Noted
in passing, not a regression: get_name()/get_resp() in
logind.lpc both contain a pre-existing printf("%O\n", ob);
debug leftover from the original archive that echoes the login
object's /obj/login#N identifier to the player right after a valid
Chinese name is entered — harmless, doesn't block registration,
present before this pass and left as-is per the project's "don't fix
unrelated cosmetic content" convention.)
3. WASM test: boots cleanly (only the expected non-fatal preload
noise — an Invalid utf8 string while restoring emote on
restore_object(), same class of harmless save-data quirk as other
libs, and the usual missing-sockets-package compile errors for any
network daemons). Full registration completed successfully under
wasm: id → confirm → Chinese name → password ×2 → email → gender →
birth-family choice → entered 民居 → look/quit both produced
correct real output, ending with the same "欢迎下次再来!" farewell as
the native session. This lib has no IP-format-dependent login gate
(no sited-style query_ip_number() check blocking new
connections), so it is fully playable under wasm, unlike several
sibling libs in this batch — the best-case wasm result observed this
pass.
WASM-enablement pass (2026-07-24)
Standard four-change pass (AGENTS.md §1.3b/§1.3e/§1.5):
1. Loopback-allow: adm/daemons/logind.lpc logon() (~line 74) —
the BAN_D->is_banned(query_ip_name/query_ip_number) gate is skipped
when query_ip_number(ob) is empty/non-string/127.*.
adm/daemons/band.lpc is_banned() also short-circuits to 0 for
loopback/localhost/malformed sites.
2. Uptime gate / anti-flood throttle: none present at connection
time (the per-command flood counters in feature/alias.lpc are
in-game robot protection, left alone per the KEEP-content-timers
rule).
3. Admin seeded: fluffos / Mud@2026 / 浮浮 → (admin) appended
to /adm/etc/wizlist. Save files: work/data/login/f/fluffos/fluffos.o
and work/data/user/f/fluffos/fluffos.o (per-id subdirectory layout;
data/ not gitignored). Verified: login shows 目前权限:(admin);
update /cmds/usr/score.lpc → 成功; cd/whoami work.
Gotcha discovered while verifying: update on its OWN file
(/cmds/wiz/update.lpc) silently no-ops — main() destructs the
command object it is executing from before reaching the write();
original behavior, not a bug introduced here.
4. Retest: fresh registration (sjplqf/秦风, deleted after test) into the
start room with look/score/quit OK. debug.log: only pre-existing
noise (restore_object(): Invalid utf8 string ... emote from
emoted.o — known; /cmds/wiz/tail uses the never-existed tail()
efun (§6.2 class) — pre-existing, hit only by my probe command).
Retrofit: fail-closed loopback check (2026-07-24)
The loopback-allow gates above were originally written per the (now
superseded) defensive instruction to also treat an empty/non-string/
malformed query_ip_number() result as loopback, since older WASM
driver builds returned garbage. That driver bug is now fixed upstream
(query_ip_number()/resolve() return real values under WASM too), so
the "malformed IP = trust it" fallback was a fail-open bypass with no
remaining justification. Tightened every gate listed above to the
strict pattern: loopback is ONLY ip == "127.0.0.1", ip == "::1", or
a leading "127." prefix — a non-string/empty/malformed IP is now
treated as untrusted/remote and subject to the gate normally, not
silently allowed through. Retested: fluffos login (127.0.0.1, real
value under the current driver) still passes every gate; debug.log
stayed clean of denied/undefined function/error in error handler.
深度功能测试 / Deep functional test (2026-07-24, AGENTS.md §10.7)
One continuous native-driver session (port 40031), following the
distilled §10.7 checklist. Read doc/help/newbie.txt first (a detailed
guide — birth families, ask <parent> about help, hp/score/i,
distribute, the 5 家/宗门 orientation, apprentice, fight/quest
progression, money units, alias). Note: the guide's own text describes
an OLDER Chang'an-centric ("长安") map layout that no longer matches
this archive's actual live map (birth-family homes are in 泰安/山东 and
福州, not 长安, and the nearest reachable sect gate is a hostile
"三才剑阵" guard, not a walk-up master) — a content/documentation staleness
observation, not a programming bug, left as-is.
Bugs found and fixed (2 classes, 4+21 files):
1. NEW instance of the §4.3 "static/…" → "nosave/…" string-literal
collision, previously unnoticed on this lib. The blanket
static→nosave conversion pass also rewrote 24 STRING LITERALS
across 21 files that happened to start with the word static/ as a
log-file path — adm/obj/master.lpc's crash handler
(log_file("nosave/CRASHES", …)), five copies of a promotion-log
write (adm/securityd.lpc, adm/daemons/securityd.lpc/securityd2/
securityd_hb/securityd_new/security.lpc/bak/securityd.lpc/
adm/tmp/securityd.lpc — log_file("nosave/promotion", …)),
cmds/usr/suicide.lpc (+2 bak/ copies), feature/vi.lpc's LOGFILE
macro, and 8 copies of a call/callme audit-log write
(cmds/arch/call.lpc, cmds/adm/call2.lpc, cmds/wiz/callme.lpc
+bak/, cmds/appr/callme.lpc +bak/, u/set/callme.lpc +
Incomplete-callme.lpc, cmds/wiz/bak/call.lpc). Confirmed by
diffing against the raw GBK archive (raw/adm/obj/master.c etc. all
say "static/CRASHES"/"static/promotion"/… — never "nosave/",
which appears NOWHERE in the raw archive) — this is purely a
conversion-pipeline artifact, not original content. work/log/static/
already exists on disk (empty) as the intended destination;
work/log/nosave/ never existed, so every one of these writes was
silently failing (log_file() on a missing directory returns 0 / is a
no-op, so this never crashed anything — it just silently discarded
promotion logs, call-audit logs, the vi edit log, and (worse) the
driver's own crash-dump log on every abnormal shutdown, defeating
its one purpose). Fixed by reverting every "nosave/…" literal in
these 21 files back to "static/…" (verified zero genuine "nosave/…"
literal exists anywhere in the raw archive, so a blanket revert across
all hits was safe). Detected via a stray boot-shutdown log line
("Wrong permissions for opening file /log/nosave/CRASHES...") while
first booting the driver for this pass — not by the standard
grep '"static pre-boot checklist (§2.2), which only greps for the
collision INSIDE source files, not for evidence of it having actually
fired at runtime. Worth adding to the §2.2 on-sight checklist: also
grep the target lib for any "nosave/…" string literal after the
static→nosave pass and diff each hit against the raw archive.
2. §7.24-class bug (new instances, not yet cataloged for this lib):
death/reincarnation code unconditionally overwrites the permanent
"startroom" login-location field, live in 4 files:
d/death/npc/wgargoyle.lpc, d/death/npc/bgargoyle.lpc, and
d/death/inn1.lpc (all three death-realm NPCs/rooms that move a
dead/reincarnating player to a revival location — either
/d/city/chenghuang or /d/fuzhou/duchang, chosen by combat_exp)
did ob->set("startroom", base_name(environment(ob))) right after the
move(), unconditionally, exactly the shape §7.24 (found on zzfy)
describes. Confirmed neither chenghuang.lpc nor duchang.lpc sets
valid_startroom — the legitimate mechanism the lib's own
cmds/usr/save.lpc uses to gate the SAME field
(if (environment(me)->query("valid_startroom")) me->set("startroom", …))
— so this death-path write was bypassing that gate entirely, silently
and permanently relocating every future full login away from wherever
the player actually lived (their family's 民居, or any inn/room they
legitimately saved in) to a random revival slum, with zero
player-facing indication anything about future logins changed. A
FOURTH copy of the identical shape was found in an unrelated context —
u/mojiao/yabian.lpc's "jump off the cliff" shortcut mechanic (two of
its three outcome branches set startroom to the cliff-base landing
room u/mojiao/yadi.lpc, also not valid_startroom-flagged) — same
root cause, different trigger, cataloged together since they're the
same bug class.
Live-reproduced and confirmed fixed, not just source-reviewed:
using the seeded fluffos admin account's call command, force-moved
the live qinfengdt test character (a real, non-wizard, in-session
player object, at the time net-dead/disconnected but not destructed)
into /d/death/gate. wgargoyle's automatic init() → death_stage()
chain fired for real (its !wizardp() skip only blocks wizards, and
qinfengdt is not one), ran its full 5-message/~25s sequence
unattended, and genuinely moved the character to /d/fuzhou/duchang
(confirmed by an admin look at that room, seeing 秦风大同
<断线中> standing there). Checked qinfengdt's "startroom" before
and after: it stayed /d/shandong/ta/minzhai1 (their real family
home) the whole time — the fix holds under the actual automatic
trigger path, not just a compile check. Then had qinfengdt quit
from inside duchang and log back in fresh: landed correctly back at
民居/minzhai1, not duchang — the concrete practical confirmation
the bug class describes (before the fix, this exact sequence would
have permanently rerouted every future login to the gambling den).
All 4 files also reconfirmed to recompile cleanly via the running
driver's own update command (重新编译 …:成功! for each).
Registration / core loop, verified in one continuous session:
registered qinfengdt/真实中文名「秦风大同」, birth family 0
(书香门第), landed in 民居 (/d/shandong/ta/minzhai1). look/score/
i/hp all correct at every state change (after register, after the
family-NPC gift interaction — ask man about help → 中年男士 hands over
旧书+腰带 and silently grants literate skill 2 — after moving through
~9 first-visit rooms in the 泰安/Tai'an zone, after a real fight, after
quit). Explored d/shandong/ta/* (city streets, pharmacy, clothing
shop, grain-store search — none in this city, only in 福州/Fuzhou — 魏家
书局 literate-teacher shop) reading room .lpc source for navigation;
debug.log stayed clean of Too deep recursion/eval cost/
couldn't find object through every first-ever room compile this boot
(no §7.17/§7.19/§7.22/§7.25-shaped reentrancy hit).
Safe-sparring mechanism: found and read (not live-reached — see
below) y/city/npc/peilian.lpc and u/tian/shaolin/npc/muren.lpc
(木头人/training dummy), both genuine accept_fight() stat-mirrors of
the attacker (per §10.7's own detection pattern) — confirmed as designed,
gated at combat_exp ceilings (20000/15000) appropriate for a
mid-progression character, not a fresh newbie. Both sit 15-20+ rooms
from the actual starting zone (Shaolin/Huashan sect grounds, not
anywhere near 泰安/福州), too far to reach live within this session's
budget on a fresh, un-equipped character — not verified live,
disclosed explicitly rather than guessed. As the best available live
proxy, fought the newbie-doc-recommended obj/npc/shangren.lpc
(combat_exp 20, env/wimpy 60, self-surrenders) with wimpy 30 set —
resolved safely (opponent surrendered, own hp/sen barely scratched,
247/250 气血), no errors in debug.log.
Sect-join mechanism: read cmds/std/apprentice.lpc (organic
apprentice <name> → attempt_apprentice() two-step flow) and 3 real
master NPCs' gates: u/taishan/npc/master1.lpc (rejects unless
class == "taishan" already — a genuine two-tier "join a junior
disciple first" design, not reachable directly; its zone entrance
guard u/taishan/npc/dizi.lpc is an ACTIVELY HOSTILE gate that attacks
any non-member on sight, matching the newbie doc's "有些门派…必须通过解
谜才能拜师" — correctly NOT walked into with a fresh, unarmed character),
d/wuhan/npc/master.lpc (海鲸帮/Whale Gang — unconditional accept
for any non-betrayer, genuinely "easy" as the newbie doc claims), and
the death-flow gargoyles (unrelated to sect-join, covered above). All
reachable sect entrances are 20+ rooms of travel from the starting
zone in THIS archive's actual live map (as opposed to the newbie
doc's stale Chang'an-centric description) — not verified live,
disclosed explicitly. The mechanism itself (apprentice.lpc's
input_to-confirmed two-step flow, recruit/attempt_apprentice
pairing) reads as functionally sound; no bug found in it by inspection.
Organic skill-teacher path: reached 魏柄林 (d/shandong/ta/npc/wei.lpc)
at 魏家书局 live and exercised ask wei about study — correctly gated
("你需要先付学费。" / "you need to pay tuition first", matching
do_learnbook()'s !me->query_temp("can_learn") check) since the
fresh character had no coin (no work-verb location exists in this
character's home city — 泰安/山东 — only in 福州/Fuzhou and huashan/etc,
a content-scope observation not a bug). Did not complete an actual
successful lesson (would need real coin, not reachable within budget)
— disclosed explicitly, not presented as fully tested. The
gate/tuition logic itself triggered correctly with no error.
Shop purchase: reached d/shandong/ta/ta-chengyidian.lpc
(成衣店/clothing shop) live, list printed real prices (绸袍 6两银子
etc.), buy choupao from guo correctly rejected with "你的钱不够。"
(insufficient funds) — no crash on the failure path. Full successful
purchase not completed (same money constraint as above), disclosed
explicitly.
Combat/death: real (not staged) combat tested against shangren
(see above, safe-proxy fight). A full player-controlled death (not the
admin-forced repro used to verify the §7.24-class fix) was not
attempted live within this session's time budget — disclosed
explicitly; the death-flow FIX itself, however, WAS verified live via
the automatic NPC-driven trigger path as described above, which is the
same code path a real death takes.
Reconnect / persistence testing (all in one continuous session,
qinfengdt):
1. Prompt net-dead reconnect (socket closed without quit, mid-session
at 魏家书局, reconnected within seconds): resumed cleanly in the same
room, no errors.
2. Clean quit (from 魏家书局, a non-valid_startroom room) → real
~3-minute wall-clock wait → reconnect: correctly landed back at the
real persistent home (民居/minzhai1, NOT 魏家书局 where the quit
happened) — confirms startroom-based persistence works as designed
and that quit doesn't wrongly persist the CURRENT room as home.
3. Net-dead disconnect (no quit, mid-session after moving south) →
real ~2.5-minute wait → (superseded by the admin-driven death-flow
test before a plain reconnect could be separately re-verified at this
exact point — the prompt-reconnect case in (1) already demonstrates
the mechanism works). This lib's net_dead() (obj/user.lpc) never
moves the player to a "void" holding room while disconnected (unlike
the §7.20 class) — the body simply stays in its current room with
heart_beat off — so there is no void-parking stranding risk to
verify separately; confirmed by reading the code.
4. The full NET_DEAD_TIMEOUT (900s / 15 min) user_dump() auto-quit
boundary was not crossed live (would cost 15 real minutes of pure
idle wait) — disclosed explicitly. Mitigated by source review: this
lib's obj/user.lpc user_dump() DUMP_NET_DEAD case calls the
lib's own tell_room() simul_efun wrapper
(adm/simul_efun/message.lpc), which IS correctly varargs-safe
(varargs void tell_room(mixed ob, string str, object *exclude)),
unlike the broken 2-arg-into-a-non-varargs-wrapper shape that caused
dtsl's severity escalation of this same class (§7.12) — so the
specific mechanism that made that bug dangerous (an aborted
user_dump() silently skipping the command("quit") at the end,
permanently orphaning net-dead sessions) does not appear to apply
here. Confirmed live that ~561 ordinary tell_room() call sites
fired without error throughout this session (combat messages, NPC
greetings, room announcements), including from call_out contexts,
reinforcing that this wrapper is safe in practice, not just by
inspection.
Driver-fatal crash — 6th independent occurrence of the AGENTS.md
§10.8 class, NEW fault signature. During this same admin-driven
verification sequence (after confirming the death-flow fix worked and
with debug.log checked clean seconds earlier), the native driver
process (PID recorded, cwd-verified) segfaulted and died outright
— Segmentation fault (Address not mapped to object [0x4a]) — while
running its own ordinary periodic object-reset sweep
(look_for_objects_to_swap → reset_object → some object's reset()
→ the environment() efun → object_visible() dereferencing a bad
current_object pointer). debug.log showed zero trace of this,
exactly matching §10.8's signature (only the driver's own captured
stdout, redirected to a file, revealed it — the same lesson §10.8 already
documents). This is a genuinely NEW fault shape for the class (a raw
SIGSEGV in f_environment/object_visible(), not a debugmalloc
consistency-check abort or a "ref count 0, but not destructed" fatal
like the five prior occurrences), but the same overall pattern: a
driver-internal memory-consistency violation, surfacing during ordinary
ambient driver activity (a ~5-minute periodic reset sweep, not any
single identifiable LPC statement), uncatchable and invisible to
mudlib-level logging. Full C++ backtrace captured to the driver's own
stdout log during the session (not committed as a file, per the
dtsl/shenzhou precedent of describing it in prose here instead) —
available on request by re-running the same sequence with stdout
captured. Honest caveat on causation: this occurred shortly after a
sequence of somewhat unusual admin-call-driven object manipulation
(moving a live, non-wizard, mid-net-dead player object across rooms
programmatically, several call invocations with malformed/unresolved
string arguments that were silently ignored by the target functions) —
unlike ordinary player action, but also not fundamentally different
from what any in-game move()/command() sequence does. Whether this
contributed to, was coincidental with, or is simply the same
low-reproducibility background corruption class documented in §10.8
(triggered eventually by ANY sufficiently long/active session,
regardless of what that session did) is not established — flagged
honestly per the §10.8 precedent's own standard, not attributed to a
specific LPC cause. Driver restarted cleanly afterward for final
verification (see below); no recurrence in the remainder of this pass.
Final clean re-verification pass (after all fixes, fresh driver
restart): registered a brand-new character (sjplfinal/真实中文名
「秦风终验」, birth family 0) through the FULL flow — id → confirm →
Chinese name → password ×2 → email → gender → birth choice → world entry
at 民居 → look/score/i all correct → quit (dropped the cloth,
"欢迎下次再来!"). debug.log fully clean of
denied/undefined function/error in error handler/bad argument/
Too deep recursion/eval cost/couldn't find object. Test character
save data (qinfengdt, sjplfinal, and the earlier pass's throwaway
ids) deleted from work/data/{login,user}/ after testing, per this
project's established convention; the seeded fluffos admin account's
save data and (admin) wizlist entry were left intact and reconfirmed
working (update <path> → 成功) throughout.
Files modified this pass: 21 files reverting the "nosave/" →
"static/" string-literal collision (listed above, item 1); 4 files
removing the unconditional startroom overwrite in death/limbo code
(d/death/npc/wgargoyle.lpc, d/death/npc/bgargoyle.lpc,
d/death/inn1.lpc, u/mojiao/yabian.lpc, item 2). No other code
changes. work/data/login/f/fluffos/, work/data/user/f/fluffos/
show only normal save-churn (mapping key reordering from a routine
save(), no data loss — startroom/combat_exp/admin status all
unchanged) from logging in as admin repeatedly during this pass.
§7.86 跨库扫描修复(留言板 post 崩溃)
BULLETIN_BOARDinherit+ 多余replace_program()致命形状(AGENTS.md §7.86,post命令崩溃):全档案 44 处命中,已删除多余的replace_program(...)调用(保留inherit),逐文件保留原有行尾格式(CRLF/LF 按文件原样)。本次为跨库 §7.86 扫描修复(触发原因:该 bug 已在 6+ 个互不相关的血统家族独立确认,属于近乎普遍的拷贝粘贴模式),仅做编译检查(驱动干净启动、端口正常监听),未做完整 §10.7 深度游玩测试。
深度功能测试第二轮 / Deep functional test round two (2026-08-15, post driver-upgrade re-test)
驱动于 2026-08-12 升级后的重测。标准检查清单发现并修复三处问题:
1. cmds/appr/update.lpc(AGENTS.md §7.106):缺少
environment(me) && 前置防护,补上(cmds/wiz/update.lpc 已是正
确写法)。
2. adm/simul_efun/file.lpc:log_file() 没有 assure_file()
目录预建保护,补上调用及前向声明;cat() 补上
read_file() || "" 空值防护。
3. obj/user.lpc::reconnect()(AGENTS.md §7.108,第七条独立确认
的血统):adm/daemons/logind.lpc 有同款 exec(old_link,
user); 踢掉重复登录写法,reconnect() 缺少
enable_commands()。按 §7.108 记录的写法预防性修复,现场用两个
真实连线复现"保持第一个连线不断开→第二个连线登录→答 y 踢掉旧连
线"验证:score 修复后立即正常显示完整角色档案。
master.lpc::log_error() 已经是正确的 "arning:" 大小写无关写法,
maximum evaluation cost 已经是 5000000,均无需改动;本档案无
adm/daemons/closed.lpc,不受 §7.107 影响。
现场验证摘要
驱动干净启动,管理员 fluffos/Mud@2026 登录确认
目前权限:(admin)(登录横幅带一处"按回车继续"分页符,第一次
update 指令被当作翻页 Enter 吃掉,重发一次即成功验证真实写入权
限)。踢掉重复登录重连路径现场验证通过(见上)。debug.log 全程
干净(338 行,无真实错误)。
本轮修改的文件
work/adm/simul_efun/file.lpcwork/cmds/appr/update.lpcwork/obj/user.lpc
§7.100 房间基类 replace_program() 扫尾修复(2026-08-19)
ROOM 宏(/std/room,见 include/globals.h)在本档案 2,290 处
房间文件的 create() 里紧跟 inherit ROOM; 之后又多余调用了一次
replace_program(ROOM);——AGENTS.md §7.100 记录的同一个休眠 bug(多
余调用给对象打上永久"pending replace"标记,对象一旦绑定任何闭包就
会崩溃)。用 fix_710_room.py(二进制模式,只删除内容严格等于
replace_program(ROOM); 的独立行)扫过 work/,删除 2,280 处标准
形状;另有几处不规则形状手工修复:
obj/roommaker.lpc一处字符串拼接变体str += "...replace_program(ROOM);...";u/losey/roommaker.lpc、u/set/obj/roommaker.lpc、u/gdjk/roomm.lpc、u/gdjk/hongnong/maker.lpc(4 份房间生成工 具副本)以及两份内嵌了同一份工具代码的 NPC 档案u/lark/xiaoyao/npc/{rich,tumu}.lpc、u/jakey/npc/rich.lpc共 7 处一种新变体:if (!mapp(env->query("item_desc"))) str += "\treplace_program(ROOM);";(只在房间没有item_desc——即没有 门——时才拼接这一行,本质上是原作者已经注意到"有门的房间会拒绝 replace"这个驱动行为、但反而把 bug 精确地保留在了"没有门"的多数 房间上)——整行删除;u/lark/yangmingzhai/ss.lpc一处真实房间文件,replace_program紧跟在一段已被注释掉的/* setup() */后面同一行,手工删除。
u/lark/luoyang/dongmen.lpc 里剩下 1 处转档之前就已经损坏、留
着未修:整个文件从 inherit ROOM(无分号)开始就缺失几乎所有语
句分号,git show HEAD 确认这个损坏在本次改动之前就存在,和这次
的 sweep 无关,保持原样。实测走读时另外撞见两个类似的、转档之前
就存在、和本次改动无关的编译错误(y/city/zhuque-n4.lpc 的
NPC_DIR "people/army" 宏拼接语法错误、u/lark/yangmingzhai/ss.lpc
文件开头一个原始 ANSI 转义字节残留导致的语法错误)——两份档案的
git diff 都确认本次改动只碰了 replace_program 那一行,编译错
误是转档遗留问题,不在本次修复范围内。work/data/ 下没有真实
.lpc 源码命中。git diff --stat 显示 2284 个文件净删 2289 行
(脚本 2280 + 手工 9 处),与预期的"2290 处存活 - 1 处保留不修"吻
合;另有 150 处转档之前已注释掉的 // 行原样保留。
驱动干净启动(零新增编译错误、端口正常监听、debug.log 无任何
"cannot replace"/"cannot bind"行),巫师账号 fluffos/Mud@2026
look/goto 走读了 3 个曾经命中过这个 bug 的房间
(y/city/baihu-n1.lpc、y/city/baozipu.lpc、y/city/bingqipu.lpc)
均正常,quit 干净退出,登录存档的 last_on 时间戳增量已用
git checkout HEAD -- 撤销,未落入提交。
§7.30 uninitialized-mapping accessor sweep (2026-08-20)
Corpus-wide mechanical sweep of the feature/skill.lpc shared-lineage
bug (confirmed independently on xiakexing2017/jqxz2015/haiyang2
via round-four testing): 3 accessor(s) in this file returned a raw
never-initialized mapping instance variable (defaults to int 0,
not ([]), until first assigned), crashing any unguarded
keys()/sizeof()/indexing caller for a fresh/untrained character.
Fixed at the accessor level (mapp(x) ? x : ([])) per the documented
remedy. Verified via lpcc --batch static compile check only (not a
live boot) as part of a large mechanical sweep; not individually
functionally re-tested live on this lib.
Round-four deep functional test (2026-08-20) — closed the two NOTES-flagged
travel-distance gaps, found and fixed a real combat-engine crash
Picked up the two items the original 深度功能测试 pass explicitly disclosed
as "read/designed correct but never live-reached due to travel distance
on a fresh character" (safe-sparring against the real training dummy,
and a real sect-join). Used admin goto/call obj->move(path) to
close the distance rather than re-deriving anything already covered.
Item 1 — real safe-sparring against the actual training dummy: REACHED
LIVE, found and fixed a genuine combatd.lpc crash. Registered a
fresh throwaway character (sjpljy/真实中文名「秦风演武」, birth family
0), then admin call sjpljy->move("/u/tian/shaolin/liangong1")
teleported it straight into the Shaolin 练功房 (the room liangong1.lpc
that stocks 2 live clones of u/tian/shaolin/npc/muren1.lpc, the actual
in-room dummy — muren.lpc itself, referenced in the original pass, is
a same-shape but currently-unloaded sibling file; muren1 is the one
genuinely reachable and reset into this room). kill muren started a
real fight. This immediately (first exchange) tripped a genuine
runtime crash, repeated 14 times across the fight —
执行时段错误:*Division by zero at adm/daemons/combatd.lpc:628, in
do_attack()'s combat_exp "bounce" damage-reduction loop, called from
the dummy's own heart_beat() → feature/attack.lpc:attack() →
combatd.lpc:fight(). Root cause: bounce starts at
your["combat_exp"] + 1 and is halved (bounce /= 2, integer
division) each loop iteration; once bounce reaches 1, one more
halving drives it to 0, and the *next* loop-condition evaluation
(absorb_vic / bounce) divides by that zero. This reliably fires
whenever the attacker's own combat_exp is near its 0 default relative
to the victim's combat_exp — exactly the shape of the SECOND dummy
clone in the room (only the one actually targeted by kill gets its
combat_exp mirrored via accept_fight(); the other clone's heart_beat
still joins the fight and calls do_attack() with its own un-mirrored,
near-zero combat_exp) — i.e. a fresh newbie sparring session, the
exact scenario this test was verifying, crashes essentially every time
in the untouched code. This is a real, crash-with-error-signature
programming bug (an integer-division edge case), not content/balance —
fixed by short-circuiting the loop once bounce is exhausted:
while (bounce > 0 && random(100) > absorb_vic / bounce). Verified: update
/adm/daemons/combatd.lpc recompiled clean (重新编译...:成功!), then
continued the SAME live fight (reconnected the test character, kill
muren again) for many more rounds including the wimpy auto-flee
trigger firing safely (看来该找机会逃跑了... → fled to 庭院, 气血
stayed 250/250 the whole time) — zero further Division by zero (or
any other) errors in debug.log after the fix, versus 14 occurrences
before it. Files modified: adm/daemons/combatd.lpc (1 line changed
+ comment).
Item 2 — real sect-join: REACHED LIVE, clean, no bug found. Admin
call sjpljy->move("/d/wuhan/dragon_hall") teleported the same test
character to 海鲸帮总舵 (d/wuhan/dragon_hall.lpc, which loads
npc/master.lpc — 余志枭/Yu Zhixiao). apprentice master → confirmed
with master a second time (the standard input_to two-step
cmds/std/apprentice.lpc flow) → completed for real: 余志枭 accepted
unconditionally (not a betrayer), recruit_apprentice() fired,
character's title changed to 海鲸帮记名弟子, score now shows 你的师
父是余志枭 and "第二代弟子", and the first quest (将长剑找来给我) was
assigned exactly as master.lpc's recruit_apprentice() codes it.
debug.log stayed completely clean through this whole sequence — the
apprentice/recruit mechanism itself has no bug, matching the
original pass's by-inspection conclusion. No files modified for this
item.
Item 3 — skill-teacher tuition follow-through: COMPLETED. With
budget remaining, closed out the previously-disclosed-incomplete 魏柄林
tuition gate. Admin call sjpljy->move("/d/shandong/ta/ta-shuju") +
goto sjpljy to join the character at 魏家书局, clone /obj/money/silver
+ call silver->set_amount(3) (value 300, above the do_learnbook()
ob->value() >= 200 gate), then — since give requires the recipient
to be interactive() or accept via accept_object(), and a net-dead
character satisfies neither — reconnected the test character first,
THEN give silver to sjpljy from the admin side while it was live.
Player-side give silver to wei triggered wei.lpc's accept_object()
("很好,你自己找位子坐下,准备上课"), then ask wei about study ran
the real do_learnbook() lesson end to end: improve_skill("literate",
...), add("potential", 1) (confirmed in score: 当前潜能 99 → 100),
delete_temp("can_learn"). No errors, no crash — the gate and the
lesson both work exactly as designed once genuinely reachable.
Standard checklist pass (all previously-fixed items reconfirmed,
none needed re-fixing): §7.90 config.fluffos eval-cost already
5000000; §7.100 live replace_program(ROOM); grep → 0 hits; §7.111
standard_trace() already has the objectp(error["object"]) ?
file_name(...) : "(driver)" guard; §7.112 — read every exit branch of
death_stage() in all 4 files touched by the earlier §7.24 fix
(wgargoyle.lpc, bgargoyle.lpc have the guard/full reentrancy
pattern; inn1.lpc/yabian.lpc never implemented death_stage() at
all, so the guard doesn't apply to them) — every present/absent/final
branch in both gargoyle files correctly clears
delete_temp("death_stage_active") before returning or before the
final revival move(), no gap; §7.79 addn() — 0 hits in this lib,
not applicable; §7.108 reconnect() already calls enable_commands()
(fixed in the 2026-08-15 round-two pass); §7.30 feature/skill.lpc
accessors already carry the mapp(x) ? x : ([]) guard (fixed in the
sweep immediately above this section).
Test character sjpljy/秦风演武 deleted from work/data/{login,user}/s/
after testing, per convention. Driver killed by exact PID after
confirming cwd matched this lib's work/ directory. Admin fluffos
save data shows only expected churn (silver clone/give, room location)
from this pass.
AGENTS.md §7.19 sweep (2026-09-01): enable_player() reentrancy from init()
Same corpus-wide bug class as mhxy/wuhanzhan (AGENTS.md §7.19): this
lib's feature/command.lpc enable_player() wrapper (around the raw
enable_commands() efun) is reachable from an NPC's init() via a
redundant create()-then-init()-calls-setup() (or reset_me()
calling setup()) chain -- confirmed live via a static scan of every
init() body in this lib: 45 NPC/item files call setup() directly
or via reset_me() from init(), after create() already called
setup() once (which already made the object living()). Calling
enable_commands() a second time on an already-living() object makes
the driver re-invoke that object's own init() as a side effect, which
re-enters this same chain while the original call is still on the
stack -- genuine reentrancy, crashing with "Too deep recursion" (most
likely to surface on an NPC's first-ever preload/compile).
feature/damage.lpc's revive() calls this_object()->enable_player() while the object is still living() (kept alive across the disabled interval by disable_player()'s own internal re-enable_commands()). This confirms a bare if (living(this_object())) return;
guard would be the WRONG fix (it would silently break that legitimate
re-enable) -- used the same true reentrancy-flag fix as mhxy instead:
a nosave private int in_enable_player_now; set for the duration of the
wrapper's body, guarding only genuine same-call-stack reentrancy while
leaving every legitimate re-enable (revive/wakeup/disguise) unaffected.
feature/command.lpc's enable_player() had a single fall-through exit
(no early returns), so one guard-at-top + one clear-at-bottom pair was
sufficient. Verified via a single-file lpcc --batch compile check
(PASS) -- not individually live-boot-tested.