Eastern Story: Dragon Slaying War

✅ 可玩

东方故事 — esI("屠龙之战")

esI

🔑 fluffos / Mud@2026 更新 35e844a 2026-10-28 源码 下载 ZIP

▶ 开始游玩 · Play Now

东方故事,站内活动主题为"屠龙之战"(Dragon Slaying War)。与本批次的 `es1_win`(东方故事·蓝天版)同属 ES / TMI 引擎家族,两者核心代码几乎一致,仅站点内容与分支细节不同。本档案独有一段完整的终局剧情:`eastland/dragon` 是一座三十余间房的洞穴迷宫,深处镇守着 19 级、2500 点血量的最终 boss"孽龙恨天"——传说中因过去屠戮生灵而被玉帝封印于此的恶龙,身披并会掉落一枚龙形护身符。

English

Another build of the ES/TMI "Eastern Story" engine, sharing its Western-fantasy world with sibling archive es1_win (human, elf, dwarf, half-orc, and goblin races; knight, mage, priest, monk, scholar, and thief guilds; the farwind trading house's list/buy shopping) -- but built around its own in-game storyline, the "Dragon Slaying War" (屠龙之战). The payoff is a real endgame encounter unique to this build: a 30-plus-room cave dungeon (eastland/dragon) guarding 孽龙恨天 ("Evil Dragon, Hatred of Heaven"), a level-19, 2,500-HP boss lore-bound as a beast the Jade Emperor sealed away for past slaughter, who wears and drops a dragon amulet. This particular snapshot also ships with a test-era configuration that grants new characters automatic wizard privileges -- a holdover from internal testing/load trials rather than intended public-play behavior.

README

内容亮点

在线试玩

https://mudlibs.fluffos.info/esI/

管理员账号 / Admin account

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

本地运行

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

游戏端口:40010。

NOTES · 移植与修复记录

esI.rar → esI

Status: DONE — boots clean, playable over telnet

Full flow confirmed: banner, staff/admin credits, uptime status, name-length validation, name-not-taken confirmation prompt.

What was fixed

Applied both fixes discovered on lib #12 (same lineage) proactively, before the first boot attempt, since a quick grep confirmed both patterns present in the raw archive: 1. std/object/prop.lpc's #include </std/object/prop_logic.c> (angle brackets, local directory, reached via preload) — convert_lib.sh's automated .c>→.lpc> fix only fixes the extension, not the bracket-vs-quote distinction, so this still needed the manual switch to quotes (#include "/std/object/prop_logic.lpc") per AGENTS.md §8d's preload-timing addendum. Worth teaching convert_lib.sh to convert local angle-bracket #includes to quotes automatically the next time this pattern recurs in enough libs to justify it (seen twice now). 2. master.lpc's domain_file() — same explode("/", "/") returning a zero-length array, !path guard not catching it — added || !sizeof(path).

Both fixes applied before ever attempting a boot; booted clean on the first real attempt. Plus the routine per-lib steps: encoding (8471 converted, 1741 already UTF-8/ASCII, 107 skipped binary, 14 lossy), .c→.lpc (7035 files, 1009 refs + 1 angle-bracket ref auto-fixed), static→nosave (121 files).

Known remaining issues (documented, not fixed)

How to run

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

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

WASM-enablement pass (loopback / admin seeding)

Identical to sibling es1_win (ES II family, 008).

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

Round-two pass (AGENTS.md §10.7) on the native driver, one continuous session per checklist item, with several deliberate net-dead disconnect/reconnect probes (both prompt and after a real ~80s wall-clock wait) plus a fresh-boot verification of every fix. Test characters (both kept, not cleaned up, as evidence):

Ported from sibling es1_win (§11 lineage, byte-identical files confirmed via diff before porting)

Both fixes below were previously found+fixed on es1_win's own §10.7 pass and explicitly flagged there as "sibling esI has byte-identical unfixed copies of both files". Confirmed via diff against es1_win's pre-fix commit (92be845f197^) that esI's copies were indeed byte-identical (module trivial pragma/comment differences), ported the same fix, and live-verified both work on esI specifically.

1. Net-dead disconnect during registration permanently strands the account (§7.21 variant) — std/user.lpc restart_heart(). Same root cause as es1_win: net_dead() only records linkdead_room if (env), so a player who disconnects between finishing the registration Q&A and pressing RETURN at the MOTD prompt (i.e. before complete_setup() ever places them in a room) has no linkdead_room to restore and gets stuck in LINKDEAD_ROOM forever on every future login. Fix ported verbatim: in restart_heart(), when linkroom is unset but the player is currently sitting in LINKDEAD_ROOM, call this_object()->complete_setup("") to finish the interrupted setup. Live-verified on esI: registered Muyunhe through to the "[请按 RETURN 键继续]" prompt, let the connection close without answering it, reconnected with the correct password — landed correctly in /d/adventurer/hall/adv_guild (the real start room), not /d/std/rooms/netdead. look/score both correct, zero debug.log signal either before or after the fix (matches §7.20's "zero log signal" warning).

2. #include <compress_obj.h>'s set_default_ob(__FILE__) — §7.14 class. __FILE__ inside the pasted-in fragment always resolves to /include/compress_obj.h itself, not whichever of the ~40 objects it was pasted into, so every affected clone's fallback property lookup throws *call_other() couldn't find object '/include/compress_obj.h' the first time it's needed (e.g. a shop's list command reading an item's price). Fix ported verbatim: base_name(this_object()) instead of __FILE__. Live-verified on esI: pre-fix, walking into /d/noden/farwind/shop and typing list (or the room's own first-ever reset(), see bug #4 below) would have hit this on the very first item lookup; post-fix, list renders the full item list with prices cleanly (油灯/绷带/魔法地图/火把), and a real purchase (buy torch) completed without error (see "What was tested" below).

New bugs found on esI (not present/not reproduced on es1_win)

3. Leftover developer debug checkpoints printed to every player on every real login — adm/daemons/logind.lpc (~line 477-490). Five tell_object(player, "ttt\n") / "ttt1\n" / ... "ttt4\n" calls, interleaved between the real ENTER_D/TOPPLAYER_D login-chain calls, unconditionally print literal ttt/ttt1/ttt2/ttt3/ttt4 lines to every player's screen on every ordinary login (confirmed reproducible — not the net-dead-reconnect path, which goes through restart_heart() instead and doesn't show them). Confirmed present in the raw archive (/bin/grep -a ttt raw/esI/adm/daemons/logind.c — same 5 lines, byte-identical), i.e. original author debug instrumentation that was never cleaned up before this snapshot shipped, not a conversion artifact. Same class as this lib's own already-documented "aadsaaaaaaaaaaaaaaaaaaaa" stray-debug-string bug (see "Native retest" section above) — zero player-facing content value, unambiguously leftover instrumentation, not a game-content/design question. Not present on es1_win (grep ttt es1_win/.../logind.lpc — zero hits; this snapshot's own addition, so no sibling port needed in that direction). Fix: deleted the 5 tell_object() lines, kept the 5 original comments and the real calls they were checkpointing. Verified: re-logged in as both Jiangfeng and Muyunhe post-fix — no more ttt* lines anywhere in the transcript, login chain output otherwise unchanged.

