Eastern Story I

✅ 可玩

东方故事(蓝天) — Eastern Story I (ES I)

es1

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

▶ 开始游玩 · Play Now

这是本项目已收录的 `es2`/`haiyang2`/`demonangel`/`xkx2001` 等"东方故事Ⅱ / ES2"家族的上一代祖先引擎——`bin` 目录里自带的旧版驱动名为 `mudos.exe`,`README.md` 自述"东方故事1MUD",管理员账号约定为 `admin`/`mudren`。虽然中文名字带有武侠色彩,实际却是一个西式 D&D 奇幻世界:注册时只需要一个纯英文角色名(不含中文名机制, `is_chinese()` 一类的功能在此版本尚未出现),新角色可以从人类、 精灵、矮人、半兽人、地精、半身人、蜥蜴人、妖精、魔族、半人马、 黑暗精灵、眼魔、吸血鬼、鸟人、变形虫共 16 个种族中挑选一个,连同性别一起降生在远风镇的"冒险者公会"。

English

The original 'ES1' engine -- ancestor of this collection's es2/haiyang2/demonangel/xkx2001 family -- but despite its Chinese name ('东方故事', Eastern Story) this is actually a Western D&D-flavored fantasy world, not a wuxia setting: registration is English-name-only, with no Chinese-name mechanic in this early snapshot at all. New adventurers choose a gender and one of sixteen playable races -- human, elf, dwarf, half-orc, gnome, halfling, lizardman, imp, demon, centaur, dark elf, beholder, vampire, birdman, or shapeshifter -- before landing together at the Adventurer's Guild in Farwind Town. Rather than martial-arts sects, advancement runs through Western-style guilds scattered across the world's domains: a mage's tower, a monastery, a knights' order, and a thieves' guild sit alongside eastern-flavored zones for killers, monks, and scholars. Score displays a six-stat sheet (strength/wisdom/agility/constitution/piety/luck) alongside stamina, spirit, and social/charisma bars, and the game maintains its own kingdom map, player leaderboard, and adventurers' bulletin board right in the guild hall.

README

原始存档:git clone https://github.com/MudRen/es(commit 4c2177e)。本次上线按本项目一贯的方式完成了 GBK→UTF-8 全量转码 与驱动兼容性修复(详见 NOTES.md)。

内容亮点

在线试玩

https://mudlibs.fluffos.info/es1/

管理员账号 / Admin account

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

本地运行

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

游戏端口:40239

NOTES · 移植与修复记录

es1 — 东方故事(蓝天) (git clone from MudRen/es)

Source: git clone https://github.com/MudRen/es (commit 4c2177e513c9f2e57fbcca3a9239e6a5096a6b7d, cloned 2026-08-26). Port: 40239. Status: done (boots clean, full registration verified end-to-end with an English login name and full race/gender character-creation flow, look/score/who/say/relogin/ quit all confirmed working, admin update ACL confirmed).

What this is

This is the original "ES1" engine -- the direct ancestor of this project's already-onboarded es2/haiyang2/demonangel/xkx2001/ rzrmud/xo "ES2 family". README.md in the raw archive says only "使用FluffOS v2017驱动,游戏管理员账号:admin|mudren" (admin account admin/mudren); the shipped prebuilt binary is named mudos.exe (a Windows MudOS build, not FluffOS -- "FluffOS v2017" in the README refers to a *later* fork's driver, not what originally shipped here). config.cfg's name field decodes (GB18030) to 东方故事(蓝天) ("Eastern Story (Blue Sky)"), and its default fail message decodes to the classic ES-family 什麽?.

Despite the wuxia-sounding mud name, this is a Western D&D-style fantasy world, not a jianghu/wuxia setting: registration is English-login-name-only, and adm/simul_efun/chinese.lpc / adm/daemons/logind.lpc have no is_chinese()/check_legal_name() machinery at all -- that whole §8.1 bug class (documented extensively for es2/demonangel) simply doesn't exist yet in this snapshot, because the feature it patches doesn't exist yet either. This, combined with the total absence of addn() and 2-arg query(prop,ob) call sites anywhere in the live tree (both greps came back empty outside one already-dead file), confirms this archive really is an earlier *generation* of the engine, not just a differently-reskinned sibling.

