Eastern Story II: The Celestial Empire

✅ 可玩

东方故事Ⅱ之天朝帝国 (Eastern Story II: The Celestial Empire)

dfgs2

🔑 fluffos / Mud@2026 更新 66a3bd8 2026-10-28 源码 下载 ZIP

▶ 开始游玩 · Play Now

属于"东方故事"/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

内容亮点

在线试玩

https://mudlibs.fluffos.info/dfgs2/

管理员账号 / Admin account

警告: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

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):

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.

`` 执行时段错误: *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.

- 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.

``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`).

What was tested and confirmed working

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.

``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.

Explicitly not verified live (time/reachability budget), stated per §10.7 item 6

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 崩溃)

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

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.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

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:

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)

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:

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)

Files modified this pass

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).

深度功能测试(§10.7,2026-10-04)— commented-out #pragma no_warnings removed

include/weapon.h carried //#pragma no_warnings (already disabled); the line is deleted so a grep for the pragma finds nothing in this lib.

深度功能测试(§10.7,2026-10-04)— negative range ends

scripts/lpc_fix_negative_ranges.py: 4 line(s) in 1 file(s) count a range from the end with < (x[a..-1] -> x[a..<1], x[-2..] -> x[<2..]; KB 06 §7.209). In this driver a negative constant end gave "" / ({ }) and a negative start the whole value, so each of these returned the wrong slice; the compile warning A negative constant as the second element of arr[x..y] is gone from the files that compile. Range lvalues (s[0..-1] = text, the prepend idiom) are left alone. A paired compile of the changed files at HEAD and in the working tree showed no new error.

深度功能测试(§10.7,2026-10-04)— stray backslash before a closing quote

A Big5 character whose second byte is 0x5C was written with one extra backslash after it (the old driver read 5C 5C as one backslash and so completed the character) and the UTF-8 conversion kept it (KB 03 §4.4). Before a closing quote it escapes the quote, the string runs into the next line and the file does not compile. scripts/lpc_fix_big5_quote_backslash.py deletes it when the character before it encodes to cp950 with 0x5C as second byte and the quote ends an expression. Here: cmds/std/enable.lpc ("force": "内功\",, "dodge": "轻功\",), so the enable command never loaded. lpcc --batch: HEAD fails, working tree loads.

深度功能测试(§10.7,2026-10-05)— tail() simul_efun

scripts/lpc_add_tail.py --apply: cmds/wiz/tail.lpc calls tail(file), an efun of the MudOS this archive ran on that FluffOS does not have (Undefined function tail: the command did not compile). adm/simul_efun/file.lpc now defines void tail(string file): the last 10 lines of the file, written to the player. Load check (HEAD against the working tree): 1 of the 1 tail command file(s) failed to load at HEAD, all load now, and the simul_efun file loads. KB 04 §6.2.

深度功能测试(§10.7,2026-10-07)— 编译警告整理

scripts/lpc_warnings.py dfgs2 --fix 加 scripts/lpc_fix_no_effect.py --apply,共改动 76 个 .lpc 和 1 个 .h(多为删去未用的局部变量)。HEAD 与工作树分别加载(新进程):75 -> 75 通过,无回退。启动时警告 23 -> 0;新进程登录(新人物确认)正常。

编译警告收尾(2026-10-13)

手工清掉剩下两条,能编译的文件警告清零;改动的对象(含双股剑 d/snow/npc/obj/broadsword 与 /obj/weapon/sword)在 HEAD 与工作树各载入一次:10 个,HEAD PASS 9,tree PASS 9,regress 0。

for (int i ...) 的循环变量在循环外还在用(2026-10-18,KB 04 §6.12)

当铺/商店的 do_buy() 写成 for (int i = 0; ...) if (...) break;,循环一结束又读 if (i == sizeof(inv))。FluffOS 里 for 头里声明的变量只在循环内有效,所以 Undefined variable 'i',整个房间(NPC)载入不了,当铺根本进不去。把 i 声明挪到函数开头(int i, index;),循环改成 for (i = 0; ...):std/room/hockshop.lpc。

move() 的目标房间文件不存在时不再抛错(2026-10-18,KB 06 §7.96)

feature/move.lpc 的 move() 把 err = catch(dest = load_object(dest)) 当存在检查,可目标文件不存在时 load_object() 返回 0、catch 不触发,接着 dest->receive_object() 抛出 Bad argument 1 to EFUN call_other()。留言板 std/bboard.lpc 的 setup() 把自己 move() 到 location 房间,那是巫师的私人房间时(档案里没有这个文件),留言板连同刷出它的房间一起载入不了。catch 之后补上 if (!objectp(dest)) return 0;(kxkj 系是 ob),移动失败、对象留在原处。抽样(每库 300 个对象)里原先因此失败的 5 个对象,现在 4 个能载入。

call_out_info() 的每一项只有 3 个元素(2026-10-20,KB 06 §7.287)

驱动返回的是 ({ 物件, 函数, 延迟 }),不带 call_out 的参数,旧代码读第 4 项 info[i][3](callouts 指令、calloutd、cod 工具)一读就报 Array index out of bounds,指令从第一行起什么也打印不出来。改为 (sizeof(info[i]) > 3 ? info[i][3] : 0)(scripts/lpc_fix_callout_info.py)。改动:/cmds/arch/callouts.lpc(1 处)。

socket / dumpsocket 指令:dump_socket_status() 驱动里没有(2026-10-20,KB 06 §7.291)

这些巫师指令 write(dump_socket_status());,MudOS 的 efun 在 FluffOS 里不存在,指令编译不过。在模拟 efun 文件里补了 dump_socket_status():用 socket_status() 排出同样的表(没有 sockets 包的构建,比如 WASM,只有表头;scripts/lpc_add_sefun.py dump_socket_status)。

登录密码超过 255 字节:crypt() 抛错(2026-10-20,KB 06 §7.294)

驱动的 crypt() 对超过 255 字节的密钥直接报错(Error in crypt(), check your salt),老驱动则按任意长度散列。在登录提示符下输入 256 字节以上的密码,密码检查那一行就抛错,连线停在一个裸的 > 提示符上,登录走不完(password 指令的旧密码检查同样);用 300 个 x 当参数的扫描发现的。/adm/obj/simul_efun.lpc 里补了一个同名的 simul_efun crypt(key, salt):只有超长的密钥才被截短(先截到 255 个字符,再逐字符截到 sizeof(to_buffer(key)) <= 255,限制按字节算,一个汉字占三个字节),再交给 efun::crypt();普通密码原样散列,已存的密码散列不受影响(scripts/lpc_add_sefun.py crypt)。

翻页器 start_more(read_file(...)):文件缺失时 explode(0)(2026-10-20,KB 06 §7.300)

东方故事系的翻页器 void start_more(string msg) { ... explode(msg, "\n") ... } 被这样调用:start_more(read_file(HELP_DIR + "help/topics"))、start_more(read_file(NEWS))。压缩包没带的文件使 read_file() 回 0,explode(0, ...) 在本驱动里报 Bad argument 1 to explode()(不带参数的 help 指令就是这样出错的),老驱动把 0 当空文本。在 start_more 开头加了 if (!stringp(msg)) return;(scripts/lpc_fix_start_more.py)。改动:/feature/user/more.lpc。

NPC 的 chat() 在 chat_msg 为空数组时下标越界(2026-10-21,KB 06 §7.334)

NPC 基类的 chat() 是 if (arrayp(msg = query(...))) { if (random(100) < chance) { rnd = random(sizeof(msg)); if (stringp(msg[rnd])) ...,不检查列表是否为空。本库现有的 NPC 文件里没有字面写成空 chat_msg 的(扫描只认 set("chat_msg", ({ })));守卫挡的是运行时才为空的列表和以后新增的 NPC。在 random(100) < chance 前面加 sizeof(msg) &&(scripts/lpc_fix_chat_empty.py;if (random(100) > chance) return 0; 的写法是 !sizeof(msg) ||)。改动:/std/char/npc.lpc(共 1 个文件)。

按字节写的汉字循环、茶馆猜诗和同盟频道标题(2026-10-23,KB 06 §7.363)

老档按字节做「一个汉字」:while (i--) if (i % 2 == 0 && !is_chinese(name[i..<0])) 只检查偶数下标,奇数位上的拉丁字母(张a)会过;茶馆猜诗 mixup() 用 strlen(str)/2 再 j=j+j、str[k..k+1] 交换两字节;泥潭系 channeld 的 switch (strlen(fname)) case 4/6/8/10 和 strlen(family)==4/6 按字节长度给同盟 / 门派名加空格。本驱动的 strlen() 和区间都按字符算。scripts/lpc_fix_byte_char.py:去掉奇偶项、把区间改成 name[i..i],按字符重写 mixup() 并把 strlen(first)>=14 折成 7,频道标题的 case / 门派长度改成字符数(对照 aoxiangtianji)。本库 汉字循环 / is_chinese 区间 1 个文件。证明(scripts/lpcc_eval.py):sjplii 的 check_legal_name 在 HEAD 接受「张a」、改后拒绝,两边都接受「张三」;xkx100 的 clubpoem->mixup("离离原上草") 得到「离离原草上」(真交换汉字);nitan_san 两字同盟名得到 【 兄 弟 】。关键对象加载通过。

血条 / 表情标题按字节算显示宽度(2026-10-23,KB 06 §7.365)

老档把汉字当两字节:bar_string[0..(val * N / max) * 2 - 1] 把「格数」换成字节下标,半格血条在本驱动(按字符算区间)上会整条画满;巫师表情 edemote 的 repeat_string("─", 13 - (strlen(emotion) / 2)) 也会少减汉字宽度。scripts/lpc_fix_display_pad.py:去掉 CJK 血条 / 空白条上的 * 2(jings/qis 赋值和 blank_string3[0..(23-a)] 的 23→11 一并改),表情标题去掉 / 2;ASCII 空格的 none_string / empty_string 仍保留 * 2(一格两个空格)。本库 2 处(1 个文件)。证明(lpcc_eval.py hhsj):tribar_graph(6,9,12) 在 HEAD 画出整条 12 个■,改后为 6 个■+3 个□;麻将 strlen(tempB)/2 是两字符牌码,未改。

深度功能测试(2026-10-27,§6.10 未知 pragma save_binary/optimize)

删除本库中 FluffOS 不认的 #pragma save_binary / #pragma optimize… 行。