Ultimate Hell: Legend of Xuanyuan

✅ 可玩

终极地狱-轩辕传说

revive

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

▶ 开始游玩 · Play Now

游戏内标题为 "EMPEROR OF ULTRA HELL(BLOOD & MAGIC) FROM 1999.12.12",和本项目已收录的 `hell`(游戏内标题 "REVIVE OF ULTRA HELL...FROM 1999.11.13")同属 Doing Lu 的"地狱"血统家族(master.lpc 的作者传承注释一字不差),但这份 GitHub 上的 `revive` 仓库是一份规模小得多的 "底层代码"发行版——仓库自述即为"mud 底层代码",只随附了投胎/阎罗殿 (`death`)、比武场 (`pk`) 和注册入口 (`register`) 三个地图分区,原版 `hell` 里的近 54 个门派/城市地图(武当、少林、峨嵋、大理、丐帮……)和整个武功技能树都没有包含在这份发行版里。

English

A stripped-down 'base engine' snapshot of the same Doing Lu 'hell' ES II-lineage codebase as this collection's hell/hellxg/zjmudhell, self-titled 终极地狱-轩辕传说 (Ultimate Hell: Legend of Xuanyuan) and opening on the banner 'EMPEROR OF ULTRA HELL (BLOOD & MAGIC) FROM 1999.12.12.' Only three domains ship in this particular release -- the reincarnation/afterlife hall, the arena (pk), and the registration entry -- rather than the roughly 54 city and sect domains of the full hell release, though the shared engine's own registration flow, combat system, and daemons are all intact. New characters go through the lineage's classic reincarnation ritual: an English login id, an optional Chinese surname and given name, dual admin/regular passwords, a 1-5 aptitude choice (猛士/智慧/耐力/敏捷/均衡), and a gender choice, arriving in the 世外桃源 (Peach Blossom Land) entry scene where the NPC 水笙 greets newly arrived souls and nudges them toward the register command to claim an email address for password recovery.

README

内容亮点

本次修复的关键 bug

详见 NOTES.md。

管理员账号 / Admin account

管理员名单存储在纯文本文件 adm/etc/wizlist 里;账号本身通过正常 注册流程创建,"目前权限:(admin)" 已在游戏内跨越一次驱动重启确认 显示正确。

警告:这是一个公开的默认密码,仅供本地/浏览器试玩。正式对外开服前
请务必修改此密码。

本地运行

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

游戏端口:40236。

注意:ftpd.lpc 等几个网络精灵在启动时会尝试绑定真实的 TCP/UDP
端口(详见 NOTES.md),不建议高频率自动重启此档案。

NOTES · 移植与修复记录

revive (终极地狱-轩辕传说) -- porting notes

Source: git clone https://github.com/MudRen/revive (commit 240ea5c4c7cd8cf2d854781648a16398e6df3b16, cloned 2026-08-26). The GitHub repo's own description is "终极地狱-轩辕传说 mud 底层代码" ("Ultimate Hell - Legend of Xuanyuan, MUD base/engine code"). Slug revive, number 934, port 40236.

Lineage -- same Doing-Lu "hell" family already in this collection

adm/single/master.lpc's header comment is byte-identical to hell/hellxg/zjmudhell's: "for ES II mudlib / original from Lil / rewritten by Annihilator (11/07/94) / modified by Xiang for XKX (12/15/95) / updated by Doing Lu for hell (2K)" (no yh2003-specific "Modify By Linux@lxtx" line, distinguishing it from the separate yanhuangwuhun/zhyx sub-family). The raw archive's adm/etc/welcome banner reads "EMPEROR OF ULTRA HELL(BLOOD & MAGIC) FROM 1999.12.12", internet mud name : hell.local, and smtp sender : [email protected] in adm/etc/config -- the same author/codebase as hell's own "REVIVE OF ULTRA HELL...FROM 1999.11.13" banner, just a different dated snapshot. A whitespace/brace-style-normalized diff of master.c against hell's already-converted master.lpc produced only cosmetic differences (indentation, static->nosave, .c->.lpc -- the same mechanical transforms convert_lib.sh already applies), confirming this is the same underlying codebase, not merely a similar title (per AGENTS.md's standing caveat that similar Chinese titles alone are NOT a lineage signal in either direction -- this one was verified by diff).

