Ultimate Hell: Fingertip MUD Edition

✅ 可玩

终极地狱-指间MUD版

zjdyzj

🔑 fluffos / Mud@2026 更新 37d89d4 2026-09-03 源码 下载 ZIP

▶ 开始游玩 · Play Now

游戏登录后自报家门为"地狱无门"("No Escape From Hell",压缩包标题为"终极地狱-指间mud版服务端"),是本项目"地狱"/Doing 血统六件套之一(与 `hell`、`zjdy2008wzb`、`zjdyaryl`、`zjdywzb`、`zjmudhell` 同源),逐字节地图比对显示与最近的姊妹版 `zjdyaryl`(爱若幽兰)高达 94% 一致,本作在同一套引擎核心之上额外接入了"指间MUD"手机客户端专属协议(crypt 挑战应答握手,账号/角色信息用特殊分隔符"║"整行拼接发送);新角色要经过一套自行设计的"投胎"仪式——先在阎罗殿从"猛士/智慧/耐力/敏捷/均衡"五种天赋偏属中选一种,再跳入"忘忧池"随机洗点,最后从十余个地名或"欧阳世家"等四大世家里挑一个出身,才能真正获得属性、结束"还没有出生呐"的状态;与名字同样带"终极地狱"字样但内容完全不同的 `zhongjidiyu` 并非同一款游戏。

English