4. Uncatchable eval-cost abort on a room's cold first compile — another instance of §7.22's general shape, config-level fix — the farwind shop's reset(). /d/noden/farwind/shop.lpc's create() calls reset() synchronously (this shop's own explicit choice, matching a lib-wide convention — 1553 files across the tree call reset() directly from create(), not just relying on the inherited std/room/room.lpc's async call_out("reset", 0)). On this snapshot's shop, that synchronous reset() populates FOUR objects on the room's very first compile, one of which — "woodman": "/d/zeq/woodman" — is a heavily-decorated level-19 guard NPC (85350 HP) that equips 10 armor pieces plus a weapon in its own create(), each an independent first-time compile+clone. On a fresh boot, the FIRST player to walk east into the shop hit a real, on-screen, cascading crash:

执行时段错误:*Too long evaluation. Execution aborted.
程式:/adm/obj/master.lpc 第 349 行
...
呼叫来自:/d/noden/farwind/shop.lpc 的 create() 第 35 行,物件: /d/noden/farwind/shop
呼叫来自:/std/room/room.lpc 的 reset() 第 47 行 ...

cascading into log_error()/domain_file() themselves (each hitting the SAME already-exhausted eval budget as soon as execution resumed), ending in *Can't catch eval cost too big error. The player's move() into the shop simply failed (they stayed in the previous room — not left environment-null, unlike §7.22's original START_ROOM instance), but the crash is loud and repeats identically every time the shop's FIRST-ever visit this boot is attempted. Confirmed first-visit-only: after the aborted attempt, east a second time succeeded cleanly (shop and all 4 NPCs already resident in memory from the partial first attempt). Root cause confirmed by direct test, not guesswork: rebooting with maximum evaluation cost raised from the shipped 300000 to 5000000 and repeating the exact same fresh-boot east into the shop now succeeds cleanly with zero errors — i.e. this is genuinely "legitimately a lot of synchronous first-load work", not an infinite loop. Worth noting the archive's OWN config comment (config.fluffos:72-73, "Because of the heavy load on the Power PC, made it larger to pass the bad time. (By Annihilator)") shows the original author already had to raise this exact value once, on their own era's hardware, for what is plausibly this same class of problem. 5000000 is also already the single most common maximum evaluation cost value across this project's other converted libs (30 of them, per grep -rh "^maximum evaluation cost" libs/*/config.fluffos), so this is a precedented, low-risk config fix, not a one-off value pulled from nowhere. Fix: libs/esI/config.fluffos, maximum evaluation cost : 300000 → 5000000. Live-verified: fresh reboot, fresh character, east into the shop on the very first attempt this boot succeeds cleanly (全部 4 只 NPC 正确显示), zero debug.log errors.

5. §7.12 tell_room() 2-arg wrapper bug — adm/simul_efun/tell_room.lpc, hit from a self-rescheduling call_out with no enclosing catch() (the severity-escalation shape, not just cosmetic). varargs void tell_room(mixed room, mixed msg, mixed exclude) passes exclude straight through to the real message() efun; called with only 2 args (the overwhelmingly common case throughout this lib), exclude defaults to int 0, which message() rejects (Bad argument 4 to EFUN message() Expected: object, array, Got: int(0)). Reproduced live: adm/daemons/weather_d.lpc's change_phase() — a self-rescheduling call_out ("it call_outs to itself recursively so there's always one version in the call_out queue", per the file's own comment) — calls tell_room(notifications[i], msg) with only 2 args inside a loop, with no catch() anywhere in the function. The uncaught throw aborts change_phase() at that exact statement, meaning the function's OWN tail — call_out("change_phase", day_phases[current_phase]["length"]) — never runs, so the entire day/night phase cycle daemon permanently stops forever for the rest of that boot, with the single runtime-error line in debug.log as the only trace (no crash, no other visible symptom; weather/ambient broadcasts and phase-change text simply never happen again). Trivially reachable: WEATHER_D->request_weather_notices() is called by every outdoor room's set_outside() (std/room/room.lpc), so registering even ONE outdoor room (any ordinary walk outside) arms the crash for whenever the phase timer next fires — some phases in adm/etc/daytime.data are as short as 1 second, so this fires quickly in practice, well within a normal play session. Confirmed via diff: es1_win's copy of tell_room.lpc is byte-identical and carries the same unfixed bug (not caught by that lib's own §10.7 pass — plausibly a timing/room-visited difference between sessions). Fix (matching AGENTS.md §7.12's documented remedy exactly):

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

Live-verified on esI two ways: (a) played normally afterward with an outdoor room registered for ~15 real seconds, zero new debug.log errors; (b) direct proof via the fluffos admin account's eval command — eval tell_room(environment(this_player()), "TESTBROADCAST_2ARG\n"); return 1; (the exact 2-arg shape that crashed weather_d.lpc) broadcast cleanly with Result = 1 and zero debug.log error, both pre-fix (reproduced the crash directly this way too) and post-fix (clean). Ported to es1_win (libs/es1_win/work/adm/simul_efun/tell_room.lpc, same one-line fix, comment cites this lib's finding) and sanity-checked there with a fresh boot + existing test-character login/look/quit (clean, zero new errors) — not a full independent replay of es1_win's own §10.7 checklist, since that lib was already marked done; flagged here for whoever next touches es1_win to note in ITS OWN NOTES.md.

