info.html · 介绍 · play.html · 游玩 · llms.txt · 下载与本地运行 · ZIP · GitHub
一個 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
内容亮点
- 这是 1994-2000 年间由 Annihilator 开发的 ES II 引擎早期版本,地图 内容只有一个"雪村"(
d/snow/:广场、客栈、铁匠铺、集市、废墟等) —— 更接近 ES2 引擎本身附带的示范村,而不是一个完整实现的门派/江 湖游戏,本轮见过的sjplgfjxb/sjplii等档案自称的"original from Lil, rewritten by Annihilator"血统就是从这里发端的。 - 登入横幅自称"天朝帝国 / Celestial Empire",和内容里侠客门派主题 几乎无关,更像是这个引擎当年在不同站点之间被反复重新命名、重新 包装的一个样本。
注冊流程
英文使用者代號(3-12 個英文字母)→ 確認建立(y/n)→ 密碼(≥5 字 元)→ 確認密碼 → 電子郵件地址 → 種族(human 等)→ 性別(m/f)→ 中 文名字(1-6 個中文字)。
本次修復的關鍵 bug
- 編碼問題(本次最重要的發現):這份壓縮檔原本是 BIG5 編碼,但 最初的自動轉換流程把它當成 GB18030 解碼——GB18030 幾乎涵蓋所有位 元組序列,即使餵給它 BIG5 內容也「成功」解碼,只是產生完全的亂碼 (落在注音符號 Unicode 區段的垃圾字元),而不是一個轉換流程可以攔 截到的解碼錯誤。本次重新用 BIG5 對整個壓縮檔做了一次轉換(先用 Python 對照 big5/gbk/gb18030/cp950 四種編碼試解碼原始
.c檔,確 認 BIG5 才是正確答案,再正式套用)。 adm/obj/master.lpc的connect(int port)用switch(port)比對 一個寫死的MUD_PORT(原始預設值 4000),但這個專案實際配置的埠 號是 40144——不管在不在 WASM 下,每一個連線都會被拒絕(驅動記錄:Can not accept connection ... due to error in connect())。已將MUD_PORT巨集改為和config.fluffos一致。adm/obj/master.lpc的valid_read/valid_write遞迴呼叫load_object(SECURITY_D)卻沒有防遞迴機制——這個驅動禁止在編譯過 程中呼叫load_object(),第一次需要編譯securityd.lpc時就觸發 「Too deep recursion」堆疊溢位。已加上防遞迴旗標(AGENTS.md 已收 錄的標準修法)。adm/daemons/userid.lpc(identd/RFC1413 反查服務,純 socket 功 能)在 WASM 下無法編譯;按慣例清空為 no-op。is_chinese()沿用舊版 GBK 雙位元組假設(要求strlen>=2),在這 個驅動上strlen()按字元計算,導致任何單一字元或奇數字元長度的 中文名字(例如三個字的名字)都會被誤判為「不是中文」。已改為 Unicode 碼點區間判斷。- 重新套用了一個先前已提交、但這次 BIG5 重新轉換(從 raw 重新產生 work)不慎覆蓋掉的既有修復:移除和驅動自動預先定義的
MUD_NAME衝 突的#define MUD_NAME(先查過 git 記錄確認那次提交只動過這一個檔 案,再重新套用)。
§10.7 深度功能測試修復的 bug(詳見 NOTES.md)
feature/dbase.lpc的query()/query_temp()對多層"exits/east"這類路徑永遠靜默傳回0(與 AGENTS.md §7.29 是同 一種 bug):match_path()是 ACL 風格的「同層最長前綴比對」,不會 像set()那樣遞迴鑽進巢狀 mapping。影響面極廣——玩家完全無法用go/方向指令移動房間,apprentice拜師的兩段式確認也失效。已改 成逐層遞迴查表。std/char.lpc的rank()在沒有對應classdaemon(本檔案 從未出貨任何daemon/class/*.lpc)時,call_other靜默傳回int 0,被score的%s格式化成字面 "0" 混進中文敘述句。已加objectp()/function_exists()防護,退回既有的姓名式rank()。data/chinese.o內嵌 13 處孤立反斜線(原始 1994-2000 年代 BIG5 檔案自帶的作者年代產物),導致整個中文翻譯詞典restore_object()失敗,to_chinese()全面失效(種族/性別等欄位 顯示英文原文而非中文)。已用tr -d '\\'清除。config.fluffos的maximum evaluation cost : 700000(本專案 模板預設值)在冷啟動後第一次登入時不夠用,觸發 eval-cost abort, 角色被送進保底的/obj/void。已按 AGENTS.md §7.90 提到5000000。
管理員帳號 / Admin account
- id:
fluffos - 密碼 / password:
Mud@2026 - 權限 / level:
(admin)
管理員名單存放在純文字檔 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 崩溃)
BULLETIN_BOARDinherit+ 多余replace_program()致命形状(AGENTS.md §7.86,post命令崩溃):全档案 1 处命中,已删除多余的replace_program(...)调用(保留inherit),逐文件保留原有行尾格式(CRLF/LF 按文件原样)。本次为跨库 §7.86 扫描修复(触发原因:该 bug 已在 6+ 个互不相关的血统家族独立确认,属于近乎普遍的拷贝粘贴模式),仅做编译检查(驱动干净启动、端口正常监听),未做完整 §10.7 深度游玩测试。
深度功能测试 / 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. 本档案实测结果(务必逐条核对,不要假设直接适用)
- 新手引导:不适用。本档案没有 8 项天赋点数分配、没有「狂想空间新手入 门房间」,注册流程是英文 id → 密码 → 邮箱 → 种族 → 性别 → 中文名字(与 README 已记录的一致),落地在
/d/snow/inn_hall(小客栈)。 fight的安全陪练分流:分流依据不同,但结论相容。本档案cmds/std/ fight.lpc判断的是civilized属性(不是 kxkj 的can_speak),而civilized由std/race/humanoid.lpc的create()无条件设成 1,且d/snow/npc/*.lpc里目前唯一出现的种族是human(set_race("human")) ——也就是说这个示范村目前没有任何非人形/非文明生物,fight分支恒定安 全(点到为止),kxkj 提醒的「挑对象」问题在本档案暂时不构成风险,但kill_ob()真杀分支代码本身还在,未来加入动物 NPC 时要留意。现场用fight child(d/snow/npc/child1.lpc,accept_fight()恒定return 1) 验证:形体全程 90/90→70/70 只随时间自然消耗,未见战斗掉血,"加油!加油! 加油!"提示符合"点到为止"预期。join公会 +apprentice拜师两段式:不适用,机制不同。全档案没有join指令,cmds/std/apprentice.lpc也没有 kxkj 那种me->query("class") == ob->query("class")强制关卡——直接apprentice <对象>由该 NPC 自己的accept_apprentice()决定要不要收徒。全档案只 有一个 NPC 实现了accept_apprentice():d/snow/npc/alchemist.lpc(陈 维侠,私塾/d/snow/school)。现场验证:apprentice chen前几次被随机 拒绝对白挡下,命中set_flag()后再次apprentice chen成功磕头拜师,init_apprentice()设class="alchemist"、title="龍圖丹派弟子",score面板称谓正确更新为「龍圖丹派弟子 王小明」。- 管理员技能快捷指令:结论相同,仍然没有。搜遍
cmds/adm//cmds/wiz//cmds/imm//cmds/arch/未找到任何直接set_skill()/improve_skill()的管理员命令;本档案连skillpass/skilldel都没有(kxkj 有,这里没 有)。如实记录:本库同样没有技能快捷路径。 quit无新手宽限期:结论相同,且额外确认quit.lpc在#ifdef SAVE_USER(include/login.h里确实定义了)下会调用me->save()——不同于 kxkj「quit 从不写存档、只有净断线才写」的既有发 现,本档案的quit会正常持久化状态(属性、称谓、位置除外——quit后重新登入回到起始的小客栈,不是净断线时才回到的「上次所在位置」,这是 这份代码本身的既有设计:obj/user.lpc的reconnect()才使用last_location,quit路径没有)。现场验证:apprentice拜师后quit,重新登入称谓「龍圖丹派弟子」与属性值都正确保留,只有位置回到小 客栈(符合预期)。另外用真实的原始 socket 断线(不送quit,直接让连线 中断)模拟净断线,等待约 75 秒真实挂钟时间后重连,reconnect()正确把 角色放回断线前所在的房间(广场/square_n)。
修复的程序性 bug
feature/dbase.lpc的query()/query_temp()对多层"a/b"属性路 径永远静默传回0,与 AGENTS.md §7.29 是完全同一种 bug(已在该条追加本 库为第二个独立确认实例,附带诊断细节):match_path()是 ACL 风格的 「同层最长前缀比对」efun,不会像set()那样真的递迴钻进巢狀 mapping; 但query()/query_temp()原本直接对完整的"exits/east"呼叫match_path(dbase, prop),永远找不到dbase["exits"]这个巢狀 mapping 本身。现场用call here->query("exits")(正确返回巢狀 mapping)对比call here->query("exits/east")(返回0)锁定问题。影响面极广:cmds/std/go.lpc的env->query("exits/"+arg)恒定失败("这个方向没有 出路",玩家完全无法在房间间移动,游戏事实上不可玩);alchemist.lpc的query_temp("pending/alchemist")恒定读到0(拜师流程的两段式确认失 效);score.lpc/std/char.lpc的query_score()/query_target_score()同样受影响。修复:把query()/query_temp()改成逐层递迴(比对set()/set_temp()自己建出的真正巢狀 mapping 形状),不再依赖match_path()处理多层路径。已在两处补充中文注释说明成因。修复后现场复测:go/单字 母方向指令(e/s/w/n)全部正常在雪村地图移动,apprentice拜师 两段式确认流程正常完成。std/char.lpc的rank()在没有掛载对应 class daemon 时把int 0泄 漏进score的中文叙述句:daemond.lpc的query_daemon()在找不到 对应档案时会回传自己(DAEMON_D本身,一个真正的 object,objectp()测不出来)而不是0;DAEMON_D没有定义query_rank(),call_other在call other type check关闭下不会报错而是静静传回int 0,score里%s格式化整数 0 变成字面上的 "0" 混进中文叙述(例如"你是一位十四歲 的 1 級男性人類0")。这份档案本身就没有出货任何daemon/class/*.lpc(register_class_daemon()从未被任何文件呼叫过,grep query_rank全档 案只有std/char.lpc自己的呼叫端,属于既有的内容缺口,不是本次要补的 游戏内容)——修复的是"呼叫一个不存在/没有该函式的 daemon 时静默漏出错误 类型数据"这个程序性缺陷本身:改用objectp()+function_exists()双 重检查,daemon 不存在或没有query_rank时比照没有 class 的角色,退回 纯粹姓名式的::rank()(与本档案原本处理"没有 class"时完全相同的分 支)。现场复测:score不再出现字面 "0",拜师后正确显示 "龍圖丹派弟子 王小明"。data/chinese.o内嵌 13 处孤立反斜线,导致整个中文翻译词典restore_object()失败、to_chinese()全面失效:chinesed.lpc的create()呼叫restore()载入dict这份 mapping 存档,但档案里有 13 处形如"...功\","的孤立反斜线(原始 1994-2000 年代 BIG5 档案本身带的 作者年代产物,和这一轮 BIG5 重新转换无关——同样的反斜线也大量出现在.lpc原始码字串里,例如alchemist.lpc的"陳維俠擺\了擺\手",只是 LPC 编译器对字串常值里的非法跳脱只给警告并悄悄丢弃反斜线本身,不影响显 示;但restore_object()解析 mapping 常值时是严格模式,直接报*restore_object(): Illegal mapping format while restoring dict.,create()里未包catch()的restore()因此整个失败,dict停留在 空 mapping([]),之后所有to_chinese()呼叫都直接原样传回输入字串)。 现场用call /adm/daemons/chinesed->chinese("human")确认修复前传回字 面"human"(应为「人類」)。属于 AGENTS.md §7.7(未防护的restore()遇到损坏存档)的一个新变体(这次损坏源头是资料档本身的位元组内容,不是 格式不兼容)。修复:用tr -d '\\'删掉全档案仅有的 13 个反斜线位元组 (档案里除了这 13 处不存在任何其它反斜线,逐一人工核对过每一处上下文, 全部是"XX功\"或"穀\衣"这种紧贴在字之间/字尾的孤立反斜线,没有任何一处 是合法跳脱如\n/\"的一部分)。现场复测:call ...->chinese("human")正确传回「人類」,score的种族/性别栏位正确显示中文。config.fluffos的maximum evaluation cost : 700000(AGENTS.md §7.90 已收录的项目模板默认值)在冷启动后第一次真正登入时不够用:全新 重启驱动后第一个连线(无论是哪个角色)触发*Can't catch eval cost too big error.(logind.lpc:659enter_world)以及后续write_prompt的*Too long evaluation. Execution aborted.,角色被摆进保底的/obj/void("最後樂園")而不是正 常的出生点,需要重新连线一次(此时相关档案已被前一次连线编译过、走过缓 存)才能正常进新手小客栈。已按 §7.90 的标准做法把maximum evaluation cost提到5000000(本项目已有 30+ 库使用这个 值)。修复后现场复测:全新重启驱动 + 第一次连线(不是第二次),全程无 eval-cost 相关的运行时错误,debug.log干净。
发现但判定为既有设计、未改动的现象
fight.lpc里"你對著$n說道﹕«自稱敬语»«自己名字»,領教這位«對方尊称» 的高招﹗"这句提示,在没有 class daemon 内容(见上)的角色身上会显示成 名字重复两次(例如"王小明王小明")——因为rank("self")修好之后正确 退回到姓名式的::rank(),但fight.lpc这句提示原本假设rank("self")会返回一个独立的自称敬语(比如"小生"/"晚生"),不是名字 本身。这是"class daemon 内容本身没有出货"这同一个内容缺口的下游表现, 不是本次程序修复引入的新问题(修复前这个位置显示的是字面 "0",同样不 正确,只是形状不同);补齐敬语系统属于游戏内容而非程序缺陷,未改动。data/chinese.o之外,.lpc原始码里还有大量类似"某某功\"、"某某\了 某某"的孤立反斜线(编译期"Unknown escape sequence"警告),但因为 LPC 编 译器对这类跳脱只警告、悄悄丢弃反斜线本身、不影响实际显示或执行,比对 AGENTS.md §6.6"只在编译器/扫描实际标记出问题的地方手动修"的原则,未逐一 清理(只修了唯一真正造成功能失效的data/chinese.o那一处)。
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
- §7.29
dbase.lpcmulti-level path bug (query()/query_temp()doing a flatmatch_path()instead of recursing into nested mappings, which madego/movement commands fail entirely viaquery("exits/east")): code-confirmed the recursive-descent rewrite is still present (thematch_path(dbase, prop[0..r-1])shape plus the explanatory Chinese comments), then live-confirmed by actually walkingeastfrom 小客棧 to 广场 as admin — worked cleanly, exactly the command path that was completely broken before the fix. std/char.lpc'srank()int-0 leak: code-confirmed thefunction_exists("query_rank", cd)guard is still present.data/chinese.ostray-backslash corruption (brokerestore_object()for the whole Chinese dictionary): confirmed zero backslash bytes remain in the file, and live-verified viacall /adm/daemons/chinesed->chinese("human")returning "人類" (not the raw "human" fallback the corrupted dict would have produced).- §7.90 eval-cost bump (300000usec original config trips a "Can't catch eval cost too big error" on the very first cold-boot login): confirmed
config.fluffosstill hasmaximum evaluation cost : 5000000, and live-verified the FIRST login attempt after a fresh driver boot completed cleanly with zero eval-cost errors (not the second, which round one's own writeup noted was the only clean path pre-fix).
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
work/adm/obj/master.lpc— new fix:log_error()severity gate (§7.10-class, previously entirely absent).work/adm/simul_efun/file.lpc—log_file()assure_file()guard,cat()null-guard.
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
- §7.111 (
master.lpc'sstandard_trace()callingfile_name(error["object"])unconditionally): does not apply here — this lib'sstandard_trace()formatserror["object"]with%O(sprintf), never callsfile_name()on it at all. Different shape, not the same bug. - §7.112 (gargoyle/judge-lineage death NPCs with unguarded
call_out()chains ininit()):findfor the full filename list in AGENTS.md §7.112 found onlyd/snow/egate.lpcandd/snow/wgate.lpcpresent in this archive — both are plain gate rooms (inherit ROOM), not NPCs, and contain nocall_out()/death_stage_activecode at all. Not applicable. - §7.113 (netdead reconnect never restoring
heart_beat): checked the actually-invoked path —obj/user.lpc'sreconnect()(called fromLOGIN_D->reconnect()) doesenable_commands()+set_heart_beat(1)+remove_call_out("user_dump")correctly. Verified live in round two already (rapid reconnects) and reconfirmed by code read this round; no fix needed. - §7.90 (eval-cost):
config.fluffos'smaximum evaluation costis still5000000(the round-one fix); confirmed clean cold-boot logins. - §7.11 class (
log_file()/write_file()into a runtime dir the archive never shipped):adm/simul_efun/file.lpc'slog_file()still has its round-twoassure_file()guard. logind.lpcenter_world()'sob->save(): present and reachable (#ifdef SAVE_USER/user->save()right aftersetup(), before the startroom move) — a newly-registered account is saved immediately on first login, not silently dropped.
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:
- Death correctly transitions
life_formfrom"living"to"ghost"(CHAR_D->make_ghost()), with the expected in-game death message and stat-panel change (精/神 shown at reduced-but-full ghost values, 氣/keerow temporarily absent fromscorewhile a ghost — this ismake_ghost()'s intentionalclear_temp_dbase()/stat-remap behavior, not a display bug). - As a ghost:
look, movement (west/east), andsayall work normally; inventory is correctly empty (nothing carried over). quitwhile a ghost, then reconnect:life_formcorrectly persists as"ghost"across save/restore (confirms ghost state isn't silently lost on disconnect/reconnect — a plausible failure mode this round specifically wanted to rule out).resurrect <name>(the only player-reachable-by-proxy revival path in this archive — see below) correctly flipslife_formback to"living"and applies the intentional death penalty (kee/氣 30 → 27, matchingchard.lpc'smake_living()'s documented "corpse-less revival loses 10% of max" logic).
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
work/feature/npc/vendor.lpc—affirm_merchandise(): don't let a failedsscanf("%s %d", ...)truncate the item name being looked up (new fix, breaksbuyfor any multi-word item name).
§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:
libs/dfgsiiv13b/work/feature/user/edit.lpc— droppedprivatefrominput_line().
§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 livereplace_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).