Eastern Story (Blue Sky)

✅ 可玩

东方故事(蓝天) — es1_win

es1_win

🔑 fluffos / Mud@2026 更新 39cb8d7 2026-09-01 源码 下载 ZIP

▶ 开始游玩 · Play Now

"东方故事"(ES)系列 MUD 的一个早期基础版本,属于 ES / TMI 引擎家族——尽管名字听起来像武侠题材,实际内容是一个西式奇幻/DND 风格的冒险世界。与本批次的 `esI`(屠龙之战)同源,共享同一套核心引擎代码。新角色降生在远风镇的冒险者公会大厅(`/d/adventurer/hall/adv_guild`),通过 `cost`/`advance`/`train`/`list`/`join` 等公会内部指令自行训练升级——没有 NPC 教师,以骑士、法师、牧师、武僧、学者、盗贼等公会体系取代门派玩法;farwind 商行的 `list`/`buy` 交易是出镇后的第一站。角色创建菜单本身即提供十余种可选种族:人类、精灵、矮人、半兽人、地精、半身人、蜥蜴人等。

English

An early foundational build of the "Eastern Story" series and part of the ES/TMI engine family — despite the wuxia-sounding name, it's actually a Western-style fantasy world in the D&D tradition, sharing its core TMI-lineage engine with sibling esI (this project's "Dragon Slaying War" build). New characters land in 远风镇's adventurer's guild hall (`/d/adventurer/hall/adv_guild`) and level through a guild-room-only training loop (`cost`/`advance`/`train`/`list`/`join`, no NPC teacher) across knight/mage/healer/monk/thief/scholar/swordman-style guild domains rather than sect-based jianghu factions, with a working shop (farwind, `list`/`buy`) as the first stop out of town. Over a dozen playable races are offered (humans, elves, dwarves, orcs, goblins, halflings, lizardfolk, and more per its own character-creation menu).

README

内容亮点

在线试玩

https://mudlibs.fluffos.info/es1_win/

管理员账号 / Admin account

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

本地运行

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

游戏端口:40009

NOTES · 移植与修复记录

es1_win.rar → es1_win

Status: DONE — boots clean, playable over telnet

Full flow confirmed: banner, "东方故事现在没人." (no one online) status line, name-length validation, name re-prompt.

What was fixed

1. Encoding: 8238 converted, 1674 already UTF-8/ASCII, 82 skipped binary, 17 lossy. .c.lpc: 6933 files, 1101 refs + 2 angle-bracket #include<x.c> refs auto-fixed. 2. master.lpc's domain_file(): explode("/", "/") (called by the DRIVER ITSELF during its own bootstrap domain-assignment, per mudlib_stats.cc) returns a zero-length array in this driver rather than ({ "", "" }), and the existing if(!path) return "NONAME"; guard doesn't catch a non-null-but-empty array — path[0] threw "Array index out of bounds", failing master's own compile. Added || !sizeof(path) to the guard. 3. std/object/prop.lpc had #include </std/object/prop_logic.lpc> (angle brackets, file in the SAME directory) — the exact §8d pattern from lib #3, but with an important addition to that lesson: the master::get_include_path() fix does NOT help here, because this particular file gets compiled during PRELOAD, before compiler_vm_context is set (confirmed via the driver source, lexer_utils.cc's init_include_path(): "No VM context: keep the config-file include path as-is -- there is no master object to ask"). get_include_path() silently never gets consulted for preload-time compiles. Fixed by changing the include to quotes instead (#include "/std/object/prop_logic.lpc") — quote-style local-directory resolution doesn't depend on any master apply/VM context at all, so it's the more robust fix regardless of when a file happens to compile. Also added the get_include_path() apply to this lib's master.lpc anyway (harmless, helps genuinely-dynamic compiles), but the REAL fix for this specific case was the quote change. This failed to reach the login prompt with NO error output at all until traced with temporary write("DEBUG X\n") statements inserted into logind.lpc's logon() — the actual error (_query/_delete/ _set undefined, cascading from the broken include, inherited all the way from /std/room/room down to /d/std/rooms/netdead) was being thrown during the very first connection's active_users() call, and the DRIVER's own new_conn_handler catches ANY error escaping logon() and just silently disconnects the user with zero message — so a real compile-cascade bug during first-connect can look exactly like a hung/dead server with nothing in reach except the debug.log.