What was tested and confirmed working

Not verified live (explicitly)

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

Independently re-verified all 5 round-one fixes against current code and live behavior rather than trusting the writeup above. All 5 confirmed still holding; no new bugs found or fixes needed this pass.

Also verified: real admin write access via update /adm/daemons/logind (succeeded, "Updated and loaded.") — used as the actual privileged-action check rather than trusting any login banner. One reconnect exercised (the connection's own quit is gated behind okip for wizards, same as es1_win's sibling architecture — a genuine safety feature, not a bug — so the reconnect went through the silent net-dead body-reattach path, landing back in the shop with identical state, which is itself valid reconnect-stability evidence).

Verification method

Booted native build-debug driver (zero eval-cost/preload errors this boot, confirming fix 4 holds from cold start), admin login (fluffos/Mud@2026). Driver killed by exact PID after testing; incidental fluffos.o/d/noden/log/d/zeq/log save churn reverted before commit (no code changes this pass — nothing else to stage).

§7.100 sub-threshold instance (2026-08-20)

Found during the §7.100 tail-sweep (the original 166-lib corpus sweep only surveyed libs with >=100 live occurrences; this lib was below that threshold and never checked). 1 live replace_program(ROOM); occurrence in d/noden/6,8.noden.lpc's create(), immediately after inherit ROOM; — same fatal shape as the rest of the §7.100 family (sets a permanent "pending replace" flag that crashes the object the first time anything binds a closure to it). No roommaker.lpc-style room-building tool exists in this lib, so no factory-template copy of the bug. 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).

Deep functional test round four (2026-08-20)

Targeted re-test of the two things every prior round explicitly left unverified: a real death→respawn cycle, and a real join into a non-starting guild. Per the project's current scope rule, only pursued actual programming bugs (crash / debug.log error / stuck state) — not content or balance.

