Eastern Story II v1.3b

✅ 可玩

東方故事II v1.3b

dfgsiiv13b

更新 66a3bd8 2026-10-28 源码 下载 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).

深度功能测试(§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-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-05)— 主控 author_file()/domain_file()

驱动的 mudlib_stats 包在主控载入时各调用一次 author_file(<主控文件>) 和 domain_file("/")。本库主控没有这两个 apply,所以每次启动都打印 author_file() in the master file does not work, using root_uid as fallback 和 domain_file() in the master file does not work, using bb_ui as fallback 两行,网页版每位访客的终端里都看得到。adm/obj/master.lpc 补上了这两个 apply:作者返回 get_root_uid(),域返回 get_bb_uid(),正是驱动缺少它们时所用的值。域名等于 backbone 域时,驱动让对象沿用创建者的域,与没有这个 apply 时相同(KB 06 §7.222)。两个新函数不加 nosave(本主控其余函数的 nosave 会触发 Illegal to declare nosave function 警告,属于本库尚未做的警告清理)。用 lpcc config.fluffos /nonexistent.lpc 前后对比,主控载入段只少了这两行。

深度功能测试(§10.7,2026-10-05)— 编译警告清理,以及 tell/reply/who 无法编译

1. 编译警告:启动时 约 40 条 → 0,整树扫描 239 条 → 0。scripts/lpc_warnings.py --fix:24 个 nosave 函数改 protected(被别处按名调用的 5 个去掉 nosave 保持 public)、删 CD/MudOS 专用 #pragma、删 120 个未用局部变量、53 处无效转义(BIG5 第二字节 0x5C 转码后残留的 \,如 不允許\你,KB 03)、10 处补 varargs;lpc_audit_removed_locals.py 复核 195 个被删声明,标出的 apply 是本文件的 apply() 函数调用,不是被删的变量。手工:logind 的 #if 0 分支变量移入同条件声明块,httpd 删无用的 q = query_ip_port()。 2. tell、reply、who 三个基本指令无法编译。 它们 #include <net/dns.h> 并引用网际互联守护 GTELL/RWHO_Q,而本档案里根本没有 net/dns.h 和这些守护(raw/ 中也没有),于是玩家打 tell/reply/who 都是指令编译失败。同源的 huoying 已把这段注掉;这里改为 #ifdef GTELL/#ifdef RWHO_Q 包住 名字@站名 分支,本站内的私聊、回答、在线列表照常工作。fingerd 的 mud 变量同理放进 #if 0。 3. version 用了旧驱动的 __DRIVER__ 宏(本驱动没有,指令编译失败),改为只印 __VERSION__(内容已是 fluffos <版本>)。cmds/adm/socket.lpc 调用 MudOS 的 dump_socket_status(),本驱动没有,改用文档中的 socket_status() 列出各套接字,并按 KB 01 §1.3(c) 以 #ifdef __PACKAGE_SOCKETS__ 保护(网页版没有套接字包)。 4. 验证:全新启动 0 警告;fluffos 登录 → look、who、version(fluffos 20260830-… / Eastern Stories 2.1.3a)、tell fluffos hello、reply hi、socket、score、quit 全部正常。无参数的 who 只列与自己等级相差 5 以内的非巫师玩家,所以管理员看到 0/1 是设计如此。

深度功能测试(§10.7,2026-10-08)— .lpc 后缀的定宽切片

.c 改名 .lpc 时只改了字符串没改切片宽度:f[<2..] == ".lpc" 拿两个字符比四个字符,永远不成立(!= 永远成立),按后缀筛文件、去后缀的代码因此从不运行或截成 x.l(KB 06 §7.238)。scripts/lpc_lpc_suffix_slice.py --apply 改了 1 处比较、0 处去后缀(如 cmds/wiz/update.lpc);改动的文件 HEAD 与工作树分别加载(新进程),无回退。

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),移动失败、对象留在原处。抽样的留言板原本就能载入,这一句只是防御。

登录密码超过 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。

chpass:先把参数写进日志,再对它 sscanf(2026-10-21,KB 06 §7.336)

管理员扫描里 chpass 不带参数报 Bad argument 1 to sscanf() Expected: string Got: 0.(cmds/adm/chpass.lpc):指令先 log_file("nosave/CHPASS", ... arg)(%s 吃得下 0),再 sscanf(arg, "%s %s", userid, new_passwd)。静态扫描只看第一次提到参数的语句,现在会越过这种只是打印或记录的提及;在记录之前加 if (!arg) return notify_fail("指令格式:chpass <使用者代号> <密码>\n");。

注册到不存在函数的 add_action(2026-10-21,KB 06 §7.349)

add_action("函数", "动词") 绑定的函数,在对象本身、它包含的头文件、它继承的父类和继承它的子类里都没有定义(scripts/lpc_scan_callout_targets.py 静态解析,子类用传递闭包复核,scripts/lpc_verify_handlers.py 运行时 function_exists() 复核)。驱动从玩家最新的一条动作往回找,遇到这样的句子就报 *Function for verb 'drop' not found.,同一个动词的其他处理函数不再尝试(不是最新的一条就悄悄跳过)。删除了 1 条注册(1 个文件,scripts/lpc_fix_dead_add_action.py),动词留给别的处理函数:/obj/clan_symbol.lpc(help)。最多的函数名:help 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-27,§6.10 未知 pragma save_binary/optimize)

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