Stray Book and Sword: Official Tutorial Edition

✅ 可玩

书剑飘零官方教学版

sjplgfjxb

🔑 fluffos / Mud@2026 更新 4d2d477 2026-09-04 源码 下载 ZIP

▶ 开始游玩 · Play Now

ES II 引擎家族(`adm/obj/master.lpc`,"original from Lil, rewritten by Annihilator"),飞白工作室出品的《书剑飘零》官方教学版,是同一批"书剑飘零"基础档案的精简子集——与同门 `sjplii`(书剑飘零II)同源,也和另一份档案 `sjpl2` 共享底子:约 2,491/2,550 个档案与 `sjpl2` 约 13,000 档案的路径重合,并非另起炉灶的独立作品;虽然名字里也带"书剑",但和 Century/adm-single 家族的 `sjecl`/`sje` 属于完全不同的引擎谱系,纯属巧合。新角色的出生地由角色创建时选择的"出生状况"(书香门第/商贾之家/贫寒农家/武力世家)决定,分别落在山东泰安或福州的一户普通民居,而非长安城;长安城的"大慈恩寺"只在角色已保存的出生点失效时才作为兜底,长安城本身仍以唐代真实地标为骨架展开(开远门、朱雀门、大慈恩寺、保定殿等),是游戏地图的另一大可探索区域。除了常见的拜师门派体系,还提供一条独立的"镖师"生涯:加入"红旗镖局"(含帐房、佛堂、武器库等一整套镖局建筑)只需申请即可上任,不必事先拜师,随时可退出;武学招式多为原创命名("醉棍""风刃""无尘步"等),不是直接照搬金庸小说里的招式名称,自成一套武侠世界观。巫师账号会遇到一处刻意保留的"吵闹"设计:新角色第一次创建时因为多个档案首次编译,会看到一串"你发现事情不大对了"提示——都只是无害的编译警告,不影响游戏本身。

English

The official tutorial edition of Feibai Studio's Stray Book and Sword, built on the ES II engine (adm/obj/master.lpc, "original from Lil, rewritten by Annihilator"). It is a trimmed-down subset of the same base archive as siblings sjplii and sjpl2 (Stray Book and Sword II) — 2,491 of its ~2,550 files share a path with sjpl2's own ~13,000-file tree — rather than an independently-written game; despite also carrying the words "Book and Sword," it belongs to an entirely different engine lineage from the Century/adm-single family's sjecl and sje, a naming coincidence and nothing more. A character's "birth circumstance" (scholarly/merchant/poor-farmer/martial family) determines their actual starting home — a commoner household in Tai'an, Shandong, or in Fuzhou, not Chang'an; Chang'an's Great Compassion Temple only serves as a fallback if a saved start room fails to load. Chang'an itself is still modeled on real Tang-dynasty landmarks (its gates use real names — Kaiyuan, Zhuque, Qixia, Yanping, Tonghua, Yanxing — alongside the Great Compassion Temple and Baodian Hall) and forms a second major explorable region. Beyond the usual sect apprenticeships, there's an independent career track with the Red Flag Escort Agency (complete with counting house, chapel, and armory), joinable and leaveable on request without a master, and move names are largely original rather than lifted straight from Jin Yong's novels. One quirk that looks like a bug but isn't: every compile warning is broadcast live to online players, so a brand-new character's first `look` can trigger a burst of "something feels off" messages — all harmless "unused local variable" warnings from first-time file compiles.

README

本次修复的关键 bug