Method note: both tests needed a wizard-controlled second connection alongside the test character, run as a two-socket Python script (not tmux_mud.sh/single-connection mudclient.py) so the admin account could eval state changes on a character that stayed connected and interactive throughout. Two real methodology traps hit and resolved along the way, worth flagging for whoever does this next on another lib: 1. The eval command's underlying _set() (std/object/prop_logic.lpc) has an intentional security guard that silently no-ops any set() call from a /open/-rooted script onto a non-wizard living object's properties (blocks /open sandboxes from writing player state directly) — so ob->set("hit_points", 0) from eval does nothing, with no error, no exception, query() afterward just returns the unchanged old value. This is by design, not a bug. The correct way to deal admin-driven damage is ob->receive_damage(N), which is a real gameplay API that mutates the property mapping directly from the target's own context, bypassing that guard exactly as real combat damage does — and it independently no-ops (also by design) if the target is currently linkdead (receive_damage()'s own guard), so the test character's socket must be kept open/connected throughout, not closed between steps. 2. A genuinely fresh (non-net-dead) login stops at the "[请按 RETURN 键继续]" MOTD gate (setup()'s input_to("complete_setup", 2)) before complete_setup() actually places the character in a room — a *reconnect* (net-dead resume) skips this entirely via a different code path (restart_heart()/"重新连线完毕"), which is why earlier rounds' sessions never hit this. A command sent into that still-armed input_to() slot (e.g. look) gets silently consumed as the RETURN-prompt's answer instead of dispatching as a real command, and complete_setup() then places the character at their *default* start room, silently overriding anything done to them (e.g. an admin move_player() teleport) in between. This produced two confusing false leads before being root-caused: an apparent "teleport didn't stick" (character 's join replied "you're already a member" because they'd been silently routed back to the adventurer guild, their real default start room) and an apparent unexplained second death (almost certainly the same mechanism misfiring on a different test character, though not re-isolated as precisely — a clean, admin-untouched 60-second silent-watch re-run with a fresh character never reproduced any second death, so this is recorded as a methodology artifact, not a mudlib bug). Fix for future sessions: after a fresh (not reconnect) login, send one more blank line and wait for the actual room description — not just the OKIP-check line — before sending any other command or doing any admin-side manipulation of that character.

Real bug found and fixed: die() crashes forever (never completes) when the character has no living last attacker

std/user.lpc's die() sets killer = query("last_attacker"); if (!killer) killer = previous_object(); and then, in the un-fixed code, used killer in two places without checking it might still be 0:

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

last_attacker is only set by std/body/attack.lpc's receive_damage() when the damage's previous_object() at the time of the call is itself a living() object — i.e. real melee/spell combat from another living creature. Any OTHER damage source (poison, bleeding, sickness, drowning/suffocation, desert exposure — all real, reachable status-effect conditions in std/conditions/, each calling receive_damage() from a non-living condition-daemon caller — or an admin-applied receive_damage() call) never sets last_attacker, and previous_object() *at the point die() itself runs* (called from heart_beat()→continue_attack(), with no calling object in that chain) is also 0. killer then stays int 0, and killer->query(...) /userp(killer) both throw a driver-level call_other/efun-argument error on this exact line, every single time, since continue_attack() re-invokes die() on every subsequent heart_beat() tick as long as hit_points < 1 — the crash never lets execution reach the link_data("dead") early-return guard further down (line 1009, *after* the crash site), so the character never actually dies: no corpse, no ghost, just an infinite repeating on-screen runtime-error spam and a permanently stuck negative-HP body. Reproduced live twice (once via the crash itself, once again confirming the SAME crash recurred one line further down after the first fix, until both call sites were guarded) using receive_damage(9999) via the fluffos admin account against a real, fully-connected test character (蜘蛛精/ testdieb's predecessor characters 夜寒霜/testmage, 白骨精/ testdie — see method notes above for why an artificial admin damage source was needed to reach this branch, since ordinary NPC combat sets last_attacker correctly and never hits it). Confirmed byte-identical unguarded code in sibling es1_win's std/user.lpc (same lineage, not yet fixed there — flagged for whoever next touches that lib).

Fix: added killer && short-circuit guards to both call sites, matching every other killer-using line in the same function (lines 1012-1014, 1084, 1089-1095, 1109-1117 were already correctly guarded with if (killer) — only these two were missed):

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

Live-verified end to end, twice, on a fresh driver boot with a fresh test character each time (testdie, then testdieb — testdie's run additionally included the admin eval diagnostic that reproduced the raw crash text on-screen pre-fix and confirmed its absence post-fix): receive_damage(9999) → 你死了。 → ghost creation → the full black_wuchang/黑无常 narrative (death1 at +10s, death2 at +30s more) → revive() restoring the character to a real living body in /d/noden/farwind/cemetery (露天英雄纪念馆) with reduced stats (10/30 HP, matching the documented "skills reduced on death" mechanic) — zero crashes, zero new lines in work/log/debug.log or work/log/log throughout. testdieb (id testdieb, password TestPassD2, human female adventurer) is left parked alive in the cemetery room as evidence; testdie and the earlier testmage mage-guild character were cleaned up (redundant once testdieb/ testguild cleanly demonstrated the same things without the methodology-artifact confusion).

Real guild join into a non-starting guild — verified twice (mage, healer)

Code review in every prior round noted std/guild.lpc's do_join() / each guild's own join_player() looked uniform across all 7 guilds but was never live-exercised for anything but the starting adventurer guild's already-a-member branch. This round completed real joins into two different non-starting guilds, both via a wizard move_player() teleport (d/mage/tower/mage_guild, d/healer/building/healer_guild) followed by the character's own real join command — matching the project's established "sect-joining needs wizard goto, not on-foot exploration" precedent (guild rooms aren't exit-reachable from the starting hall within a reasonable test budget):

Both joins used the SAME std/guild.lpc→per-guild-file→std/guild_ob.lpc mechanism the prior rounds' code review predicted (the guild token object, moved into the player, sets owner->set("class", class_name) via guild_ob.lpc's move hook) — this round's live tests confirm that prediction was correct, generalizing cleanly to at least 2 of the 5 previously-unverified guilds (mage, healer). Knight/monk/thief/scholar remain code-reviewed-only, not live-verified, though there's no reason to expect them to differ given the shared mechanism.

Standing bug checklist sanity pass

All checked via direct code read against the CURRENT work/ tree (no live re-trigger needed for any of these — either already fixed, or confirmed the vulnerable shape was never present):

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

同一血统(屠龙之战)。

深度功能测试(§10.7,2026-10-04)— compile warnings: 3795 -> 4, none in a file that compiles

scripts/lpc_warnings.py esI: 5859 files compile, 1176 do not; 4462 distinct diagnostics (3795 warnings, 667 errors). Now 5918 / 1117; 598 diagnostics: 594 errors in the files that still do not load (missing ../hole.h-style includes, set_closed_long and other functions of another lib, syntax errors) and 4 warnings, all in three of those files (adm/daemons/mailmesg.lpc, d/eastland/mumar/monster/cook_master.lpc, d/noden/asterism/monster/gnome_archelder1.lpc). 1208 files edited.

Mechanical (--fix): 603 #pragma save_binary lines (two more spelled #pragma save_binary ;), 385 nosave functions (364 protected, 21 left public because another object calls them), about 1100 unused locals, 8 range ends ..-N] -> ..<N], 31 escapes. 438 stray backslashes after the Big5 characters whose second byte was 0x5C (功, 许, 摆, 缕, 髅, 盖, 枯, 豹 ...; KB 03 §4.4): the driver prints Unknown escape sequence at the end of the string or statement, not at the backslash, so the fixer now cleans every \ before a multi-byte character in a file that reports one. 24 macros the old globals.h and the ES headers both define get an #undef before the later #define (the later one was already in effect).

By hand:

Pristine tree, HEAD against the working tree, same driver: look, score, who, skills, help answer identically for the admin; the HEAD console prints 769 compile warnings while it boots, the new one none. scripts/lpc_audit_removed_locals.py HEAD checked 2149 deleted declarators: none is read afterwards.

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

scripts/lpc_fix_negative_ranges.py: 8 line(s) in 2 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-04)— <../x.h> includes resolve: 184 more files load, 0 warnings left

