Eastern Story II (ES II)

✅ 可玩

东方故事Ⅱ — Eastern Story II (ES II)

es2

🔑 fluffos / Mud@2026 更新 fdf581a 2026-09-12 源码 下载 ZIP 上游 mudchina/es2

▶ 开始游玩 · Play Now

中文MUD鼻祖——根据 GitHub `mudchina` 组织的介绍,这是现存最早的中文 LPC mudlib 之一。它不是某个既有引擎的中文化移植,而是一份从零写起的原创代码(署名 [email protected],1994-1995 年间开发,建立在经典的 Lil mudlib 与 TMI 功能混入代码之上)——本项目里反复出现的"ES2/TMI-2/Falcon 家族"这一说法,追根溯源,指的正是这一脉传承。玩家注册时需取一个真正的中文名字,新人物降生在雪亭镇的"饮风客栈",探索由京师城门街道、雪山门派、丹霞谷竹林、绿林庄园、坟场幽冥等多个 domain 构成的东方奇幻世界,门派技能修炼与精/气/神三维状态条并行;NPC 中甚至有一位来自《七龙珠》的客串角色"孙悟空",游戏内被称作 ESII 首个"自我成长型"NPC,从创建起战斗经验便持续累积。

English

One of the earliest Chinese-language LPC mudlibs -- GitHub's mudchina org describes it as "the progenitor of Chinese MUDs" (中文MUD鼻祖). A from-scratch LPMud-lineage codebase (credited to [email protected], 1994-1995, built on the Lil mudlib with TMI feature-mixin code) rather than a reskin of an existing engine -- this project's own recurring "ES2/TMI-2/Falcon family" terminology for dozens of later Chinese mudlibs traces back to this exact lineage. Players register with a real Chinese name, land at a mountain inn (饮风客栈), and explore a wuxia/xianxia world of domains (city gates and streets, snow-capped mountains, a temple, a bamboo canyon, a ghost/graveyard area) with guild-style skill training, a two-tier reputation system, and NPCs including a Dragon Ball crossover cameo (孙悟空/Son Goku, described in-game as ESII's first self-growing NPC whose combat experience permanently accumulates from the moment he's created).

README

原始存档:git clone https://github.com/mudchina/es2(commit ae8adf6)。仓库本身是纯 GBK 编码的原始快照,本次上线按本项目一贯 的方式完成了 GBK→UTF-8 全量转码与驱动兼容性修复(详见 NOTES.md)。

内容亮点

在线试玩

https://mudlibs.fluffos.info/es2/

管理员账号 / Admin account

警告:公开架站前请务必修改此默认密码。

本地运行

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

游戏端口:40214

NOTES · 移植与修复记录

es2 — 东方故事Ⅱ (git clone from mudchina/es2)

Source: git clone https://github.com/mudchina/es2 (commit ae8adf6, cloned 2026-08-25). Port: 40214. Status: done (boots clean, full registration verified end-to-end with a real Chinese name, look/ score/quit/whoami/update all confirmed working post-login).

Note: the sibling repo mudchina/es2-utf8 (pre-converted) was deliberately NOT used — per this project's established methodology (AGENTS.md §4), doing the GBK→UTF-8 restoration itself, on the original GBK snapshot, is the point.

What this is

A first-generation, from-scratch Chinese LPC mudlib, not a reskin of an existing English codebase. bin/es2.info names it MUDNAME=ES2; adm/obj/master.lpc's header credits "[email protected]" (1994-11-07) with feature/treemap.lpc crediting "Buddha@TMI" and noting it is "part of the TMI mudlib distribution" — i.e. this archive genuinely is (part of) the historical root of the "ES2/TMI-2" lineage name this project's AGENTS.md catalog uses for dozens of unrelated LATER Chinese mudlibs. mudlib directory is the repo root itself (no nested subdirectory); master file : /adm/obj/master, simulated efun file : /adm/obj/simul_efun.