New characters pick from 16 races (human/elf/dwarf/orc/gnome/halfling/ lizardman/imp/daemon/centaur/drow/beholder/vampire/hawkman/ shapeshifter) and a gender, then land at /d/adventurer/hall/adv_guild (冒险者公会, the Adventurer's Guild in "远风镇"/Farwind Town).

Layout mirrors the family: adm/ (master/simul_efun/daemons), cmds/ (wiz/arch/imm/usr/std/open/file/object/... command dirs), feature/ (only clean_up.lpc survives here -- the rest of the later family's mixin-class system, e.g. action/attack/dbase/equip/move/ name/save/skill/team, doesn't exist yet; this codebase instead puts nearly everything directly in std/body*.lpc/std/user.lpc/ std/npc*.lpc), std/ (body/living/user/npc class hierarchy, adt/ generic data-structure classes, races/ per-race mixins, weapon/, board/), d/ (game-world domains: abyss, adventurer, deathland, eastland, erosmia, god, healer, island, killer, knight, mage, magic, martial, monk, moorth, noden, scholar, swordman, thief, translate, wiz -- zero overlap with es2's domain list), u/ (wizard workspaces). mudlib directory is the repo root itself; master file : /adm/obj/ master, simulated efun file : /adm/obj/simul_efun.

Comparison against es2 (sanity check per task brief)

Confirmed genuinely different content, not a duplicate of es2 or anything else already onboarded:

However, es1 turned out to be near-identical to this project's own already-onboarded es1_win (008, sourced from a separate es1_win.rar archive, also named "东方故事(蓝天)"/"Eastern Story (Blue Sky)" in its own meta.json) -- discovered only *after* the es2 comparison came back clean, so flagged here explicitly even though it wasn't the comparison the task brief anticipated. A byte-level diff (after normalizing es1_win's CRLF line endings) shows d/ domain lists, feature/ (both just clean_up.lpc), and individual files (e.g. std/body.lpc) are identical once line endings are stripped. A broader whitespace-insensitive diff across all 6,927 .c/.lpc files common to both trees found 6,222 (89.8%) byte-for- byte identical and only 702 (10.1%) genuinely differing in content -- overwhelming confirmation this is the same underlying codebase as es1_win, just a different point-in-time snapshot (a live git history here vs. a .rar archive there), not two independently authored mudlibs that happen to share an engine. Per this project's established precedent for this exact shape (jyqxc/jyqxc2, two near-identical browser-download copies of the same archive, both onboarded independently with duplicate_of: null and the relationship documented in prose), this lib is onboarded as an independent entry rather than folded into es1_win's number, since the two are not byte-identical and this git-history snapshot has a verifiable, citable commit hash the .rar archive does not.

Conversion (GBK→UTF-8)

scripts/convert_lib.sh libs/es1/raw/es libs/es1/work (default GB18030 encoding -- confirmed correct via the config's name field decoding cleanly). Results: 9,215 files scanned, 7,545 converted, 1,570 already UTF-8, 15 lossy (invalid-byte-dropped) files, 85 binaries skipped. .c.lpc renamed 6,929 files; 1,102 literal .c" references fixed; 1 #include <x.c> angle-bracket reference fixed; 607 local #include <x.lpc> angle-bracket includes converted to quoted form. Also removed the copied-in .git/ directory from work/ (an artifact of cp -ring the git-cloned raw checkout) -- not part of the mudlib, and left the shipped mudos.exe binary and .vscode/ project folder in place untouched since nothing in the mudlib references them.

Config / driver-compat fixes

libs/es1/config.fluffos modeled on the raw config.cfg (decoded name/messages), pointed at libs/es1/work, port 40239; dropped the directives this driver flags obsolete (address server ip/port, binary directory, swap file, reserved size) and reduced maximum local variables from the raw config's absurd (and clearly already a no-op typo on the original driver) 50000000-adjacent value down to 200, in line with the rest of this project's ES-family libs.

Followed this session's RAM-safety discipline throughout: compiled master.lpc/simul_efun.lpc/std/room.lpc/std/body.lpc/std/user.lpc and a handful of other widely-inherited base classes individually first and got them clean before ever running a full lpcc_check.sh batch sweep; watched the batch lpcc --batch process's RSS every ~15-20s for each of the 5 sweeps run this session -- peaked around 1.2GB, nowhere close to the ~10GB kill threshold.

1. adm/daemons/usage_d.lpc's AVERAGING_NUM.0 macro-pasting trick -- #define AVERAGING_NUM 12 then x / AVERAGING_NUM.0 relied on old-MudOS macro expansion re-lexing 12 immediately followed by a literal .0 into a single float token; this driver's preprocessor doesn't re-lex after substitution, producing syntax error, unexpected '.' at all 4 call sites (plus a 5th cascading "Illegal LHS"). This file is loaded very early and widely enough that its own compile failure was blocking the RAM-safety pre-check of several other base classes (std/user.lpc, std/living.lpc, std/npc.lpc, etc. all transitively reference it) -- fixed first, before any batch sweep, per this session's own stated discipline. Fixed: AVERAGING_NUM.0(AVERAGING_NUM*1.0) (4 sites). 2. std/npc2.lpc declared a global variable (nosave int hb_status;) before its own inherit LIVING; -- "Illegal to inherit after defining global variables." Reordered (inherit first). Five more files had the identical shape (global var, or an #include that itself declares file-scope globals, before their inherit): d/eastland/beggar/monster/master.lpc, d/eastland/ legend/monster/captain1.lpc, d/eastland/story/wiz/monster/ wiz_elder1.lpc, d/noden/hawk/monster/pontiff.lpc, obj/tools/ barney.lpc (all: moved inherit above the offending declaration) and d/mage/tower/monsters/patolas.lpc (moved the #include "detect-magic.h" that carries the global array declarations to *after* its own inherit MONSTER;, matching the correct order already used by its sibling d/mage/tower/obj/crystal_orb.lpc). 3. adm/daemons/ref_d.lpc used a local variable literally named ref -- this driver reserves ref as a keyword (its native pass-by-reference parameter feature), so string ref; inside resolv_ref() produced syntax error, unexpected L_REF -- cascading into ~120 downstream parse-recovery errors in the rest of the same (short, 83-line) file, since this daemon is inherited widely enough to appear as the "same root cause" in dozens of unrelated command files' failure traces. Renamed the local variable to refstr (3 sites) -- did not touch the file's own "ref" *string* property-name literal, which is unrelated data, not the colliding identifier. 4. class used as a variable/parameter name in ~15 files (a second reserved-keyword collision, same shape as fix #3 but for this driver's struct-like class feature) -- void receive_message(string class, string msg), void relay_message(string class, ...), void set_guild(string class), local string class; declarations, etc., all producing syntax error, unexpected L_CLASS. Renamed the colliding identifier to chr_class (character-class, matching what these variables actually hold: knight/mage/healer/etc.) throughout each affected function, using a quote-aware substitution that left ->query("class") string-literal lookups (a legitimate stored- property key, unrelated to the reserved word) untouched. Affected: d/monk/guild.backup/monsters/saulin_monk.lpc, d/eastland/ beggar/monster/donkey.lpc, d/eastland/liang_shan/monster/ chi_yu/{knight,mage,healer}.lpc, d/eastland/liang_shan/monster/ test2.lpc, u/b/boss/monk/inner_room{,1,2}.lpc, u/b/boss/god/ guild/new_guild.lpc, u/c/creative/knight/force_room.lpc, u/s/ sage/test/pet.lpc, u/s/snake/monster/healer.lpc, u/m/moon/ user.lpc, u/m/moon/new_guild.lpc. 5. std/object/ob_logic.lpc's set_name() had varargs on its forward-declared prototype but not on its real definition -- varargs void set_name(string str, string c_str); (line 22) vs. the actual void set_name(string str, string c_str) { ... } (line 135, missing varargs); this driver enforces the arity from the real definition, not the prototype, so every one-argument set_name("foo") call site (the overwhelming majority -- this codebase has no separate Chinese-name concept) failed with Wrong number of arguments to 'set_name', expected: 2, minimum: 2, got: 1. The adjacent sibling function varargs void set_short(string shortname, string c_short) two lines below already has the correct shape, confirming this was a dropped keyword, not a deliberate arity change. Fixed by adding varargs to the real definition. 6. The std/room/room.lpcreplace_room.hreplace_program(ROOM) redundant-self-replace pattern, AGENTS.md §7.100 -- every one of 17 per-domain replace_room.h header files (plus 2 inline call sites, d/noden/6,8.noden.lpc and the in-game u/s/sage/test/ roommaker.lpc room-generator's own code-template heredoc -- the exact "the bug lives in the factory" shape §7.100 also documents) contained a bare replace_program(ROOM);, called from create() in ~159 rooms across domains this file is #included into, always immediately after (or as the last statement of) that room's own inherit ROOM;-derived create(). On the FluffOS build this project uses, this doesn't produce §7.100's originally-documented "closure-crash landmine" symptom -- instead it manifests as an immediate, deterministic runtime crash on the room's very first load: *Nesting call_out(0) level limit exceeded: 1000, thrown from std/room.lpc's own create()call_out("reset", 0) (line 28) via the room's create()reset() → replace_program() interaction on this driver. Root-caused via a minimal single-file lpcc test on /d/eastland/tomb/square6 (the single largest affected bucket, 69 rooms) before touching anything else. Fixed by commenting out the replace_program(ROOM); line in all 17 shared replace_room.h files (one edit per file, each carrying an explanatory comment) and both inline call sites, rather than editing the ~159 individual room files that #include them -- this is the same one-line-deletion fix §7.100 already validated as safe, just applied at the shared-header level this archive happens to centralize it at, making a full, reviewable fix tractable in one pass instead of the "future sweep backlog" punt §7.100's own closing note describes for a lib where the calls aren't centralized. Verified with individual lpcc re-tests (not just the batch sweep, see the caveat below) on 5 sample rooms spanning 3 different previously-100%-failing domains (d/eastland/tomb, d/eastland/liang_shan/lake) -- all load clean post-fix. 7. Content-tree-drift class (AGENTS.md §7's "sibling copy dropped a file"/"copied from another wizard's home dir without updating the include" shapes), ~50 files individually diagnosed and fixed -- mechanically similar to, but each individually verified rather than guessed, the following sub-patterns: - Genuinely dropped local #includes: 9 files under d/noden/ hawk/monster/ (bird, bernice, boy, girl, man, older, older1, servant, woman) had zero #includes despite bare inherit MONSTER;, while 30 sibling files in the same directory all correctly carry #include <mudlib.h> + #include "../hawk.h" -- restored the same two-line header. 27 more files across a dozen other directories (cmds/{open,std}/_tune.lpc, several d/eastland/story/{,wiz/}armor/*.lpc, d/island/samurai/obj/ dagger1.lpc, d/noden/drow/square{1,2}.lpc, 7 d/noden/hawk/ armor/*.lpc, 2 d/noden/hawk/weapon/*.lpc, u/m/moon/cloth.lpc, u/s/snow/obj/{icy,shoe}.lpc) matched the exact same shape (inherit-only file, no include, in a directory where the dominant/majority sibling pattern is a single #include <mudlib.h> or a two-line domain-header pair) -- restored via an automated per-directory sibling-header-majority-vote script, each result spot-checked before applying. - inherit before its own domain #include (2 files, d/noden/hawk/monster/{crying,saw}.lpc): reordered. - Wrong relative-path depth: d/island/monster/{feller, old_feller}.lpc's #include "../hole.h" should have been "../hole/hole.h" (the header actually lives one directory deeper, at d/island/hole/hole.h) -- confirmed via old_feller. lpc's own live (non-commented) use of HOBJ"saw", a macro only hole.h defines, ruling out the alternative "doesn't need this header at all" fix. d/eastland/liang_shan/obj/fish_plate.lpc's bare #include "takeda.h" (missing the ../ every other sibling in that directory uses) got the same one-token fix. - Stray absolute path to a nonexistent/wrong wizard-home file, where the file doesn't actually need anything domain-specific: d/island/samurai/obj/{mercury_pen,weapon}.lpc (/u/t/takeda/ takeda.h, /u/o/odin/odin.h -- neither exists anywhere in the archive, and neither file references any macro either would have defined) → swapped to the dominant sibling convention, bare #include <mudlib.h>. Same fix for d/translate/cwa/ cut_tails.lpc (/u/m/maverick/maverick.h), d/scholar/scholar/ stone1.lpc (had no include at all), u/m/moon/ftpd.lpc (ditto), d/monk/gonfus/test/fon-mo.lpc + u/c/creative/fo-mo.lpc (/u/w/wind/wind.h, a copy-pasted "gonfu skill" template shared between two wizards' homes), and 7 files under d/scholar/ demand/where/ (cave31, fire_beast, fire_elephant, ohio, pawnbroker, princess, sky_guilt) which each #included a *different*, unrelated other domain's header (layuter.h, goomay.h, saulin_temple.h, oldcat.h, almuhara.h) despite using none of those headers' macros -- confirmed via grep before touching each one, then swapped all 7 to <mudlib.h>. - **Stray absolute path where the *correct* file exists elsewhere in the archive, unambiguously**: d/god/cmds/_god.lpc + u/b/ boss/god/cmds/_god.lpc + d/killer/cmds/_killer.lpc (missing #include <mudlib.h> for the DAEMON macro their inherit DAEMON; needs -- diff-confirmed near-identical to each other, all three missing the exact same line); d/god/cmds/_god.lpc's own "../god.h" needed d/god/guild/god.h to exist (it was missing -- restored by copying the byte-identical d/god/god.h, confirmed identical to u/b/boss/god/{,guild/}god.h, into place); d/noden/hawk/monster/{elder,god,god1}.lpc's "/u/s/smore/ hawk.h" (nonexistent) → "../hawk.h" (the real, local header every other sibling in that directory uses); u/s/sage/guild/ killer_guild.lpc's "../killer.h""killer.h" (the real file is in the *same* directory, not the parent); u/b/boss/sword.lpc's "../moon.h""moon.h" (ditto, same-directory not parent); u/c/creative/weapon/sword.lpc's "../moon.h" (doesn't exist under u/c/) → the absolute "/u/m/moon/moon.h" (byte-identical content to u/b/boss/moon.h, confirmed via diff, and this file genuinely needs its ARM macro); d/noden/mj/{emsd,monster/ ronardo}.lpc's "../lilia.h" (nonexistent) → the local mj.h that already exists in d/noden/mj/ and is byte-identical to d/noden/ruin/lilia.h (confirmed via diff -- mj/ is evidently the same zone as ruin/, just renamed, with the header copied over but not the two .lpc files that still referenced the old name); u/m/mnb/wenro5.lpc's "../lilia.h""lilia.h" (the real file, u/m/mnb/lilia.h, is in the same directory). 8. std/board/archive.lpc had no #include at all, needing SAVE_EXTENSION (from <config.h>) for its board-archive-file suffix check -- confirmed reachable (d/wiz/archiverm.lpc, a live d/ domain file, inherits it). Added #include <config.h>. 9. cmds/{open,file}/_tail.lpc and cmds/wiz/_log.lpc called a bare tail() efun that was never real on MudOS/FluffOS (same class as es2/demonangel's identical fix) -- reimplemented locally in all 3 files with read_file()+explode()+implode() (last 20 lines), plus a forward declaration in each (this driver needs one for same-file forward references, same as fixes documented in the sibling libs' own NOTES.md).

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

Interactive test result — full registration + relogin flow

Registration shape (English-only, no Chinese-name step): login name (a-z, ≤12 chars) → confirm new-character (y/n) → Chinese display name (optional, blank defaults to the capitalized English name -- note this prompt exists in the code but nothing downstream actually requires a real Chinese name, unlike es2/demonangel) → password (≥5 chars, entered twice) → gender (m/f/n) → race (English race name, or ? <race> for a help blurb) → email (user@host or $ for none) → real name (optional) → lands in /d/adventurer/hall/adv_guild (冒险者公会, Farwind Town's Adventurer's Guild).

Verified live with a raw Python socket test script end-to-end:

Admin account

fluffos / Mud@2026. This codebase's admin mechanism predates securityd.lpc's wizlist-file approach used by es2/demonangel entirely -- it doesn't exist here. Instead, adm/obj/master/groups.lpc loads a plain-text /adm/etc/groups file at boot, defining nested groups by colon-separated member lists ((root) :mudren:admin, (admin) (root) :admin, (arch) (admin), (sage) (arch), (wiz) (sage)). Added fluffos to the (root) group's member list ((root) :mudren:admin:fluffos), then registered fluffos through the normal flow. Verified via the canonical read+compile ACL check (AGENTS.md §1.5): update /adm/obj/master succeeded ("Updated and loaded.") with no permission error. Note: the login banner's cosmetic "目前权限:" line reads from a *different*, unrelated mechanism (DOMAIN_D->query_domain_level(), a domain-builder-list lookup) and still shows "player" for this account -- this is a separate, purely cosmetic display and does not reflect actual write/compile permission, which is confirmed working via the update test above.

Batch-sweep artifact caveat (important for reading lpcc_fail.log)

The final lpcc_check.sh batch sweep in this lib's lpcc_fail.log reports 943/6,929 failures (86.4% pass), but this understates the true fix rate: after applying fix #6 (the §7.100 replace_program sweep, ~159 affected rooms across a dozen domains), the raw batch number did not move at all, even though individually re-testing 5 sample rooms from the previously-100%-failing d/eastland/tomb and d/eastland/liang_shan/lake buckets with a *fresh, isolated* lpcc invocation showed all 5 loading clean with zero errors. This matches this project's own documented "lpcc sweep eval-cost artifact" gotcha: lpcc --batch runs every file through a single, long-lived VM process rather than a fresh boot per file, and something in that shared process's call_out/eval-cost accounting appears to accumulate across thousands of sequential object loads, producing spurious repeats of the exact same Nesting call_out(0) level limit exceeded signature for objects that are actually fine in isolation. The real driver boot test above (a fresh process, not the batch tool) is the trustworthy signal for whether the fix works, and it does. The remaining ~943-row failure list should be read as a conservative upper bound including this batch-mode noise, not a precise count of genuinely-broken files; most of the individually-diagnosed content- tree-drift items in the "Not fixed" section above were confirmed via single-file lpcc runs specifically because of this caveat.

Deep functional test (round two) — 2026-08-27

Full continuous playthrough per AGENTS.md §10.7: registered a fresh English-named character (Questorix, later several more throwaway characters as earlier ones got stranded testing death), full race/gender flow, look/score/i at every state change, shop visit, combat against d/noden/farwind/monster/traveller.lpc (level 7, the shop's ambient NPC — no dedicated safe-sparring/training-dummy mechanism exists in this codebase, confirmed via a accept_fight/ stat-copy-loop grep; train <skill> in d/adventurer/adv_trainer.lpc requires exploration points a fresh character doesn't have), several full deaths, quit+relogin, and the six standing cross-cutting bug patterns (§7.121/§8.3a/§7.122/§7.123/§7.124/§7.126). Found and fixed four real programming bugs, the most severe of which (a corpus-wide tell_room() crash) completely broke the death/revival system for every player from this archive's very first boot. Admin account fluffos/Mud@2026 (already seeded from the initial onboarding pass) used throughout; all throwaway test-character saves (questorix, quillbramble, diagtester, diagtwo, diagthree, willowfen) were deleted before committing, and a bank card cloned onto fluffos for the autoload test was removed and the account quit cleanly.

Bug 1 (SEVERE, new AGENTS.md §7.129) — tell_room()'s omitted exclude argument crashes as int(0), silently breaking the ENTIRE death/revival system from first boot

adm/simul_efun/tell_room.lpc forwarded its optional 3rd parameter unconditionally to message(): message("tell_room", msg, room, exclude). This driver's message() declares that 4th parameter void | object | object * — it accepts the argument being genuinely *absent*, or a real object/array, but not a literal int. Since an omitted varargs parameter in LPC defaults to int 0 (not to "no argument"), every 2-argument tell_room(room, msg) call site crashed with *Bad argument 4 to EFUN message() Expected: object, array, Got: int(0). std/body.lpc's own create_ghost() — called from EVERY player death — makes exactly this 2-arg call for its "a white shadow rises from the corpse" room announcement, so every single death in this codebase crashed, uncaught, at that exact point, from the very first boot of this archive.

The practical effect, reproduced live: the crash aborts the entire call chain (create_ghost()die()continue_attack()heart_beat()), so the new ghost object — already new()'d, half-initialized, and left dangling in the game world — never gets returned to die(), so the death-god NPC encounter (std/user.lpc's DEATH->start_death(ghost), /d/abyss/hell/monster/black_wuchang.lpc) and the final call_out("remove", 0) that destroys the old body NEVER RUN. Because the dying body's own ob_data["ghost"] flag was never set (only the orphaned ghost object got it), continue_attack()'s hit_points < 1 && !ob_data["ghost"] guard stays true forever — die() re-fires on the SAME already-"dead" body on every subsequent heartbeat tick, re-running corpse creation and wealth transfer each time and crashing again at the identical line. A single test death left 5-6 duplicate corpses in the shop and repeated KILLS-log entries accumulating every ~9 seconds, forever, with the player stuck looking at an inert "ghost" that never got dragged into the death-god sequence described in the game's own room flavor text. Zero visible symptom beyond "你死了" printing normally — no crash message reaches the player, debug.log/driver stdout is the only place this ever surfaced (and even that required debug_message()-based tracing rather than log_file(), since log_file() calls made from a player-body/domain- NPC euid context silently no-op with no error — see the AGENTS.md §7.129 entry for the full diagnostic trail and root-cause method).

A grep -rnE 'tell_room\([a-zA-Z_()]*, *"' across the whole tree found dozens more 2-argument call sites in ordinary content (cave-in traps in d/noden/elf/elf/elf2.lpc, a statue awakening in d/island/monster/ holy_knight.lpc, an NPC vanishing in d/thief/hall/monsters/ pickering.lpc, etc.) sharing the identical crash, each invisible until its specific story beat actually fires.

Fix: adm/simul_efun/tell_room.lpc now only forwards the 4th argument to message() when a real exclude value was given:

if( exclude )
	message( "tell_room", msg, room, exclude );
else
	message( "tell_room", msg, room );

One-file fix, transparently repairs every 2-argument call site project-wide. Verified live end-to-end (test character Willowfen): after the fix, a full death sequence played out correctly for the first time — the "黑无常" (Black Wuchang, death-god) encounter and dialogue, the drag to "奈何桥" (Naihe Bridge), the ~40-second automatic revival timer chain (death1death2 in black_wuchang.lpc), and a full character restoration (real body, correct stats 10/30, correct starting item) at "露天英雄纪念馆" (the cemetery) — with zero runtime errors for the whole session. Both the 2-arg and 3-arg (with a real exclude, confirmed the exclude target still correctly doesn't see their own message) forms were independently re-verified via eval post-fix.

Bug 2 (SEVERE, extends AGENTS.md §7.14's __FILE__-in-fragment class) — include/compress_obj.h's clone/master default-object pattern resolves to its own header path, crashing query() for any unset property on 44 shared "simple item" files

include/compress_obj.h (#included directly inside create(), not inherited, by 44 files: bandages, torches, maps, spellbooks, bank cards, potions, weapons, etc.) does if(clonep(this_object())) { set_default_ob(__FILE__); return; }, intending to point a clone at its own un-cloned master/blueprint copy (which alone ran the item's real create() and holds the actual property values) so query() can fall back to it for any property the clone itself never sets. But __FILE__ in an #included fragment expands to the FRAGMENT's own path, not the includer's (a general trap, AGENTS.md §7.14) — every clone's default_ob ended up literally "/include/compress_obj.h", so the FIRST query() of any property the clone doesn't set locally (e.g. prevent_drop/query_auto_load(), both checked on every player death in std/user.lpc's die(); or a shop list/buy/inventory-display of any of the 44 affected items) crashed with *call_other() couldn't find object '/include/compress_obj.h'. Reproduced live via an admin-cloned /std/cards/bank_card (a compress_obj.h user): query_short()/balance both crashed pre-fix, and the trashcan/ shop's own torch/bandage/map stock items would have hit the identical crash on first purchase or death, given they use the same header.

Fix: set_default_ob(__FILE__)set_default_ob(base_name (this_object())). Note this is a DIFFERENT correct replacement than §7.14's usual file_name(this_object()) — that form includes the clone's own #nnn suffix, which here would make default_ob point back at the clone ITSELF, infinite-looping through query(). The correct fix depends on what the fragment's __FILE__ was actually trying to name (documented generally in the AGENTS.md §7.14 update). Verified live post-fix: the same admin-cloned bank card's query_short()/balance both work correctly, and a full quit+ relogin cycle carrying it showed no crash and no duplication (also verifies Bug 4 below).

Bug 3 (confirmed 3rd instance of AGENTS.md §7.122) — std/user/autoload.lpc's unguarded load_autoload_obj() duplicates every carried marker item across abrupt-disconnect/quit-then-restore cycles

This file's own header credits "Truilkan@TMI" — es1 turns out to share this exact TMI-2-lineage save/autoload mechanism with mortremains and tmi2 (already-documented §7.122 instances), despite its completely different D&D/English-registration content and no other visible TMI-2 branding. std/user.lpc's quit path (remove()) calls save_me() (line 410, which runs compute_autoload_array() before save_object(), baking any carried marker item into ordinary inventory data) BEFORE destroy_autoload_obj() (line 414); net_dead() (abrupt disconnect) calls save_data() and never calls destroy_autoload_obj() at all. load_autoload_obj() (run from every login via std/body.lpc's init_setup()) then unconditionally clones one more copy of every auto_load entry with no check for whether a matching item is already present. Real marker items using this mechanism: std/cards/{bank_card,credit_card}.lpc, obj/amulet.lpc, obj/tools/{memopad,staff,scroll}.lpc, obj/shells/shsh.lpc, wedding rings under d/noden/farwind/wedding_obj/ and d/std/wedding_object/.

Fix: added the same inventory-snapshot idempotency guard used for mortremains/tmi2load_autoload_obj() now skips cloning any auto_load entry whose base_name() already matches an item already carried. Verified live on the admin test account (fluffos): cloned a /std/cards/bank_card, carried it through a real quit→relogin cycle (exercising the save_me()-before-destroy_autoload_obj() gap) and then two direct repeat eval calls to load_autoload_obj() (the tmi2-style direct-call verification) — held steady at exactly 1 copy throughout, with i/balance both correct.

Family relevance: since es1 is the direct ancestor of es2/ haiyang2/demonangel/xkx2001/rzrmud/xo/zhyx/naruto, and turns out to share TMI-2-lineage base-library code most of its ES2-era descendants likely replaced with their own feature/-mixin architecture, this specific bug is NOT expected to recur in that later family (their feature/ directories don't exist yet in this snapshot at all — see "What this is" above). The much stronger sibling candidate is es1_win, the near-byte-identical separate archive of this same pre-ES2 codebase already onboarded in this project (see the "Comparison against es2" section above) — worth a direct check.

Bug 4 (minor, obviously-wrong-macro) — a newly-created ghost is placed in the wrong room, using the login-room macro instead of the dedicated ghost-destination macro

std/body.lpc's create_ghost() moves a fresh ghost with ghost->move(START) inside an #ifdef GHOST_START_LOCATION block — but START (/d/adventurer/hall/adv_guild, the ordinary new-player login room, from config.h) is a completely different macro than GHOST_START_LOCATION (/d/noden/farwind/cemetery, from body.h, defined specifically for this purpose). The two lines directly above, commented out, already used the correct macro (ghost->move(GHOST_START_LOCATION)) — this reads as a debug-era substitution that was never reverted. Low practical impact once Bug 1 is fixed, since the death-god NPC's start_death() unconditionally drags the ghost to a third location (the Naihe Bridge) immediately afterward regardless of where it started, but still an unambiguous wrong-constant bug worth fixing on its own merits. Fix: ghost->move(START)ghost->move(GHOST_START_LOCATION).

Minor hygiene fixes (return-type mismatches, compiler warnings only, no observed runtime effect)

While tracing Bug 1, the compiler's own warnings surfaced two forward-declaration/definition return-type mismatches predating this session, both harmless in practice (the mismatch never actually corrupted a returned value in any reproduced scenario — Bug 1's ghost=0 symptom traced to the pre-existing if(!link) return 0 early-exit on a repeated die() call, not to type coercion) but worth correcting for hygiene since they're compiler-flagged declaration mismatches, the same class as AGENTS.md §7.127: std/user.lpc's and std/npc.lpc's forward declarations of create_ghost() said int/ protected int; the real definition in std/body.lpc returns object. Similarly std/body.lpc's own forward declaration of save_data() said void; the real definition (std/user/save.lpc) and std/ghost.lpc's override both return int. Fixed all three declarations to match their real definitions. (Left u/m/moon/user.lpc — a wizard's personal home-directory fork of std/user.lpc — with its own copy of the same mismatch untouched, matching this project's usual treatment of wizard-workspace scratch content.)

Six standing cross-cutting bug patterns — checked, clean

Interactive test state left as evidence

Admin account fluffos/Mud@2026, clean (no test items carried, clean quit). No throwaway test-character saves remain (all deleted per this project's clean-up policy). The fix for Bug 1 was verified against a real, complete death-to-revival cycle on a fresh throwaway character (Willowfen) before that save was deleted; the full transcript (registration → shop → combat → death → death-god encounter → bridge → automatic revival → correct restored stats/inventory) is summarized above and was captured live via a raw Python socket test script with the connection held open across the full ~60-second revival timer window (closing the connection early, simulating an abrupt disconnect, was itself how Bug 3's net_dead()-without- destroy_autoload_obj() gap and one red herring around a link=NONE race were distinguished from the real, connection-independent Bug 1 root cause — the link field turned out to populate correctly under normal held-open-connection conditions, ruling out a suspected connection-race explanation for the stalled death sequence before the real tell_room() crash was found).