Three Realms Myth (Test Zone 2)

✅ 可玩

西游记2003-2

xyj20032

🔑 fluffos(密码见 README) 更新 13eedd7 2026-09-04 源码 下载 ZIP

▶ 开始游玩 · Play Now

档案名叫"西游记2003-2",但游戏本身开机后自报家门是三界神话(SanJie Myth)"测试二区"——和 `sjcs`、`sanjieshenhua` 是同一套"三界"代码库家族的另一个成员(`get_gender()`/`enter_world()`/`make_body()` 等核心函式和 `sanjieshenhua` 逐字比对完全一致),开机横幅还留着一段"西游记之新纪元"的 ASCII 字画;这层西游记渊源不只是残留美术:字节级比对显示,这份档案约 99.5% 的房间/NPC 文件路径与本合集 `xyj2000`、`xyj2006n` 的西游记地图一一对应(例如小和尚 NPC 疥顶小僧、泾水桥场景在几份档案里仍逐字相同),只是同路径下多数文件内容已被现任维护者大幅改写、并换上了与之完全无关的"三界"引擎,说明这份档案曾以同一份西游记地图为起点,后来才改名换皮;游戏内还保留着"天地劫"全服事件广播系统(`disasterd.lpc`),会向所有在线玩家推送天灾/异变类事件,不只是常规聊天频道。

English

Archived under the name "Journey to the West 2003-2," but the game itself identifies at login as "Three Realms Myth," Test Zone 2 — the same codebase family as this archive's sjcs and sanjieshenhua (core functions such as get_gender(), enter_world(), and make_body() are byte-for-byte identical to sanjieshenhua). Its boot banner still carries leftover ASCII art from "Journey to the West: New Era," apparently a remnant of an earlier rebrand -- and that remnant runs deeper than cosmetics: a file-level check this pass found roughly 99.5% of this archive's room/NPC file paths under d/ match, one-for-one, the map shipped in this collection's own xyj2000/xyj2006n (e.g. the young monk NPC 疥顶小僧 and the bridge scene 泾水桥 are still word-for-word identical), even though the live engine is the unrelated Three-Realms codebase. Only a minority of those shared-path files remain byte-identical, so this archive appears to have started from the same Xiyouji map before its current maintainers heavily re-edited most rooms and swapped in the Three Realms engine. It keeps a distinctive server-wide "Heaven-and-Earth Calamity" event channel (disasterd.lpc) broadcasting rare disaster/anomaly events to every connected player, beyond ordinary chat.

README

内容亮点

本次修复的关键 bug

这份档案一共踩中 4 个 bug,后两个完全没有任何可见的错误讯息,是靠在 get_gender()make_body()new(USER_OB) 这条链路上逐步插入 write() 断点,一步步二分定位出来的:

1. §7.61 message() 精灵的 exclude 参数默认值 bugadm/simul_efun/message.lpcmessage() 转接函式只传 3 个参数 呼叫 efun::message(),第 4 个 exclude 参数落空成裸的整数 0, adm/daemons/disasterd.lpcannounce()(天地劫系统事件广播) 等多处呼叫点因此崩溃报"Bad argument 4 to EFUN message()"。改成 exclude || ({})。 2. §7.60 log_error()CHANNEL_D 还没预加载时呼叫它adm/obj/master.lpclog_error()每一条编译警告(包括 完全无害的"Unknown #pragma, ignored")都会呼叫 CHANNEL_D->do_channel();开机时最早被预加载的几个档案编译产生警 告的那一刻,CHANNEL_D 自己还没编译完成,这个跨物件呼叫会在"正在 编译中"的状态下触发一次新的编译,被驱动拒绝并报"Object cannot be loaded during compilation"——然后这个错误本身又被 log_error() 再 记录一次,如此循环,刷出成千上万行重复的错误堆叠。用 find_object(CHANNEL_D) 判断守卫。 3. channeld.lpcdo_channel()environment(me) 没做保护: 上一条 bug 修好之前,log_error()CHANNEL_D 广播会把 master 物件本身当作"me"传进去——master 物件没有 environment, environment(me)->query("no_chat") 直接对 0 取属性崩溃。加上 environment(me) && 判断。 4. 最深的一个 bug——master.lpcvalid_read() 拒绝驱动自身发起 的编译请求:新玩家选完性别后,get_gender() 呼叫 make_body()make_body() 呼叫 new(USER_OB) 第一次编译完整的 玩家身体类别(std/char/char.lpc 一大串 F_* mixin)。驱动内部 的 load_object() 会先呼叫 master_ob->valid_read(...) 做权限检 查,这次呼叫把 master 物件自己当作"user"参数传进去;这份档案 的 master.lpcvalid_read() 原样把这个 user 转呼叫给 securityd.lpcvalid_read(),没有对"系统自身发起的加载"做任 何特殊处理,而 geteuid(master_ob) 在这个时机点算出来是空字串, 权限判断逻辑因此判定"拒绝读取"——new() 静静地返回 0, make_body() 理论上该走的失败分支 write() 讯息由于目标是还没 exec() 过的登入物件,实际上从未真正显示出来。结果是每一次新 角色注册在选完性别后都会无声无息地卡死,之后所有输入都变成"什 么?",没有任何报错、没有任何提示。修法是在 master.lpcvalid_read() 里加一条 if (user == this_object()) return 1;, 放在转呼叫 SECURITY_D 之前。 5. 经典 §8.1 GBK 字节区间 bugadm/daemons/chinesed.lpcis_chinese()(按字节区间+奇偶校验判断)和 adm/daemons/ logind.lpccheck_legal_name()(同样的奇偶校验假设)都改成 逐码点检查(0x4e00–0x9fff)——这也是为什么"小浮侠"这样三个字的名 字一开始会被拒绝的原因。