#include <../hole.h> (185 files under d/) names a header relative to the including file. The driver refuses .. in a <...> name and resolves only a quoted include against the including file, so every one of those NPCs, weapons and armours died with Cannot #include, and so did everything that used the macros the header defines (the earlier section's "missing ../hole.h-style includes" was this, not missing headers: 178 of the 184 headers are where the name says). A wizard's goto /d/deathland/dwarf/center answered No program in object, as did the Deathland fog, palace and troll rooms, cook_master, the island vampire and the dragon-bone amulet. adm/obj/master.lpc now answers include_file() with "./" + path for a name that starts ../ (KB 04 §6.1). lpcc --batch (it runs create()) loads 178 of the 186 files that carry such an include (HEAD: 1), and a live session against HEAD and the working tree shows the rooms and clones working where HEAD printed the error. The 8 that still fail are content gaps, left: liang_shan/monster/guard2.lpc wields gurad_sword that no weapon directory has, mumar/monster/temp.lpc is a draft with a broken if, d/island/monster/{feller,old_feller}.lpc include ../hole.h where the header lives in d/island/hole/, d/scholar/demand/where/{fire_beast,princess}.lpc include the headers of two other areas, and mumar/UNDER/MONSTER/{armor,weapon}_smith.lpc sit in a directory of upper-case DOS names (*.C) that is no part of the game. d/noden/drow/mob/ghost.lpc had lost the ); after its set_long here-document and the () of my_tactic; both are back.

Newly loading files showed what they had been hiding (24 unused locals and 11 stray backslashes, mechanical), and the hand-fixes: obj/shells/shsh.lpc used buffer and class as names (both are types here: the saved colour pair is now buffer1, so a shell saved with the old name loses that one colour setting; class -> cls) and 50 of its functions were nosave; adm/daemons/mailmesg.lpc had //#include <config.h> commented out and no SAVE_EXTENSION, so the daemon never compiled; d/noden/asterism/monster/gnome_archelder1.lpc used ref, and its die() is the body's varargs void die(int silent); goomay/monster/army.lpc remove() is void. scripts/lpc_warnings.py esI: 5918 files compile, 1117 do not -> 6102 / 933; 0 warnings and 246 errors, all in files that still do not load. scripts/lpc_audit_removed_locals.py HEAD found nothing a function still reads.

深度功能测试(§10.7,2026-10-04)— stray backslash before a closing quote

