Eastern Story (Blue Sky)

✅ 可玩

东方故事(蓝天) — es1_win

es1_win

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

▶ 开始游玩 · Play Now

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

English

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

README

内容亮点

在线试玩

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

管理员账号 / Admin account

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

本地运行

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

游戏端口:40009。

NOTES · 移植与修复记录

es1_win.rar → es1_win

Status: DONE — boots clean, playable over telnet

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

What was fixed

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

Known remaining issues (documented, not fixed)

How to run

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

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

WASM-enablement pass (loopback / admin seeding)

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

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

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

Bugs found and fixed

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

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

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

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

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

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

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

What was tested and confirmed working

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

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

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

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

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

Not verified live (explicitly)

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

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

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

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

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

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

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

Other checklist items: mostly not applicable, different lineage architecture

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

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

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

Verification method

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

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

Found during the §7.100 tail-sweep (below the original 166-lib survey's

=100-occurrence threshold, never checked). 2 live

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

scripts/lpc_fix_negative_ranges.py: 16 line(s) in 9 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)— compile warnings: 2583 -> 46, and 191 more files load

scripts/lpc_warnings.py es1_win: 5764 files compile, 1169 do not; 3282 distinct diagnostics (2583 warnings, 699 errors). Now 5955 / 977; 358 diagnostics: 46 warnings, all in two files that cannot compile (std/adt/body.lpc inherits the dbase twice, u/s/sage/test/dragon.lpc is two files pasted one after the other), and 312 errors in files that still do not load. 929 files edited.

183 files under d/ did not load: #include <../hole.h>. A header named relative to the including file, in angle brackets (71 x ../hole.h, 30 ../goomay.h, 23 ../replace_room.h, 18 ../mumar.h, 10 ../island.h, 9 ../zeus.h, 9 ../takeda.h ...). The driver refuses .. in a <...> name and resolves only a quoted include against the including file, so every NPC, weapon and armour of those areas died with Cannot #include and each user of the macros it defines with a second error (218 files). 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 ../ (a different string makes the driver resolve it the way a quoted include is, through merge(), which understands ..; KB 04 §6.1). 177 of the 184 files then load through lpcc --batch (it runs create()), and a live session against HEAD and the working tree shows the same rooms and clones working where HEAD printed the error. The 7 that still fail are content gaps, left: liang_shan/monster/guard2.lpc wields gurad_sword and guard1.lpc guard_dagger, neither of which exists in the area (no room places either guard); mumar/monster/temp.lpc is a draft of cook_master 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 that are not there. d/noden/drow/mob/ghost.lpc had lost the ); after its set_long here-document and the () of my_tactic; both are back.

Mechanical (--fix): 429 nosave functions made protected (21 left public because another object calls them), 305 stray backslashes after the Big5 characters whose second byte was 0x5C (KB 03 §4.4), 809 unused locals. The esI hand fixes carried over (see its section of 2026-10-04): the second mapping alias of std/npc.lpc (1116 of the warnings), class -> cls (16 files), ref -> refl, new -> new_mud, == typed for = in _bribe.lpc (13), _who.lpc, the acupuncture and jousting files and the two shield spells, the leaf NPCs whose two parents both define init() or reset() (chained), the prototypes that disagreed with their definitions, levels -> sort_levels in the five guild books that redeclared the guild object's table.

Found here, not in esI:

Checked: scripts/lpc_audit_removed_locals.py HEAD over 2526 removed declarators found nothing a function still reads, and a live pair (HEAD and working tree, fluffos admin) logged in, walked look / goto / clone through the Deathland and island files that HEAD refused with no new error in the driver output.

(2026-10-04, later the same day) obj/shells/shsh.lpc's blind buffer -> buffer1 / cursor -> cursor1 conversion had also changed text the player sees: the /buffer verb itself, the Cursor [ / Buffer [ prompts, the buffer view's banner and its [Top of Buffer] / [End of Buffer] / [Buffer view closed] lines and the recall message. The strings read as in esI again; the function-name strings ("open_buffer1", "cursor1_colour", ...) keep their renamed targets.

(2026-10-04, same pass) d/eastland/saulin_temple/obj/vege_food.lpc had the stray backslash before the closing quote of set_name and set_short (素食大餐\"; 餐 is cp950 C0 5C, KB 03 §4.4): the file did not compile. Fixed with scripts/lpc_fix_big5_quote_backslash.py.

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

lpcc --batch over every .lpc (KB 08 §10.4; scripts/lpcc_real_fails.py lists the failures): 6048 load, 761 only hit the batch's call_out(0) nesting limit (they compile and load alone), 124 do not load. After the <../x.h> fix these were read file by file:

Left, as content gaps, not invented: 16 inherits of files that are not in the archive (/u/e/elon/drink, /u/m/mercury/obj/special, /std/object/sec_ob, /std/map, ...), 11 NPCs that wield or wear files that were never shipped (gurad_sword, guard_dagger, ARM "earing" ...), headers of wizard homes (/u/s/smore/hawk.h, ../lilia.h, ...), the Megatower area whose header says /d/noden/megatower while the files are in backarea/megatower, draft files (temp.lpc, temp2.lpc, jin.lpc, _bed2.lpc is three copies of a room one after the other), functions of another driver (opcprof, function_profile, set_limbs, setup_race_body), and the commands saved without the leading underscore (flee, praise, typo, idea, query: only _name.lpc files become commands). std/adt/body.lpc and u/s/sage/test/dragon.lpc hold the last 46 warnings and cannot compile.

深度功能测试(§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 一轮:d/eastland/liang_shan/obj/fish_plate.lpc 没用的局部变量 i 删去。能编译的文件警告清零,regress 0。(同轮报的 6 处“需人工”的重叠继承都在不能编译的文件里,未动。)

储藏室东西过多时清不掉(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。

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

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

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

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 级的角色可测)。

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

include/commands.h 的 CMD_PRAISE "/cmds/std/_praise",实际文件是 praise.lpc(归档里就没有下划线);idea.lpc、typo.lpc 都 inherit CMD_PRAISE; 编译失败。这两个文件也没有下划线,CMD_D->find_cmd() 找的是 /_ + 命令名,所以 idea、typo 在这个库里本来就是 "什麽?",不报错。宏改成 "/cmds/std/praise",两个文件加载(HEAD 1、树 3),命令仍然不可达(与归档一致)。esI 的同一处改法见 esI 的 NOTES(那里 _idea.lpc 有下划线,命令可达,错误可见)。

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

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

当铺 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/erosmia/ekravia/object/chain_fw.lpc 的 do_light(对象本身编译不过,登记留着,等对象能加载再处理)、/d/erosmia/ekravia/object/chain_fw.lpc 的 write_message(对象本身编译不过,登记留着,等对象能加载再处理)、/d/noden/mj/emsd.lpc 的 do_gongxian(对象本身编译不过,登记留着,等对象能加载再处理)。

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:96)。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:86)。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 截断;同样收成空循环。证明:改后加载。