Not a duplicate, but a MUCH smaller release than its hell sibling: 1,901 files total (vs. hell's 8,549) and only 3 of the original domain directories under d/ -- death, pk, register -- vs. hell's ~54 (baituo, beijing, changan, dali, emei, fuzhou, gaibang, hangzhou, huashan, kunlun, shaolin, songshan, suzhou, wudang, xiaoyao, xingxiu, ...). kungfu/skill/ doesn't exist at all in this archive (only kungfu/special and kungfu/condition, the skill *framework*, survive). This matches the repo's own self-description as "底层代码" (base/engine code) -- it reads as a stripped release of the shared Doing-Lu engine with the actual built-out game world/content held back, not a conversion failure or an accidental subset.

Pipeline

1. Cloned directly into libs/revive/raw/revive/ (git-hosted source, .git stripped before copying in, per this project's convention). 2. scripts/convert_lib.sh libs/revive/raw/revive libs/revive/work GB18030: already_utf8=422 converted=1461 lossy=11 skipped_binary=7; renamed 1,169 files to .lpc; fixed 252 literal .c" references + 5 #include <....c> refs; converted 9 local angle-bracket includes to quotes; static->nosave touched 106 files. No string-literal ("static/...) or #define nosave/protected static shim collisions found on inspection (both known §4.3 classes checked and clear). 3. Full-tree straggler sweep (plain Python UTF-8-decode walk, per AGENTS.md §4.1 beyond what the extension-based scan covers): 6 non-UTF8 files remained after the main pass, all genuine GB18030 text that file(1)'s binary heuristic had misclassified (skipped_binary=7 counted these plus one genuinely-tiny no-magic file, adm/etc/banned_sites, correctly left alone): doc/board/{yb010,yb028}, doc/legend/xkx10 (dev-era player message-board/story archive text, same extensionless-help-text gap class already documented for zhyx/yanhuangwuhun, though different specific files) and clone/game/{8_hlp,helppai,21_hlp} (card-minigame help text -- again the same class, different content, as zhyx's 8_hlp/21_hlp). Converted with iconv -f GB18030 -t UTF-8 in place. No uppercase-.C stragglers found (unlike zhyx). 4. No hardcoded MUD_PORT/PORTNO constant found in include/*.h (§5.3 checked, clear).

Fixes applied (driver-incompatibility / robustness bugs)

``lpc if (strsrch(message, "Warning") == -1 && strsrch(message, "warning") == -1) ``

- check_legal_name(name, maxlen): strlen(name) < 2 || strlen(name) > maxlen -> < 1 || > maxlen / 2. - get_name()'s combined surname+given-name floor: strlen(fname) < 4 -> < 2. - named.lpc's #define PATH(name) (name[0..1] + "/" + name) -> name[0..0]; every strlen(name) < 2 guard (remove_name, map_name, who_is, invalid_new_name) -> < 1; the name[0..1] bucket-key reference in remove_name -> name[0..0]; invalid_new_name()'s sliding-window near-duplicate check: guard strlen(name) < 4 -> < 2, loop bound i <= l - 4 -> i <= l - 2, 2-char window name[i..i+3] -> name[i..i+1], 3-char window name[i..i+5] (guarded i+6<=l) -> name[i..i+2] (guarded i+3<=l).

```lpc object make_inventory(string file) { object ob;

catch(ob = new(file)); if (! objectp(ob)) return 0; catch(ob->set("startroom", base_name(this_object()))); catch(ob->move(this_object())); return ob; } `` ...plus an objectp(ob[list[i]]) guard added to reset()'s case 1 branch before its own subsequent environment(ob[list[i]]) call (the default branch already guarded correctly via its ! objectp(...) { ...; continue; } shape -- only case 1 needed the fix). With both changes, /d/register/entry loads cleanly (diyun` just silently has no clothes, a narrow, cosmetic content gap) and every new player reaches the actual game world.

``lpc string calc_sec_id(int raw) { return "none"; } ` save() stores this raw, un-hashed placeholder verbatim (set("sec_id", calc_sec_id())); restore() then checks crypt(calc_sec_id(1), sec_id) != sec_id -- i.e. re-hashes the SAME literal "none" using the stored value as the crypt salt, and compares the result against that same stored value. Since the stored value is the raw plaintext "none", not a real crypt hash, crypt() can never reproduce it byte-for-byte, so this check failed unconditionally, for every account, every time, setting user_cracked = 1 and making restore() return 0. Traced through adm/daemons/logind.lpc::check_ok()'s if (user->restore()) { ...enter_world...} else { destruct(user); write("无法读取你的数据 档案,您需要和巫师联系。\n"); ... } -- confirmed live: a freshly registered, fully-saved character (testqin, real Chinese name, admin+regular passwords, full stat block on disk) could complete registration and play normally within the SAME driver session (any still-net-dead reconnect skips restore() entirely via a different check_ok() branch), but could never log back in again after any driver restart -- exactly reproduced, then fixed and reproduced clean, via two full kill+reboot+reconnect cycles. This is the same structural idiom already used correctly, with a REAL non-stub implementation, by sibling lib hell's own calc_sec_id() (clone/user/user.lpc:932, a genuine checksum over the whole dbase/skills/inventory, gated by the exact same if (!raw) str = crypt(str, 0); return str; shape) -- confirming this placeholder was simply never finished in this specific archive, not a driver incompatibility invented by conversion. Rather than inventing a real checksum algorithm (out of scope -- that would be completing missing game-content logic the original author explicitly punted on), this was fixed as a narrow, mechanical round-trip correction: restore the missing crypt()-salt gating so the SAME placeholder value round-trips consistently (i.e. make the un-implemented check a true no-op instead of an always-fail trap), reusing the exact gating shape already present verbatim in hell: ``lpc string calc_sec_id(int raw) { string str;

str = "none"; if (! raw) str = crypt(str, 0); return str; } `` Verified: a fresh account registered, saved, and quit; driver killed and restarted; the SAME account reconnected cleanly all the way back into the game world ("你连线进入revive" / room redisplay / clean quit`) -- reproduced the failure once pre-fix (exact "无法读取你的 数据档案" message) and the fix twice post-fix (including once through this lineage's legitimate, unrelated 30-second reconnect-cooldown gate, which is intentional throttling, not a bug).

Confirmed content gaps (NOT fixed, per project policy)

Boot / registration verification

Not run this session

2026-08-27 深度功能测试 / Deep functional test (round two, §10.7)

Read help/newbie first (registration -> pick personality via out in one of 4 rooms -> wash for stats -> born <地名> into the world). Ran a real, continuous session with a fresh Chinese-named test character (surname/given-name 秦/石, then 秦/言 for a second, never-born control character used for the restart/reconnect check), plus the seeded fluffos/Mud@2026 admin for goto-based exploration of the two non-register domains. Native build-debug driver, port 40236, one lib at a time, killed by exact PID between reboots.

SEVERE, confirmed live: every new player's first move out of the

registration room threw an uncaught driver error (d/register/npc/ diyun.lpc's unguarded carry_object()->wear())

d/register/npc/diyun.lpc::create() (the "狄云" NPC who sees new players off at the register-room exit) did:

carry_object("/d/city/obj/cloth.lpc")->wear();

/d/city is one of the ~51 domains this stripped archive never shipped (already documented above), so carry_object() (inherit/ char/npc.lpc) correctly returns 0 on the missing file -- but the call site immediately dot-calls ->wear() on that 0 with no objectp() guard, which the driver rejects as *Bad argument 1 to EFUN call_other() Expected: object, string, array, Got: int(0). Reproduced live: a freshly-registered character's very first east (or any exit) out of /d/register/entry triggered d/register/ entry.lpc::valid_leave()'s own ad-hoc new(__DIR__"npc/diyun") (not the room's normal make_inventory()-populated objects list, so the onboarding session's make_inventory() catch() fix -- which only covers population via reset() -- never protected this path), which ran diyun's create() uncaught, dumping a full driver stack trace straight to the connecting player:

执行时段错误:*Bad argument 1 to EFUN call_other()
程式:/d/register/npc/diyun.lpc 第 16 行

This is universal -- every single player who successfully registers and decides hits it on their first move, though it's non-fatal (the error is swallowed after being printed and the move itself still succeeds; diyun just has no visible clothes afterward). Fix (same shape as the onboarding make_inventory() fix, §7.25 family -- guard the call, don't fabricate the missing content):

object cloth;
...
if (objectp(cloth = carry_object("/d/city/obj/cloth.lpc")))
        cloth->wear();

Verified live post-fix: east out of /d/register/entry now prints only "狄云对你一抱拳,道:人生路全靠自己走,朋友走好!" with zero error trace, across a fresh boot.

Same exact unguarded carry_object(missing)->wear()/->wield() shape found and fixed at 3 sibling call sites (grepped every carry_object( ...)-> call site in the tree, cross-checked which referenced targets are actually missing in this archive vs. real, shipped /clone/... items -- most of the ~30 hits are fine, these 3 aren't):

All 4 fixes verified via a scoped lpcc --batch run before and after (diyun and mm flipped FAIL->PASS cleanly; youxun/meng-zhu flipped from the wear()-crash FAIL to the pre-existing, correctly-untouched *F_SKILL: No such skill FAIL) and a full lpcc_check.sh sweep (1081/1169 -> 1087/1169, zero new failures anywhere else). Likely to recur in hell/hellxg/zjmudhell: this is boilerplate NPC-dressing code copied across many files in this lineage; those siblings ship the full city content so their own carry_object() targets probably resolve, but it's worth a quick grep of carry_object(...)-> there too in case any of *their* target paths are themselves stale/renamed.

SEVERE, confirmed via lpcc: the entire quest subsystem (5 of 6

quest-type files) failed to compile -- a shared wrapper's parameter type didn't match how every real caller used it

inherit/misc/quest.lpc::set_information(string key, string info) -- but clone/quest/{shen,supply,explore,search,judge}.lpc (every quest type except the already-content-gapped girl.lpc and the plain avoid.lpc) call it with a function pointer as the second argument (set_information(NPC1_NAME, (: ask_npc1 :)), etc.) so an NPC's own name/id can trigger a dynamic answer instead of a fixed string. The underlying daemon side (adm/daemons/questd.lpc:: set_information(object qob, string key, mixed info)) already declares info as mixed and handles this fine -- only the thin per-quest- object wrapper in quest.lpc had the wrong, narrower string declaration, which this driver enforces strictly at compile time:

error: Bad type for argument 2 of set_information ( string vs function )

Since EVERY quest-type file needs this call, all 5 real quest classes failed to compile entirely, and the quest daemon's own heart_beat()- driven start_quest() (an ambient, ~15-minute-interval background process, ran unprompted mid-session) threw a fresh uncaught *No program in object '/clone/quest/search'! (and identically for the other 4) every time it tried to spin one up -- caught by the driver's error handler so it didn't crash the mud, but it silently broke 100% of this stripped archive's quest content and spammed debug.log forever. Fix: widen the one declaration to match its own daemon counterpart and every real caller:

void set_information(string key, mixed info)

Verified via lpcc --batch: all 6 quest files (shen, supply, explore, search, judge, avoid) now PASS; girl.lpc still fails on its own, separate, already-documented undefined-variable content gap (untouched, correctly). This is a narrow shared-file type- declaration bug, not a content decision -- fixing it doesn't invent any of the missing quest content, it just lets the existing quest *code* run instead of refusing to compile.

Companion fix, found via the same ambient heart_beat error: adm/daemons/quest/{supply,explore}.lpc::start_quest() both do env = get_object(room); ...present(rcv_npcs[room], env)... with no objectp(env) check -- since rcv_npcs' keys are real, well-formed room paths but every single one of them lives in a domain this archive never shipped (/d/city3, /d/dali, /d/changan, /d/city, /d/beijing, /d/shaolin, /d/suzhou, /d/hangzhou, /d/fuzhou, /d/quanzhou, /d/lingzhou -- a pure content gap, not a code bug), get_object() always returns a non-object, and present(x, 0) throws *Bad argument 2 to present() Expected: object Got: 0, also caught but also spamming debug.log every ~heart_beat forever. Fixed with a one-line ! objectp(env) guard added to each, mirroring the already-fixed quest/girl.lpc sibling (which coincidentally can't even reach this line, since it fails to compile first on its own undefined-variable gap). adm/daemons/quest/{judge,search,shen}.lpc use a different, already-guarded room-selection shape and needed no change at THIS call site -- but see the next finding, which hit all three of them at a different call site.

Second content gap uncovered by the same set_information() fix, in the very next line the fix unblocked: once the quest files could actually compile, letting the daemon's heart_beat() reach past the old compile failure, clone/quest/{search,judge}.lpc::init_quest() (npc1 = new(CLASS_D("generate") + "/questnpc"); ... npc1->set_temp( ...)) and clone/quest/shen.lpc::init_quest() (same shape, single npc/"/shennpc") immediately hit a FRESH ambient uncaught error, live, unprompted, within the very next heart_beat cycle after the reboot:

执行时段错误:*Bad argument 1 to EFUN call_other()
Expected: object, string, array,  Got: int(0).
程式:/clone/quest/search.lpc 第 67 行

Root cause: CLASS_D("generate") expands to /kungfu/class/generate, and this WHOLE directory -- not just /kungfu/skill/, a separate, already-documented gap -- never shipped in this stripped archive (confirmed: find . -path "*kungfu/class*" returns nothing at all). new() on the missing .../questnpc/.../shennpc template correctly returns 0 (per the onboarding session's virtuald.lpc fix), but none of the three call sites checked before dot-calling ->set_temp() on the result -- the exact same missing-guard shape as the carry_ object()->wear() family above, just one more content-gap directory deep. Fix, same established pattern (guard + self-destruct via the function's own pre-existing "can't proceed" idiom, already used one branch up in the very same functions for the "item already claimed by another quest" case):

npc1 = new(CLASS_D("generate") + "/questnpc");
npc2 = new(CLASS_D("generate") + "/questnpc");
if (! objectp(npc1) || ! objectp(npc2))
{
        destruct(this_object());
        return;
}

(single-npc variant for shen.lpc). Verified via lpcc --batch (all 3 still PASS) and live: rebooted again, let the driver idle through 3+ heart_beat cycles (supply/explore/search/judge/shen all fire on their own timers), debug.log grepped clean of any 执行时段 错误/*-prefixed runtime error for the whole idle window post-fix.

/kungfu/class/generate/ is a third, previously-undocumented missing content subtree (distinct from /kungfu/skill/), also referenced unguarded by adm/daemons/questd.lpc (receiver.lpc, killed.lpc -- the letter-delivery and bounty-quest NPC generators) and adm/daemons/ npcd.lpc (chinese/japanese/european/indian generic-NPC templates). Those 6 additional call sites were deliberately left unfixed: unlike the 3 quest-heart_beat sites above, none of them are reachable via any live path in this archive -- they only fire from inside quest-giver dialogue that no NPC in the 3 shipped domains actually offers, so fixing them would be speculative hardening against an unreachable trigger rather than a live, in-scope bug, and the missing-content directory itself is the same well-established "don't fabricate content" boundary as /kungfu/skill/. Flagged here so a future session doesn't have to rediscover the same gap from scratch, and doesn't mistake "reachable via heart_beat" for "reachable at all" when triaging which of a batch of similar-looking call sites are worth fixing.

§8.3a variant confirmed: two more private mixin functions

dispatched by name via call_out(), not just add_action()

Grepped every private-declared function in the tree against same- file call_out("name", ...) / add_action("name", ...) call sites, then filtered out the (majority) false positives where the private function lives in a standalone daemon/NPC/command file that's never inherited into a different top-level body (for those, the driver's own internal dispatch on this_object() never crosses the private/DECL_HIDDEN boundary, so there's no bug). Two real, live mixin instances remained, both already named verbatim in this project's own §8.3a catalog as confirmed demonangel sibling instances of the exact same class:

Fix (both, matching the established pattern): drop private, keep the function otherwise unchanged --

// feature/action.lpc
void eval_function(function fun) { evaluate(fun); }
// inherit/item/combined.lpc
void destruct_me() { destruct(this_object()); }

Verified via lpcc --batch: no regressions (all previously-passing files affected by either inherit chain still pass). Likely to recur in hell/hellxg/zjmudhell: feature/action.lpc and inherit/ item/combined.lpc read as core, shared engine files in this lineage (not stripped-archive-specific content), so this exact start_call_out/eval_function and combined_item/destruct_me shape is a strong candidate to check on those 3 siblings the next time any of them gets a §10.7 pass -- grep private.*eval_function and private.*destruct_me directly.

Confirmed clean (all six standing cross-cutting patterns checked)

Registration / personality / born / quit-reconnect -- all confirmed

working correctly (post-fixes)

Full continuous session with 秦石 (male, 光明磊落 personality via the east-room NPC 陆天抒, matching the newbie-help-documented flow exactly): register -> decide -> out of the east personality room -> arrived at 阎罗殿 (d/register/yanluodian.lpc) -> wash (rolled str/int/con/dex 19/22/19/20) -> born 扬州人氏 correctly failed gracefully ("牛头一呆,搔搔头说:怎么好像有问题...", the already-documented /d/city content gap, zero crash) -> score/i both rendered a full, sensible status display (age 14, personality, starting clothes from account creation) despite the failed born. quit was clean both times tested (dropped the now-worthless newbie book, correct farewell message), debug.log grepped after each quit (clean both times, post-fix). A second, never-washed/never-born control character (秦言) was registered specifically to verify the onboarding session's calc_sec_id() restart-lockout fix still holds after this session's edits: registered -> decide -> quit -> driver killed by exact PID and rebooted -> reconnected after the restart -> correctly restored into /d/register/entry with the right "上次光临" timestamp and pre-birth score message -> quit cleanly again. The fix holds.

One notable downstream interaction, not a new bug: d/register/ yanluodian.lpc::do_born() sets me->set("startroom", dest) *before* checking whether the destination object actually loaded, so a failed born (this archive's universal case, since every born target is a missing domain) still permanently records a nonexistent startroom. A player in this state who later quits and reconnects will hit logind.lpc::enter_world()'s own catch(load_object(startroom)) failing (a SEPARATE, already-existing catch from the onboarding session, working exactly as designed) and land in the graceful "你无 法进入这个世界" fallback message with no environment, rather than back in the register room -- reproduced live on 秦石 post-fix-boot. This is a downstream consequence of the well-documented, deliberately- untouched missing-city-domains content gap (not a new code bug: the existing catch already prevents a crash, and there is no live- reachable born target in this archive that would ever leave a character in this state on a fully-built release), so left as-is -- flagged here only so a future session doesn't mistake it for a new regression.

Combat / skill / sect / shop / death-respawn: **not reachable

live in this stripped archive** -- content gap, not a code bug

Confirmed via direct testing that born cannot succeed for ANY of the 18 listed destinations (every single one targets a domain this archive never shipped: /d/city, /d/guanwai, /d/beijing, /d/ taishan, /d/changan, /d/shaolin, /d/xingxiu, /d/xiangyang, /d/suzhou, /d/hangzhou, /d/fuzhou, /d/city3, /d/dali, /d/ foshan, /d/baituo, /d/yanziwu, or the intentionally-disabled 0 placeholders for 蒙古/黔中), so a player can never leave the register domain through the game's own intended path. The only other two shipped domains (d/death, d/pk) were explored directly via admin goto instead:

Net: skill/sect acquisition, real combat, shop/economy, and death/ respawn are all unreachable via any live path in this archive -- not a testing gap, a genuine, already-well-documented content gap (no kungfu/skill/ tree, no sect NPCs, no shops, no city domains at all survive in this "engine only" release). This was verified directly this session rather than assumed from the onboarding notes.

wasm_status 审计(2026-09-01):VERSION_D 登入闸门缺 find_object() 守卫,验证为 playable

meta.json 的 wasm_status 此前一直留空(此前只做过原生驱动验证, NOTES.md 已注明 WASM/§10.7 深测"out of scope for this onboarding session")。本次批量审计(见 [[project_wasm_status_audit]])补上这 一步。

scripts/wasm_client.js 起跑本身没有触发编译期致命错误(这份 "底层代码"精简发行版没有自己的 simul_efun 级 socket 依赖),但每一 个新玩家连线都会被 adm/daemons/logind.lpc 直接踢掉,提示"现在本站 正在同步版本中,暂时不能登录,请稍候再尝试。"——这正是 AGENTS.md §1.3(c) 目录里"中华英雄/终极地狱血统家族的 VERSION_D->is_version_ok() 式闸门"那一类(此前在 zhonghua2/zhongjidiyu/zjdyzj/ yanhuangwuhun/yhyxs 都出现过):adm/daemons/versiond.lpc 自己 的 socket_create()/socket_bind()/socket_listen()/socket_close() 调用在 WASM 下编译失败(没有 sockets 包),使得 VERSION_D 这个 对象始终"不存在",VERSION_D->is_version_ok() 每次都以运行期错误 收场、等效于返回 0,而 logind.lpc 把"未确认在线"当成"正在同步 版本"直接拒绝所有非巫师连线。

修复(标准 find_object() 守卫,缺席即放行,与 AGENTS.md 现有目录 写法一致):adm/daemons/logind.lpc 两处新玩家/老玩家登入检查、 adm/daemons/questd.lpc 的任务系统启动检查、adm/daemons/closed.lpc 的 heart_beat()(从开机起每 3 秒执行一次,此前每次都要重新尝试 编译一遍上千行的 versiond.lpc,是明显的额外开销)都加上 find_object(VERSION_D) && 前缀。cmds/arch/version.lpc(巫师手动 version 指令)未改,不在登入路径上,属正常的"外围指令编译失败"。

修复后跑通一次完整会话:英文 ID(3-10 个英文字母)→ 中文姓氏(留空) → 中文名字 → 管理密码×2 → 普通密码×2 → 角色资质(3)→ 性别(m) → 落地"世外桃源",水笙/狄云 NPC 均在场,水笙主动提示注册 email → look 正常显示房间描述 → quit 干净退出("欢迎下次再来!"),全程 无未捕获错误。踩到一个纯测试脚本层面的坑,记录以免下次误判为 bug:这个 lineage 的游戏内提示符自带每秒刷新的实时时钟 (19:43:47> 这种),wasm_client.js 的空闲检测是"距离上一次驱动 输出满 N 秒才发送下一条指令"——只要 --idle 设得比 1 秒长,输出 永远赶在空闲阈值之前刷新一次,后续指令就会一直排队发不出去,直到 外层 --timeout 超时为止(观察到的现象是:注册正常走完、进入 游戏、然后卡住不再有任何指令生效)。换成 --idle 0.3(小于时钟 刷新间隔)后 look/quit 才能正常送达并看到回显。wasm_status 设为 playable。

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

scripts/lpc_fix_negative_ranges.py: 4 line(s) in 1 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-06)— 编译警告(机械部分)

scripts/lpc_warnings.py revive --fix:删 62 处 #pragma save_binary,15 个 nosave 函数改 protected,删 435 个未用局部变量,补 9 处 varargs。lpc_audit_removed_locals.py 复核 558 个,提示 1 个(adm/daemons/newsd.lpc 的 int time,后面读的是外部函数 time(),不是这个变量)。改动的 216 个 .lpc 在只含被跟踪文件的树里 HEAD 与新版各编一次:都是 201 个通过,无差异。启动警告 105 → 2,错误 0(剩下 natured.lpc 的 lt[LT_MON]; 空语句,原档案如此)。实测 fluffos 登录、look(世外桃源,水笙、狄云在场)、i、quit 正常。本库未经 §9 格式化。

深度功能测试(§10.7,2026-10-06)— 与 zhonghua2 同源的三处编译警告

验证:改动的文件和包含 npc/indian 的守护进程在 HEAD 与工作树分别加载(scripts/lpc_listcheck.sh,新进程),无回退。

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

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

深度功能测试(§10.7,2026-10-11):“无副作用表达式”警告复查(批 3)

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

scripts/toolkit_fix.sh 一轮后只剩一处,手工清零;改动的对象在 HEAD 与工作树各载入一次,regress 0。

find_player() 只认在世界里的身体(2026-10-15,KB 07 §8.26)

simul_efun 的 filter_player() 加上 environment(ob):注册中途断线留下的、不在任何房间的身体不再被当成玩家,两个以上时 find_player() 不会再报 The player(<id>) has existed in the world.(wllz3 实测过的问题)。本库若注册流程不会留下带玩家 uid 的身体,这一改动不改变行为。

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 载入不了,刷出它的房间也少一个人。本库目前没有对象设了缺失的武功;三处都改成 return:这一门武功不给、不映射,对象照常载入,只是少了它(缺的武功本身仍是内容缺口)。(scripts/lpc_guard_set_skill.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 文件。

member_array('/', str) 改为 strsrch(str, "/"):字符字面量的 member_array 按字节搜索(2026-10-20,KB 06 §7.283)

member_array('/', str) 里的 '/' 是整数,驱动按 UTF-8 字节搜索,返回字节偏移(带第三个参数时起点也是字节);最新的 FluffOS(923cdbc0)在字符串里有非 ASCII 文字时,对每个调用点警告一次(Warning: member_array(int, string) searches BYTES)。而 str[0..idx-1] 按字符切,中文字符串会在错误的位置截断。改为 strsrch(str, "/")(找不到同样返回 -1,按字符计数;scripts/lpc_fix_member_array_char.py,JSON 编码器等少数几处手改)。改动:/adm/daemons/vrm_server.lpc(1 处)、/cmds/arch/sameip.lpc(1 处)。

登录密码超过 255 字节:crypt() 抛错(2026-10-20,KB 06 §7.294)

驱动的 crypt() 对超过 255 字节的密钥直接报错(Error in crypt(), check your salt),老驱动则按任意长度散列。在登录提示符下输入 256 字节以上的密码,密码检查那一行就抛错,连线停在一个裸的 > 提示符上,登录走不完(password 指令的旧密码检查同样);用 300 个 x 当参数的扫描发现的。/adm/single/simul_efun.lpc 里补了一个同名的 simul_efun crypt(key, salt):只有超长的密钥才被截短(先截到 255 个字符,再逐字符截到 sizeof(to_buffer(key)) <= 255,限制按字节算,一个汉字占三个字节),再交给 efun::crypt();普通密码原样散列,已存的密码散列不受影响(scripts/lpc_add_sefun.py crypt)。

get_config(__MUD_PORT__) 经旧版 runtime_config.h 得到字符串(2026-10-20,KB 06 §7.302)

本库的 include/runtime_config.h 是老 MudOS 的编号(整数设置从 15 起,本驱动从 256 起),get_config(__MUD_PORT__) 读到的是另一项设置(字符串),(int) 又只是编译期标注,于是 socket_bind(fd, LOCAL_PORT()) 报 Bad argument 2 to socket_bind(),版本守护进程 port = get_config(__MUD_PORT__) + VERSION_PORT 是字符串相加。头文件自己的注释写明 get_config(__MUD_PORT__) == __PORT__,所以代码里的这个调用(连同外面的 (int) / to_int())改成了驱动的预定义 __PORT__(scripts/lpc_fix_mud_port.py)。改动:/adm/daemons/cruised.lpc(1 处)、/adm/daemons/v2.lpc(1 处)、/adm/daemons/versiond.lpc(1 处)、/include/getconfig.h(1 处)。

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) ||)。改动:/inherit/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() 都找不到它,调用方多半把失败当成「没有这个东西」悄悄跳过。本库 1 个存档共 1 处按 scripts/lpc_fix_save_dotc.py 改成 .lpc(只改 .lpc 存在而 .c 不存在的字符串;玩家存档、备份、留言板、巫师目录和文件索引缓存不动)。改动:/data/dbased.o(1 处)(共 1 个存档)。

注册到不存在函数的 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.,同一个动词的其他处理函数不再尝试(不是最新的一条就悄悄跳过)。删除了 4 条注册(3 个文件,scripts/lpc_fix_dead_add_action.py),动词留给别的处理函数:/adm/npc/referee.lpc(do_halt)、/clone/megazine/obj/maikaren.lpc(do_drop)、/inherit/misc/fboard.lpc(do_list)。最多的函数名:do_halt 2 处、do_drop 1 处、do_list 1 处。

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

这个库的代码往 log/ 的 1 个子目录写日志:log/user(adm/daemons/examined.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/user 记进 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] == "澄" 等 4 处(2026-10-22,KB 06 §7.360)

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

本库 4 处(2 个文件):与汉字常量比较 3 处、to_chinese()[..] 1 处。例:chk[0..1] == "你" → chk[0..0] == "你"(/adm/npc/ganjiang.lpc:592);to_chinese(martial)[4..7] → to_chinese(martial)[2..3](/cmds/skill/perform.lpc:131)。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,不要重跑。

长度检查按字节写,旁边的提示按汉字写:2 处(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 你好吗)。本库 2 处(1 个文件):if ((strlen(name) < 4) || (strlen(name) > 12 )) {(/adm/npc/luban.lpc:1927)。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)。本库 channeld 同盟 / 门派标题 1 个文件。证明(scripts/lpcc_eval.py):sjplii 的 check_legal_name 在 HEAD 接受「张a」、改后拒绝,两边都接受「张三」;xkx100 的 clubpoem->mixup("离离原上草") 得到「离离原草上」(真交换汉字);nitan_san 两字同盟名得到 【 兄 弟 】。关键对象加载通过。

血条 / 表情标题按字节算显示宽度(2026-10-23,KB 06 §7.365)

老档把汉字当两字节:bar_string[0..(val * N / max) * 2 - 1] 把「格数」换成字节下标,半格血条在本驱动(按字符算区间)上会整条画满;巫师表情 edemote 的 repeat_string("─", 13 - (strlen(emotion) / 2)) 也会少减汉字宽度。scripts/lpc_fix_display_pad.py:去掉 CJK 血条 / 空白条上的 * 2(jings/qis 赋值和 blank_string3[0..(23-a)] 的 23→11 一并改),表情标题去掉 / 2;ASCII 空格的 none_string / empty_string 仍保留 * 2(一格两个空格)。本库 2 处(1 个文件)。证明(lpcc_eval.py hhsj):tribar_graph(6,9,12) 在 HEAD 画出整条 12 个■,改后为 6 个■+3 个□;麻将 strlen(tempB)/2 是两字符牌码,未改。

中文名 / 验证码仍按 GBK 取「两字节一字」(2026-10-23,KB 06 §7.373)

pname = pname[(random(sizeof(pname)) & 0xFFFE)..<1]; pname = pname[0..1]; 在按字符计下标时会取出两个汉字。scripts/lpc_fix_pname_slice.py 去掉掩码并把收尾改成 [0..0](pname/pname2/sec_name/str/str1)。本库 4 处。证明:nt1 生成名长度 2–4;HEAD 常见 3–5。

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

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