深度功能测试(§10.7)修复的 bug

已知但非 bug 的测试摩擦:注册流程的 call_out(0) 竞态

角色创建完成后,天赋点数分配菜单是靠 d/wiz/init.lpcinit() 里一个 call_out("get_start0", 0, me) 延迟到下一个 tick 才触发的; 玩家身体类别(std/char/char.lpc)第一次编译的负担很重,会和测试用 客户端按固定 idle 秒数发送后续输入的节奏抢跑,导致偶尔卡在性别选择 之后。这和本次 WASM 通关测试里 xhcii/xkyxciii/xsfyssjb 已经记 录过的时序竞态是同一类问题,不是 mudlib 的 bug——已经用一次完全干净 的通关记录(从性别选择一路到 look/score 全程零报错,天地劫系统 事件正常广播、自动存盘正常触发)确认核心逻辑没有问题。

管理员账号 / Admin account

警告:对外公开架设前请务必修改此密码。

本地运行

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

游戏端口:40119

NOTES · 移植与修复记录

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

压缩包名叫"西游记2003-2",但游戏本身自报家门是三界神话(SanJie Myth)"测试二区"——是同一个三界代码库家族的另一个成员,和 059 号 sjcs、060 号 sanjieshenhua 属于同一血统(get_gender()/enter_world()/make_body() 函式体和 sanjieshenhua 逐字节对照一致,已通过直接 diff 确认),残留着一段早期改名之前留下的西游记题材开场 ASCII 横幅。WASM 修复了 4 个 bug,最后两个是靠对 write() 做定点插桩、逐步二分排查才找到的,因为它们完全静默失败(连错误堆栈都没有):(1)adm/simul_efun/message.lpc 的 message() simul_efun 包装函式,把裸的整数 0 当作 3 参数版本 exclude 的默认值直接传给 efun::message(),导致多处呼叫点(包括 disasterd.lpc 的 announce())报"Bad argument 4 to EFUN message()"崩溃——经典的 AGENTS.md §7.61 bug,用 'exclude || ({})' 修复。(2)adm/obj/master.lpc 的 log_error() 对每一条编译警告(包括无害的)都无条件呼叫 CHANNEL_D->do_channel(),而由于 CHANNEL_D 在开机最初编译那批档案时还没被预加载,这次跨物件呼叫会在编译过程中递归触发一次新的编译,导致"Object cannot be loaded during compilation"崩溃,并连锁引发一大片重复的堆栈转储——经典的 AGENTS.md §7.60 bug,已加上 find_object(CHANNEL_D) 前置判断修复。(3)adm/daemons/channeld.lpc 的 do_channel() 未加保护地呼叫了 environment(me)->query('no_chat');log_error() 的 CHANNEL_D 广播(bug 2,修复之前)会把 master 物件本身当作 'me' 传进去,而它没有 environment,导致对 0 目标呼叫崩溃——已用 'environment(me) && environment(me)->query(...)' 修复。(4)最深的一个 bug,靠对 get_gender()->make_body()->new(USER_OB) 每一步都插入 write() 定点排查才找到:驱动自身的 load_object() 安全检查(在 new() 首次编译玩家角色类的过程中被内部触发)会以 master 物件本身作为 'user' 参数呼叫 master.lpc 的 valid_read();master.lpc 的 valid_read() 只是直接转发给 securityd.lpc 的 valid_read(),没有对系统发起的加载做特殊处理,而 geteuid(master_ob) 在那里解析出的是一个假值 euid,于是路径权限逻辑拒绝了这次读取(完全静默——没有任何错误信息,因为驱动的"Read access denied"只是让 new() 回传 0,而 make_body() 自己的失败路径 write() 从来没触发,因为写入目标——那个尚未执行的登录物件——产生的信息不做定点插桩根本不会被注意到)——每一次新玩家注册都会在选性别这一步悄无声息地死掉,除了后续所有输入都回显"什么?"之外没有任何可见症状。已通过给 master.lpc 的 valid_read() 短路修复(如果这类 bug 再次出现,valid_write() 也应该配上同样的保护):在转发给 SECURITY_D 之前先加一句 'if (user == this_object()) return 1;'。另外还修复了 adm/daemons/chinesed.lpc 的 is_chinese()(基于字节区间/奇偶判断)和 adm/daemons/logind.lpc 的 check_legal_name()(同样的奇偶假设)里标准的 §8.1 GBK 字节区间 bug,都重写成逐码点检查——这正是修复之前"小浮侠"过不了中文名字校验的原因。管理员账号播种:fluffos (admin) 加入 adm/etc/wizlist(securityd.lpc 真的会在开机时读取 WIZLIST)。注册流程在多次连续的 WASM 客户端会话里完整验证过,因为一个由 call_out(0) 驱动的注册后天赋赠礼菜单会和测试工具基于 idle 的发送节奏产生竞争(首次玩家角色编译的大量负荷,符合本次会话已经记载过的 xhcii/xkyxciii/xsfyssjb WASM 时序竞争模式,不是 mudlib 本身的 bug——一次完全干净的运行确认能到达 look/score 且零错误):GB/BIG5 选择→这个手足档案没有未成年人门槛→new→英文 id→管理员密码+确认→普通密码+确认→中文名字→电子邮件→性别(m/f)→属性分配菜单(9 接受默认值,y 确认)→静默进入 /d/wiz/init 的游戏世界→实时的灾难事件频道广播和自动存档都正常触发(证明 message()/CHANNEL_D 的修复在真实游戏流程下依然有效)→look/score 都干净。LPC 格式化工具对全部 12362 个档案运行(写入 12194 个,36 个报错——转档之前就存在的未结束字符串/文本块内容 bug,和格式化工具无关,132 个未改动)。没有 :: 父类呼叫拆分命中,没有 CJK 重新加空格命中;case 标签带尾随注释的盲点找到了好几处匹配行(ftpd.lpc/natured.lpc/esman.lpc/vi.lpc),但逐一 diff 复核确认格式化工具在这次运行里正确保留了后面的每一条语句(没有吞掉代码)。追加修复:scripts/scan_known_bugs.py(新写的静态扫描工具)标记出 15 处 is_killing(me) 呼叫点(§7.50,feature/attack.lpc 声明的是 is_killing(string id)),分布在各个 kungfu 技能的 daemon/class/*.lpc 档案和 cmds/std/surrender.lpc 里——已改成 is_killing(me->query("id"));第 16 处命中在 daemon/class/pansi/chixin-jian/MIE.C 里是在 // 注释内,保持原样。当前真正生效的 adm/daemons/logind.lpc 的 check_legal_name()/master.lpc 的 valid_read() 在最初那一轮就已经修好了;只有 u/mudring/ 和 u/kuku/ 里两份从未被 LOGIN_D 加载的死代码副本在扫描里还显示奇偶门槛模式(未修复)。修复后重新验证干净。

深度功能测试(§10.7,2026-08-05)

§7.100 跨库扫描修复(ROOM 基类同款 replace_program() 致命形状)

§7.30 uninitialized-mapping accessor sweep (2026-08-20)

Corpus-wide mechanical sweep of the feature/skill.lpc shared-lineage bug (confirmed independently on xiakexing2017/jqxz2015/haiyang2 via round-four testing): 3 accessor(s) in this file returned a raw never-initialized mapping instance variable (defaults to int 0, not ([]), until first assigned), crashing any unguarded keys()/sizeof()/indexing caller for a fresh/untrained character. Fixed at the accessor level (mapp(x) ? x : ([])) per the documented remedy. Verified via lpcc --batch static compile check only (not a live boot) as part of a large mechanical sweep; not individually functionally re-tested live on this lib.

AGENTS.md §7.19 sweep (2026-09-01): enable_player() reentrancy from init()

Same corpus-wide bug class as mhxy/wuhanzhan (AGENTS.md §7.19): this lib's feature/command.lpc enable_player() wrapper (around the raw enable_commands() efun) is reachable from an NPC's init() via a redundant create()-then-init()-calls-setup() (or reset_me() calling setup()) chain -- confirmed live via a static scan of every init() body in this lib: 73 NPC/item files call setup() directly or via reset_me() from init(), after create() already called setup() once (which already made the object living()). Calling enable_commands() a second time on an already-living() object makes the driver re-invoke that object's own init() as a side effect, which re-enters this same chain while the original call is still on the stack -- genuine reentrancy, crashing with "Too deep recursion" (most likely to surface on an NPC's first-ever preload/compile).

same as sanjieshenhua/sjshv2578bb: feature/damage.lpc's revive() call is dead/commented, but d/qujing/qujingren/qujingren.lpc's wakeup() calls me->enable_player() on a still-living() disabled object. This confirms a bare if (living(this_object())) return; guard would be the WRONG fix (it would silently break that legitimate re-enable) -- used the same true reentrancy-flag fix as mhxy instead: a nosave private int in_enable_player_now; set for the duration of the wrapper's body, guarding only genuine same-call-stack reentrancy while leaving every legitimate re-enable (revive/wakeup/disguise) unaffected. feature/command.lpc's enable_player() had a single fall-through exit (no early returns), so one guard-at-top + one clear-at-bottom pair was sufficient. Verified via a single-file lpcc --batch compile check (PASS) -- not individually live-boot-tested.

深度功能测试(2026-09-03,第三轮)

新角度:2026-08-05 第一轮明确跳过的商店 / 五庄观拜师。死亡/复活与 留言板不再复测。