A Big5 character whose second byte is 0x5C was written with one extra backslash after it (the old driver read 5C 5C as one backslash and so completed the character) and the UTF-8 conversion kept it (KB 03 §4.4). Before a closing quote it escapes the quote, the string runs into the next line and the file does not compile. scripts/lpc_fix_big5_quote_backslash.py deletes it when the character before it encodes to cp950 with 0x5C as second byte and the quote ends an expression. Here: d/eastland/saulin_temple/obj/vege_food.lpc (set_name and set_short end in 素食大餐\"; 餐 is cp950 C0 5C), which did not compile.

深度功能测试(§10.7,2026-10-04)— the files that still did not load, read one by one: 177 of 7036 left

lpcc --batch over every .lpc (KB 08 §10.4; scripts/lpcc_real_fails.py): 6136 load, 723 only hit the batch's call_out(0) nesting limit, 177 do not load. The same classes as in es1_win (its section of today has the reasons), read here file by file:

Left as content gaps (not invented): 40 inherits of files that are not in the archive, 30 NPCs that wield or wear files that were never shipped, wizard-home headers, drafts, the upper-case DOS directory d/eastland/mumar/UNDER (*.C), a stray std/user/复件 bitflags.lpc ("copy of bitflags"), and the same commands saved without the leading underscore (flee, praise, typo).

深度功能测试(§10.7,2026-10-04)— paths named in another case

The archive comes from a case-insensitive host: files are stored as BAGUA0.lpc, longjing.C, d/SHAOLIN/, the code names them in lower case (__DIR__ "bagua0", "tea/longjing", "/d/shaolin/..."), so the rooms, NPCs and items behind them could not be loaded (a .C file is not an .lpc file; KB 03 §4.5). scripts/lpc_case_paths.py --convert --apply esI: 72 file(s) renamed to lower case (.C -> .lpc, .H -> .h) and 0 path reference(s) in 0 file(s) rewritten to the new spelling; 0 skipped, 0 ambiguous. Load check of the 75 objects that were renamed, edited or name a renamed path (HEAD against the working tree, an object is the same under any spelling of its path): 1 PASS -> 5 PASS, 0 that loaded before fail now, 4 more load.

72 files moved (63 .C, 6 .lpc, 2 .H, 1 .h); mostly under d/eastland/mumar (71), d/DONY/VALLEY (1). A rescan with scripts/lpc_warnings.py esI is still due: the renamed files are compiled for the first time.

深度功能测试(§10.7,2026-10-05)— 主控 author_file()

驱动的 mudlib_stats 包在主控载入时调用一次 author_file(<主控文件>),要求返回字符串,否则每次启动都打印 author_file() in the master file does not work, using root_uid as fallback 一行,网页版每位访客的终端里都看得到。本库 adm/obj/master.lpc 的 author_file() 只为 /u/<字母>/<名字>/ 下的文件返回作者,其余返回 0。而且驱动传入的路径是 "/" 加上配置里的 /adm/obj/master,即 //adm/obj/master,sane 模式的 explode() 会保留一个开头的 ""。praise 和 find -author 直接调用 master()->author_file(),把 0 当作“没有作者”,所以没有把 0 改成字符串。现在先去掉空段(explode(...) - ({ "" })),/adm 下的文件返回 ROOT_UID(与同文件 domain_file() 的 adm 分支一致),这样也回答了开机探测;/u 分支加了长度检查(原来 /u/<x> 这样的短路径取 path[2] 会越界出错,而 author_file() 在每个对象载入时都会调用)(KB 06 §7.222)。用 lpcc config.fluffos /nonexistent.lpc 前后对比(主控和 preload 全程),只少了这一行。

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

scripts/toolkit_fix.sh 一轮:std/adt/body.lpc 多余的继承去掉;std/npc2.lpc 的 nosave 函数改 protected(外部调用的 die() 只去掉 nosave),没用的局部变量删去;cmds/ghost/_llook.lpc、std/npc2.lpc 字符串里 BIG5 转换留下的 功\能、许\多 多余反斜杠去掉;d/eastland/liang_shan/obj/fish_plate.lpc 没用的局部变量删去。能编译的文件警告清零;改动的对象在 HEAD 与工作树各载入一次,regress 0。

储藏室东西过多时清不掉(2026-10-15,KB 06 §7.245)

std/room/std_storage 的 my_clean_func() 在物品超过上限一半时从最后一件往前清:for (i = number; ; --i) 从 inv[number] 开始,第一次就越界,报 Array index out of bounds,储藏室从来没有被清过;循环也没有下界。改为 for (i = number - 1; i >= 0; --i)。跳过玩家的判断写成了 ->querry("npc"),一并改为 query(原意是留下玩家、NPC 也清)。测试里六件物品、上限二:改前越界报错,改后剩两件。

去掉的预定义宏 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):include/net/oldftp/ftpdsupp.h。

d/noden/asterism/monster/dwarf_healer 的台词里引号错位(2026-10-18,KB 03 §4.6)

台词那一行 "……只记得那个东西叫" 优 波 罗 花 "。\n":“叫”和“花”两侧的引号把字符串断开,中间的 优 波 罗 花 成了代码(Illegal character),NPC 载入不了。改成 '优波罗花'。载入:通过。

补回本库没有的基类文件(2026-10-18,KB 04 §6.15)

下面这些基类本库没有,继承它们的对象(房间、NPC、物品)整批载入不了(inherit XXX; 的宏没定义,或者 inherit "/std/…"; 的文件不在)。同系的库里有同名文件,两库有大量逐字节相同的文件(亲缘关系),所以从那边原样拷来,缺的宏补进全局头文件:/d/translate/tmp/drink←es1_win,3 个对象继承它,抽样 3/3 载入。每个基类补回后单独载入、再抽样载入使用它的对象,基类载不进来或者一个用户也载不进来的就撤掉(本库撤掉了它们,不再补)。(scripts/lpc_restore_bases.py)

区域物件的零碎修正(2026-10-19,KB 06 §7.267–§7.271)

逐个物件与 HEAD 对比载入,没有退步。

补回丢失的标点(2026-10-19,KB 03 §4.6)

转换时丢掉的分号、逗号、引号或文件末尾的 },共 1 个文件,修前 HEAD 载入失败。别的库里同路径的完好副本只比这里少几个标点,按它补回(括号里是那个库):/d/eastland/saulin_temple/board.lpc(es1)。修后与 HEAD 逐个对比载入:这 1 个物件全部载入,没有退步(scripts/lpc_restore_from_twin.py、lpc_fix_eaten_quote.py --comma、lpc_fix_eof_braces.py)。

