Demon Angel (DA)

✅ 可玩

恶魔天使 — Demon Angel (DA)

demonangel

🔑 fluffos / Mud@2026 更新 66a3bd8 2026-10-28 源码 下载 ZIP 上游 MudRen/DemonAngel

▶ 开始游玩 · Play Now

一个由 es2 1.1e mudlib 改造而来的中文武侠/仙侠 MUD——项目自己的 README 明确写着"本 DA mudlib ... 由 es2 1.1e mudlib 修改而成",`README.ES2` (es2 原作者 Annihilator 的原版说明文件)也原样保留在存档中。与本项目已收录的 `es2`、`haiyang2`、`xkx2001` 等"ES II 引擎家族"一样,共享同一套 `adm/`、`feature/` 底层框架,但游戏世界完全不同:城市/地图以 gourd(葫芦城,新手出生点)、grid、pet、ship、special、wiz 等 domain 构成,和 es2 的 canyon/choyin/city/temple/village 等世界完全独立,并非重复收录。

English

A Chinese wuxia/xianxia MUD built on the ES II engine (the same framework as this collection's es2/haiyang2/xkx2001 family) but with its own completely independent game world. New characters land in the central plaza of Gourd City (葫芦城中央广场), a hub with roads leading to four city gates, a bulletin board, and an NPC greeter. The world is organized into six domains -- gourd (the starting city), grid, pet, ship, special, and wiz -- and score displays a six-stat sheet (strength/wisdom/agility/willpower/constitution/luck) plus stamina and mana bars. Rather than a traditional wuxia master-disciple sect system, character progression runs through a profession office (职业介绍所) offering mage/warrior/priest classes, and further advancement is tied to a kingdom-building economy: players can construct a magic tower, training yard, or hero's guild once a kingdom accumulates enough gold and territory. A separate marriage/kinship system (swear/adopt/marry) handles social bonds instead of the genre's usual apprenticeship convention. An in-combat party/team-formation mixin is misleadingly named 'irc' in the source but has nothing to do with actual IRC chat.

README

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

内容亮点

在线试玩

https://mudlibs.fluffos.info/demonangel/

管理员账号 / Admin account

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

本地运行

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

游戏端口:40218。

NOTES · 移植与修复记录

demonangel — 恶魔天使 (git clone from MudRen/DemonAngel)

Source: git clone https://github.com/MudRen/DemonAngel (commit 9487693, cloned 2026-08-26). Port: 40218. Status: done (boots clean, full registration verified end-to-end with a real Chinese name, look/score/who/say/whoami/relogin/quit all confirmed working, admin update ACL confirmed).

What this is

DA (Demon Angel, "恶魔天使") is a Chinese wuxia/xianxia MUD explicitly derived from es2. Its own README.md states in so many words: "本 DA mudlib 属于一个国家制度的 mudlib 由 es2 1.1e mudlib 修改而成" ("this DA mudlib ... was modified from the es2 1.1e mudlib"), and the archive ships es2's own original README.ES2 unmodified alongside it (signed "By Annihilator (06/15/95)"). This makes DA part of this project's "ES II engine family" (es2, haiyang2, xkx2001, es1_win, rzrmud, xo) -- same adm/+feature/ framework, same recurring driver-compat bug classes -- but with an entirely independent game world: d/gourd (the starting hub city, literally named for a calabash/gourd), d/grid, d/pet, d/ship, d/special, d/wiz, vs. es2's canyon/choyin/city/force/temple/village. A prior research pass confirmed this is genuinely independent content, not a duplicate of es2 or any other onboarded ES2-family lib.

Layout mirrors es2 almost exactly: adm/ (master/simul_efun/daemons), cmds/ (adm/debug/king/min/npc/usr/wiz command dirs), feature/ (action, alias, apprentice, attack, attribute, autoload, clean_up, command, condition, damage, dbase, edit, equip, finance, irc, liquid, message, more, move, name, save, skill, spell, team, treemap, vendor), std/ (armor, bboard, char, char/npc, equip, item, room, ship, weapon/*), d/ (game-world domains), daemon/ (class/skill/spell tables). config.mud's mud name decodes (GB18030) to "葫芦" (gourd), matching the d/gourd starting domain; mudlib directory : . (repo root is the mudlib root, same as es2, no nested subdirectory). Start room /d/gourd/gourd_recall (中央广场, a hub plaza with roads to four city gates), death room /d/special/death.

feature/irc.lpc is a red herring: despite the name, it implements an in-combat team/party-formation mixin (add_irc_team_member, is_irc_team_leader, etc.), not an actual IRC bridge -- confirmed no socket_connect()/outbound-network calls anywhere in it.

Network / outbound-connections check (AGENTS.md caveat)

adm/daemons/network/ ships a full but incomplete Intermud-style subsystem (dns_master, mail_serv, ftpd, telnetd, inetd, name_server, pingd/pingtcp, ms, userid, netmail, services/) with real socket_connect()/resolve() calls (in ftpd.lpc). Confirmed both dns_master and ftpd are commented out of adm/etc/preload in the original archive (#/adm/daemons/ network/dns_master, #/adm/daemons/ftpd) -- the only two daemons that could reach this code are never loaded at boot. This lib makes zero outbound network connections on boot; safe for an automated reboot loop. (Same subsystem, same disabled state, as in es2.)

Conversion (GBK→UTF-8)

scripts/convert_lib.sh libs/demonangel/raw/DemonAngel libs/demonangel/ work GB18030. Results: 1117 files converted, 86 already UTF-8, 0 lossy conversions, 21 binaries skipped (unlike es2's 8 lossy files -- this archive's encoding was completely clean). .c→.lpc renamed 709 files; 181 literal .c" references fixed; 8 local #include <x.lpc> angle-bracket includes converted to quoted form.

Config / driver-compat fixes

libs/demonangel/config.fluffos modeled on the raw config.mud (decoded mud name 恶魔天使, decoded ANSI-wrapped default error message preserved byte-for-byte with real escape codes, not re-typed), pointed at libs/demonangel/work, port 40218; dropped the obsolete directives this driver flags (address server ip/port, binary directory, swap file, reserved size) and reduced the raw config's absurd maximum local variables : 10000000 to 200 (in line with es2's 40 / haiyang2's 30 -- the raw value was clearly a typo/no-op on the original driver too).

1. adm/obj/master.lpc's valid_override() was 2-arg (AGENTS.md §10.4, identical to es2) -- simul_efun.lpc #includes 8 fragment files needing the 3-arg (file, name, main_file) signature. Widened the signature and OR'd in main_file == SIMUL_EFUN_OB. 2. adm/simul_efun/file.lpc's log_file() had no assure_file() guard (AGENTS.md §7.11, identical to es2's fix #2) -- added one with a forward declaration (this driver needs forward decls for same-file forward references). Also guarded master.lpc's own log_error() bypass write (efun::write_file(home + "log", ...)) the same way. 3. destruct() AND exec() both overridden as simul_efuns (AGENTS.md §7.38 covers destruct(); this driver rejects exec() the same way, undocumented there until now) -- adm/simul_efun/object.lpc had void destruct(object ob) (pre- cleanup ob->remove(euid) before efun::destruct(), "es2 1.2b") and int exec(object to, object from) (restricted callers to /adm/daemons/logind and /cmds/wiz/update before efun::exec()). Both hard-rejected at compile time ("Invalid simulated efunction override"). Deleted both wrappers; bare destruct()/exec() calls now hit the real efuns directly, losing the pre-cleanup step and the caller restriction respectively (same documented tradeoff as every other lib in this collection that hit the destruct() half of this class -- es2, the haiyang family, xkx100). 4. is_chinese()/check_legal_name() GBK byte-range vs. CJK- codepoint mismatch (AGENTS.md §8.1, identical to es2) -- adm/simul_efun/chinese.lpc's is_chinese() was strlen(str)>=2 && str[0] > 160 && str[0] < 255 (a GBK-lead-byte heuristic that never matches this driver's UTF-8-native character indexing) and adm/daemons/logind.lpc's check_legal_name() had the matching byte-calibrated strlen(name) < 2 || > 12 bound plus an i%2==0 GBK-lead-byte loop gate, even though its own error message says "1 到 6 个中文字". Fixed both to the §8.1 playbook: is_chinese() now checks the CJK-Unified codepoint range (str[0] >= 0x4e00 && str[0] <= 0x9fff); check_legal_name()'s bound is the literal 1..6 characters the message states, checking every character (no %2 gate). Live-verified: 秦风 (2 characters) registered cleanly. 5. std/weapon/throwing.lpc's setup() referenced damage out of scope -- damage is a parameter of the sibling init_throwing() function, not setup() (every OTHER weapon type in std/weapon/ has this exact damage+query_weight()/N+1 line, but inlined directly inside their own init_X() -- only throwing.lpc splits it into a separate setup(), exposing the scope bug). Fixed to query("weapon_prop/damage")+query_weight()/20+1, matching what init_throwing() itself stores via set("weapon_prop/damage", damage). 6. obj/token.lpc used the bare macro SAVE_EXTENSION, never defined anywhere in this archive's includes -- this driver's real predefine is __SAVE_EXTENSION__ (confirmed against ~/src/fluffos/build-debug/src/options.autogen.h; matches es2's feature/save.lpc usage). Fixed both call sites (obj/token.lpc:111,188). (cmds/adm/purge.lpc also uses a bare SAVE_EXTENSION but defines its own local #define SAVE_EXTENSION ".o" at the top of the file -- self-contained, not a bug.) 7. open/sky/42.lpc and cmds/min/develop.lpc share the exact same copy-pasted author typo: set("short", "马厩\"); -- a stray backslash immediately before the closing quote escapes it, so the string literal swallows everything up to the NEXT unescaped quote in the file. In 42.lpc this ran straight into a @TEXT/TEXT heredoc a line later, producing "End of file in string"; in develop.lpc (a mapping literal, not a heredoc) it produced cascading "Illegal character"/syntax errors a line down as the lexer resynced mid-token on raw UTF-8 bytes. Fixed both (removed the stray backslash: "马厩"). Three more instances of the identical 马厩\ typo exist in cmds/min/build.lpc but land mid-string before an ordinary Chinese comma rather than before a closing quote -- those only produce non-fatal "Unknown escape sequence" warnings (build.lpc compiles fine) and were left as-is (cosmetic, matches AGENTS.md's "don't fix content" boundary for a warning-only typo). 8. include/ship.h's do_save_ship() had an unescaped-quote string bug: str += "#include "ansi.h"\ninherit SHIP;..."; -- the embedded "ansi.h" quotes broke out of the outer string literal mid-expression, producing "Cannot #include config.h"/"Illegal LHS" cascading errors in every file #include-ing ship.h (only cmds/adm/put_ship.lpc does). Fixed: "#include \"ansi.h\"\n...". 9. cmds/wiz/tail.lpc called a bare tail() efun that was never real on MudOS/FluffOS (identical to es2's fix #10) -- reimplemented with read_file()+explode()+implode() (last 15 lines). 10. cmds/npc/meditate.lpc: unbalanced parenthesis -- mana_gain = sen_cost * ((int)me->query_skill("spells"); has one extra unmatched ( before the cast, with nothing closing it (a straightforward stray-character typo, same class as es2's goddd.lpc paren fix). Removed the stray open-paren rather than guessing at an intended divisor (the comment above claims the result "ranges from 1 to 15 when sen_cost = 30", which no simple reading of the surrounding code supports either way -- left the arithmetic itself untouched per the "don't redesign game balance" boundary, only restored valid syntax). 11. cmds/usr/mount.lpc's main() referenced ob, never declared or assigned in that function (ob is only a parameter name of the sibling do_ride()) -- main() never actually resolved arg into a target object at all, an outright dropped line rather than a scope typo. Restored the minimal, standard resolution idiom this same codebase uses in dozens of sibling commands (e.g. cmds/usr/ident.lpc: object ob; ... if(!(ob = present(arg, environment(me)))) return notify_fail(...)) -- deliberately did NOT add the pet-registration bookkeeping cmds/usr/dismount.lpc expects (me->query("list/pet/file"), etc.), since that's a genuine content/design gap in how mount/dismount are wired together, out of scope here. 12. Cross-inherit clean_up(int) signature mismatch silently truncated std/room.lpc's function table when inherited -- a genuinely confusing one, root-caused via a minimal standalone repro (inherit "/std/room"; void create(){create_door(...);}) before touching anything. feature/clean_up.lpc defines int clean_up() (0 args); std/room.lpc overrides with int clean_up(int inherit_flag) (1 arg), producing a "Number of arguments to 'clean_up' disagrees with previous definition" warning -- but that warning turned out to be a red herring (fixing the signature mismatch alone did NOT fix the actual bug, confirmed by re-testing the repro). The real cause, found by bisecting which of std/room.lpc's functions were visible after inheriting it: lines 76-187 of std/room.lpc (the ENTIRE door subsystem -- look_door, open_door, close_door, check_door, create_door, query_doors, query_door) are wrapped in one giant /* ... */ block comment, with valid_leave()'s own door-check logic separately commented out right below it. This is deliberate, mudlib-wide-disabled functionality, not a driver parsing bug -- nosave mapping doors; at the top of the file is ALSO commented out. The only live caller of create_door() anywhere in the archive, d/wiz/entrance.lpc, is a leftover that was never updated when doors were disabled. Fixed the (harmless, unrelated) signature mismatch anyway for hygiene, then disabled the orphaned create_door() call in entrance.lpc to match the already-disabled subsystem -- the exact same fix shape as es2's §7 fix #11 (city gates calling create_door() against an already-disabled exit). 13. std/char/npc.lpc's carry_object() didn't honor its own documented 0-on-failure contract: ob->move(this_object()); return ob; returns the newly-new()'d object regardless of whether move() actually succeeded. move() (feature/move.lpc) can fail silently via its own notify_fail()+return 0 when the destination's encumbrance capacity is exceeded -- and critically, no plain NPC ever gets max_encumbrance initialized at all (set_max_encumbrance() is only ever called from adm/daemons/chard.lpc and the adm/daemons/race/*.lpc files, all of which only run for player characters, never for std/char/npc.lpc-based NPCs). So every NPC that calls carry_object(heavy_item)->wear() in its own create() -- obj/npc/heaven_soldier.lpc's 50000-weight armor, obj/npc/garrison.lpc's cloth/longsword, d/obj/npc/guard_1.lpc's sword -- has move() silently fail, carry_object() returns the un-contained object anyway, and the chained ->wear()/->wield() then crashes with *Bad argument 1 to EFUN call_other() ... Got: int(0) once wear()'s own environment() check hits 0. This is exactly the "MudOS tolerated a nil call_other, this driver enforces strict typing" class already catalogued for the ES2 family (es2's fix #6/#7) -- just reached via a silently-failed move() instead of a missing content path. Fixed at the root: carry_object() now checks move()'s return value, destruct()s the orphaned clone and returns 0 on failure (honoring the contract add_money() right below it already assumes); guarded all 4 real call sites (heaven_soldier.lpc x2, garrison.lpc x2, guard_1.lpc x1) with if (ob = carry_object(...)) ob->wear();. Left the underlying encumbrance-never-initialized-for-NPCs gap itself alone (a genuine pre-existing content/design incompleteness, not something to invent a fix for) -- NPCs affected by this now start without their listed starting gear instead of crashing their own create(), same net effect the original MudOS driver would have had. 14. feature/move.lpc's string-destination resolution can hard- crash on a nonexistent target file: move(dest) when dest is a string does call_other(dest, "???") purely to force-load the file (ignoring the nonsense-function-name return), then find_object(dest) and a notify_fail() fallback if that's still 0. But call_other() on a path with NO compiled object AND no file to compile throws a hard, uncatchable-by-the-caller *call_other() couldn't find object ... error on this driver instead of just failing to compile -- so the existing if(!ob) return notify_fail(...) fallback never gets a chance to run. Surfaced via 5 of the 7 obj/board/*_b.lpc files (all bulletin boards), each move()-ing itself in create() into a hardcoded room path left over from es2's own world (/d/temple/temple1, /d/snow/inn, /d/sanyen/temple, /daemon/class/fighter/ guildhall, /d/snow/schoolhall -- domains that plainly do not exist in DA's own gourd/grid/pet/ship/special/wiz world) that were never updated when DA repurposed es2's board templates. Rather than guessing which of DA's own rooms each board was meant to end up in (a content decision, out of scope), wrapped the forced-load call in catch() (both the move() and direct_move() copies of this code in feature/move.lpc) so a genuinely-missing destination degrades to the pre-existing "destination unavailable" notify_fail() instead of crashing the board's create() outright. This is a shared low-level utility fix (parallel to the std/room.lpc catch()-around-new()/ move() guard es2's fix #6 added), benefiting every caller uniformly rather than patching each board individually.

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

Interactive test result — full registration + relogin flow

Registration shape: English id (3-12 lowercase letters) → confirm new-character (y/n) → real Chinese name (1-6 characters, after the §8.1-class fix above) → password (≥5 chars, entered twice) → email (free text) → gender (m/f) → MOTD → lands in /d/gourd/gourd_recall (中央广场, the gourd-city central plaza).

Verified live with a raw Python socket script (UTF-8 throughout -- this archive, unlike some sibling ES2-family libs, needed no special client-side encoding gymnastics once converted) using a real Chinese name (秦风):

Admin account

fluffos / Mud@2026, Chinese display name 秦风. Granted via this lineage's own mechanism (identical to es2): 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 bor (admin) entry, then registered fluffos through the normal flow. Verified: update /adm/obj/master succeeds ("master: loaded successfully. 重新编译 /adm/obj/master.lpc:成功!") -- the canonical read+compile ACL check per AGENTS.md §1.5.

Compile sweep summary

scripts/lpcc_check.sh: 709 total, 41 initial failures → 22 remaining after fixes (96.9% pass rate). All 22 remaining failures are confirmed dead code, deliberately-disabled subsystems, or genuine pre-existing content gaps documented above (10 of them are the same already-known adm/daemons/network/ tree as es2) -- none affect the live, reachable game.

深度功能测试 / Deep functional test (round two, AGENTS.md §10.7)

First genuinely hands-on *playthrough* pass (the earlier "done" status above only ever verified registration + look/score/who/say/ whoami/relogin/quit, never real gameplay: movement, class/skill acquisition, combat, economy, or death). One continuous session on the native driver (~/src/fluffos/build-debug/src/driver config.fluffos), a raw Python socket client throughout, plus a second admin connection for teleporting (goto) to distant content and for a controlled death test.

Newbie path: no help newbie/help intro topic exists (doc/help/topics itself is missing too, so even bare help prints nothing useful) -- a genuine content gap, not a bug, left as-is per scope. Learned the real onboarding shape by reading source instead: register lands in /d/gourd/gourd_recall (中央广场) as a generic "冒险者" (Adventurer); the actual class system is d/gourd/choice_class.lpc (职业介绍所, reached north/north/north/west from the start room) -- list shows 魔法师/战士/牧师, advance <职业> locks it in once. No traditional wuxia master-disciple/sect system is reachable anywhere in this archive: feature/apprentice.lpc's recruit_apprentice() API exists but is never called from anywhere else in the tree -- this game's actual family/social system (swear/adopt/marry, per doc/help/family) is unrelated (marriage/kinship, not martial apprenticeship), and skill advancement past the initial class pick runs through the 王国 (kingdom) economy instead (buildable magic_tower/trainyard/hero_guild rooms, gated behind kingdom gold/room-count thresholds cmds/min/build.lpc enforces) -- confirmed this is intentional world design, not a missing feature, and left untouched.

Test character: id qinlong, 秦龙 (male, 战士 after class pick), password Test@2026 -- kept as a representative playthrough character (save files work/data/login/q/qinlong.o, work/data/user/q/qinlong.o). Admin account fluffos/Mud@2026 (per this lib's existing seeding) used for goto teleports and the death test.

Bug 1: private nomask command_hook and 15 sibling private-callback functions across the ES2-inherited codebase -- AGENTS.md §8.3a's class, not previously swept on this lib

feature/command.lpc's central dispatcher was declared private nomask int command_hook(string arg), registered via add_action("command_hook", "", 1) in the SAME file's enable_player(). command.lpc is inherited into std/char.lpc (the base class for both obj/user.lpc and every NPC), which demotes the inherited private function to DECL_HIDDEN on this driver -- add_action's later efun-origin dispatch then hard-refuses to call it (apply() with insufficient permission: ... function: command_hook, origin: efun, needs: private, has: hidden), reproduced live in driver_stdout.log during ordinary play (triggered from an internal command()-style self-call, not a directly-typed command -- matches this bug class's documented "typed commands can still look fine" caveat exactly).

A full repo-wide grep (private-function-name declared in the same file as an add_action("name" or call_out("name" registration of that exact name) found 8 more affected files, 16 functions total, all genuinely inherited elsewhere (verified each has a real inherit site, not dead/unused):

| File | Function(s) | Registered via | |---|---|---| | feature/command.lpc | command_hook | add_action (the confirmed-live crash above) | | feature/action.lpc | eval_function | call_out, from start_call_out() -- the shared delayed-effect primitive documented in AGENTS.md for xuanjianlu/zjdyaryl; confirmed dead in this specific archive (start_call_out() itself has zero callers anywhere in the tree), fixed anyway for consistency with the sibling pattern in case a future skill/drug file starts using it | | std/item/combined.lpc | destruct_me | commented-out call_out("destruct_me", 0) -- not currently live: the real code path calls destruct_me() directly (if (v == 0) destruct_me();), a normal same-file call unaffected by this bug class. Fixed anyway since the dead call_out line is right there as a latent trap (this base class is inherited by std/money.lpc, i.e. every coin in the game, plus several potions/weapons) | | std/room/furnace.lpc | do_load, furnace_count_msg, furnace_drop, do_charge, charge_furnace | add_action/call_out (魔力炉, inherited by open/sky/19.lpc, 24.lpc) | | std/room/misc_shop.lpc | do_list, do_buy | add_action (杂货店, inherited by open/sky/28.lpc, 71.lpc) | | std/room/herb_shop.lpc | do_list, do_buy | add_action (药店, inherited by open/sky/30.lpc, 31.lpc, 70.lpc) | | std/room/magic_tower.lpc | do_list, do_study | add_action (法术研习营, inherited by open/sky/36.lpc, 73.lpc) | | std/bboard.lpc | do_post, do_discard, do_followup | add_action (see Bug 2 below -- these compound with the redundant replace_program() bug on every board) | | std/jboard.lpc | do_followup | add_action (obj/board/wizard_j.lpc) |

Fix, identical everywhere: drop private, keep any other modifiers (nomask, etc.) -- these functions are never meant to be player-typed verbs themselves, so exposing them as ordinary (non-private) functions adds no real capability. Left alone (confirmed genuinely unaffected, since this driver only demotes private-in-an-*inherited* file, not a private function declared directly in a file nobody else inherits): obj/user.lpc's user_dump (net-dead timeout, USER_OB is a leaf, nothing inherits obj/user.lpc), and three other leaf files with the same shape (obj/npc/demogorgon.lpc's countdown, d/gourd/ gourd_recall.lpc's do_relax, cmds/npc/suicide.lpc's slow_suicide) -- all confirmed via grep -rn "inherit.*<name>" to have zero inheriting files anywhere in the tree.

Verified live post-fix: buy bean at /open/sky/28 (misc_shop), charge furnace at /open/sky/19 (furnace, call_out path), kill girl/movement/class-advance (command_hook path) all worked with zero insufficient permission lines in driver_stdout.log across the whole session -- see the sections below for the actual playthrough these were exercised in.

Sibling libs to check: es2, haiyang2, xkx2001, es1_win, rzrmud, xo share this codebase family; xkx2001/rzrmud already have their own §10.7 passes and may or may not have hit this independently -- worth a grep -rn 'private.*command_hook' sweep plus the same same-file add_action/call_out cross-check across all six, since this session found it recurs in non-command_hook functions too (the furnace/shop/board pattern above), not just the one function AGENTS.md's existing §8.3a catalog already tracks by name.

Bug 2 (AGENTS.md §7.86's class, confirmed on a 4th codebase lineage): every bulletin board redundantly self-replace_program()s its own already-inherited class, permanently crashing post/followup

Reproduced live, first try, on a completely fresh boot: post <title> at the very first board a player meets (obj/board/gourd_recall.lpc, the board sitting in the start room) crashed instantly --

执行时段错误:*cannot bind an lfun fp to an object with a pending replace_program()
程式:/std/bboard.lpc 第 107 行, 物件: /obj/board/gourd_recall
呼叫来自:/std/bboard.lpc 的 do_post() 第 107 行,物件: /obj/board/gourd_recall ("留言板")

Exact match for AGENTS.md §7.86 (previously seen on xhcii, zxty, hy2000, xyj2000 -- three unrelated ES2/金庸/西游记 lineages, now a 4th, independent DA/es2 lineage). Root cause identical: every board file does inherit BULLETIN_BOARD; (giving it do_post's this_player() ->edit((: done_post, this_player(), note :)) closure-creation code directly) and then also calls replace_program(BULLETIN_BOARD) in its own create() -- completely redundant since the class is already directly inherited, and actively harmful on this driver: the object's "replace program pending" flag is set the moment replace_program() runs and is never observed to clear, so do_post()'s and do_followup()'s attempt to bind an unbound lfun closure (done_post, implicitly bound to this_object(), the board itself) fails forever, for the lifetime of that board object. do_read()/list/look all work fine (no closure involved), which is exactly why the earlier onboarding smoke-test pass never caught it -- a board looks completely healthy right up until someone actually tries to write something.

Found and fixed in all 13 real board instances plus the in-game builder's own code-generation template:

Verified live, before and after: before the fix, post <title> crashed exactly as above on a completely fresh boot (first command tried). After the fix (all 13 files + the template, LPC formatter run, fresh reboot), post 深度测试标题2 / Test body... / . on the exact same board opened the editor normally, printed "留言完毕", and read 2 confirmed the new post saved with correct author/timestamp/body; followup 2 / body / . on the same message also completed cleanly ("留言完毕"). Zero insufficient permission/cannot bind lines in driver_stdout.log for the rest of the session.

Also checked and confirmed NOT affected by the general inherit X; ... replace_program(X); redundant-idiom shape (grepped all 107 files in this archive using replace_program(), cross-checked each against its own inherit line): dozens of open/sky/*.lpc room files redundantly self-replace_program() the exact same way for ROOM, SHOP, TAVERN, ARMORY, SMITH, GATE, BANK, CHURCH, STABLE, PORT, PET_SHOP, WEDDING, SWEAR, POST_OFFICE, HERO_GUILD, BARRACKS, TRAINYARD, ACTIONROOM -- but none of those base classes ever create a self-bound (: closure anywhere in their own code (confirmed by grep), so the "pending" flag never actually gets exercised for them and they are not currently broken. Left untouched (removing a redundant-but-harmless call on ~90 working files would be pure churn, out of scope for a bug-fix pass) -- flagged here in case a future pass adds an editor/closure-using command to any of these classes, since the same landmine is sitting there dormant.

Sibling libs to check: same recommendation as AGENTS.md §7.86 itself -- any ES2-derived lib with a BULLETIN_BOARD/bboard.lpc-style board class is worth a one-line grep (grep -rln 'inherit BULLETIN_BOARD' | xargs grep -l 'replace_program(BULLETIN_BOARD)') before assuming its own post command works.

What was tested and confirmed working

ES2-family cross-check (2026-08-27): AGENTS.md §7.129 tell_room() bug found and fixed — NOT on the core death path, but real elsewhere

Following up on es1's severe §7.129 find (a shared tell_room() wrapper forwards its omitted exclude arg to message() as a literal int(0), which this driver's message() rejects), checked this direct ES2-family descendant. This lib's core die() (feature/damage.lpc) uses message_vision(), not tell_room(), for its death announcement — explaining why the earlier §10.7 deep-test pass above (which did reach a real death) never tripped this bug. But adm/simul_efun/ message.lpc's tell_room(ob, str, exclude) still unconditionally forwarded the bare, defaulted exclude straight to message(), and two real 2-argument call sites exist: obj/user.lpc's user_dump() (the disconnect-timeout and idle-timeout auto-kick announcements) and adm/daemons/chard.lpc's corpse-decay heartbeat daemon (the "一阵风吹 过,把X化成骨灰吹散了" message). Both would have crashed uncaught the first time either fired. Fix: message("tell_room", str, ob, exclude) → message("tell_room", str, ob, exclude || ({})), identical pattern to es1/es2/haiyang2/etc. Verified live: cloned a throwaway /std/item-based test object, called its own test_2arg()/ test_3arg() wrapper functions via the wizard call command post-fix — both the 2-arg and 3-arg tell_room() forms completed with no crash (TESTMSG_2ARG_OK/TESTMSG_3ARG_OK both printed); pre-fix the 2-arg form would have thrown *Bad argument 4 to EFUN message(). Test object deleted before commit. include/compress_obj.h (the sibling §7.14 bug from the same es1 finding) does not exist anywhere in this lib's tree — not applicable.

ES2-family sibling sweep (2026-08-27): wimpy dbase-key mismatch found and fixed — same shape as es2's finding

Following up on es2's NOTES.md finding (its cmds/usr/wimpy.lpc read/wrote dbase key "wimpy" while the only real consumer, std/char.lpc's heart_beat() auto-flee check, read "env/wimpy" instead — a silent, permanent no-op for the player-facing wimpy command), checked this direct ES2-lineage sibling and found the exact same bug. cmds/usr/wimpy.lpc read/wrote me->query("wimpy") / me->set("wimpy", wmp) (no env/ prefix), while std/char.lpc's heart_beat() reads wimpy_ratio = (int)query("env/wimpy") for its HP-based auto-flee check, and this lib's own NPC templates (obj/example/ghost.lpc, obj/example/drooler.lpc) already correctly set "env/wimpy" in their own create(). Fixed identically to es2: changed both the read and write in wimpy.lpc to use "env/wimpy". Live-verified: booted the driver, logged in as the seeded fluffos admin account, ran wimpy 30, and confirmed the resulting save file persists "env":(["wimpy":30,]) under the player's dbase — matching exactly what heart_beat() reads, where pre-fix it would have written to a top-level "wimpy" key nothing ever consumed. (Two other files — open/test/npc/0.lpc/open/sky/npc/0.lpc setting "wimpy",0 on throwaway test NPCs, and cmds/min/set_mob.lpc's admin diagnostic display of dbase["wimpy"] — also reference the bare key, but neither is a functional consumer of the player auto-flee setting, so left untouched.) Checked the sibling ES-family libs haiyang2, xkx2001, bmxkx2001, rzrmud, xo/xo_final, zhyx, and naruto for the same shape: all clean (setter and getter already agree on the same key, or — xo/xo_final's generic set wimpy N command and naruto's per-stat set_stat_notify/query_stat_notify mechanism — use a different, internally-consistent implementation shape entirely). demonangel was the only lib in the family carrying the bug.

深度功能测试(§10.7,2026-10-04)— negative range ends

scripts/lpc_fix_negative_ranges.py: 6 line(s) in 3 file(s) count a range from the end with < (x[a..-1] -> x[a..<1], x[-2..] -> x[<2..]; KB 06 §7.209). In this driver a negative constant end gave "" / ({ }) and a negative start the whole value, so each of these returned the wrong slice; the compile warning A negative constant as the second element of arr[x..y] is gone from the files that compile. Range lvalues (s[0..-1] = text, the prepend idiom) are left alone. A paired compile of the changed files at HEAD and in the working tree showed no new error.

深度功能测试(§10.7,2026-10-08)— 编译警告整理

scripts/lpc_warnings.py demonangel --fix 加 scripts/lpc_fix_no_effect.py --apply,共改动 154 个 .lpc(多为删去未用的局部变量)。HEAD 与工作树分别加载(新进程):146 -> 146 通过,无回退。启动时警告 34 -> 0;新进程登录(输入名字)正常。

编译警告收尾(2026-10-13)

scripts/toolkit_fix.sh 一轮后剩两处,手工清零;改动的对象(含 /obj/user、/adm/daemons/ftpd)在 HEAD 与工作树各载入一次:3 个,regress 0。

去掉的预定义宏 SAVE_EXTENSION(2026-10-18,KB 04 §6.11)

老版 MudOS 给每个 LPC 对象预先定义了 SAVE_EXTENSION(".o");FluffOS 的 ChangeLog(COMPAT_BUSTER 一条)把它删了,改用 __SAVE_EXTENSION__。本库没有自己 #define 这个宏,用到它的文件一律 Undefined variable 'SAVE_EXTENSION',整个对象载入不了。改成 __SAVE_EXTENSION__(scripts/lpc_fix_predefined_macro.py):adm/daemons/network/netmail.lpc、include/net/ftpdsupp.h。netmail 是网络邮件守护进程(本库缺 mailer.h 等头文件,改后仍载入不了)。改后用 lpcc 逐个载入这些对象:0 个通过,1 个还有别的错误(adm/daemons/network/netmail:/adm/daemons/network/netmail.lpc:21: error: Cannot #include mailer.h)。

set_skill() 遇到本库没有文件的武功不再抛错(2026-10-18,KB 06 §7.256)

feature/skill.lpc 里的 set_skill()、map_skill()、prepare_skill() 碰到没有武功文件的名字就 error("F_SKILL: No such skill (…)"),调用它们的 NPC create() 整个中断:NPC 载入不了,刷出它的房间也少一个人。本库有 1 个对象设了本库没有文件的武功(1 个名字,最多的是 move)。三处都改成 return:这一门武功不给、不映射,对象照常载入,只是少了它(缺的武功本身仍是内容缺口)。抽样载入这些对象:1 个里 1 个通过。(scripts/lpc_guard_set_skill.py)

调用了父类没有的函数的对象(2026-10-19,KB 06 §7.259)

::create();、::init();、return ::buy_object(who, item); 这类调用写的是父类里有这个函数的版本;本库的父类没有,旧驱动把它当空操作(返回 0),现在的驱动在编译时就报 Unable to find the inherited function,整个对象载入不了。单独成句的调用删掉,return ::f(...) 改成 return 0;(void 函数里是 return;),与旧驱动的行为一致。共 1 处、1 个文件:::create()(1 处)。只改了出错原因全是这一类的文件;还带着别的错误(多半是父类本身没了)的文件没动,等父类补回后再看。(scripts/lpc_fix_dangling_super.py)

RANK_D 对 0 的称呼(2026-10-19,KB 06 §7.273)

adm/daemons/rankd.lpc 的 query_respect()、query_rude()、query_self()、query_self_rude() 里 4 个函数开头补上 if (!objectp(ob)) ob = this_object();。NPC 在 create() 里写 "…" + RANK_D->query_respect(this_player()) + "…"(inquiry 对话、chat_msg),驱动自己跑 reset() 时没有人在场,this_player() 是 0。旧驱动里 0->query("age") 返回 0,称呼走默认分支;新驱动直接报 Bad argument 1 to EFUN call_other(),房间的 make_inventory() 吞掉错误,被打死的 NPC 再也刷新不回来。用 scripts/lpc_reach_census.py --walk 找到(xkx100 的武当弟子、一灯、汴梁书生…);修在守护程序里,不动 1,800 个 NPC 文件。

if (x = 常量):条件里写成赋值的比较(2026-10-20,KB 06 §7.290)

if (arg = "button" || arg = "按钮")、if (!arg || arg = "") return 0;、else if (check = 0 && tempB != ""):把 == 写成了 =,条件的值就是那个常量,分支对所有非零数和任何字符串(连 "" 都算)永远成立、对 0 永远不成立,变量还被覆盖;驱动对此没有任何警告。每一处 = 改成 ==(scripts/lpc_fix_cond_assign.py,共 1 处)。改动:/adm/daemons/fingerd.lpc(1 处)。

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

sefun cat():缺文件时 write(0) 报错(2026-10-20,KB 06 §7.297)

void cat(string file) { write(read_file(file)); }:文件不存在(压缩包没带的欢迎/公告文件,或只有运行中的 mud 才会写的日志)时 read_file() 回 0,write(0) 在本驱动里报 Bad argument 1 to receive(),调用者(登录对话、cat 指令)被中断;老驱动什么也不打印。改成 write(read_file(file) || "");(scripts/lpc_fix_cat_unchecked.py)。改动:/adm/simul_efun/file.lpc。

翻页器 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/more.lpc。

NPC 的 chat() 在 chat_msg 为空数组时下标越界(2026-10-21,KB 06 §7.334)

NPC 基类的 chat() 是 if (arrayp(msg = query(...))) { if (random(100) < chance) { rnd = random(sizeof(msg)); if (stringp(msg[rnd])) ...,不检查列表是否为空。本库现有的 NPC 文件里没有字面写成空 chat_msg 的(扫描只认 set("chat_msg", ({ })));守卫挡的是运行时才为空的列表和以后新增的 NPC。在 random(100) < chance 前面加 sizeof(msg) &&(scripts/lpc_fix_chat_empty.py;if (random(100) > chance) return 0; 的写法是 !sizeof(msg) ||)。改动:/std/char/npc.lpc(共 1 个文件)。

存档里的 .c 文件名改成 .lpc(2026-10-21,KB 03 §4.2 第 9 项)

存档里写着 "/d/city/npc/wei.c" 这样的文件名;extract 时源文件已经全部改成 .lpc,而驱动对带扩展名的路径只认字面,new() / load_object() / move() 都找不到它,调用方多半把失败当成「没有这个东西」悄悄跳过。本库 2 个存档共 28 处按 scripts/lpc_fix_save_dotc.py 改成 .lpc(只改 .lpc 存在而 .c 不存在的字符串;玩家存档、备份、留言板、巫师目录和文件索引缓存不动)。改动:/open/sky/kingdom_data.o(24 处)、/open/test/kingdom_data.o(4 处)(共 2 个存档)。

巫师命令不带参数时对 0 调用 efun(2026-10-21,KB 06 §7.336)

巫师(管理员)把命令敲成不带参数时,驱动把 0 传给字符串参数;下面的命令第一句就把这个 0 交给 efun,运行时报 Bad argument 1 to ...,这条指令从来没有用过。用 scripts/lpcc_bare.py 不登录、在一次 lpcc 求值里以 0 调用 main() 逐个确认后,由 scripts/lpc_fix_bare_arg.py 在第一处使用参数的语句前加守卫:文件自带 int help(object me) 的,守卫是 if (!arg) return help(me);(help() 定义在 main() 之后的 1 个文件补了原型 int help(object me);,否则编译报 Undefined function help)。改动(1 个文件,括号里是接住 0 的 efun):/cmds/wiz/test.lpc(present)。加载检查 HEAD 与工作树对照,没有退步。

长度检查按字节写,旁边的提示按汉字写:6 处(2026-10-22,KB 06 §7.362)

老档的 strlen() 数字节(一个汉字 2 字节),转成 UTF-8 后它数字符:提示写「必须是 2 到 6 个中文字」的检查还是 strlen(name) < 4 || strlen(name) > 12,于是两三个字的房名、帮会名、群名、聊天室名、称号被提示自己说允许的那句话拒绝,上限却是提示的两倍;留言板的标题检查 i < 4 || i > 24 || i % 2 还会拒绝所有奇数个字的标题(post 你好吗)。本库 6 处(3 个文件):if( (strlen(name) < 4) || (strlen(name) > 12 ) ) {(/cmds/usr/chfn.lpc:41)。scripts/lpc_fix_strlen_gate.py 以提示为准:条件里每个 strlen 门槛都恰是提示所说数字的两倍才折半,存长度的变量(i = strlen(arg);)的门槛和 || i % 2 奇偶项、其后循环的 i % 2 == 0 && 一并处理;提示里有英文 / 字母 / 字节的不动。证明(scripts/lpcc_eval.py,bxsj 的留言板,求值对象继承 NPC 并重写 edit() 记录调用,command("post 标题")):HEAD 拒绝「你好」和「你好吗」、放行 23 个字的标题;改后接受前两个、拒绝第三个;shenmo 的 named.lpc invalid_new_name("张三"):HEAD 回 名字只能是2-4个中文字。,改后回 0。抽样 24 + 15 个库的 HEAD / 工作树加载对比 0 退步。

按字节写的汉字循环、茶馆猜诗和同盟频道标题(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 区间 6 个文件。证明(scripts/lpcc_eval.py):sjplii 的 check_legal_name 在 HEAD 接受「张a」、改后拒绝,两边都接受「张三」;xkx100 的 clubpoem->mixup("离离原上草") 得到「离离原草上」(真交换汉字);nitan_san 两字同盟名得到 【 兄 弟 】。关键对象加载通过。

blue_cloth F_CLOTH → BODY(2026-10-27,KB 04 §6.25 Also)

d/obj/npc/wp/blue_cloth 写 inherit F_CLOTH,本库无该宏。改为本库的 inherit BODY,护甲 2 写入 armor_prop/armor。证明:HEAD 失败;改后加载成功。

深度功能测试(2026-10-27,§6.10 未知 pragma save_binary/optimize)

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