Eastern Story II v1.3b

✅ 可玩

東方故事II v1.3b

dfgsiiv13b

更新 85d6a35 2026-09-02 源码 下载 ZIP

▶ 开始游玩 · Play Now

一個 1994-2000 年間由 Annihilator 開發的台灣 ES2 血統 MUD(正體中文),登入橫幅另稱「天朝帝國 / Celestial Empire」。地圖内容目前只有一座「雪村」——廣場、客棧、鐵匠鋪、集市、廢墟——比起完整的門派江湖遊戲,更接近 ES2 引擎自帶的示範村規模,也是這批檔案裡 「original from Lil, rewritten by Annihilator」這一支血統說法可能的發端之處。

English

An early build (1994-2000) of the ES II engine created by Taiwanese developer Annihilator, in Traditional Chinese, whose login banner also styles itself "Celestial Empire." Its only map content is a single snow village — plaza, inn, blacksmith, market, ruins — closer to the engine's own bundled demo village than a fully realized sect-and-jianghu game, making it the likely origin point for the "original from Lil, rewritten by Annihilator" lineage seen in later archives in this project.

README

内容亮点

注冊流程

英文使用者代號(3-12 個英文字母)→ 確認建立(y/n)→ 密碼(≥5 字 元)→ 確認密碼 → 電子郵件地址 → 種族(human 等)→ 性別(m/f)→ 中 文名字(1-6 個中文字)。

本次修復的關鍵 bug

§10.7 深度功能測試修復的 bug(詳見 NOTES.md)

管理員帳號 / Admin account

管理員名單存放在純文字檔 adm/etc/wizlist 裡(原始壓縮檔並未附帶此 檔案,本次新建);帳號本身透過正常注冊流程建立。

警告:這是一個公開的預設密碼,僅供本地/瀏覽器試玩。正式對外開服前
請務必修改此密碼。

本地運行

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

遊戲埠號:40144

NOTES · 移植与修复记录

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