Known remaining issues (documented, not fixed)

How to run

cd libs/es1_win
~/src/fluffos/build-debug/src/driver config.fluffos
python3 ../../scripts/mudclient.py 127.0.0.1 40009 --timeout 10 --send "" --send "look" --send "quit"

Re-verification pass: driver rebuild + LPC formatter + WASM build

WASM-enablement pass (loopback / admin seeding)

深度功能测试 / Deep functional test (2026-07-24)

First real *playthrough* pass (AGENTS.md §10.7) — every prior pass on this lib had only verified registration + look/score/quit + admin login, never a continuous real playthrough. Native driver (build-debug), one continuous session per checklist item, with several deliberate net-dead disconnect/reconnect probes. Test characters (both kept, not cleaned up, as evidence):

Correction to this file's own earlier "How to run" section: log directory : /log in config.fluffos resolves against the driver's LAUNCH cwd, not the mudlib root (AGENTS.md §5.2) — with the documented cd libs/es1_win && driver config.fluffos invocation, debug.log lands at libs/es1_win/log/debug.log, not libs/es1_win/work/log/debug.log (a long-stale file left over from an earlier era/invocation convention that this pass initially — and wrongly — kept checking for several rounds before noticing it never changes). Use the top-level log/ path when grepping for errors on this lib.

Bugs found and fixed

1. Net-dead disconnect during the registration Q&A permanently strands the account in the net-dead holding room on every future login — std/user.lpc:902 restart_heart().

```lpc // BEFORE linkroom = (object)this_object()->query("linkdead_room"); if (linkroom) { this_object()->move_player(linkroom, "SNEAK"); tell_room(linkroom, ...); }

// AFTER linkroom = (object)this_object()->query("linkdead_room"); if (linkroom) { this_object()->move_player(linkroom, "SNEAK"); tell_room(linkroom, ...); } else if (base_name(environment(this_object())) == LINKDEAD_ROOM) { this_object()->complete_setup(""); } ```

2. #include <compress_obj.h>'s set_default_ob(__FILE__) points every affected clone at the wrong file, crashing on any property lookup that falls through to it — already-cataloged AGENTS.md class §7.14 ("__FILE__ in an #included fragment expands to the FRAGMENT's path, not the includer"), previously seen on xlqy_early/ longyunmeng; this is a new (fixed) instance of the same class.

`` 执行时段错误:*call_other() couldn't find object '/include/compress_obj.h'. 程式:/std/object/ob.lpc 第 127 行 物件: /obj/bandage#40 ` Every item in this lib that inherits only the lightweight /std/object/ob base (checked: obj/bandage.lpc, obj/bowl.lpc, obj/torch.lpc, obj/map.lpc, obj/bag.lpc, obj/light_ball.lpc, and ~35 more across d//u/, one grep -rl compress_obj away) is affected, since set_default_ob()'s stored value is consulted (std/object/prop.lpc:126-127, value = default_ob->query(label)) on ANY property read that isn't already set directly on the specific clone — trivially reachable any time such an item's price/weight/etc. needs to be displayed, which is exactly what a shop's list` does for every item it sells.

```lpc // BEFORE if ( clonep(this_object()) ) { set_default_ob(__FILE__); return; }

// AFTER if ( clonep(this_object()) ) { set_default_ob(base_name(this_object())); return; } `` base_name(this_object()) is evaluated at RUNTIME inside the actual compiling object's own create(), so it always correctly resolves to whichever real file (e.g. /obj/bandage`) the fragment was pasted into, regardless of the header's own physical path.

What was tested and confirmed working

Full continuous playthrough as Shenqingxue (real Chinese name, human female adventurer), covering the whole §10.7 checklist:

Addendum (2026-07-24): §7.12 tell_room() fix ported from sibling esI

esI's own §10.7 pass found that adm/simul_efun/tell_room.lpc's 2-arg call shape (exclude defaulting to int 0, rejected by message()) crashes adm/daemons/weather_d.lpc's self-rescheduling change_phase() call_out — no enclosing catch(), so the uncaught throw aborts the function before it reaches its own tail-end reschedule, permanently killing the day/night phase daemon for the rest of that boot the first time any outdoor room is visited (set_outside() registers it, some phases in adm/etc/daytime.data are as short as 1 second). Confirmed via diff that this lib's tell_room.lpc is byte-identical to esI's pre-fix copy, so it carries the same bug (not independently reproduced live on THIS lib during either pass — plausibly a session-timing/ room-visited difference). Ported the same one-line fix:

message("tell_room", msg, room, exclude || ({}));

Sanity-checked only (fresh boot + existing test character shenqingxue login/look/quit, clean, zero new debug.log errors) — not a full independent §10.7 replay, since this lib was already marked done. See libs/esI/NOTES.md's own "深度功能测试" section (bug #5) for the full live-reproduction detail (including a direct eval-command proof of the crash and the fix).

Not verified live (explicitly)

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

Independently re-verified against current code rather than trusting the round-one writeup above. Confirmed all 3 prior fixes still hold (the net-dead-during-registration restart_heart() fix, the compress_obj.h __FILE__/§7.14 fix, and the ported §7.12 tell_room() fix), and found one new §7.90-class issue: a real preload crash, not just a theoretical risk.

New fix: §7.90-class eval-cost abort during boot preload — chinese_d's dictionary build genuinely failed, not just at-risk

config.fluffos had maximum evaluation cost : 300000, below even this project's common 700000 template default. Unlike some other libs this session where a low eval-cost config was flagged as a latent risk without a confirmed failure, THIS lib's boot log showed a real, live preload crash:

Eval interrupted: object adm/daemons/chinese_d cost limit reached, limit: 300000 usec.
执行时段错误:*Too long evaluation. Execution aborted.
程式:/adm/daemons/chinese_d.lpc 第 60 行
呼叫来自:/adm/daemons/chinese_d.lpc 的 make_dictionary() 第 60 行
执行时段错误:*Can't catch eval cost too big error.

chinese_d's make_dictionary() (called from its own create(), itself called from master.lpc's preload()) never finished building its dictionary — every boot, unconditionally, not an occasional flake. Fixed with the established §7.90 remedy: maximum evaluation cost5000000. Verified: fresh reboot, zero cost limit/Too long evaluation/Can't catch hits anywhere in the boot log.

Re-verified live: all 3 round-one fixes still hold

Other checklist items: mostly not applicable, different lineage architecture

This lib's TMI/Discworld-family codebase doesn't share the ES2/大唐 lineage's adm/simul_efun/file.lpc/cmds/wiz/update.lpc shapes this session's checklist was tuned around — no update.lpc exists at all (the wizard reload command is _update.lpc, invoked as update, a different implementation entirely; tested live, works correctly). log_error() (adm/obj/master.lpc:348) never broadcasts compile diagnostics to the connected player at all (only writes to a log file, with a fallback write() only on write FAILURE) — confirmed this lineage simply doesn't have the §7.10-class leak risk by construction, not because it's correctly gated. work/log/ is a real, git-tracked, already-existing directory (not gitignored, unlike some other libs), so no assure_file()-class gap applies to its own log writes either.

A cosmetic finding, not a bug: fluffos's displayed rank shows "apprentice", not an admin-sounding title

First login this pass printed 目前权限:apprentice, which looked concerning at a glance. Investigated rather than assumed broken: this label comes from DOMAIN_D->query_domain_level(player), a separate "domain" title-display system, NOT from the actual ACL groups that control real permissions. /adm/etc/groups still correctly lists fluffos under both (root) and (admin). Verified REAL admin access directly rather than trusting either label: update /adm/daemons/logind succeeded ("Updated and loaded."). Left the cosmetic domain-title as-is — this project's own "don't fabricate content" principle applies to game-facing labels too, and actual access is unaffected.

Verification method

Booted native build-debug driver, admin login (fluffos/Mud@2026), update /adm/daemons/logind as the real privileged-action check (succeeded despite the cosmetic "apprentice" display). One rapid reconnect (the connection's own quit is gated behind okip for wizards — a genuine safety feature, not a bug — so the reconnect exercised the silent net-dead body-reattach path instead, landing back in the exact same room/state, which is itself valid reconnect-stability evidence). Driver killed by exact PID after testing; incidental fluffos.o save churn and a stray .tmp save file reverted/removed before commit.

§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). 2 live

replace_program(ROOM); occurrences: d/noden/6,8.noden.lpc's create() (same shape/location as sibling esI), and this lib's own room-building tool u/s/sage/test/roommaker.lpc's heredoc ROOM_CODE template — so newly-built rooms were inheriting the bug too. Both fixed by deleting the redundant line. Verified via a clean native driver boot (zero new debug.log errors, port listening, killed by exact PID after ~8s).