x != "a" || x != "b" 和 x == "a" && x == "b":同一个值的比较链永远成立/永远不成立(2026-10-20,KB 06 §7.282)

if (arg != "wan" || arg != "丸") return notify_fail(...) 对任何 arg 都为真,dir == "south" && dir == "north" 永远为假:条件下面的守卫要么永远拦、要么永远不拦。作者要的是“既不是 A 也不是 B”(&&)或“是 A 或 B”(||)。!= 链的 || 改成 &&(仅当条件下面是 return 的拒绝/守卫),== 链的 && 改成 ||(scripts/lpc_fix_cmp_chain.py)。改动:/d/eastland/yogor/room37.lpc(1 处)。另有 /d/island/obj/box.lpc(1 处) 的条件下面做的是别的事(== 和 || 也可能是本意),保持原样。

call_out_info() 的每一项只有 3 个元素(2026-10-20,KB 06 §7.287)

驱动返回的是 ({ 物件, 函数, 延迟 }),不带 call_out 的参数,旧代码读第 4 项 info[i][3](callouts 指令、calloutd、cod 工具)一读就报 Array index out of bounds,指令从第一行起什么也打印不出来。改为 (sizeof(info[i]) > 3 ? info[i][3] : 0)(scripts/lpc_fix_callout_info.py)。改动:/cmds/xtra/_callouts.lpc(1 处)、/obj/tools/cod.lpc(3 处)。

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,共 7 处)。改动:/cmds/xtra/_graph.lpc(1 处)、/d/adventurer/hall/obj/salt_bottle.lpc(1 处)、/d/adventurer/hall/obj/test.lpc(1 处)、/d/eastland/yogor/mob/paladin.lpc(4 处)。

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

advance:5 级以上时 check_explore() 除以零(2026-10-21,KB 06 §7.331)

公会的 advance 在等级 5 以上要问 EXPLORE_D->check_explore(me),它对每个区域算 探险点 * 100 / query_total_explore(区域)。区域总数是巫师用命令 explored 登记的探险点数,登记数据(存档)不在归档里;公会自己的 explore 指令对 max < 1 的区域写了 continue,check_explore() 没有,所以新树里只要有一个区域没有登记点数,advance 就抛 Division by zero。先取总数,总数小于 1 的区域当作没有可探索的东西(mark++; continue;),与 explore 指令一致。三个库(es1、es1_win、esI)的 explore_d.lpc 逐 token 相同,同一处改法。只做了加载检查(没有 5 级的角色可测)。

idea:CMD_PRAISE 指向不存在的 _praise(2026-10-21,KB 06 §7.325)

include/commands.h 的 #define CMD_PRAISE "/cmds/std/_praise",而归档里(raw/ 也是)这个基类文件叫 praise.c,没有下划线(typo.c 也没有)。_idea.lpc 和 typo.lpc 都 inherit CMD_PRAISE;,敲 idea 时命令守护进程按 /_ + 命令名找到 _idea,编译时 Inherited file '/cmds/std/_praise' does not exist!,玩家的命令作废。宏改成 "/cmds/std/praise",_idea、typo、praise、_bug 四个都加载(HEAD 2、树 4),实测 idea 打开编辑器("Begin writing your idea report.")。typo.lpc 没有下划线,守护进程的命令表只认 _命令名,typo 仍然是 "什麽?",和归档一致,没有改文件名。

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

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

当铺 hochshop:赎回物品后“记住的物品”往前挪的循环用错了下标(2026-10-21,KB 06 §7.347)

当铺在玩家身上记着最多十件当出去的东西(REM_ITEM_PROP_ + DIGITS[n]),赎回一件之后后面的往前挪一位:for (j = i; j < my_item_num - 1; j++) me->set(REM_ITEM_PROP_+DIGITS[i], me->query(REM_ITEM_PROP_+DIGITS[i+1]));,循环变量 j 没用上,一直在写第 i 格,后面的格子根本没动,记录就乱了。改成 DIGITS[j] / DIGITS[j+1]。

工匠 artisan.lpc:my_tactic(int damage) 里的 victim 没赋值(2026-10-21,KB 06 §7.348)

attack_d 每回合调用 tactic_function(victim)(call_other(this_object(), tactic, victim))。工匠的 my_tactic(int damage) 把对手声明成了 int,局部的 object victim 从未赋值,random(20)>10 的那几回合 victim->query("armor/shield") 抛错。改成 int my_tactic(object victim),开头 if (!(victim = query_attacker())) return 0;(本库 teacher.lpc、mob_special.lpc 的写法)。改动:/d/eastland/mumar/monster/artisan.lpc。

拼错的 call_out() / input_to() 目标(KB 06 §7.349,2026-10-21)

计时器或输入提示给驱动的函数名和文件里定义的差一个字母;驱动对不存在的函数静默不理,所以对应的计时从来没有触发过。调用和配套的 remove_call_out() 改成已定义的拼法:idle_teme → idle_time。改动:/d/monk/guild/inner_room.lpc。

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