ES2 血统代码库(台湾,1994-2000,Annihilator),繁体中文。重大编码发现:最初的 convert_lib.sh 转档流程把这份档案当成 GB18030 解码——GB18030 几乎涵盖所有字节序列,即使这份档案实际是 BIG5 编码也会"成功"解码,只是产生完全的乱码(落在注音符号 Unicode 区段的垃圾字元,比如 BIG5 本该解码成"使用者"的地方变成了"ㄏノ祅祘"),而不是一个转换流程可以拦截到的解码错误。已改用 BIG5 重新跑一遍完整的 raw->work 转换(套用前先用 Python 对照 big5/gbk/gb18030/cp950 四种编码试解码原始 .c 档案,确认 BIG5 才是正确答案)。另外还需要:(1)master.lpc 的 connect(int port) 用 switch 比对一个写死的 MUD_PORT(4000,泥潭原始默认值),而不是本项目实际分配的端口(40144)——不管在不在 WASM 下,每一个连线都会被拒绝(驱动记录:"Can not accept connection ... due to error in connect()"),已改为更新 MUD_PORT 宏;(2)master.lpc 的 valid_read/valid_write 递归呼叫 load_object(SECURITY_D) 却没有防递归机制——经典的 §7.1,第一次真正的安全检查就触发"Too deep recursion",已用文档记载的防递归旗标手法修复;(3)adm/daemons/userid.lpc(identd/RFC1413 反查服务,纯 socket 功能)按 §7.52 掏空处理;(4)is_chinese() 的经典 GBK 字节配对判断(要求 strlen>=2)在 Unicode 码点模型下会拒绝单字符或奇数长度的名字切片——已改成 CJK 码点区间判断。重新套用了一个之前已提交、但这次 BIG5 重新转换(从 raw 重新生成 work/)不慎覆盖掉的原生修复(移除一个和这个驱动自动预定义的 MUD_NAME 冲突的 #define MUD_NAME,§7.39)——先查过 git 记录确认那次提交还动过什么其它地方,只有 globals.h。管理员账号(fluffos/Mud@2026)通过真实注册流程 + 全新创建的 adm/etc/wizlist 播种(原始压缩包里没有附带这个档案)。

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

深度功能测试 / Deep functional test (AGENTS.md §10.7)

先读 README 和本文件(WASM 修复摘要已确认之前一轮播种了 fluffos/Mud@2026 管理员账号,透过真实注册流程建立)。本档案的名字本身就是「東方故事II」, 经内容比对(d/snow/ 只有一个「雪村」示范村,没有门派/公会系统,doc/help/ topics/ 是空目录,cmds/std/ 没有 join 指令)确认这很可能就是 AGENTS.md §11 lineage map「ES II / 东方故事 mega-family」这一支的literal ES II 引擎 本身(不是某个分支重新命名的衍生品),kxkj/kxkj1/xkxz2/xiyouji/ rzrmud 等库的 feature/dbase.lpc 很可能都是从这里的同一份代码分叉出去 的——本轮的核心发现(见下)直接命中这份共享代码,值得作为标准清单条目提 醒后续同宗测试。

kxkj 血统先验 vs. 本档案实测结果(务必逐条核对,不要假设直接适用)

修复的程序性 bug

发现但判定为既有设计、未改动的现象

WASM 未验证说明

按本 session 约定:emsdk 固定从 storage.googleapis.com 拉取,被本 session 出站代理策略拒绝,WASM 编译/运行验证本轮继续跳过,仅做原生驱动 (linux-debug 预设,ASAN/UBSAN)下的完整 §10.7 测试,全程 debug.log 干净 (仅编译期无害警告)。

Deep functional test round two (2026-08-14)

Independently re-verified against current code rather than trusting the round-one writeup above. Found and fixed one new bug (a real, live- reachable instance of AGENTS.md §7.10, not previously caught here); all 4 of round one's fixes re-confirmed live, not just via diff.

New fix: adm/obj/master.lpc's log_error() had no severity gate at all — broadcast every compile WARNING (not just real errors) to whichever player happened to trigger a lazy compile

Distinct from the more common §7.10 case-mismatch shape this session has been checking for ("Warning:" vs "warning:") — this lib's log_error() had no gate concept whatsoever:

// BEFORE:
if (this_player(1)) efun::write("編譯時段錯誤﹕" + message);
// AFTER:
if (this_player(1) && strsrch(message, "arning:") == -1)
  efun::write("編譯時段錯誤﹕" + message);

No wizardp() check either — this fires for whichever player is this_player(1) at compile time, which for a lazy first-visit room compile can be an ordinary player, not just a wizard running update. Live- verified the fix: update /adm/daemons/enterd (a file with a real, harmless "Unused local variable" warning, confirmed present in the boot log) now completes with only "成功。" on screen — pre-fix this would have dumped the raw compiler warning text at whoever was connected. The underlying write_file(home + "log", ...) call that persists the compile record has its own separate, lower-severity gap (no assure_file() guard on home, so the write can silently no-op if that wizard's log directory doesn't exist) — left alone this pass, out of scope: it doesn't affect players, isn't part of this session's established checklist, and touching it wasn't necessary to fix the actual player-facing leak.

Re-verified live: all 4 of round one's fixes still hold

Standard checklist gap found and fixed

adm/simul_efun/file.lpc had the common §7.11-class gap: log_file() called write_file() with no assure_file() guard, and cat() had no null-guard on read_file(). Fixed both, matching the pattern applied across this session's other libs. No §8.9 food/water wrong-object read, no printf debug leak in logind.lpc.

Verification method

Booted native build-debug driver, admin login (fluffos/Mud@2026) — clean on the very first attempt (confirming the §7.90 fix). update /adm/daemons/logind and update /adm/daemons/enterd as real privileged- action checks. Two rapid consecutive admin reconnects, both clean. Driver killed by exact PID after testing; incidental fluffos.o/data/daemon.o save-timestamp churn reverted before commit.

Files modified this pass

Round three deep functional test (2026-08-19)

Went deeper than rounds one/two, which only tested "safe sparring" combat and never pushed a character through an actual death. This pass did a full death→ghost→reconnect→admin-resurrect cycle, real economy transactions (buy from a vendor NPC), and re-checked the AGENTS.md checklist items current at the time of this session (§7.111, §7.112, §7.113, §7.90, §7.11 class, logind.lpc registration-save).

Checklist items — all clean, no action needed

New fix: feature/npc/vendor.lpc's affirm_merchandise() corrupted

multi-word item names, breaking buy <item> from <vendor> for any item whose primary/full name has a space in it

Live-reproduced at /d/snow/herb_shop (the only working vendor NPC in this archive, 藥鋪掌櫃/Herbalist): buy pill from herbalist (single-word alias) succeeded, but buy black pill from herbalist (the item's actual full name, "black pill") always failed with the generic "對方好像不願意 跟你交易" decline — even though "black pill" is a directly registered alias on /obj/medication/black_pill.lpc. This is a live, easily-hit bug: a player who types the item's real name (as printed by list, e.g. "Black pill") rather than guessing a shorter alias gets a false rejection every time.

Root cause: affirm_merchandise() uses sscanf(what, "%s %d", what, index) to split an optional trailing item-index number (buy sword 2) off the item name — but LPC's %s stops at the *first* embedded space, not the last, so for "black pill" the match attempt sets what = "black" and then fails to parse "pill" as %d, so the overall sscanf() returns 1 (not 2). The old code only guarded the index variable on failure (if (sscanf(...) != 2) index = 1;) but never restored what, so the corrupted "black" was used for the subsequent item->id(what) lookup — which fails to match any of black_pill.lpc's aliases ("烏心丹", "black pill", "pill"), even though the correct full name was typed. Confirmed the exact sscanf() truncation behavior with a throwaway diagnostic wizard command (sscanf("black pill 1", "%s %d", what, index)n=1, what="black" — even with a valid trailing index number it only manages to peel off the first word, not the whole multi-word name before it).

Fix: parse into a separate temp variable and only commit it to what when sscanf() actually matched both pieces (return value == 2); otherwise leave what untouched.

// BEFORE:
if (sscanf(what, "%s %d", what, index) != 2)
  index = 1;
// AFTER:
if (sscanf(what, "%s %d", base, index) == 2)
  what = base;
else
  index = 1;

Live-reverified after a fresh driver restart: buy black pill from herbalist now succeeds (money deducted, item delivered), and a subsequent purchase attempt that's correctly short on funds (buy wild ginseng from herbalist) now fails with the correct "你身上的錢不夠" (can't afford) message instead of the misleading "won't trade with you" one — confirming the item-name match itself now works and the failure path is the genuinely-intended one. debug.log stayed clean throughout.

Same buggy sscanf(arg, "%s %d", arg, index) != 2 → index = 1 idiom (without restoring arg/what on failure) also exists in std/room/hockshop.lpc's do_buy() and obj/clan_symbol.lpc — left unfixed this pass because neither is reachable from any room actually shipped in this archive's d/snow map (no hockshop room instance exists, and obj/CLAN is an empty directory) — a content gap, not a live bug here. Flagging in case either becomes reachable in a future content addition, or as a pattern worth checking on other libs that build on the same ES2-family feature/npc/vendor.lpc/hockshop.lpc lineage.

Deeper death-cycle testing (new ground, not covered by rounds one/two)

Forced a real death via call me->consume_stat("HP",9999) as admin (round one/two only ever tested harmless point-to-point sparring against a civilized child NPC, never an actual death). Full cycle exercised and confirmed clean end to end, debug.log empty throughout:

Content-gap finding, not a bug, left alone: the *only* code path that calls CHAR_D->make_living() (the ghost→living transition) in this archive is cmds/adm/resurrect.lpc, an admin-only command — there is no player-facing temple/prayer/NPC mechanism to self-resurrect in d/snow's content, and no hostile/non-civilized NPC exists there either (per round two's finding), so in practice a mortal player who somehow dies (there is currently no reachable way to, since all d/snow NPCs are civilized) would need a wizard to resurrect them by hand forever, or be killed again as a ghost to trigger LOGIN_D->reincarnate() (karma-based character reset). This matches the same "missing daemon/class content" gap class already documented in round one (no player-facing progression content shipped beyond the single demo village) — the death/ghost/revival *machinery itself* is sound and bug-free, it's just missing a player-reachable front door in this content-limited archive.