1. master.lpcreport_error()CHANNEL_D 尚未加载时呼叫 它(§7.60 类的第三个变体——这份档案里 log_error()/ standard_trace()/report_error() 是三个各自独立的函数,只有 report_error() 缺了保护):补上 find_object(CHANNEL_D) 判断。 2. adm/daemons/whod.lpc 用了未定义的 REMOTE_DIR 常量,而这 个精灵在 preload 列表里,直接导致它编译失败。硬盘上没有对应的目 录可以推断原意,但 get_dir() 对不存在的目录只会返回空数组,所 以在 globals.h 里补上 #define REMOTE_DIR "/data/remote/" 是 安全的(哪怕这个目录本身从未真正被创建)。 3. §7.41 类损坏的存档数据adm/daemons/emoted.lpccreate() 对自己损坏的 emoted.o 存档做了未加保护的 restore(),preload 时未捕获抛出——已包一层 catch(),并显式补 上 emote=([]) 兜底。 4. §7.34 类调试遗留adm/daemons/logind.lpcget_resp()/ get_name()(角色创建流程中确认中文名字的两条并行路径)各留了 一行 printf("%O\n", ob),会在设定密码提示前把登录物件的内部路 径(/obj/login#N)原样打印给玩家看——已删除两行。 5. 一处编译期 ERRORd/fuzhou/npc/chess_player.lpc(棋摊老板 韦守儒)的 play_chess() 把继承自 feature/name.lpc 的本地方法 name(int raw) 当成"取得对方名字"的自由函数误用为 name(this_player())——传对象给一个只接受 int 的参数,编译直接 报错,导致这个 NPC 全程无法编译,福州"茶馆"(d/fuzhou/ tearoom2)填充该 NPC 时级联出 *No program in object 崩溃並反 复刷屏。已改为 this_player()->name(),顺带删掉同一行紧挨着的一 处无意义 printf 调试输出(§7.34 类)。 6. §7.86 类第三个变体obj/board/wizard_j.lpc(巫师工作进度报 告板)inherit "/std/jboard" 之后又多余 replace_program( "/std/jboard"),与已修复的 31 处 BULLETIN_BOARD/inherit 实例是同一 bug 形状,只是换了一个板类基类名字;/std/jboard.lpc 自己的 do_report()/do_describe_project() 也用 this_player()->edit((: lfun, ... :)) 建闭包,一样会崩。已删除 多余的 replace_program() 调用。

排查过程中确认"不是 bug"的现象

注册过程中反复出现"你发现事情不大对了,但是又说不上来。"——这是这 份档案自己(有点吵闹但故意如此)的设计:master.lpclog_error() 会把每一次编译警告都告诉当时正好连线中的玩家,而 新角色第一次创建时,其继承的各个 feature 档案(alias/damage/more/ move/skill/troop)恰好都是第一次编译。临时让 error_handler 无条 件显示完整细节后确认:每一条都只是无害的"Unused local variable"警 告。

管理员账号 / Admin account

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

本地运行

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

游戏端口:40134

NOTES · 移植与修复记录

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

ES II 血统(adm/obj/master.lpc,"original from Lil, rewritten by Annihilator"),飞白工作室《书剑飘零》教学版。WASM 修复:(1)§7.60 类的 master.lpc report_error()→CHANNEL_D 编译期崩溃(这是这个模式的第三个呼叫点,在常见的 log_error()/standard_trace() 之外——report_error() 在这里是它自己独立的函式),已用 find_object(CHANNEL_D) 守卫。(2)adm/daemons/whod.lpc 用了未定义的 REMOTE_DIR 常量,破坏了它(被预载)的编译——已在 globals.h 里加上 #define REMOTE_DIR "/data/remote/"(硬盘上没有对应目录可以推断原意,但 get_dir() 对不存在的目录只会返回空数组,所以这样做是安全的,哪怕这个目录本身从未真正被创建)。(3)§7.41 类损坏的存档数据:adm/daemons/emoted.lpc 的 create() 对自己损坏的 emoted.o 做了未加保护的 restore(),预载时抛出未被捕获的异常;已包一层 catch(restore()),并显式补上 emote=([]) 兜底。深入调查后排除了一个疑似 bug:注册过程中反复出现的"你发现事情不大对了"讯息是这份 mudlib 自己的(有点吵闹但故意如此的)设计——master.lpc 的 log_error() 会把当时正好连线中的玩家告知每一次编译警告,而新角色第一次创建时其继承的各个 feature 档案(alias/damage/more/move/skill/troop)恰好都是第一次编译;临时让 error_handler 无条件显示完整细节后确认,每一条都只是无害的"Unused local variable"警告。管理员账号播种:fluffos (admin) 加入 adm/etc/wizlist(这份档案没有 sj 里那种 securd/securityd 分裂——securityd.lpc 在这里是真正、唯一的安全精灵)。已验证:完整注册(id→确认→名字→密码→确认→电子邮件→性别→出生地选择)→look/score/quit 全部干净,权限正确显示 (admin),update 成功。LPC 格式化工具对全部 2310 个档案运行;还原了 1 个确认有损坏的档案(一种丢引号的损坏,不是常见的 CJK 重新加空格形态,但被同一个去空格比对扫描抓到),覆盖 17 个格式化工具触碰过的 CJK 间距档案;这份档案里没有 ASCII 地图档案。格式化后重新验证过,干净。

§7.86 跨库扫描修复(留言板 post 崩溃)

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

血统澄清