Layout: adm/ (master/simul_efun/daemons), cmds/ (wiz/arch/imm/usr/ std command dirs, Lima-esque ADM_PATH/WIZ_PATH/... dispatch by wizhood()), feature/ (mixin classes inherited into std/char.lpc: action, attack, command, dbase/treemap, equip, food, message, move, name, save, skill, team, vendor, ...), d/ (game-world domains: city gates/streets, a snow-mountain sect, a temple, a bamboo canyon, a green forest estate, a graveyard/death area, a moon-pavilion zone, ...), u/ (individual wizard-workspace directories, e.g. u/cloud/), quest/ (experience-tier quest-list tables), daemon/class/* (character-class skill masters). Start room /d/snow/inn (饮风客栈), death room /d/death/gate.

Conversion (GBK→UTF-8)

scripts/convert_lib.sh libs/es2/raw libs/es2/work (default GB18030 encoding — confirmed correct via name : 东方故事Ⅱ decoding cleanly from the raw config.ES2's mojibake bytes). Results: 1933 files converted, 616 already UTF-8, 8 lossy (invalid-byte-dropped) files, 77 binaries skipped. Also stripped raw/'s .git/ from work/ (not part of the mudlib) and removed the prebuilt bin/driver/bin/addr_server binaries' influence (this project uses its own driver; left the files in place since nothing references them).

Config / driver-compat fixes

libs/es2/config.fluffos modeled on the raw adm/etc/config.ES2 (decoded name/messages), pointed at libs/es2/work, port 40214; dropped 5 directives the driver flags obsolete (address server ip/ port, reserved size, binary directory, swap file).

1. adm/obj/master.lpc's valid_override() was 2-arg (AGENTS.md §10.4) — simul_efun.lpc #includes 7 fragment files, needing the 3-arg (file, name, main_file) signature for efun:: calls inside them to resolve cleanly. Widened the signature and OR'd in main_file == SIMUL_EFUN_OB. 2. Missing runtime directories, §7.11: /log/ and /data/ were never shipped (gitignored in the upstream repo) — mkdir -p log data. More importantly, the shared log_file() simul_efun (adm/simul_efun/file.lpc) had NO assure_file() guard at all (unlike feature/save.lpc's save(), which already calls assure_file()) — added one, with a forward declaration (this driver doesn't resolve forward references within a file without one — hit "Undefined function assure_file" until added). Also guarded master.lpc's own bypass call (efun::write_file(home + "log", ...) in log_error()) the same way. Without this, the FIRST diagnostic/crash log write of the whole boot threw before Accepting telnet connections. 3. feature/treemap.lpc's _query/_delete/_set were private (AGENTS.md §7.48) — feature/dbase.lpc inherits F_TREEMAP and calls them directly, illegal on this driver (private is file-local only here, not "protected-like"). Changed to protected in feature/treemap.lpc. Same class found and fixed in feature/action.lpc's continue_action()/eval_function() and feature/attack.lpc's attack() — all three called cross-file from std/char.lpc, which inherits F_ACTION/F_ATTACK. 4. origin() returns a string on this driver, not an int bitmask (AGENTS.md §7.47) — feature/team.lpc:20's origin()==ORIGIN_LOCAL || origin()==ORIGIN_CALL_OUT is always false because of incompatible types. This one line, inherited into every std/char.lpc-based object, was the single largest contributor to the initial compile-sweep failure count (1126 of 1610 raw failure lines — every NPC/player-body compile unit hit it once). Fixed: origin()=="local" || origin()=="internal" (the §7.47 mapping table: ORIGIN_CALL_OUT (0x10) → "internal"). 5. adm/simul_efun/object.lpc overrode destruct() (AGENTS.md §7.38, "Invalid simulated efunction override" — this driver hard-rejects it). Deleted the wrapper (lost its ob->remove(euid) pre-cleanup step, same documented tradeoff as every other lib that hit this). 6. std/room.lpc's make_inventory() had no guard around new()/move() (AGENTS.md §7.25, textbook match) — a room's "objects" mapping referencing a missing/broken content path crashed that room's first-ever population (*Bad argument 1 to EFUN call_other() ... Got: int(0), /std/room.lpc in the trace). Wrapped new() in catch(), added objectp() checks at both call sites in reset(). Root-caused (not just guarded) 3 of the crashing rooms: d/latemoon/{obj,npc/obj}/latebook.lpc's "objects" entry pointed at __DIR__"npc/girl", but girl.lpc only exists under the sibling d/latemoon/npc/ directory, not d/latemoon/{obj,npc/obj}/npc/ — a pre-existing content-tree drift (multiple parallel copies of the same zone), not a conversion artifact. 7. Same content-tree-drift class, but via carry_object(...)->wear()/ wield() chained straight off a possibly-0 return (no std/room.lpc helper involved, so #6's fix didn't cover these): d/village/ flowergirl.lpc (__DIR__"obj/flower" missing, only exists at /obj/flower.lpc or under a sibling npc/obj/), d/latemoon/room/ npc/aaa.lpc (__DIR__"houndbane", only exists under an unrelated d/class/fighter/), d/latemoon/room/npc/fong.lpc and .../jane.lpc (__DIR__"obj/{deer_boot,blue_dress,redbelt}", only exist under sibling d/latemoon/{obj,npc/obj}/). Guarded each call site (object ob = carry_object(...); if (ob) ob->wear();) rather than guessing which sibling copy was "the" intended path. 8. adm/simul_efun/message.lpc's tell_room() passed a defaulted int 0 as message()'s 4th (exclude) argument when called with only 2 args (tell_room(room, str), the overwhelmingly common call shape — 71 call sites across the tree) — this driver's message() requires that argument be void | object | object *, never a bare int, and throws *Bad argument 4 to EFUN message(). First surfaced via d/choyin/stove.lpc's create() crashing, but this was a shared root cause affecting essentially every tell_room() call in the game, not a per-file bug. Fixed once in the simul_efun: omit the 4th argument to message() entirely when no exclude list was passed, instead of passing a defaulted 0. 9. cmds/std/to.lpc used an old-MudOS call_other-shaped functional literal: (: this_object(), ({ "do_to", me, arg }) :), illegal on this driver ("Illegal to use local variable in functional"). Every sibling ->edit() call site in this archive (std/{bboard,jboard} .lpc, obj/mailbox.lpc) already uses the modern (: func, bound_args... :) shape — rewrote to match: (: do_to, me, arg :), plus a forward declaration for do_to (defined later in the same file; same forward-reference requirement as fix #2 above). 10. cmds/wiz/tail.lpc called a bare tail() efun that was never real on MudOS/FluffOS (not in this driver's .spec files at all). Reimplemented with read_file()+explode()+implode() (last ~15 lines) since this driver's read_file() also has no "negative start = from end" mode. 11. d/city/{south,east1,east2,west1,west2}door.lpc (all 5 city gates) call create_door("out", ...) unconditionally, but each one's own "exits" mapping has the "out" entry commented out (its target "outskirts road" room was never shipped in this snapshot) — create_door() hard-error()s without a matching exits[dir] entry, crashing all 5 gate rooms' first load. Disabled the now-inconsistent create_door() call to match the already- disabled exit, rather than fabricating the missing target room. 12. adm/daemons/securityd.lpc's valid_write() save-file check forgot the driver appends the save extension (AGENTS.md §7.49, textbook match) — compared the driver's file argument (full filename, .o included) against the bare query_save_file() return value, so it NEVER matched and every player save threw *Denied write permission in save_object(). This is the reason a freshly-registered character landed with "你的四周灰蒙蒙地一片, 什麽也没有" (blank void) instead of the start room: enter_world()'s user->save() call errors out midway through the function, before the user->move(startroom) line ever runs — boots clean, registers clean, only breaks on the FIRST real save (exactly the "survives a smoke test, only shows up on the full flow" signature §7.49 warns about). Fixed: file == qsf || file == qsf + __SAVE_EXTENSION__. Re-verified with a full fresh registration after the fix: character correctly lands in 饮风客栈 (start room), look/score/quit all produce correct output, and the save files (data/{login,user}/f/fluffos.o) are written successfully.

Author-typo fixes (pre-existing in raw/, AGENTS.md §6.6)

Confirmed against the raw GBK bytes for each before fixing (none are conversion artifacts):

eat_func/eat_tonic — dead-but-broken data, not fixed as content

d/snow/{obj,npc/obj}/{agaric,ginseng,ice_lotus,ebony_lotus}.lpc (all 4 tonic herbs, both obj/+npc/obj/ copies) and d/goathill/{obj,npc/obj} /dead_leech.lpc all set("eat_func", (: eat_tonic :))eat_tonic is referenced NOWHERE else in the archive (grepped the whole tree), a hard compile error for an undefined function pointer. Even if it existed, feature/food.lpc (the ONLY food class anything in this snapshot actually inherits — feature/food{old,bak}.lpc are unused duplicates, see "Not fixed" below) never reads "eat_func" at all (the one line that would have — //if (query("eat_func")) return 1; — is itself commented out). Dropped the dead set() call in all 10 files rather than inventing tonic-healing logic to give eat_tonic a body.

Not fixed (pre-existing content gaps / dead code, confirmed via raw/)

Interactive test result — full registration flow

Registration shape: English id (3-12 letters) → confirm new-character (y/n) → real Chinese name (2-6 characters; check_legal_name()/ is_chinese() both needed the AGENTS.md §8.1 byte-range→codepoint-range fix, see below) → password (≥5 chars, entered twice) → email (free text) → gender (m/f) → lands in /d/snow/inn (饮风客栈). Verified live with a real Chinese name (秦风): look showed the full room description, exits, board, and NPCs (店小二/旅客×3/孙悟空); score showed a fully populated character sheet (age/gender/ganzhi birthdate, 精/气/神 bars, combat attack/defense, food/water, kill counts, potential %, overall score); quit produced a clean "你丢下一件布衣。...欢迎下次再来!" and correctly wrote both save files (data/login/f/fluffos.o, data/user/ f/fluffos.o) with zero runtime errors in debug.log.

§8.1 GBK byte-range Chinese-detection fix

adm/simul_efun/chinese.lpc's is_chinese() was the length-gate variant (strlen(str)>=2 && str[0] > 160 && str[0] < 255) and adm/ daemons/logind.lpc's check_legal_name() had the matching byte- calibrated length bound (strlen(name) < 2 || strlen(name) > 12 for a message that says "1 到 6 个中文字") plus an i%2==0 GBK-lead-byte loop gate. Both fixed per the AGENTS.md §8.1 playbook: is_chinese() now checks only the first character's CJK-Unified codepoint range (`str[0]

= 0x4e00 && str[0] <= 0x9fff`, guarded against an empty string);

check_legal_name()'s bound is now the literal 1..6 characters the message states, and the loop checks every character (no %2 gate). Live-verified: 秦风 (2 characters) registered cleanly on the first try.

Admin account

fluffos / Mud@2026, Chinese display name 秦风. Granted via this lineage's own mechanism: adm/daemons/securityd.lpc's create() reads adm/etc/wizlist (a plain <euid> <status> text file) into the in-memory wiz_status mapping every boot — added a fluffos (admin) line alongside the pre-existing lin (admin) entry, then registered fluffos through the normal flow. wizhood()/get_status() both read this mapping; (admin) status makes feature/command.lpc's enable_player() call set_path(ADM_PATH) + the real enable_wizard() efun. Verified: login banner shows "目前权限:(admin)", whoami shows User ID = fluffos, and update /adm/obj/master succeeds ("master: loaded successfully. 重新编译 /adm/obj/master.lpc:成功!") — the canonical read+compile ACL check per AGENTS.md §1.5.

WASM status update (2026-08-25, another session)

Promoted wasm_status from "" to playable — booted clean on the first try, zero fixes needed (unlike several sibling libs onboarded the same day, e.g. ds386/discworld/deadsouls_fluffos, which all hit an eager-simul_efun socket_status() compile failure specific to the WASM build's missing sockets package). Verified with a real scripted WASM session: login as fluffos/Mud@2026, arrival at the documented 饮风客栈 with the 孙悟空 crossover NPC present and active, and score producing the full character sheet (stats, 精/气/神 bars, combat ratings) matching the native-tested output exactly. quit wasn't distinctly captured in this transcript (孙悟空's own ambient action lines kept the client's idle-detector from settling, the same issue seen testing discworld's womble NPC) but is already verified clean under native testing above and untouched by anything WASM-specific.

Deep functional test (round two, AGENTS.md §10.7, 2026-08-27)

Prior work on this lib had only ever run a basic boot/registration smoke test (see "Interactive test result" above) plus the narrow §7.129 sibling-sweep fix (item 8 in "Config / driver-compat fixes"). This pass is the first FULL §10.7 playthrough: registration → look/score/i at every state change → movement → sect join (apprentice) → skill learning (learn) → safe combat (fight) → a full death/revival cycle → quit + debug.log check → reconnect after a real wall-clock gap, plus an explicit systematic grep for all thirteen standing cross-cutting bug patterns (§7.121/§8.3a/§7.122/§7.123/§7.124/§7.126/§7.129/§7.130/ §7.131/§7.132/§7.133/§7.134/§7.135). Test character: qinfeng/秦风二 (Test@2026), joined 封山剑派 (Fengshan sword sect) under 柳淳风, learned sword to skill level 2-3, went through a full smash-induced death → 白无常 ghost dialogue → reincarnation → revive-room cycle, then quit cleanly — save files removed post-test, only the seeded fluffos/秦风 admin account remains. Four bugs found and fixed:

1. §8.3a, feature/command.lpc's command_hook — declared private nomask int command_hook(string arg), registered via add_action("command_hook", "", 1) in the same file's enable_player(), but feature/command.lpc is inherited into std/char.lpc (the base of obj/user.lpc AND every NPC) — the textbook §8.3a shape, already confirmed broken the same way on 4 sibling ES2-lineage libs (demonangel, xuanjianlu, etc.) but never checked on es2 itself. Fixed: dropped private, kept nomask. 2. §8.3a variant, std/item/combined.lpc's destruct_me — also declared private, and inherited by std/money.lpc (every coin in the game), std/medicine/{pill,powder}.lpc, std/weapon/throwing.lpc, and several obj//d/ content items. Unlike the sibling demonangel instance of this exact file (where the call_out("destruct_me", ...) call site was dead/commented-out), here it is live: set_amount() does if( v==0 ) call_out("destruct_me", 1); — reached whenever a stacked item (most commonly money) is reduced to zero (spending your last coin, dropping/combining stacks down to 0). Fixed identically: dropped private, kept nomask. 3. New bug, not a prior AGENTS.md class — dbase key mismatch silently disables the wimpy auto-flee safety net for every player, forever: cmds/usr/wimpy.lpc (the player-facing wimpy [<percentage>] command) reads and writes the dbase key "wimpy" — but the ONLY consumer of the auto-flee threshold, std/char.lpc's heart_beat() (wimpy_ratio = (int)query("env/wimpy")), and every NPC's own create() (set("env/wimpy", N), ~30 files), all use the key "env/wimpy" instead. These are two entirely separate dbase entries on this driver's feature/dbase.lpc/treemap — running wimpy 30 as a player set "wimpy"=30, which nothing ever reads, while "env/wimpy" stayed permanently unset (0) for every player character ever created, so heart_beat()'s intp(wimpy_ratio = query("env/wimpy")) && wimpy_ratio > 0 guard was always false and the auto-flee branch never ran — the player-facing command was a complete, silent no-op from the very first player who ever typed it. Fixed by changing both the read and write in wimpy.lpc to use "env/wimpy", matching every other consumer. Live-verified: after wimpy 30, a fight against a training-hall NPC (武馆弟子) correctly triggered an auto-flee/retreat line ("你向後一纵,躬身做揖说道...") once damage pushed a stat below the 30% threshold — this did NOT happen pre-fix in code inspection terms (the key was never read) — and the save file correctly persisted "env":(["wimpy":30,]). Flagged for a sibling sweep: since this is the ES2 codebase's own feature/dbase.lpc/env/ naming convention (not something es1 shares — es1 uses a different stat system entirely), check haiyang2/demonangel/xkx2001/rzrmud/ xo/zhyx/naruto (the later ES2-derived family) for the same cmds/usr/wimpy.lpc vs. env/wimpy key mismatch — grep each for query("wimpy")/set("wimpy" (no env/ prefix) outside of std/char.lpc's own correctly-prefixed reads. 4. AGENTS.md §7.112 class, d/death/npc/{wgargoyle,bgargoyle}.lpc — both NPCs' init() unconditionally schedules a 5-stage call_out("death_stage", ...) narrative chain (ghost dialogue → reincarnate() → move to REVIVE_ROOM) with no re-entry guard. This is the exact, previously corpus-swept (150+ libs) §7.112 pattern under its most common filenames, but es2 was never part of either sweep wave. Fixed with the same death_stage_active set_temp()/ delete_temp() guard already applied to sibling haiyang2's copy of this same file (see AGENTS.md §7.112's reference implementation), adapted to each file's own exit points (bgargoyle.lpc has an extra "not actually a ghost yet, attack them" branch that also needed the flag cleared). Live-verified via a full smash→ghost→白无常 dialogue→reincarnate()REVIVE_ROOM(城隍庙) cycle with zero debug.log/driver-log errors throughout.

Confirmed clean (checked, no fix needed):

Verified via scripts/lpcc_check.sh (same 21 pre-existing FAILs as before this pass — all previously-documented dead/gapped content, zero new regressions) and a full native driver boot + the playthrough above, with zero debug.log/driver-stdout errors across the whole session.

深度功能测试(2026-09-04,round three,paid shop)

新角度:补上 2026-08-27 round two 没做过的真实购买。那次已经 apprentice 封山剑派柳淳风,本轮不再拜师。

修复的 programming bug

1. log_error() 把编译警告当「编译时段错误」广播给玩家(§15w)adm/obj/master.lpclog_error()this_player(1) 无条件 write("编译时段错误:" + message)。本驱动把 warning: Unused local variable / Unknown #pragma 也送进 log_error(),第一次 登录刷了 34 条「编译时段错误」。闸门改成 this_player(1) && strsrch(message, "arning:") == -1,警告仍写入 work/log/log。修复后同路径 0 条玩家可见「编译时段错误」。 2. can_afford() 在没有白银时把任何非整两黄金的价格都判成「零钱」feature/finance.lpc 的 1996-10-14 改写在 silver 缺失时直接 else if (amount%10000) return 2。身上已有足够铜钱支付 15 文包子 (一百文钱 + 一两黄金)仍报「你没有足够的零钱,而对方也找不开」。 补上「无银、有铜、铜钱覆盖 amount%10000」分支。修复后 buy 包子 from waiter 成功,找零 100−15=85 文。

黄金不能拆成铜钱仍是设计(纯黄金买 15 文会走同一句零钱提示); vendor_goods 的键是中文「包子」不是 dumplingbuy dumpling from waiter 会「对方好像不愿意跟你交易」——list 列出的就是中文键,不是 bug。

实测过程

管理员 fluffos / Mud@2026。第一输入是「您的英文名字」,没有内码 菜单。端口 40214。落地饮风客栈 /d/snow/inn,店小二 id waiter。 孙悟空在场,ambient 不算 live clock,idle 0.45 够用。clone /obj/money/coin + call coin->set_amount(100)list 包子十五文 / 鸡腿三十文 / 酒二十文 / 匕首五十文。修复后 buy 包子 from waiter: 「你向店小二买下一个包子。」i 包子 + 一两黄金 + 八十五文钱。

live debug.loglibs/es2/log/debug.log(Boot Time Fri Sep 4 03:48:47 2026),无 error: / Too deep recursion / F_FINANCE。 管理员存档未提交。