die() null-killer crash — ported from sibling esI (2026-08-20)

esI's round-four §10.7 deep test found a real crash in std/user.lpc's die(): killer (set from query("last_attacker"), falling back to previous_object()) can end up int 0 whenever a character dies from a source other than direct living-creature combat (poison/bleeding/sickness/drowning conditions, or an admin-applied receive_damage()) — two lines then call killer->query("npc") / userp(killer) unconditionally, throwing a driver-level error on every heart_beat() tick for as long as hit_points < 1, which means the character never actually finishes dying (no corpse, no ghost, stuck forever). Confirmed via diff that this lib's copy of the same two lines is byte-identical/unfixed. Ported the same fix verbatim (killer && guards on both call sites, matching every other killer-using line in the function which was already correctly guarded):

// BEFORE
if (!wizardp(this_object()) && killer->query("npc"))
if (this_object()->query_level() < 5 && userp(killer) && query("last_attacker")) {
// AFTER
if (!wizardp(this_object()) && killer && killer->query("npc"))
if (this_object()->query_level() < 5 && killer && userp(killer) && query("last_attacker")) {

Sanity-checked here with a fresh native driver boot + look/quit smoke test (clean, zero new debug.log lines, killed by exact PID after) — not a full independent replay of this lib's own death/respawn flow, since esI's pass already live-verified the fix's actual behavior end-to-end twice. Flagged here for whoever next does a round-four (or deeper) pass on this lib specifically, to do that full live verification in its own right.

Round-four (§10.7) deep test: net-dead-during-registration gap closed live (2026-08-21)

Closed the one remaining explicitly-flagged gap from this lib's own NOTES.md: the restart_heart() net-dead-during-registration fix (std/user.lpc:921-932, the else if (base_name(environment(...)) == LINKDEAD_ROOM) { ... complete_setup(""); } branch) had been code-confirmed present across two prior passes but never re-walked live end-to-end since the original fix/verification. Actually walked it this pass, with a fresh, full-budget session:

Fast checklist pass: three newer session-wide findings, not applicable to this lib

``lpc mapping query_skills() { if (!skills) return ([]); else return copy(skills); } `` identical in both files. No unguarded raw-variable return found. Confirmed clean, no fix needed.

§7.122 sibling-sweep fix: unconditional autoload-marker reclone on net_dead (2026-08-28)

Flagged in AGENTS.md's §7.122 entry as a "near-certain candidate" — es1 is es1_win's literal direct ancestor/near-byte-identical sibling (shared "东方故事" lineage), and es1 itself was already confirmed and fixed for this exact bug on its own §10.7 round-two pass. Checked directly rather than assumed: work/std/user/autoload.lpc's load_autoload_obj() was indeed byte-for-byte the same vulnerable TMI-2-lineage code (// autoload.c: adapted from 2.4.5 code by Truilkan@TMI) — unconditionally clones every entry in auto_load[] on login with no presence check. Confirmed the same triggering ordering as es1: std/user.lpc's remove() (clean quit path) calls save_me() *before* destroy_autoload_obj(), and net_dead() (abrupt-disconnect path) calls save_data() but never calls destroy_autoload_obj() at all — so an abrupt disconnect bakes every carried autoload-marker item (bank cards, rings, disease/guild markers, etc.) into the ordinary save data AND leaves it physically in inventory, and the next login's load_autoload_obj() then clones a second copy on top with no guard, duplicating without bound across repeated net_dead cycles.

Fixed by porting es1's exact proven remedy verbatim (adapted only to this file's own K&R brace style): load_autoload_obj() now snapshots all_inventory(this_object()) once, and skips cloning any auto_load entry whose base_name() already matches something already carried, appending each freshly-cloned object to that same tracking array so duplicate entries within one auto_load array can't double-clone either. Verified via a clean native driver boot (Initializations complete, port 40009 listening, zero new compile errors/warnings introduced) — not independently live-reproduced with a real net_dead cycle on this specific lib (the fix is a verbatim port of an already live-verified remedy from the literal ancestor codebase, not a novel fix needing its own from-scratch reproduction).

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

东方故事基础版(蓝天)。