README.md 一直写"ES II 引擎家族",但 AGENTS.md §11 的"ES II / 东方故事 mega-family"成员列表(es1_win/esIxkx2001/bmxkx2001xuanjianlusyxjl 等)里并没有 sjplgfjxb/sjplii 这一支——本次没 有专门去 diff 核心档案确认这支到底是不是同一个 §11 家族的成员,只是确认 了 master.lpc 文件头注释"original from Lil, rewritten by Annihilator" 与 §11 家族描述的血统吻合。留给下次同族横向比对(尤其是 sjplii)时顺 手做一次真正的 diff 确认,而不是只看注释文本。

本次修复的 bug(均已在原生驱动上逐条改前/改后验证,见下)

1. §7.34 类调试遗留,adm/daemons/logind.lpc 两处get_resp() (第 273 行,接受随机中文名字路径)和 get_name()(第 308 行,自 己输入中文名字路径)各有一行 printf("%O\n", ob);,紧跟在中文名 字确认之后、密码提示之前,把登录物件的内部路径原样打印给玩家——例 如注册时输入中文名"云中鹤"后,屏幕上会多出一行 /obj/login#0。 两条路径都命中(对应"接受系统建议的随机名字"和"自己打字取名"两条 分支),已各删一行。改前:register→...→您的中文名字:云中鹤 后 紧接着输出 /obj/login#0,然后才是"请设定您的密码:";改后:中 文名字确认后直接跳到"请设定您的密码:",无任何内部路径泄漏,用一 个全新注册的 sjplcheck(中文名"钱塘潮")账号验证过。 2. 一处真正的编译期 error(不只是 warning)d/fuzhou/npc/chess_player.lpc(棋摊老板"韦守儒",第 39-40 行) play_chess()printf("%s", name(this_player()));command("give chess to " + name(this_player())); 把继承自 feature/name.lpc 的本地方法 varargs string name(int raw) (返回*自己*的名字/显示名,参数是"要不要去掉头衔"的整数开关)当 成"取得*对方*名字"的自由函数误用,传了一个 object 进去。驱动的 静态类型检查在裸调用(非 ->)上直接拒绝,报错原文: d/fuzhou/npc/chess_player.lpc:39:36: error: Bad type for argument 1 of name ( int vs object )(第 40 行同样报错)。这两行 error (不是 warning)导致这个 NPC 档案全程无法编译——福州"茶馆" (d/fuzhou/tearoom2)第一次填充这个 NPC 时,master.lpcreport_error()/log_error() 链会级联把 *No program in object '/d/fuzhou/npc/chess_player'! 连同一份 完整的物件状态 dump 反复转发给当时在线的每一个人,刷屏严重。修 复:printf 那行本身也是一处无意义的调试输出(§7.34 同款,删 除),command("give chess to " + name(this_player())); 改为 command("give chess to " + this_player()->name());。改前: update /d/fuzhou/npc/chess_player 报上面两行 error;改后: 重新编译 /d/fuzhou/npc/chess_player.lpc:成功!debug.log 里此后再没有 chess_player 相关的 No program in object。 3. §7.86 类第三个变体(新基类名字)obj/board/wizard_j.lpc (巫师"工作进度报告"留言板,/d/wiz/jobroominherit "/std/jboard"; 之后,create() 尾巴又多余调用了 replace_program("/std/jboard");——和已经扫描修复过的 31 处 BULLETIN_BOARD/BBS_BOARD 实例是同一个致命形状,只是这份档案 的留言板基类不叫 BULLETIN_BOARD 而是 /std/jboard.lpc(它自己 的 do_report()/do_describe_project() 也是 this_player()->edit((: lfun, ... :)) 建闭包,同样会撞上"cannot bind an lfun fp to an object with a pending replace_program()")。 已删除多余的 replace_program()。改前:编译时确认过 wizard_j.lpc/wizard_bb.lpc 与其余 31 处一样带着这个形状(当 时只做过编译检查,没做过 post 的实机验证);改后:以 fluffos 管理员账号在 /d/wiz/hall(普通 BULLETIN_BOARD 板,编号已在此 前的批量修复里处理过)post test title → 输入正文 → . 结束, 留言完毕。look board 正确显示 [ 1] test title 云中鹤 (Sat Aug 8 05:05)update /obj/board/wizard_j 单独重新编译也确认无报错(只有两条与本次修 改无关的 Unused local variable 'myid' warning)。检测方法grep -rn 'replace_program' work --include='*.lpc',对每个命中 反查它前面 inherit 的是哪个基类,而不是只搜 BULLETIN_BOARD/BBS_BOARD 这两个最常见的名字。