add_action("函数", "动词") 绑定的函数,在对象本身、它包含的头文件、它继承的父类和继承它的子类里都没有定义(scripts/lpc_scan_callout_targets.py 静态解析,子类用传递闭包复核,scripts/lpc_verify_handlers.py 运行时 function_exists() 复核)。驱动从玩家最新的一条动作往回找,遇到这样的句子就报 *Function for verb 'drop' not found.,同一个动词的其他处理函数不再尝试(不是最新的一条就悄悄跳过)。删除了 7 条注册(6 个文件,scripts/lpc_fix_dead_add_action.py),动词留给别的处理函数:/d/eastland/liang_shan/monster/fish11.lpc(kill_me)、/d/eastland/saulin_temple/brass/zeng7.lpc(do_auction)、/d/eastland/story/monster/elder1.lpc(do_embattle)、/d/eastland/story/weapon/elder1.lpc(do_embattle)、/d/noden/hawk/obj/tape.lpc(do_cast、do_tear)、/std/npc2.lpc(quit)。最多的函数名:do_embattle 2 处、kill_me 1 处、do_auction 1 处、do_cast 1 处、do_tear 1 处。没有动(读过):/d/noden/mj/emsd.lpc 的 do_gongxian(对象本身编译不过,登记留着,等对象能加载再处理)。

战斗探针(管理员克隆 NPC,2026-10-22):干净

scripts/play_combat_probe.py esI --account ... --clone ...:用库里预置的管理员账号把 /d/eastland/liang_shan/monster/test, /d/noden/ruin/monster/deltazeus, /d/island/monster/lady, /d/island/samurai/mob/ekars, /d/eastland/volcano/monster/wjailor, /d/island/forest/monster/older 逐个克隆到自己所在的房间并各攻击 20 秒(读了 6 场战斗):驱动没有输出,战斗画面里没有错误文字。(KB 08 §10.16)

log/ 下的日志子目录没有人建:写日志的 write_file() / log_file() 报错(2026-10-22,KB 06 §7.358)

这个库的代码往 log/ 的 1 个子目录写日志:log/harass(std/body.lpc)。压缩包、git clone 和站点包里都没有这些目录(本地启动没有写过它们,gen_keep_dirs.py 没记),write_file() 对不存在的目录抛 *Wrong permissions for opening file /log/dlog/money for append. "No such file or directory",调用它的函数在写日志那一行中断,后面的奖励、标志、步骤都没有执行。本库的 log_file() 是 write_file(LOG_DIR + file, text),目录不存在就抛同样的错误(实测)。把 log/harass 记进 scripts/wasm_keep_dirs.txt(scripts/lpc_keep_log_dirs.py;站点和 pristine 树按它建空目录)。证明(scripts/lpcc_eval.py,在 /adm/daemons 这类目录下的求值对象里写 /log/zzdirprobe/...,新进程)见 KB 06 §7.358 的实测表。

汉字按字节切片的老写法没有换成按字符:改法名 "澄" + name[2..3]、辈分判断 name[0..1] == "澄" 等 2 处(2026-10-22,KB 06 §7.360)

原档按字节算字符串区间(一个汉字 2 字节):name[2..3] 是名字的第二个字,name[0..1] == "澄" 问名字是不是以「澄」开头。转成 UTF-8 之后,这个驱动的区间和 strlen() 都按字符算(strlen("澄观") 是 2,"澄观"[0..1] 是两个字),老算术原样留着、意思全变:NPC 的称呼和提示里取到两个字而不是一个字。

本库 2 处(2 个文件):query("name")[..] 2 处。例:(string)ob->query("c_name"))[0..1] → (string)ob->query("c_name"))[0..0](/d/eastland/easta/monster/taxman.lpc:98)。scripts/lpc_fix_gbk_slice.py 把下标按字符重算(社区的 UTF-8 版 xkx100utf8 里就是同样的写法,作对照);没有改的:截断(name = name[0..7],现在最多 8 个字,只是放宽)、ASCII 的区间(fname[0..3] == "room"、ctime(time())[4..19])和上下文看不出是汉字的区间。证明(scripts/lpcc_eval.py,bixiecanyang;其余各库是同一份文本):让澄字辈掌门 attempt_apprentice() 一个名叫「张三」的访客,HEAD 的法名是 慧,改后是 慧三;抽样 28 个库的 HEAD / 工作树加载对比 0 退步。工具不幂等([0..3] 折半得 [0..1],又是对齐区间),改过的文件记在 scripts/gbk_slice_converted.txt,不要重跑。

长度检查按字节写,旁边的提示按汉字写:4 处(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 你好吗)。本库 4 处(3 个文件):if (strlen(rn) > 14) {(/adm/daemons/newuserd.lpc:132);if (strlen(ncname) > 14) {(/cmds/std/_chfn.lpc:85)。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 退步。

samurai dagger MSPECIAL 指向缺失 /u/(2026-10-27,KB 06 §7.359 Also)

d/island/samurai/obj/dagger.lpc 写 #define MSPECIAL "/u/m/mercury/obj/special",档案无该巫师路径,驱动报 Inherited file ... does not exist。改为本库已有的特殊攻击基类 /d/eastland/goomay/obj/special(同文件头注释所述用途)。证明:HEAD 加载失败;改后加载成功。

深度功能测试(2026-10-27,§6.25 death_echoall 截断)

std/fun/death_echoall 同 es1 截断;同样收成空循环。证明:改后加载。