Board/guild system investigation — confirmed unreachable, but for a

benign content-gap reason (not a crash)

Tried board/list/post at /adm/guild/guildhall (the only bulletin board room this archive references) — all returned "什麼?" (unknown command). Traced this to guildhall.lpc's create() calling load_object("/daemon/board/wizard"), but /daemon/board/ doesn't exist anywhere in this archive (confirmed via update: "沒有 /daemon/board/wizard.lpc 這個檔案"). Since the board object never loads, its add_action("do_post","post") etc. never registers, so post/read correctly fall through to "unknown command" rather than crashing — graceful degradation, no uncaught error, debug.log stayed clean through this. /adm/guild/ itself is confirmed (again) to be the OOC wizard-workshop area (巫師公會/巫師學院/會議廳, all (admin)-flavored text about researching LPC), not a player-facing sect/guild system — consistent with round one/two's finding that d/snow ships no player guild/sect content. academy.lpc has the identical load_object("/daemon/board/lpc") gap. Not fixed: missing shipped file (content gap), not a programming defect, and it doesn't affect any player-facing system.

Verification method

Booted native build-debug driver fresh (debug.log removed first). Admin login (fluffos/Mud@2026). Tested: board/guild area (unreachable, traced to missing shipped file), store/herb_shop economy exploration (list correct), real buy transactions (multi-word bug found + fixed + reverified), full death→ghost→quit→reconnect→resurrect cycle (twice — once pre-fix baseline, once post-fix on a fresh restart). Used a throwaway wizard diagnostic command (cmds/wiz/sstest.lpc) to directly confirm sscanf()'s truncation behavior; deleted before commit, not part of the mudlib. Driver killed by exact PID after testing (single SIGTERM sufficed, no respawn this time). Incidental .o save-timestamp churn on data/daemon.o and the fluffos login/user saves (from cloning test money/items and the death/resurrect cycle) reverted via targeted git checkout -- before commit; boot.log and work/log/debug.log (gitignored scratch) removed.

Files modified this pass

§7.114 fix: private input_line() broke multi-line to/board/mail/chfn input (2026-08-19)

Same ES2-ancestry bug as zzhj/shzs/huoying/dfgs2 (AGENTS.md §7.114): work/feature/user/edit.lpc (this lib's F_EDIT mixin, inherited by obj/user.lpc) declared its input_to() re-arm target private: private void input_line(string line, string text, function callback). The recursive re-arm from inside input_line() silently failed to register once reached through the inherited mixin, so any multi-line .-terminated input session (board post, to, mail, chfn) dropped everything after the first line. Fix: dropped private.

Live-verified on a fresh driver boot (port 40144), fresh character (秦肆八/qinsiba, human/male): to say → two lines of text → . → resulting say output showed both lines (你說道﹕test line one\ntest line two), not truncated after line 1. Killed the driver by exact PID when done.

Files modified this pass:

§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). Sibling of dfgs2: 30 live

replace_program(ROOM); occurrences across 29 d/snow/*.lpc and adm/guild/*.lpc room files plus this lib's own room-building tool obj/roommaker.lpc's string-builder template (mixed tab/space-indent variant, \n\tsetup();\n replace_program(ROOM);\n}\n", hand-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).