排查过程中确认"不是 bug"的现象

已验证正常工作(原生驱动,fluffos/Mud@2026 管理员账号 + 两个

一次性测试角色"赵日天"/"钱塘潮")

死亡/复活流程:仅代码走读,未能在预算内实机触发(诚实标注为未验

证)

std/char.lpcheart_beat() 只有当 eff_kee/eff_sen/eff_gin (而不是 kee/sen/gin)跌破 0,或 kee/sen/gin 跌破 -10*dur 时才会调用真正的 die();否则只会反复调用 unconcious()(见上一节验证过的"半昏迷→自动苏醒"循环)。本次尝试过 两种方式让测试角色真正死亡:

1. 管理员账号 fluffoscall 指令把自己的 kee/gin/sen 直 接设成 1、并把 env/wimpy 设成 0(关闭自动逃跑)后继续和大胖子 互殴——结果只反复触发 unconcious()(因为 eff_kee 等派生字段 没有同步跌破 0),且 disable_commands() 在昏迷期间连 call 指 令本身都被封锁,没能在昏迷的间隙里补一刀把 eff_kee 也设成负 数。 2. 另注册的一次性非管理员角色"赵日天"(普通玩家权限)没有 call 指令的路径权限(不在 PLR_PATH 搜索范围内),且附近能找到的两个 可攻击目标(大胖子 combat_exp 5、"卧龙岗强盗" d/fuzhou/npc/ gangster.lpc 未被任何房间引用、是死内容)双方战力都太弱,几分钟 的持续搏斗里角色和大胖子交替进入又走出"半昏迷"状态,始终没有真 正累积到 eff_kee<0 的门槛。

在预算内没有进一步升级测试(例如去几个路程较远、combat_exp 高得多的 真正杀气 NPC,如 d/city/npc/bing.lpc,combat_exp 30000,在长安城城 门一带)。改为代码走读死亡序列:feature/damage.lpc::die()userp() 角色的 gin/kee/sen 都设成 1、ghost=1、存档后 move(DEATH_ROOM)/d/death/gate,鬼门关),DEATH_ROOM->start_ death() 之后由 d/death/npc/wgargoyle.lpc(白无常)的 init() 排 定 call_out("death_stage", 5, ...),五阶段对话后 reincarnate() + move() 到复活地点。两点值得记录、供下次(尤其是 sjplii)复用

管理员账号播种

adm/etc/wizlist 里此前已经有 fluffos (admin) 一行,但账号本身 (data/{login,user}/f/fluffos/)在这次会话之前从未被真正注册/提 交过。本次走标准注册流程创建(id fluffos,密码 Mud@2026),登 录后 目前权限:(admin) 正确显示,update /d/fuzhou/npc/chess_ playerupdate /obj/board/wizard_j 两次写权限验证都成功(§1.5 第 3 步的标准检查)。

§7.100 sweep (2026-08-19)

Fixed the corpus-wide inherit ROOM; ... replace_program(ROOM); redundant-replace bug (AGENTS.md §7.100). 228 live occurrences deleted: 227 via scripted sweep (fix_710_room.py), plus 1 hand-fixed roommaker-tool template (obj/roommaker.lpc, simple string-builder variant). 23 already-commented-out instances left untouched. No real .lpc source found under work/data/. Verified via build-debug driver boot: clean compile, zero new "cannot replace"/"cannot bind" debug.log lines; confirmed serving via raw-socket connect on port 40134.

§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()/reset_me() chain -- confirmed via a body-aware static scan of every init() in this lib: 15 NPC files call setup()/reset_me() from init() (e.g. npc/wu43.lpc, npc/wu33.lpc, npc/wu11.lpc), after create() 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, re-entering the same chain while the original call is still on the stack -- genuine reentrancy, crashing with "Too deep recursion" (most likely on an NPC's first-ever preload/compile).

feature/damage.lpc's revive() calls enable_player() again while the object is still living(). (cmds/std/sleep.lpc's wakeup() has its own me->enable_player(); call commented out/dead in this lib, so revive() alone is the confirmed legitimate re-enable path here.) This confirms a bare if (living(this_object())) return; guard would be the WRONG fix -- 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 the legitimate revive re-enable unaffected. 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-08 第一轮已经走完注册、战斗、留言板;死亡/复活当时 只做了代码走读。本轮补商店,并顺带实机确认红旗镖局拜师已经写入存档。