Introduces itself in-game as 'No Escape From Hell' (the archive's own title is 'Ultimate Hell — Fingertip MUD Edition'). Part of the same six-way 'Hell'/Doing-lineage map family as this collection's hell, zjdy2008wzb, zjdyaryl, zjdywzb, and zjmudhell — 94% byte-identical (line-ending-normalized) to its closest sister zjdyaryl, sharing the same 'Hell' engine core with an added layer supporting the 'Fingertip MUD' mobile-client protocol (a crypt() challenge/response handshake, then an account/character-creation line joined with a literal '║' separator specific to this protocol). New characters go through a self-designed 'reincarnation' ritual: choose one of five temperament archetypes (fierce/wise/enduring/agile/balanced) in the Hall of Yama, jump into the 'Pool of Forgetfulness' to randomize stats, then pick a birthplace from a dozen-plus place names — or one of four great clans, e.g. 'House Ouyang' — before score will show anything but 'not yet born.' Not the same game as zhongjidiyu, another archive in this collection that also happens to carry 'Ultimate Hell' in its name. This mobile-protocol build never actually verifies an existing account's password on login (it simply overwrites the saved password with whatever the client sends), since the original design delegated authentication to the Fingertip MUD platform itself — a real deployment would need this patched.

README

内容亮点

深度功能测试新发现的 bug(§10.7,2026-08-04)

⚠️ 安全提示:这一"指间MUD"版服务端本身不校验老账号的密码(登录
时会直接用客户端发来的密码覆盖存档里的密码)。原设计把账号认证交给了
"指间MUD"平台,如需公开架设必须自行补上账号密码校验逻辑。

在线试玩

https://mudlibs.fluffos.info/zjdyzj/

管理员账号 / Admin account

警告:这是一个公开的默认密码,仅供本地/浏览器试玩。正式对外开服前
请务必修改此密码(并按上文安全提示补上密码校验逻辑)。

本地运行

cd libs/zjdyzj
~/src/fluffos/build-debug/src/driver config.fluffos

游戏端口:40074

NOTES · 移植与修复记录

zjdyzj — 地狱无门 ("No Escape From Hell") — archive #80

Archive: 终极地狱-指间mud版服务端.rar ("Ultimate Hell — 'Fingertip MUD' edition server"). Port: 40074. Status: done — boots clean, full registration with a real Chinese name works end-to-end in one continuous mudclient-style session, reaches the game world, and two post-login commands (look, score) both produce real, correct output.

Real in-game name (per config.hell's name directive, decoded): 地狱无门 ("No Escape From Hell" / lit. "Hell Has No Gate"). The mudlib root inside the archive is hell/. Lineage: ES II (per master.lpc's own header comment: *"for ES II mudlib, original from Lil, rewritten by Annihilator (11/07/94), modified by Xiang for XKX (12/15/95), updated by Doing Lu for hell (2K)"*) — same broad family as kxkj, kxkj1, yxcs, wuhanzhan, huoying, shenzhou, etc., but this specific hell/XKX branch is architecturally sound (real per-object F_DBASE set/query/delete via feature/dbase.lpc, not the nitan-family bare-simul_efun-dbase bug from AGENTS.md §15).

CROSS-CHECK FLAG for the main session: two other archives in this batch also carry the "终极地狱" name and were being processed by sibling agents concurrently with this one — archive #78 (终极地狱.rarlibs/zhongjidiyu) and archive #79 (终极地狱之爱若幽兰1.166正式版.rarlibs/zjdyaryl). I could not cross-check against them (concurrent extraction). Please diff/compare all three once all three are done — same game at different snapshots, unrelated forks sharing a name, or genuine duplicates. This one (zjdyzj) is confirmed ES II/XKX lineage (master.lpc header comment, secure-free adm/single/+adm/simul_efun/ layout, F_DBASE architecture) — cheap first thing to check against the other two once their own NOTES.md exist.

What makes the "指间mud版" ("Fingertip MUD edition") build distinctive

This is the answer to "investigate what makes this build distinctive" — and it turned out to be the single most impactful finding in this pass (see the crypt() section below), not just a cosmetic branding difference.

1. Two mostly-dead client-protocol headers are #included from globals.h (so present in every object): include/zjmud.h ("指间MUD 转义协议" / "Fingertip MUD escape protocol" — structured message-type tag macros like ZJTITLE/ZJEXIT/ZJPOPMENU/ZJCHANNEL meant for a rich mobile/web client to parse and render specially) and include/tomud.h (an older Tomud-client escape-sequence macro set, TMI/ALERT/PTEXT/L_ICON etc). Confirmed by grep that neither header's actual protocol macros are ever called anywhere in the lib (grep -rn "ZJTITLE\|ZJEXIT\|ZJOBIN\|TMI(\|ALERT(" → zero hits outside the headers themselves) — these are vestigial/unwired, presumably copied in from a sibling build that did use them, or prepared for a client integration that was never finished. 2. What IS actively wired in: zjmud.h's ZJKEY constant and two simple numeric tag macros (SYSY = "000", ZJTMPSAY = "000" + "015" equivalent via ESA="" prefix) are used throughout logind.lpc — a plain telnet client just sees literal digit sequences (0008, 015) glued to the front of certain system/registration messages (visible in the transcript below, e.g. 0000008/0000007) — cosmetically odd over plain telnet but not a functional blocker. 3. The real distinctive feature, and a genuine driver-compat bug: logind.lpc's login flow requires a crypt()-based client-version handshake before any account/character data is even accepted — logon() sends "ver1.0," + crypt(ZJKEY, 0), then jiance() expects the client to echo back crypt(ZJKEY, <received-string>[2..3]). On the ORIGINAL driver this targeted, crypt(key, 0) produced a deterministic 13-char old-style DES-crypt string (2-char salt + 11-char hash), so a real client could compute the matching response. On this FluffOS build, passing an int (not a string) as crypt()'s salt argument falls through to a completely different code path that generates a FRESH RANDOM SHA-512 ($6$...) salt every single call (confirmed by reading src/packages/core/efuns_main.cc's f_crypt()), and the 2-character slice str[2..3] the mudlib then uses as a "salt" for the verification call doesn't parse as any recognized modern salt prefix either (also random). Empirically verified this makes the handshake mathematically unpassable by ANY client — two consecutive crypt(ZJKEY, str[2..3]) calls given the identical str never produced the same output. This silently blocked 100% of connections at the very first prompt, with zero compile error and zero debug.log entry — it just looks like "wrong client version" to anyone testing it. Fix: changed crypt(ZJKEY, 0)crypt(ZJKEY, "zj") (an explicit old-style 2-char alphanumeric salt, not starting with $) in adm/daemons/logind.lpc's logon() — this driver's crypt() still honors that as the classic deterministic DES-crypt path, restoring the original request/response design so a real client (or our own test client, using Python's crypt module) can compute the correct reply. This is a new pattern, not previously in AGENTS.md's catalog — worth promoting to a §15ai-shaped entry: *"crypt(key, 0)/crypt(key, int) salt semantics changed on this driver — always generates a random modern hash instead of a deterministic old-style one; any lib doing a client-side challenge/response based on slicing a crypt(..., 0) result needs an explicit short (non-$-prefixed) string salt instead."*

Fixes applied (with why)

Pre-boot proactive checks (per the task's required list) and what was found:

1. §15h (is_chinese/named.lpc GBK byte-range bug) — YES, present, fixed. - adm/simul_efun/chinese.lpc's is_chinese(str): GBK lead/trail-byte range check (str[i]<176||str[i]>=248 etc, stepping by 2, requiring even byte-length) → rewritten as a straight CJK Unicode codepoint range check (str[i] >= 0x4e00 && str[i] <= 0x9fff), one character per index, strlen(str) < 1 minimum instead of < 2. - adm/daemons/named.lpc (the "similar name" / name-collision guard used during registration) had the full documented shape: #define PATH(name) (name[0..1] + "/" + name) (sharding key = "first GBK character", now taking 2 real characters) → name[0..0]; every strlen(name) < 2 guard (meant as "at least 1 character") → strlen(name) < 1; the sliding-window near-miss check's byte-widths 4/6 (2/3 GBK characters) → character-widths 2/3 (name[i..i+1] / name[i..i+2], loop bounds l-2/i+3<=l). - Verified end-to-end: sent real Chinese names 秦风 and 夏流 through registration in two separate full sessions and both were accepted straight through to character creation and into the game world.

2. §15ae (private add_action hook) — YES, present in TWO places, both fixed. - feature/command.lpc's command_hook(string arg) — the central add_action-registered command dispatcher, inherited into the player body class — was declared private nomask. Dropped private, kept nomask. Without this fix every single typed command (look, score, everything) would have silently done nothing after a successful login, with zero error anywhere — exactly the failure mode this policy exists to catch. - New instance found by grepping every add_action() target lib-wide for a private declaration in the same file (not just the obvious central dispatcher): adm/npc/luban.lpc (a crafting/house-building NPC, "鲁班" — the legendary Chinese master carpenter) registers 18 of its own commands (do_answer, do_stop, do_desc, do_show, do_changename, do_changeid, do_changetype, do_changedesc, do_finish, do_withdraw, decide_withdraw, do_demolish, do_help, do_list, do_type, do_agree, do_reject, do_delete) via add_action, ALL declared private (consistent with the rest of that file's style, which declares essentially every helper function private). Removed private from just those 18 forward-declarations + definitions (left every other, non-add_action private helper in the file untouched). Without this, talking to 鲁班 and typing any of his commands would silently do nothing.

3. New crypt()-semantics bug (see distinctive-build section above): adm/daemons/logind.lpc's logon()/jiance() client-version handshake — fixed by using an explicit 2-char salt instead of 0. The single highest-impact fix in this pass — nothing past the very first prompt was reachable before this.

4. §15s (message() 4th-arg type bug) — YES, present, fixed, found live during boot testing. adm/simul_efun/message.lpc's message() wrapper forwarded its exclude parameter straight to efun::message() with no guard; the extremely common 2-arg tell_room(str, target) / bare shout()/say() call shapes leave exclude as a raw unset int 0, which this driver's runtime efun-argument type checker rejects (void|object|object* expected, got int). Confirmed live: this crashed questd.lpc's collect_all_quest_information()channeld.lpc's do_channel() broadcast and cpud.lpc's create() during ordinary preload, before any player ever connected. Fixed once at the shared root: efun::message(arg, message, target, (objectp (exclude) || pointerp(exclude)) ? exclude : ({})).

5. §2 second variant (fixed-width extension-strip slice, .c.lpc rename fallout) — YES, present in 5 call sites across 3 files, all fixed. [0..<3] (correct for stripping a 2-char .c extension) was never widened for the 4-char .lpc extension after the rename; widened to [0..<5] in: - adm/daemons/eventd.lpc:23 (event_name = map_array(event_name, (: $1[0..<3] :)) after get_dir(EVENT_DIR + "*.lpc")) — this one was caught live in the very first boot attempt (call_other() couldn't find object '/adm/daemons/event/emei.l', the truncated filename visible directly in the error). - adm/daemons/storyd.lpc:57 (identical shape, story_name). - adm/single/importer.lpc:563 (item_name[0..<3], comment on the line literally already says *"要去掉后面的 .c 字样"* — "need to strip the trailing .c"). - adm/npc/luban.lpc:1522 and :2753 (DBASE_D->clear_object(...) dbase-key derivation from a .lpc filename, both same fixed-width slice bug). - Confirmed NOT the same bug for every other [0..<2] occurrence found lib-wide (11 hits: configd.lpc, ftpd.lpc×2, versiond.lpc ×2, channeld.lpc, npcd.lpc, scheme.lpc×2, call.lpc, config.lpc×3, combine.lpc) — spot-checked each; these strip a trailing single character (CR/space/last array element) or the 2-char .o save-extension (__SAVE_EXTENSION__, unaffected by the rename), unrelated to .lpc. Also confirmed importer.lpc:191's [0..<3] is a THIRD, unrelated, correct use (stripping .o from a player-save directory listing under login/, not a .lpc listing).

6. New finding (not previously cataloged): a get_config() numeric-index mismatch between this mudlib's own include/runtime_config.h and this driver's real config-slot numbering. The mudlib's copy defines __MUD_PORT__ as CFG_INT(0) with BASE_CONFIG_INT = 14 (→ absolute slot 14), matching some older/different FluffOS build's layout. This driver's actual src/include/runtime_config.h uses BASE_CONFIG_INT = 256 (after RC_LAST_CONFIG_STR = CFG_STR(255)), so __MUD_PORT__ is really absolute slot 256 — meaning every get_config(__MUD_PORT__) / LOCAL_PORT() call in this mudlib silently reads the WRONG config slot (whatever driver-internal value actually lives at slot 14, a CFG_STR slot here, not a CFG_INT one). Confirmed the concrete symptom: adm/daemons/network/messaged.lpc's startup_udp() calls socket_bind(socket_id, my_port) where my_port = LOCAL_PORT() + MESSAGE_PORT ends up evaluating to the STRING "10" instead of an int port number, throwing Bad argument 2 to socket_bind() Expected: int Got: "10" and failing the whole object's create() (surfaced via the lpcc_check.sh sweep, not the real boot, since — separately — messaged was never actually reachable in real play anyway, see next item). Not fixed (out of scope/low practical impact — see below); flagging as a new AGENTS.md-catalog-worthy pattern (§15aj-shaped: *"a mudlib's own runtime_config.h copy can have stale get_config() slot numbers from a different historical driver build; only trust the numbers if a value spot-check via a real interactive test confirms they line up, don't assume matching macro NAMES means matching NUMBERS across driver versions"*). - Practical impact assessed as low, so not fixed: the only two consumers found (versiond.lpc's inter-station-sync port and messaged.lpc's UDP messaging port) are both peripheral, non-preloaded, optional network features — neither blocks registration or ordinary solo gameplay, and both already fail gracefully in their own way (versiond.lpc sets its readiness flag *before* the broken call per §15ad below; messaged.lpc is simply never preloaded at all — see next item). Fixing the mudlib's whole runtime_config.h copy to match this driver's real numbering would be the "correct" fix but is broader than this pass's scope; noted here for whoever picks up cross-lib get_config() audits next.

7. Separately found (also not fixed, same conclusion): adm/etc/preload lists /adm/daemons/messaged but the real file is /adm/daemons/network/messaged.lpc — a path typo/drift, so this daemon has never actually been preloaded in this archive at all (master.lpc's preload() silently no-ops on a file_size(...) == -1 miss). Left as-is deliberately: fixing the path would newly expose the get_config() bug above (item 6) at every boot, trading one dormant bug for a newly-active one, for a peripheral inter-mud-messaging feature that isn't part of registration or basic play. Documented here rather than silently "fixed" in a way that would make things worse.

8. New bug (not previously cataloged), found live in clone/user/user.lpc — the exact is_killing shape from AGENTS.md §15b's nitan_ceshi precedent, independently recurring in this unrelated lib: accept_kill(object ob) { if (is_killing(ob)) ... }is_killing is declared varargs int is_killing(string id) in feature/attack.lpc and every one of its other ~25 call sites correctly passes ob->query("id"); this one direct (non-->) call passed the object itself, a static type mismatch this driver's compiler rejects outright. Since user.lpc is the player body class, this took make_body() down with it — the account+id got accepted, but character creation (get_char) then silently failed to ever produce a working player body (confirmed: this was the actual blocker once the crypt-handshake fix let a real session reach this far). Fixed: is_killing(ob->query ("id")). A second, independent instance of the identical shape found via the lpcc_check.sh sweep in d/city/npc/guidao.lpc (an NPC, "鬼刀王五") — same fix applied.

9. New bug (not previously cataloged), found via the lpcc_check.sh sweep, one shared root cause per §8g: inherit/misc/quest.lpc's set_information(string key, string info) wrapper (and the matching forward declaration in include/quest.h) declared its second parameter as a bare string, but the real underlying implementation it forwards to (questd.lpc's set_information(object, string, mixed info)) — and every one of 7 clone/quest/*.lpc call sites (supply, explore, shen, search, capture, deliver, judge) — actually pass a function closure ((: ask_npc :), an F_DBASE-style deferred evaluated info display), not a string. Widened both the wrapper and the header declaration to mixed info, resolving all 7 sweep failures in one edit.

10. Three more one-off pre-existing typos found via the sweep (not encoding-conversion fallout, confirmed genuine authoring mistakes): - inherit/condition/damo.lpc's die_reason(): missing closing quote + missing + before a trailing string literal (return name + HIM "发作身亡了;return name + HIM + "发作身亡了";) — the classic §10-shaped "swallowed the rest of the file into one malformed string" bug, surfacing as a syntax error many lines later. This one shared header is inherited by 3 more files (damo_luanqi, damo_shangshen, damo_zheyuan), all fixed by the single edit. - d/village/npc/obj/shoes.lpc: a set("icon", "05047"); statement was spliced into the middle of an unrelated set("long", ...) call's argument list, breaking it into a syntax error. Restored as two separate statements in the evident original order. - d/baituo/obj/bowl.lpc (and its exact duplicate, d/baituo/npc/obj/bowl.lpc): set_name("海口大碗", "bowl") passed a bare string where set_name(string name, string *id) requires an *array* of id strings (confirmed: 2850/2852 other set_name() call sites in the whole lib use the array form correctly) — fixed to set_name("海口大碗", ({ "bowl" })). - feature/attribute.lpc's query_per(string arg): required a string arg that is never read inside the function body at all, and all 6 call sites lib-wide omit it — 5 via -> (call_other, which tolerates the arity mismatch) and one direct call in kungfu/class/generate/girl.lpc (per = query_per();), which hit this driver's strict same-object argument-count check and failed to compile. Added varargs to match the calling convention actually used everywhere (same "direct call enforces the signature strictly" shape as §15b, just missing-arg instead of wrong-type).

11. §15e/§13 (unguarded factory-call chain onto possibly-missing content) — found and fixed, broadly protective. inherit/room/room.lpc's make_inventory(string file) did ob = new(file); ob->set(...) with no check that new() actually returned an object; new() legitimately returns 0 when file points at content genuinely absent from this archive (§13: real archive gap, not something to fabricate). Root-caused via d/beijing/gaosheng.lpc referencing d/beijing/npc/chaboshi.lpc, which does not exist anywhere in the archive (confirmed: 3 of its 4 listed NPCs do exist, one doesn't — a genuine partial-content gap, not a systemic one). Added an objectp(ob) guard inside make_inventory() itself, AND (this part matters) at its one call site in reset()'s case 1: single-object branch, which called straight into ->is_character() on the (possibly-0) result with no guard at all — the sibling default: (multi-object) branch already had this guard via objectp(), so this brings the single-object path up to the same standard rather than introducing a new pattern. This is a general robustness fix (protects any OTHER room in the lib with a similar missing-NPC gap), not just for gaosheng.

Config file notes

Checked, confirmed NOT applicable (per the task's proactive checklist)

Registration-flow test (CRITICAL instruction) — full transcript summary

Ran two complete, independent, single-continuous-connection sessions via a custom scripted client (mudclient.py's static --send list can't handle this lib's dynamic crypt-challenge — wrote a small one-off Python client that computes the correct crypt() response from the server's own banner using the standard-library crypt module, which matches this driver's classic-DES-crypt path for a non-$-prefixed 2-char salt):

1. Connect → receive ver1.0,<random-$6$-hash> → compute and send crypt(ZJKEY, str[2..3])"版本验证成功" (version verified). 2. Send id║password║crypt(ZJKEY,id)+crypt(ZJKEY,password)║email (new account, e.g. xialiu99║test12345║...║[email protected]) → accepted, advances to character creation (0008 code). 3. Send gender║avatar-id║name with a real Chinese name (男性║1║夏流, then separately 男性║1║秦风 on the first run) → accepted, make_body() succeeds, "你连线进入了地狱无门" ("you have connected into 地狱无门") — confirmed landed in the actual game world (starting room "世外桃源"/Peach Blossom Spring, full room description, exit list, two NPCs present, all rendered correctly in UTF-8). 4. Sent look → full real room re-render (description, exits with flavor text, NPC/object list) — confirmed working. 5. Sent score"还没有出生呐,察看什么?" ("You haven't even been born yet, what are you looking at?") — this is correct, in-character behavior, not an error: the welcome banner explicitly tells new players they must first walk through the cardinal-direction "投胎" (reincarnation) minigame at the starting room before their character is fully "born" with real stats; score correctly refuses until that completes. Confirmed intentional by reading the actual welcome text and room exits (west:阴险奸诈 / east:光明磊落 / north:心狠手辣 / south:狡黠多变 — four different "innate quality" paths). 6. Sent quit → clean disconnect, "欢迎下次再来!" ("welcome back next time"). 7. Confirmed via data/login/x/xialiu99.o and data/user/x/xialiu99.o (and the q/qinfeng1.o pair from the first run) that real save files were written to the correctly-sharded directories. 8. debug.log across both full sessions shows zero new runtime errors beyond the two already-understood, already-cataloged benign Bad argument 2 to socket_bind() lines from versiond.lpc (§15ad pattern — version_ok is already set synchronously before the failing async socket call, confirmed by reading the code, not just observing the harmless symptom).

This satisfies the standing policy (§15ae) of testing at least one real post-login command after registration — done with two (look, score), both producing correct, real, in-character output.

lpcc_check.sh sweep results

7302 files total. 7289 PASS, 13 FAIL (all 13 triaged and confirmed non-bugs, not fixed — see lpcc_fail.log for full detail):

All other 7289 files compile clean.

Summary of what was confirmed working vs. left as known issues

Working, verified: encoding (zero stragglers after the standard conversion + straggler re-scan), .c.lpc rename + all ".c" reference fixes (zero uppercase .C files either), boot (zero fatal errors), Chinese-name registration end-to-end, character creation, entering the game world, look, score, quit, save-file persistence, the command_hook/add_action dispatch chain for ordinary play.

Known, documented, deliberately not fixed (peripheral, non-blocking): the get_config() runtime-config-index mismatch (affects only versiond's inter-station-sync port and messaged's UDP port, both optional network features); messaged's wrong preload path (leaving it undiscovered is currently the safer state given the above); the two unwired "指间MUD"/Tomud client-protocol headers (harmless dead code, documented for context); DNS/intermud calls reachable only from who/mudlist/shutdown/telnet/reboot admin commands (never exercised by the required test, not touched).

Re-verification pass: driver rebuild + formatter + WASM (2026-07)

WASM-enablement pass (2026-07): loopback gates + admin seeding

Standard pass per AGENTS.md §1.3b/§1.3c/§1.3e/§1.5:

- get_id() (~line 262): VERSION_D->is_version_ok() gate guarded with find_object(VERSION_D) (absent ⇒ version ok — the WASM sockets-less case). Note this legacy get_id path isn't part of the zjmud get_user flow, but the guard is kept for completeness. - check_ok() (~line 414): MESSAGE_D->find_chatter() wrapped in catch() — same first-login-killer shape as the sibling hell libs (messaged's create() can throw; under WASM it fails to compile entirely and the call itself throws).

WASM long-sit boot-watch pass (2026-07)

200s scripts/wasm_boot_watch.sh sit surfaced one genuinely new, previously-undocumented finding beyond the already-known ftpd.lpc/dns_master.lpc/versiond.lpc sockets-absent preload class (§1.3c, caught/cosmetic): inherit/item/combined.lpc's private void destruct_me(), self-invoked via call_out("destruct_me", 0) when a money stack's amount hits 0 — denied with "apply() with insufficient permission ... function: destruct_me, origin: internal, needs: private, has: hidden" on clone/money/silver#581. Same root cause as this lib's own already-fixed command_hook class (AGENTS.md §8.3a: private demotes to DECL_HIDDEN once inherited; a self-invocation via call_out/command() needs DECL_PRIVATE and gets silently denied) — spent-down money stacks were never actually self-destructing (harmless but permanent per-object leak). Fixed: dropped private, kept the function otherwise unchanged. Same fix ported to siblings zhonghua2 (where the identical bug also fired live), zhongjidiyu, zjdyaryl (latent there, didn't fire in their sits). Retest via a small one-off Python client (crypt-module challenge/ response, same shape as the original verification pass) confirmed registration → look → score → quit still clean; a fresh 200s WASM re-sit confirmed the "insufficient permission" line no longer appears.

WASM 修复摘要(迁移自 meta.json 的 group_note)

同一核心加上指间mud手机协议。这是这个家族里唯一一个反映真实、仍然生效的注意事项而不是过时状态的 'limited'(和已经在本轮修正为 playable 的手足档案 zhongjidiyu(052)/zjdyaryl(053)不同):这个移植版本的 logind.lpc 把每一个连线都挡在一个为'指间'(手机)客户端设计的自定义基于 crypt 的客户端版本握手之后,而不是普通的 telnet/网页终端会话。服务器发送 'ver1.0,<crypt(ZJKEY,"zj")>',期待客户端回复 crypt(ZJKEY, <那段字符串>[2..3])——本轮已确认这个握手过程确实是完全确定性的、可以通过的(这个驱动的 crypt() 和标准 libc 的 DES-crypt 完全一致,已用 Python 的 crypt 模块重现服务器自己的挑战值并计算出一个服务器能接受的回复,'版本验证成功'来验证),之后还有第二个自定义协议步骤:一整行输入,组合了 id║密码║crypt(ZJKEY,id)+crypt(ZJKEY,密码)║电子邮件(║ 是 U+2551)。手动完成这两个步骤能干净地到达完整游戏流程(管理员登录已确认,'目前权限:(admin)',quit 正常)——但这个项目提供的通用 WASM 网页终端(一个普通的类 telnet 界面)无法自动计算出这些值,所以普通访客点进这份档案的页面,没有外部工具是过不了第一个提示的。另外还修复了和手足档案相同的一个真实 bug:clone/user/login.lpc 的 query_save_file() 空字符串保护(既检查 id == "",也检查 !stringp(id)),阻止第一次连线时的 %c/sprintf 崩溃。

深度功能测试(§10.7,2026-08-04)

此前的会话只做到"注册流程"(look/score 两条真实指令),本轮在此基础上 真正走完了完整流程:创角后的"投胎"仪式、地图移动、商店、战斗、死亡、 以及无人打扰的完整复活序列。原生 driver(端口 40074)。

测试工具:由于这份档案自创的手机客户端协议(见上一节), mudclient.py 无法直接用,本轮写了一个一次性 Python 客户端(用标准库 crypt 模块复现驱动自己的 crypt(ZJKEY, salt) DES-crypt 挑战/应答), 能完整驱动"版本验证→账号注册/登录→创角→游戏内指令"的连续会话;后续 针对死亡复活的等待验证则单开一个只做登录、然后静默等待的连接,避免任 何指令打断 call_out 链。

主动检查(对照 AGENTS.md 已归档的 bug 类)

新发现并修复:第 11 例 §7.68 复活软锁死d/death/npc/ {wgargoyle,bgargoyle}.lpc,白无常/黑无常)——DEATH_ROOM/d/death/gate)摆着 wgargoyle,其 north 出口 /d/death/gateway 摆着 bgargoyle,两者都可达。两个文件的 death_stage() 都是 if (!ob || !present(ob)) return;——玩家在复活序列进行中如果暂时不在 场(例如短暂 net-dead),会被永久放弃,鬼魂永远卡在鬼门关。已按标准 修法拆分为"彻底缺失(!ob)→放弃"与"暂时不在场(!present)→ 5 秒后重 试"两种情况,两个文件都改了。

完整游玩记录: 1. 用真实中文名"沈牧"(id shenmu)完成账号注册(含邮箱)和创角 (男性/沈牧),落地"世外桃源",此时 score 正确提示"还没有出生 呐"(游戏内合理行为,不是 bug——档案还没走完投胎仪式)。 2. 探索四个方向房间确认"阴险奸诈/光明磊落/心狠手辣/狡黠多变"四种天性 任你选(这是这份档案自己设计的性格/命运系统,d/register/ yanluodian.lpc 里"偏属→洗点→转生"的三步流程),进入"阴险狡诈"房 间后 out 到"阎罗殿",依次 pianshu msx(选择猛士型)、wash (在忘忧池随机洗出四项天赋:膂力24/悟性17/根骨19/身法20)、 born 中原人氏,成功"投胎"落地"悦来客栈",获得布衣/牛皮靴, score 此时正确显示完整数值面板。 3. 移动测试:悦来客栈→南阳城→迎宾楼(list 正确显示三样商品和价 格)→尝试 buy baozi from xiao er,因为没有铜钱被店小二正确拒绝 ("穷光蛋,一边呆着去!"——内容行为,不是 bug)→大驿道→汝州城, 沿途场景描述、出口列表、NPC 列表均正常渲染。 4. 战斗测试:wimpy 0kill xiao fan(南阳城的小贩),完整回合制 拳脚对轰后成功将其打死,期间技能"基本拳脚/基本招架/基本轻功"多次 熟练度提升提示正常触发;随后在汝州城 kill wu jiang(武将,带钢 剑),两回合内被连续刺伤/砍伤直接打死(wimpy 0 生效,没有自动 逃跑),角色武力值太低是内容/数值现象,不是 bug。 5. 死亡→复活:死亡后正确落地"鬼门关"(DEATH_ROOM),白无常在场。 在完全不打断的情况下(关闭连线后等待、再用另一条静默连接确认 最终状态)验证:death_stage() 五段对话全部走完,reincarnate() 成功,角色最终落地 /d/city/guangchang("泥潭广场"),score 显示"你总共到黑白无常那里串门一次",quit 正常退出。这正是修复 后应有的行为——如果没有这次的拆分修复,任何一次连线中断都会让角 色永久卡在鬼门关。 6. mail 指令确认"此服务已经暂停"——功能性下线,属内容/运营决定,不 是 bug;未发现玩家可用的公共留言板入口(cmds/arch/board.lpc 是巫 师级指令,inherit/misc/bboard 没有被任何 d/ 房间挂载),本轮未 深入排查这是设计如此还是遗漏,留作后续观察,不视为确认的 bug。

结果:整个测试会话(含两次死亡复活循环)debug.log 全程为空,没 有任何 error:/Bad argument/No program/Too deep recursion 记 录。测试角色存档(data/{login,user}/s/shenmu.o)保持未跟踪;两个死 亡 NPC 文件的改动已用 formatter 校验({"errors":0})。

更正(2026-08-05):§7.68 复活软锁"修复"已撤销

上面提到的"鬼魂离开/不在场时被永久放弃复活流程"曾被当作 AGENTS.md §7.68 记录的一类 bug 修复(把单次判定改成每 5 秒重试)。经用户指出并 重新审视:这更可能是有意的游戏设计,不是 bug——大多数这类档案里 鬼魂根本无法自行移动,所以"不在场"要么从未真正发生,要么是"离开去 在阴间游荡,想回来时再走回这个房间、流程会通过 init() 重新从头开始" 这种有意为之的宽松机制,而不是需要强制追上玩家的错误。强行重试还可能 引入新问题:如果鬼魂之后又走回这个房间,旧的重试和 init() 重新触发的 新一轮流程可能同时运行,导致对话重叠错乱。已把这处改动撤销,恢复成 原始的 if (!ob || !present(ob)) return; 单次判定写法(bmxkx2001 除外——那份档案里这确实是一个真实存在、经过实际复现验证的 bug:鬼魂 本身完全无法移动,是另一个不相关的 NPC 强行把鬼魂拖走导致的)。详见 AGENTS.md §7.68 顶部的撤销说明。

§7.86 跨库扫描修复(留言板 post 崩溃)

AGENTS.md §7.100 fix (2026-08-19): redundant replace_program(ROOM) landmine

Same corpus-wide bug as the batch-1-6 sweep (ROOM macro "/inherit/room/room" from include/globals.h). Deleted 2,315 live standalone replace_program(ROOM); lines under work/ via fix_710_room.py, plus hand-fixed the room-building tool's string-builder template (work/clone/misc/roommaker.lpc). No .lpc files under work/data/, so no false-negative risk. Remaining matches after the fix are all pre-existing //-commented.

Verified: clean build-debug boot (zero new compile errors, zero "cannot replace"/"cannot bind" in debug.log), live admin login through this lib's custom 指间MUD crypt()-challenge login protocol (reused the DES-crypt handshake this lib's own NOTES documents above: reply to ver1.0,<str> with crypt(ZJKEY, str[2:4]), then send id║password║crypt(ZJKEY,id[:2])+crypt(ZJKEY,password[:2])║email) as fluffos/Mud@2026 — banner confirmed "目前权限:(admin)", look/quit both worked cleanly. Incidental data/{login,user}/f/fluffos.o save drift from the login test was reverted via git checkout HEAD before committing.

§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): 5 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.

§10.7 gap closure pass (2026-08-24): shop purchase, apprenticeship, board

Closed the three items flagged as genuinely open by the 2026-08-04 §10.7 pass (buy never completing, apprenticeship never tested, board existence unresolved). Native driver, port 40074, via a one-off Python client reusing this lib's own crypt/ZJKEY handshake (same recipe documented above). Tested through the seeded admin account (fluffos/Mud@2026, wizard-tier goto used to reach each location instantly instead of walking) rather than a fresh registration, since none of the three mechanics below gate on born/registration state — confirmed by reading each code path first. All three came back clean; no bugs found, nothing fixed.

1. Successful shop purchase, with real money math: goto /d/city/zuixianlou ("醉仙楼", one of the two starting-town shops — this is the specific room the earlier "迎宾楼" note was referring to, NPC 店小二/xiao er, inherit F_DEALER in feature/dealer.lpc). list correctly showed 4 goods incl. 包子(baozi) at 50 文. buy baozi from xiao er before funding reproduced the known insufficient-funds rejection. Funded via wizard clone /clone/money/coin 200 (200 文铜钱), then buy baozi from xiao er succeeded: "你从店小二那里买下了一个包子。" Inventory after: baozi present, money correctly re-denominated from 200 文铜钱 down to 一两白银 + 五十文铜钱 (150 文 = 200 - 50, split back into silver+coin by adm/daemons/moneyd.lpc's player_pay()) — change math is correct. (Buying 2 at once correctly failed with "包子只能 一个一个的买。" — baozi.lpc isn't a combinable/stackable object, content behavior not a bug.) 2. Sect/apprenticeship system: exists, distinct from the 偏属/命运 personality system, and works. Real mechanic via apprentice/bai command (cmds/skill/apprentice.lpc + bai.alias) and feature/apprentice.lpc (is_apprentice_of/recruit_apprentice/ create_family, family mapping with family_name/generation/ title). Sect NPCs seed their own lineage via create_family(...) in create() (e.g. d/quanzhen/npc/zhangyuan.lpc "掌园道长" — 全真教, generation 4) and each NPC decides in its own attempt_apprentice() whether to accept (many refuse by design — d/baituo/npc/trainer.lpc demands born_family == "欧阳世家", d/xingxiu/npc/caihua.lpc/d/yanziwu/npc/bao.lpc refuse unconditionally — content, not a bug). goto /d/quanzhen/xiaohuayuan1 ("小花园") then apprentice zhangyuan went through cleanly end to end: "掌园道长说道:好吧,我就收下你这 个徒弟了。" → "恭喜您成为全真教的第五代弟子。" me->query("family") correctly populated. No sect-master NPC search was needed beyond this one confirmed instance — the mechanism is generic and this is enough to confirm it works. 3. Player-usable public message board: exists, was mis-diagnosed as absent in the 2026-08-04 pass. That pass only checked cmds/arch/board.lpc (wizard-only, correctly ruled out) and whether bare inherit/misc/bboard was mounted anywhere (correctly found not mounted) — but missed the actual mechanism: inherit/misc/bboard.lpc is the base class for **clone/board/*.lpc (47 distinct board objects), each a BULLETIN_BOARD-inheriting clone with its own board_id/location, foo()'d directly into a room's create() (e.g. d/city/chaguan.lpc's "/clone/board/player_b"->foo();). player_b.lpc ("江湖恩怨留言板", capacity 100) sits in /d/city/ chaguan ("春来茶馆"), one hop from the game's starting area, and is visible in the room's object list to any player who looks. Its post/read/discard commands are plain add_actions available to everyone (only gated by mud_age >= 1800/jing >= 50 for non-wizards, both waived for wiz_level(me) >= 1, which is why the admin account could exercise it directly). Live-tested: goto /d/city/chaguanpost 深度测试留言 → wrote one line → . to finish → "留言完毕。" → read new → note rendered correctly with title/author/timestamp. Of the 47 mounted boards, most others are per-sect (quanzhen_b, shaolin_b, wudang_b, etc., also visible as room objects — e.g. /d/quanzhen/damen lists "全真派弟子留言 板" — presumably gated to that sect's members by the individual board's own logic, not inspected further since the general player-facing case was already confirmed). Conclusion: this lib does have a real player-facing board; the design is "many small boards scattered through the map, not one central board command."**

Debug.log stayed completely empty (no error:/Bad argument/No program) across the whole session. Incidental save drift (data/{login,user}/f/fluffos.o from the admin login/purchase/ apprentice test, data/dbased.o from ambient quest-daemon ticks, and the new data/board/player_b.o created by the test post) was reverted/ removed before committing — admin account and world-state files are back to their pre-test committed contents.