Discworld MUD (FluffOS bundle, v3)

✅ 可玩

Discworld

discworld

更新 1309d15 2026-10-22 源码 下载 ZIP

▶ 开始游玩 · Play Now

The official Discworld MUD's mudlib, as redistributed by Cratylus (Dead Souls' maintainer) in a self-contained "Discworld Bundle" paired with a matching FluffOS build (dw_fluffos_v3 is the latest of three snapshots this project holds; v1/v2 are independently converted as libs/dw_fluffos_v1 and libs/dw_fluffos_v2). One of the longest-running, most-studied LPMuds in existence (running continuously since 1991), set in Terry Pratchett's Discworld. New characters start in the famous circular Discworld Room in Ankh-Morpork's Cabbage Warehouse Rooming House, guided in by a wandering "womble" NPC. Uses a directory-based command dispatcher (a verb like `look` resolves to a file `cmds/living/l_ook.lpc`, the underscore marking its minimum abbreviation) layered under a `drunk_check()`/`lower_check()` eval-cost and command-queueing throttle.

README

The official Discworld MUD mudlib, set in Terry Pratchett's Discworld and running continuously since 1991 -- one of the longest-lived, most widely studied LPMuds in existence. This copy comes from Cratylus's (the Dead Souls maintainer's) unofficial "Discworld Bundle" v3 redistribution, which pairs the mudlib with a matching FluffOS build (we use this project's own driver instead, per usual). This project also holds independently converted v1 and v2 snapshots of the same bundle (libs/dw_fluffos_v1/ port 40271, libs/dw_fluffos_v2/ port 40272).

Highlights

Registration flow

N (new character) -> character name -> confirm (y/n) -> password -> confirm password -> capitalisation (accept the default with Enter, or type your own) -> gender (male/female) -> a Terms and Conditions screen, which the server deliberately pauses ~30 seconds before showing the final yes/no prompt for (a real server-side call_out, not a client hang -- just wait it out) -> yes -> you arrive in the Discworld Room.

Admin account

Registered through the normal registration flow like any other character. Discworld's admin model layers a hardcoded TRUSTEES macro (in secure/master.lpc, containing Root and the original archive's cratylus) on top of a real, persisted positions mapping saved in secure/master.o -- and since granting trustee rank normally requires already *being* a trustee (add_trustee() checks the caller), the bootstrap grant was seeded directly into that save file: positions (["fluffos":2,]) (2 is master.lpc's own TRUSTEE constant). Verified live: wizard-only login banners appear, and a wizard-level recompile command (compile <path>) passes its ACL check cleanly (it hits an unrelated pre-existing bug further in, documented in NOTES.md, not a permission denial).

Status

Boots clean (zero compile errors in debug.log). Fresh registration, look, score, and quit all verified end-to-end with a real driver session, including the initial automatic room description on arrival and a full server-driven quit sequence. Re-login (the restore path) also verified. See NOTES.md for the full list of driver-compatibility bugs found and fixed during this port -- the headline one: this codebase's add_action(fn, "*", ...) catch-all convention doesn't exist on this driver (the catch-all verb here is ""), so every single command silently failed until that was fixed across five call sites, alongside a separate [0..<3] fixed-width extension-stripping bug in secure/command.lpc that broke the directory-based command dispatcher the same way. A few genuine content gaps in this third-party archive (a missing /std/outside.lpc base class, missing /obj/armours/ and /obj/clothes/ content directories, no MySQL server available for the map/pathfinding subsystem) are documented in NOTES.md rather than invented or patched over.

WASM status: playable. Full registration through to the Discworld circular hall is verified under the shared WASM driver (NOTES.md WASM section). Play: https://mudlibs.fluffos.info/discworld/

NOTES · 移植与修复记录

概述

discworld(901)是官方 Discworld MUD 的 mudlib,本次转换自 archives/901_discworld_dw_fluffos_v3.zip(Cratylus 打包的 "Discworld Bundle" 第三版,随附一份匹配的 FluffOS 驱动源码——按 AGENTS.md §2 惯例忽略捆绑驱动,只用本项目自己的驱动)。这是本项目 第一个真正意义上的纯英文 mudlib(此前"deprioritize English libs" 的旧策略已被项目负责人撤销)。archives/ 里同时还有 v1 (901_dw_fluffos_v1_dw_fluffos_v1.tar.gz) 和 v2 (901-1_dw_fluffos_v2_dw_fluffos_v2.zip) 两份更早的快照,未发现 v3 有 v1/v2 没有的结构性问题,因此直接采用 v3 作为正式转换对象; v1/v2 已按用户指示单独转成 libs/dw_fluffos_v{1,2}/(编号 901-1/901-2,端口 40271/40272),不是这份 v3 work/ 的静默 overlay。 主编号 901 仍是这份真正转换的 libs/discworld/。

编码

英文 mudlib,GB18030→UTF-8 转档步骤基本不适用(convert_lib.sh 的 "already_utf8" 分支覆盖了绝大多数文件)。但发现并修复了三个真正的 编码坑,跟 AGENTS.md §4.1 描述的"GB18030 静默误转"是同一类问题, 只是编码对不是 GBK/BIG5,而是 Mac Roman(老 Mac 文字编辑器留下 的遗产,符合这份档案 1990 年代初 Discworld 开发史的年代背景): doc/lpc/intermediate/chapter1、doc/concepts/conversions (两份游戏内 LPC 教程文本)、www/external/java/telnet/README (bundle 自带的 Java telnet 小程序说明,非 mudlib 运行时内容)。 convert_lib.sh 默认的 GB18030 转码对这三个文件要么产出明显乱码 (conversions 被标记为 lossy),要么"成功"但内容变成 garbled 的 合法 UTF-8(chapter1 静默出现 mud's → mud誷;这正是 §4.1 警告过的、GB18030 广义超集特性带来的静默误转类别,只是这次的源编 码是 Mac Roman 而不是 BIG5)。用 Python 对整个 work/ 树做了一次 全量 UTF-8 可解码性扫描,逐一用 mac_roman/cp1252/latin-1 试 解码确认后,从 raw/ 原始字节用 mac_roman 重新解码替换修复。

.c → .lpc 重命名的连锁反应(AGENTS.md §4.2 类,本 lib 最集中

的 bug 来源)

secure/command.lpc:同一个定长切片 bug,但后果是"每一条指令都

失效"

这是本次修复里影响面最大的一个:eventRehash()/ eventGuildRaceRehash() 扫描 cmds/{living,player,creator,...}/ 目录下的 *.lpc 文件建立指令名索引,原文件对每个文件名做 file[0..<3] 去掉 .c 后缀(比如 l_ook.c → l_ook,下划线是 Discworld 自己的"最短缩写标记"约定)。convert_lib.sh 已经把 get_dir(...) 的通配符从 "/*.c" 改成了 "/*.lpc"(这是被 ref-fixup 扫描到的引号字符串,能自动修),但紧接着的 [0..<3] 是纯算术定长切片,扫描不到。结果是索引表里存的键从 "l_ook" 变成了带着半截扩展名尾巴的 "l_ook.l",任何真实指令名 都查不中——look/get/drop 等等全部指令,不管是自动触发的还是 玩家手打的,全部落到驱动的默认 fail message "What?"。改成 [0..<5](去掉 4 字符的 .lpc)后修复,同时把两处几乎一致的 调用点都改了并加了详细注释解释切片算术。

add_action 的 "*" 万能动词约定,在这个驱动上根本不存在

(AGENTS.md §6.2 类,本次深挖最有价值的发现)

这份 mudlib 出身的原始驱动把 add_action(fn, "*", priority) 当成 "匹配任意指令"的万能动词写法,并且把第三个参数当优先级数字用(从 -10000 到 10000 都有)。整份代码库有 5 处这样的注册: std/living/living.lpc 的 exit_command(出口/移动检测)、 global/psoul.lpc 的 lower_check/drunk_check(eval-cost 节流 和指令排队)、global/new_parse.lpc 的 new_parser、 global/command.lpc 的 cmdAll(真正的目录式指令分发器,见上一 节)。

但本项目这份 FluffOS 驱动(packages/core/add_action.cc)里 add_action 的动词匹配是精确字符串比较(或前缀匹配,取决于标 志位),源码里明确写着"if was add_action(blah, "") then accept it"——**万能动词约定用的是空字符串 "",不是 "*"**(驱动自带的 testsuite/clone/user.lpc 也是这么用的)。字面上的 "*" 不会被 特殊处理,就是个永远不会等于任何真实指令的普通字符串。后果:这 5 个处理函数在这个驱动上一次都不会被调用——不是报错,是彻底静默 失效。用 efun::write()(§10.3 建议的、能绕过 write_file() 潜在 ACL 拒绝的调试手段)在 command_commands()/cmdAll() 里加临时探针才 现场坐实:command_commands() 确实执行了、add_action 也确实调用 了、commands() 里也确实能看到 5 条以 "*" 为动词的记录——但玩家 打 look 时 cmdAll 的探针从未触发过,"What?" 直接从驱动兜底逻 辑冒出来。全部 5 处 add_action(fn, "*", N) 改成 add_action(fn, "", N)(连同两份未被实际 inherit 的历史备份文件 std/living/living.eff_shad.lpc/living.no_eff_shad.lpc 里的同款 调用,为了一致性也顺手改了,虽然它们目前不在真正的继承链上)。

同一个坑的第二层,不改完全部指令依然会崩:这个驱动上 add_action 的第三个参数根本不是优先级,而是一个 2-bit 标志位 (flag & 3;V_SHORT=1、V_NOSPACE=2,见 vm/internal/simulate.h),决定驱动传给处理函数的参数是"整行原 文"还是"undefined"。原代码库那些看似"优先级"的数字被这个驱动重新 解释成了几乎随机的标志组合:cmdAll 恰好拿到 -1&3=3(含 V_NOSPACE,侥幸拿到整行文本)、new_parser 拿到 -2&3=2(同样 侥幸),但 exit_command(1&3=1,只有 V_SHORT)、 lower_check/drunk_check(±10000&3=0,两个标志都没有)在这个 驱动上对不带空格的单字指令(比如 look)拿到的参数是 undefined 而不是字符串 "look"。现场验证:只改动词字符串后重新开机,look 不再是 "What?",但会在 exit_command() 内部因为 explode(word," ") 对 word==0 报 *Bad argument 1 to explode() 崩溃(这是这些处理函数第一次真正被这个驱动调用,之前从未被真实执 行过,所以这个参数类型 bug 此前完全没有暴露的机会)。把全部 5 处 的第三个参数统一显式改成 2(V_NOSPACE),确保驱动总是把整行原 文传给这些处理函数——这才是它们的代码本身一直假设的行为。两处改动 (动词字符串 + 标志位)叠加后,look/score/quit 等全部指令才 真正跑通。

secure/simul_efun.lpc:query_multiple_short 的先有鸡先有蛋

std/object.lpc 等大量文件把 query_multiple_short() 当 simul_efun 裸调用。原代码库自带一份可用的 polyfill (secure/simul_efun/multiple_short.lpc),但在 secure/simul_efun.lpc 里被注释掉了,附注"现在通过 parser 包提供"——说明原始目标驱动把它 实现成了真正的 C 层 efun。这个驱动没有这个 efun,取消注释后仍然编 译失败:secure/simul_efun/modified_efuns.lpc(在 secure/simul_efun.lpc 里比 multiple_short 更早被 inherit)内部 调用了 query_multiple_short(),但每个 inherit 目标文件是被当独 立编译单元处理的——simul_efun 对象自身还在"第一次构建中",驱动尚未 把它注册为可解析的 simul_efun,所以裸调用在这个阶段无法按 simul_efun 兜底解析,报 Undefined function query_multiple_short。修法参考 同一个文件里已有的先例(base_name 就是用同样手法处理的):让 modified_efuns.lpc 自己直接 inherit "/secure/simul_efun/multiple_short", 这样它在调用点之前就作为真正的同编译单元继承函数存在,不再依赖 simul_efun 运行时解析。

global/psoul.lpc:process_input() 里 time_expression{} 块

按分支不对称收尾

process_input() 用 int t = time_expression{ ...; #if efun_defined(add_action) return str; #else _process_input(str); #endif }; 这种写法,但闭合花括号 }; 只出现在 #else 分支内部,#if 分支(return str;)完全没有闭合。在原始目标驱动上 add_action 不是原生 efun,#else 分支恒定被选中,从未暴露过这个不对称。这个 驱动上 add_action 是原生的,#if 分支被选中,time_expression{} 块(以及整个 process_input() 函数)实际上从未正确闭合,把后面几 百行代码(_process_input/command 函数定义、lower_check() 等) 都错误地解析成了 process_input() 内部悬空语句,最终在文件末尾的 int lower_check(...) 处以 unexpected L_BASIC_TYPE 报错——报错位 置离真正的病灶(一次 #if/#else 分支闭合不对称)隔了 100 多行, 容易误判成 lower_check() 自己的语法问题。修法:把 }; 挪到 #endif 之后,两个分支统一在同一处闭合;#if 分支的 return str; 依然会在闭合前提前退出函数,行为不变。

global/wiz_channels.lpc:命名参数 lambda 里混用了 $1

map_func = function (object ob) { ...; str = $1->query_cap_name(); ... } 用显式命名参数 ob,函数体里却写成匿名闭包才用的 $1——这 个驱动对这种混用直接报编译错误($var illegal inside anonymous function pointer),大概率是从紧邻的 (: strcmp($1->query_name(), $2->query_name()) :) 匿名闭包写法复制粘贴时漏改的作者笔误。把三处 $1 全部改成 ob。

obj/handlers/armoury.lpc:目录缺失时的空指防护

walk_directory() 对 get_dir(dir,-1) 的返回值不做非数组判断直接 foreach;这份第三方压缩包本身就缺 /obj/armours/、 /obj/clothes/ 两个内容目录(见下面"已知内容缺口"),get_dir() 对不存在的目录返回 0 而不是空数组,每次 rehash("armours"/"clothes") 都会崩一次 *Bad argument 2 to foreach(被 preload 的 catch 兜住, 不影响开机,但会持续往 log 里写噪音)。加了 foreach(file in (tmp || ({}))) {...} 防护,属于通用防御性写法, 和目录缺失是否"该修"无关——不管以后是否补上这两个内容目录,这个 guard 都是对的。

预加载卫生(AGENTS.md §7.6 标准策略)

secure/config/preload 里的 /net/intermud3/intermud(真实的 Intermud-3 协议精灵,会试图连接外部 intermud 路由器)按标准策略提 前注释掉,避免在沙盒环境里挂起/拖慢开机。它的 create() 本身不会 立即联网(真正的 socket 连接是懒加载的),只有玩家主动执行 mudlist 才会隐式自动编译并触发;这不在核心验证路径上,未做进一 步处理。

管理员账号播种

secure/master.lpc 用一个硬编码的 TRUSTEES 宏(含 Root 和 cratylus)+ 一个真正可持久化的 positions mapping(存在 secure/master.o 里,通过 add_trustee()/add_senior() 等指令维 护)两层机制判断管理员身份。positions 在原始档案里是空 mapping, 且 add_trustee() 本身要求调用者已经是 trustee 才能提升别人(先有 鸡先有蛋,符合 §1.5 提到的典型引导场景)。直接编辑 secure/master.o 的存档数据,把 positions (["fluffos":2,]) 写进去(2 是 master.lpc 里 #define TRUSTEE 2 的值)。验证:fluffos 正常走 完注册流程 → 密码验证成功注册 → 重新登录时能看到只有巫师才会看到 的"To all creators.../To all Domain Leaders..."提示横幅 → 执行 compile /std/room(本 lib 里 wizard 级别的重编译指令,等同其它 lib 常用的 update)成功走到实际尝试编译的阶段(报 Undefined function generate_source,而不是任何权限拒绝提示——说 明 ACL 授权本身是通的,卡在的是下面这条独立的、和管理员授权无关的 预置代码 bug)。

已知内容缺口(第三方重打包档案本身缺失,非本次转换造成,不予

"修复")

验证记录

WASM

2026-08-25 更新(另一次 session):真正跑通了 WASM 通道,wasm_status 由 ""(未测试)提升为 playable。用的是 CI 同款的预编译 WASM 驱动 release(fluffos/fluffos 的 *-wasm.zip),配合 scripts/pack_lib_for_web.sh + scripts/wasm_boot_check.js / scripts/wasm_client.js 本地复现。

首次打包后驱动完全无法启动——simul_efun 是唯一"急切"(非懒惰)加载 的对象,而它下面的 secure/simul_efun/dump_socket_status.lpc 无条件 调用了这台驱动没有的 socket_status()(WASM 构建没有 sockets package),导致 *No program in object '/secure/simul_efun/...'!、 整个驱动拒绝启动——和同一个 session 早些时候在 ds386(Dead Souls) 上发现并修复的问题是同一类根因,详见 AGENTS.md §7.52 那条追记。 按同样的套路把 dump_socket_status() 挖空成安全桩后,又连续撞上 compress package 缺失的两处编译错误(secure/master/ create_dom_creator.lpc 里 6 处 compress_file()/uncompress_file() 闭包引用、secure/login.lpc/obj/handlers/login_handler.lpc/ global/player.lpc 里 3 处 compressedp()),逐一挖空/常量化后驱动 终于干净启动、能接到连接。

完整验证:真实跑完一遍注册向导(含文档里提到的、服务端故意暂停 ~30 秒的条款确认环节——用 wasm_client.js 的 --idle 参数需要设得 比这个暂停更长,否则 yes 会在暂停期间过早发出而落空,这是脚本 本身的坑,不是驱动 bug),成功进入著名的 Discworld 圆形大厅,look 输出与原生测试记录的房间描述完全一致,MCCP 提示("You are logged in uncompressed!")也正确出现(验证了 check_mccp() 的 compressedp() 桩替换按预期工作),womble NPC 的环境对话正常播放。quit 本次因为 womble 持续产生环境消息、脚本的 idle 检测一直"看不到真正的安静"而 没能在这次转录里捕获到,但 quit 路径本身没有触碰任何 compress/ socket 相关代码,原生测试已经完整验证过,不需要重复。

另发现一个非致命的预加载期编译失败:/net/daemon/board_thingy (继承自 /net/inherit/server.lpc,一个 BBS/公告板网络监听服务) 同样因为 sockets package 缺失而编译失败,但这不影响驱动整体启动 (预加载会跳过失败的对象继续走),也不在登录/游玩路径上——按这个 项目"只修必经路径,可选管理工具留作已知缺口"的既定做法,本次没有 处理,留给以后如果真的有人报告这个 BBS 功能坏了再修。

本 session 的一个失误(如实记录)

排查驱动进程时先后两次误用了匹配范围过宽的 kill:一次是用精确 PID kill 时没有先核实该 PID 属于哪个工作目录,误杀了另一个并发 session (libs/ds386)正在运行的驱动;另一次是用 pkill -f "driver config.fluffos" 排查端口占用,这个文件名是全项目几乎所有 lib 共用 的约定命名,极可能同时误杀了当时并发运行的 nightmare3/lima 两个 lib 的驱动进程(进程列表在那次 kill 前后确认消失)。两次都不 是数据/文件层面的损坏,只是让对应 session 的驱动测试被迫中断重启; 已按项目惯例(“Kill drivers by exact PID / never pkill -f”)改回精 确 PID 方式,后续操作未再复发。

深度功能测试(round two,2026-08-27)

本次是 discworld 第一次真正的 §10.7 round-two 全流程游玩测试(此前 NOTES.md 里的"验证记录"一节只是转档时的安装期烟雾测试,没有 深度功能测试 标题,AGENTS.md 的 §10.7 完成度统计里从未把这份 lib 计入)。用真实驱动(~/src/fluffos/build-debug/src/driver config.fluffos,端口 40206)连续开了 6 次机(每次都是干净重启, grep -c "error:" debug.log 恒为 0),全程用一份 Python 原始 socket 脚本(dw_client.py)交互,测试角色 Roundtwo(真实注册 流程,含服务端强制的 ~30 秒条款确认暂停)。

发现并修复的 bug

obj/handlers/armoury.lpc 的 request_item() 在找不到道具时返回 0(有文档、有先例:早前转档时 walk_directory() 就因为同一个 "目录缺失返回 0 而不是空数组"的根因加过防御性 guard),但代码库里 至少 5 处 ARMOURY->request_item(...)->move(...) 调用链完全没有判 空,直接对返回值做 ->move()。 由于这份 "distribution lib" archive 本身缺失 /obj/clothes/、/obj/armours/ 两个内容目录(转档时已 记录的已知缺口),任何请求这两类道具的调用都会返回 0,0->move() 在这个驱动上报 *Bad argument 1 to call_other()(未捕获),把调用 方的 create()/setup() 从崩溃点截断。

live 复现:管理员测试角色进入新手战斗训练室(d/liaison/NEWBIE/ combat.lpc)后尝试 one/two/three 三个训练间任一个入口, combat_room1.lpc(等)会 clone_object 训练假人 d/liaison/NEWBIE/dummy.lpc,假人 setup() 里 ARMOURY->request_item("dirty rags", 30)->move(this_object()) 崩溃(dirty rags 是缺失目录里的衣物),玩家侧看到"A runtime error occurred.",log/runtime 记录 *Bad argument 1 to call_other() (未捕获,直接从驱动兜底冒出)。这条路径正是本轮方法论第 3 步要求 的"safe-sparring 机制",属于核心验证路径,不是可选管理工具。

修复:给全部 5 处受影响的调用改成"先存局部变量再判空"写法(不改变 道具存在时的行为,只是让缺失时优雅跳过而不是崩溃),跟转档时 walk_directory() 的修法同一个思路:

刻意没有动的同款代码:d/learning/cutnpaste/althea.lpc(教学沙盒 目录下的"抄写练习"示例 NPC,就是用来教这个确切写法的,不是真实 游玩内容)以及两份 .htm/.txt 教程文档——按项目一贯的"内容/文档不 在修复范围"处理。

新的 AGENTS.md bug 类:这是 §7.6"缺失目录"系列的一个新变种—— 不是 get_dir()/read_file() 返回 0 被当数组用,而是一个有据可 查、文档明确写着"找不到就返回 0"的工厂函数(request_item()), 它的返回值被链式 ->move() 调用,中间没有存局部变量、也没有判空。 由于道具类文件层层散布在整个代码库里,这类调用天生就是"批量隐患" 的形状——同一个安全 factory 函数,调用方各自决定要不要判空,任何一 个偷懒的调用方在对应内容缺失时都会阻断自己的 create()。已在下面 新增 AGENTS.md §7.147 记录这个模式,供以后其它 archive 遇到同类 "factory-on-missing-content returns 0" + "unguarded ->call chain" 组合时参考。

std/shops/print_shop.lpc 的 add_auto_load_info() 用驱动保留字 nosave 当参数名,整个印刷厂片区(print_shop.lpc 自己 + 它的四 个子类 print_shop_office/print_shop_foyer/print_shop_press/ print_shop_binding)在这个驱动上完全无法编译,从转档以来一直是 死代码。 nosave/static 在这个驱动的词法分析器里都是真正的 L_TYPE_MODIFIER 保留字(~/src/fluffos/src/compiler/internal/ lexer_utils.cc),protected int add_auto_load_info(string nosave, string dynamic); 这行原型声明里把 nosave 当参数名用,编译器直接 报 error: syntax error, unexpected L_TYPE_MODIFIER——不是我这次 交互测试里现场触发的(这个函数从没被玩家路径直接调用过),是用 lpcc 单文件编译核对旁边一处不相关问题时顺带发现的:查 log/error-log.old 发现同一行报错从转档以来的历次开机日志里反复 出现过至少 7 次,说明这从来没被人注意到过。函数体内部 $(nosave) 这个 lambda 捕获、连同文档注释里其实早就写着 @param static_arg 而不是 nosave(暗示原作者自己心里想的参数名 跟实际写的不是一回事,大概率是笔误)。修法:把参数名从 nosave 改成 nosave_arg(跟文档注释的命名意图一致),原型声明和函数定义 两处、函数体内部的 $(nosave) 引用一并改。用 lpcc 核对:修复前 std/shops/print_shop.lpc 和 d/dist/pumpkin/rabbit/ print_shop_office.lpc 单独编译都直接报错;修复后两个都干净通过 (无 warning 之外的输出)。全库 grep 过其余全部 nosave/static 两个保留字被当裸参数名用的情况,只有这一处命中。已新增 AGENTS.md §7.148 记录这个模式。

检查过、确认干净的标准 bug 模式(§10.7 清单里列出的横切模式)

新观察:这是一份"distribution lib"裁剪版,公会系统本来就没有随

包分发(不是 bug,如实记录)

include/config.h 定义了 __DISTRIBUTION_LIB__,d/liaison/NEWBIE/ path.h 在这个宏打开时把 GUILDS 宏定义成字面量 "None currently", d/liaison/NEWBIE/guilds_foyer.lpc 里全部六个公会大门出口 (witch/wizard/thief/assassin/warrior/priest)都包在 #ifndef __DISTRIBUTION_LIB__ 里——也就是说这份 archive 设计上就 不随包分发任何公会加入内容,/std/guilds/ 目录下也确实只有 warrior.lpc/standard.lpc 两个基类文件,没有任何一个公会总部房 间(d/guilds/ 整个目录都不存在;开机 preload 列表里的 /d/guilds/ wizards/books/beginners、/d/guilds/wizards/Ankh-Morpork/inside/ gymnasium、/d/guilds/wizards/chars/frenkel 三条也全部指向不存在 的文件,被 preload 的 catch 静默吞掉,零 debug.log 痕迹)。这意味着 本轮方法论第 4 步(公会/技能习得路径测试)在这份具体 archive 上 从设计上就不可达,不是我漏测——如实记录,不视为 bug,不尝试编造 公会内容去"修复"。

顺带发现一个由此衍生的真实崩溃(同一根因,不单独归为新 bug):地 图上"迷路仙女教母"NPC(magrat.lpc/granny.lpc)的 setup() 都 调用了 set_guild("witch"),而 /std/guilds/witch.lpc 在这份 archive 里根本不存在,std/race.lpc:199 的 set_level() 内部对它 做 call_other() 直接报 *call_other() couldn't find object '/std/guilds/witch'(live 复现,log/runtime 有记录)。同样属于 "distribution lib 没有公会内容"这一句话能完全解释的已知限制,不修。

另外,d/liaison/NEWBIE/foyer.lpc 的 guilds 出口本身也会崩 (d/liaison/NEWBIE/guilds_foyer.lpc inherit PATH+"outside" -> d/liaison/NEWBIE/outside.lpc inherit "/std/outside" -> 转档时就已经记录在案的已知缺口"/std/outside.lpc 完全不存 在"),live 复现:*Inherited file '/std/outside' does not exist! (log/runtime)。这是对已有已知缺口条目的一次扩大确认——此 前的记录只提到"多个教学/示例房间和至少一个真实房间(room/air.lpc)" 受影响,这次确认它还directly 挡住了新手大厅九个出口里唯一通往 "guilds"(公会花园)的那一个,即使 /std/outside.lpc 真的补上了, 这个花园本身在这份 distribution 配置下也只有一个 "foyer" 返回出口 (六个公会大门出口全部被 #ifndef __DISTRIBUTION_LIB__ 排除),没 有任何实际可加入的公会内容——两个独立限制(缺失基类 + 故意裁剪的公 会内容)叠加在同一个房间上。仍然判定为"已知内容缺口,不予修复"(需 要编造一整个 /std/outside.lpc 基类文件的内容,属于内容/设计猜测, 不是程序 bug),只是把这条记录写得更完整。

环境/测试基础设施问题(非 mudlib 代码 bug,直接修复)

已验证的核心流程(本轮 round-two 清单)

管理员账号重新播种

上一次转档 session 种下的 fluffos 管理员角色存档本身没有随仓库提 交(save/players/ 整棵树不在 git 跟踪范围内),这次沙盒环境里已 经找不到了(secure/master.o 的 positions trustee 记录本身还在, 只是玩家存档丢了)。用同样的账号名 fluffos/密码 Mud@2026 走完 整套注册流程,真实挂机 30+ 分钟(满足 global/player.lpc 的 MIN_TIME_TO_SAVE=1800 秒新手存档门槛)后 quit,验证 save/players/f/fluffos.o.gz 已生成且 #/global/lord.lpc 开头(确 认是真正的 creator/lord 类实例,不是普通玩家),全程 debug.log/ log/runtime 干净。

商店切片(2026-09-04)

补完 2026-08-27 §10.7 标成「list 时序、未完全验证」的商店。根因 不是 soul 排队:是 command() efun 阴影(v1/v2 本轮已修;v3 living.lpc 早已有 #if !efun_defined(add_action))。本轮不再重挖 那一处。

端口 40206。global/newbie_junk.lpc 仍把 8 Pumpkin dollar / 100 Pumpkin pence 放在未判空的 request_item("bucket small") 之后, 已按 v1/v2 改成先发钱再对 armoury/clone 判空。money_handler.lpc 同样缺档案里的 /save/money_handler.o,补了与 v2 相同的 Pumpkin / Newbie Area 币种表(比值来自本树 money_symboliser,P$1 == 100)。

冷启动后南瓜菜单 N 注册 shoplite / Play2026x。commerce 立刻 打出商店房间。同一会话只发一条 buy a lightable torch(中间出现 Queued command: buy a lightable torch,约十余秒后刷出回执): You buy a lightable torch for 50 Pumpkin pence coins.

2026-08-27 的「list 时序」不再算未完成。没有拜师(distribution 公会 内容未随包)。

2026-10-04 — #pragma no_warnings removed

std/effects/basic.lpc carried a mid-file #pragma no_warnings (before query_attrs()); it hid nothing: the file compiles without a diagnostic once the line is gone. The same file, byte for byte, sits in skylib, discworld, dw_fluffos_v1 and dw_fluffos_v2.

深度功能测试(§10.7,2026-10-04)— compile warnings: 248 -> 47

scripts/lpc_warnings.py discworld: 1699 files compile, 316 do not; 647 distinct diagnostics outside the 45 creator-help examples under d/learning/help_topics/error_messages/ (248 warnings, 399 errors). Now 446: the same 399 errors (files written for another lib: Undefined variable, Cannot #include, the player-shop office_code/*.lpc fragments, www/; none touched) and 47 warnings. Those 47 are 33 in std/living/living.eff_shad.lpc (revision 1.72 of living.lpc, kept beside it and inherited by nothing; it adds inherit "/std/living/effects" to a container that has it already) and 14 in files that do not compile (office_code fragments, net/daemon/nhttp.lpc, std/environ/weather.lpc, secure/simul_efun/add_action.lpc). The creator-help examples show one compiler message each; their warnings are what they are for, so lpc_warnings.py skips that directory (its first pass here emptied five of them: restored).

128 files for the 248 warnings (unused locals 95 -> 5, override arity 72 -> 0, inherited from both 38 -> 30, redeclarations 27 -> 4, the 7 expressions without effect stay, eight others fixed):

Checked on pristine trees with the same driver, HEAD against the working tree: boot and login of the seeded admin look the same, the HEAD console prints 21 compile warnings and the new one none, the 3 compile errors it prints are the same (finderror_helper.lpc, CONFIG_DB_ERRORS_USER). A throwaway object under /obj/ that loads or clones 31 of the changed things through the lib's own master -- rings, wand, staff, the camera with an autoload round trip, the dog, the disease effects, the pickler's autoload keys, the credit note, the two closing shops, the club, cmr, pop3, imap4, ftp_auth and parcel handlers, room_save, the four terrain bases, query_multiple_short(), the present, mtf2, shop_base, craft_shop, read_desc -- answers identically in both trees, while HEAD's batch prints 55 warning lines and the new one none. (exec from a telnet session returns nothing here: the game queues each command and the answers never arrive.) scripts/lpc_audit_removed_locals.py lists every deleted declaration the function still reads: it found the four locals the first pass deleted from inactive branches (mtf2.lpc, read_desc.lpc, shop_base.lpc, craft_shop.lpc; back in their branch), and a read of the diff found a deleted object ob in the include fragment office_code/lists.lpc whose next line reads it (the compiler's "unused" came from a scope an earlier error had confused; restored).

深度功能测试(§10.7,2026-10-04)— include/runtime_config.h was the MudOS-era one

The header numbered the integer settings from 15 where this driver numbers them from 256 (KB 06 §7.89), so get_config(__MAX_BUFFER_SIZE__) answered "" and /net/daemon/http's create() died with Trying to put string in int (line 133) every time it was loaded; the web daemon never came up. The header is the driver's own copy now. The four settings http.lpc reads (__MAX_BUFFER_SIZE__, __MAX_STRING_LENGTH__, __MAX_READ_FILE_SIZE__, __MAX_BYTE_TRANSFER__) are the only symbols the lib takes from it; load_object("/net/daemon/http") answers an object now and the rest of the load-and-clone check is unchanged.

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

驱动的 mudlib_stats 包在主控载入时调用一次 domain_file("/"),要求返回字符串。本库 secure/master/creator_file.lpc 的 domain_file() 在 creator_file() 没有答案时返回 0,"/" 正是这种情况,所以每次启动都打印 domain_file() in the master file does not work, using bb_ui as fallback 一行,网页版每位访客的终端里都看得到(author_file(<主控文件>) 本来就返回 root uid,不报警)。现在 domain_file() 对 "/" 直接返回 get_bb_uid(),正是驱动缺省时用的值。其他路径不变:创作者的 ls 和 ftpd 直接调用 master()->domain_file(),返回 0 时显示 "Root",所以没有把 0 一律改成字符串(KB 06 §7.222)。用 lpcc config.fluffos /nonexistent.lpc 前后对比,主控载入段只少了这一行。

深度功能测试(§10.7,2026-10-05)— 压缩存档改为明文;board_backups 目录

网页版的 WASM 驱动没有 zlib,restore_object() 只读 <名>.o,打包脚本也排除 *.gz;本库以 .o.gz 存的种子存档(save/players/f/fluffos.o.gz 等 7 个:管理员、bastards、boards、hist_handler、armoury、intermud、rcs_handler)在网页版上全部缺失:文档里的管理员 fluffos / Mud@2026 在网页版上不存在,登录时打印 Failed to restore bastards.。现已 gzip -d 成内容逐字节相同的明文 .o(KB 01)。本机驱动在没有 .o.gz 时读 .o,之后自己的压缩存档会替换它。另外 board handler 启动时把 boards 备份到 /save/board_backups/,目录不存在,每次报 Error copying file;加了 save/board_backups/.gitkeep。验证:本机登录 fluffos / Mud@2026 成功,无 bastards / 复制错误;WASM(解开的 site zip + 明文存档)同一密码登录成功。

2026-10-05 — CONFIG_DB_ERRORS_USER 未定义

obj/handlers/finderror_helper.lpc(finderrors 创作者命令与各领域错误追踪的助手)和 www/secure/creator/bug_top.lpc 用了 CONFIG_DB_ERRORS_USER,公开发布版的 include/config.h 删掉了这个定义,两个文件都编译失败。在全新检出(及网页版)里启动时会载入它,打印 3 条编译错误。config.h 补上 #define CONFIG_DB_ERRORS_USER CONFIG_DB_USER;全新树启动 0 错误 0 警告。

深度功能测试(§10.7,2026-10-08)— .lpc 后缀的定宽切片

.c 改名 .lpc 时只改了字符串没改切片宽度:f[<2..] == ".lpc" 拿两个字符比四个字符,永远不成立(!= 永远成立),按后缀筛文件、去后缀的代码因此从不运行或截成 x.l(KB 06 §7.238)。scripts/lpc_lpc_suffix_slice.py --apply 改了 11 处比较、5 处去后缀(如 www/footer.lpc);改动的文件 HEAD 与工作树分别加载(新进程),无回退。

Compile warnings (2026-10-13)

scripts/toolkit_fix.sh round: secure/simul_efun.lpc inherited /secure/simul_efun/multiple_short directly although modified_efuns (inherited just above) already brings it, so every polyfill function was compiled twice; the direct line is gone and the comment says where the polyfill comes from. std/environ/weather.lpc's untyped move(arg) is now mixed move(arg). Both load the same at HEAD and in the tree (regress 0).

Left as is: std/living/living.eff_shad.lpc (33 warnings), an older variant of living.lpc that nothing inherits or loads. It inherits /std/living/effects (a one-line inherit "/std/basic/effects") beside /std/container, which brings the same effects code, and calls effects::create(), effects::add_effect() and effects::stats() by that scope; the live living.lpc dropped the second inherit and reaches effects through container::create(). Removing the duplicate means rewriting how this unused file is put together, so it stays as archived.

Base classes the lib did not ship, restored from sibling libs (2026-10-18, KB 04 6.15)

These base classes are not in the lib, so every object that inherits them (rooms, NPCs, items) failed to load (inherit XXX; with no macro, or inherit "/std/..."; with no file). A sibling lib carries the file and shares many files byte for byte with this one, so the file is copied as it stands: /std/effects/healing/basic_cure <- skylib, 1 object(s) inherit it, sample 1/1 load. Each base was loaded alone and a sample of its users after it; a base that did not load, or none of whose users did, was taken back (scripts/lpc_restore_bases.py).

Macros the global header lost (2026-10-19, KB 04 6.16)

/include/all_inc.h did not define C_TEMP, C_CLOUD, C_RAIN; the objects that use them (4 on the list) stopped at Undefined variable/Undefined function, and since the compiler stops at the first undefined name the errors and warnings behind it stayed hidden. The sibling libs of the same lineage carry these lines in their headers: the copy of the sibling with the most files in common was added after the last #define of /include/all_inc.h; every path in a macro body exists in this lib. the listed objects have other errors as well and still do not load. (scripts/lpc_restore_defines.py)

The unused living.eff_shad.lpc variant, finished (2026-10-19, KB 06 7.240)

Old-driver type slack: missing arguments, untyped parameters, array initializers, class variables (2026-10-19, KB 04 6.19, 6.20)

MudOS filled the missing arguments of a call with 0, took an untyped parameter as mixed, never compared a declared type with its initializer and let a variable be named class; this driver refuses all of it at compile time and the whole object fails. 4 file(s) fixed: /d/learning/examples/terrain_map/basic/field_handler.lpc, /obj/fungi/glow_toadstool.lpc, /std/environ/weather.lpc, /std/npc/taliswoman.lpc (the repair rounds of scripts/lpc_round_files.py: zeros written out at the call, mixed for untyped parameters, string X = ({...}) given its star, void functions returning values, and class variables renamed pick_class). Loaded at HEAD and in the tree, per file: 2 of them failed before and load now, none regressed.

x != "a" || x != "b" and x == "a" && x == "b": comparison chains of one value that can never differ (2026-10-20, KB 06 7.282)

if (arg != "wan" || arg != "pill") return notify_fail(...) is true whatever arg is, dir == "south" && dir == "north" is never true: the guard under the test never opens, or never closes. The author meant "neither of these" (&&) or "either of these" (||). != chains joined by || became && (only where the statement under the test is a return, a refusal or a guard clause), == chains joined by && became || (scripts/lpc_fix_cmp_chain.py). What it was: push lever always answered "You must push an existing lever." (str == "levers" && str == "lever"). Changed: /std/environ/weather_room.lpc (1).

member_array('/', str) became strsrch(str, "/"): the character-literal member_array is a byte search (2026-10-20, KB 06 7.283)

'/' in member_array('/', str) is an int, so the driver searches the UTF-8 bytes and answers a byte offset (a third argument is a byte offset too). FluffOS 923cdbc0 warns once per call site (Warning: member_array(int, string) searches BYTES) when the string holds non-ASCII text before the match, and str[0..idx-1] counts characters, so a non-ASCII string was cut in the wrong place. Now strsrch(str, "/") (-1 when absent, a character index; scripts/lpc_fix_member_array_char.py, the JSON encoders and a few other sites by hand). Changed: /obj/misc/housing_creator.lpc (1).

if (x = CONST): a comparison written as an assignment (2026-10-20, KB 06 7.290)

if (arg = "button" || arg = "pill"), if (!arg || arg = "") return 0;, else if (check = 0 && tempB != ""): = was typed for ==, so the condition is the constant: the branch is always taken for a non-zero number or any string (even "") and never for 0, and the variable is overwritten; the driver does not warn. Every such = became == (scripts/lpc_fix_cond_assign.py, 1 terms). Changed: /cmds/theft_base.lpc (1).

A login password over 255 bytes: crypt() raises (2026-10-20, KB 06 7.294)

The driver's crypt() raises (Error in crypt(), check your salt) for a key over 255 bytes, where the old driver hashed any length. A password of 256 bytes or more typed at the login prompt threw an error in the password check and left the connection at a bare > prompt, so the login never completed (the old-password test of password the same); found by the sweep shape of 300 x. /secure/simul_efun.lpc now defines a simulated efun crypt(key, salt) of the same name: only an over-long key is cut (to 255 characters, then character by character until sizeof(to_buffer(key)) <= 255: the limit is in bytes, a CJK character takes three) before it is handed to efun::crypt(); every ordinary password hashes as before and the saved hashes are untouched (scripts/lpc_add_sefun.py crypt).

time did not compile: config.h came after the __DISTRIBUTION_LIB__ test (2026-10-20, KB 06 7.312)

cmds/player/time.lpc had #ifndef __DISTRIBUTION_LIB__ #include <special_day.h> #endif before #include <config.h>, the header that defines __DISTRIBUTION_LIB__, so the test was always true and the file included special_day.h, which the distribution lib does not ship: the time command never compiled. #include <config.h> now stands before the test.

Heart beat every two seconds: the command queue never refilled (2026-10-20, KB 06 7.314)

psoul.lpc query_time_left() refills the command budget with time_left += ((time() - tl_updated) / 2) * ROUND_TIME and then sets tl_updated = time(); the player's heart_beat() calls it every beat. The MudOS Discworld beat every two seconds. The driver's default heartbeat interval msec is 1000, so time() - tl_updated is 1, the integer division is 0 on almost every call, and time_left stays at -20: after the first command every other one answered Queued command: ... and nothing ran for minutes (restart and stop did not help; a reconnect to a net-dead body was the same). config.fluffos now has heartbeat interval msec : 2000; live, i, score, date, who and look answer at once in a first visit and after a reconnect.

Player saves need every letter directory (2026-10-20, KB 06 7.315)

save_object() makes no directories, and the Discworld mudlib has no on-demand mkdir for save/players/<letter>/, save/newsrc, save/mail or banish: they existed in the archive only for the letters somebody had played with (only f), so a character whose name starts with another letter was never written (Could not open /save/players/s/NAME.o.gz.tmp for a save in log/catch at the quit and at every autosave; the login survey said in, saved 0). scripts/lpc_keep_letters.py --apply completed the four buckets in scripts/wasm_keep_dirs.txt (the site loader and pristine_tree.sh make them); live, save/players/s/sweepzq.o.gz is written at the quit. The mortal sweep also needs letters-only names (sweepab) and the N entry of the login menu.

Timed reboot: the mud asked everybody to leave 25 minutes after the boot (2026-10-20, KB 06 7.319)

obj/handlers/garbage.lpc check_reboot() shuts the mud down when uptime() > max_time; max_time is saved in save/garbage.o and shrinks by a sixth at every boot that follows a crash. The committed file had been shrunk by earlier boots to 14255 s, so log/REBOOT said Shutdown requested automatically about four hours after the boot, Offler announced the reboot, logins were refused (It is too close to the shut-down) and the driver exited: on the site nothing starts the mud again. NO_TIMED_REBOOT is now defined (the original ran 61 hours between reboots; the museum has no supervisor). Do not commit the churn of save/garbage.o.

Creator commands that did not compile: actions_defined, generate_source, mud_list, lpc_info, function_profile, __OPTIMIZATION__ (2026-10-20, KB 06 7.323)

The creator sweep as the seeded admin (play_sweep_lib.py discworld --account fluffos:Mud@2026 --dirs creator,lord,playtester --burst 1.3) printed compile errors at the first verb of 13 commands. actions_defined() is called by comment (and the eight objects that inherit cmds/report_base.lpc), which and loc_alcmd; the lib's stub for it sits in secure/simul_efun/add_action.lpc, a file simul_efun.lpc inherits only #if !efun_defined(add_action): this driver has add_action, so the stub was skipped with the file. compile needs generate_source() (the original driver's LPC-to-C compiler), lpc_info needs lpc_info(), prof needs function_profile() (a build option this driver lacks), fry called mud_list() where its own first line says mud_name(), and mudinfo defined __OPTIMIZATION__ only inside its #ifndef __COMPILER__ guard (this driver defines the compiler macro and not the other). New secure/simul_efun/driver_gaps.lpc (inherited by simul_efun.lpc; each stub behind its own #if !efun_defined(x)): actions_defined() answering ({}) as the old stub did (varargs: loc_alcmd passes one argument), generate_source() raising LPC to C compilation is not available on this driver. (compile catches it and prints the message), lpc_info(), function_profile() (an empty profile); the stub is gone from add_action.lpc; fry uses mud_name(); mudinfo has its own #ifndef __OPTIMIZATION__. Gate: 464 objects (cmds, secure/cmds, secure/simul_efun) at HEAD and in the tree, regress 0, 13 newly load; live as the admin which look lists /cmds/living/l_ook and comment, compile, loc_alcmd print without a console line. Still not loading, and not bugs of this port: pstatus (the word broken stands before its #include: the authors disabled it), errors_base2 (MySQL), theft_base and sh_out (headers of areas the distribution lacks), sit (its create() asks this_player(), it loads when a player types it).

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

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

俱乐部控制室 do_transfer():失败分支销毁没赋值的 obj(2026-10-21,KB 06 §7.348)

do_transfer() 在 remove_money() 失败时执行 obj->dest_me();:这是从 do_withdrawl() 抄来的(那里先造了一笔钱 obj,失败时销毁它),do_transfer() 里什么也没造,object obj; 从未赋值。转账失败时抛错,而不是提示 "Unable to remove the money from the account."。去掉声明和那一句。改动:/std/room/inherit/club_control_room.lpc。

add_action() registrations of functions nobody defines (2026-10-21, KB 06 §7.349)

An add_action("fn", "verb") whose function is defined neither by the object, nor by its headers, nor by its parents, nor by any object derived from it (scripts/lpc_scan_callout_targets.py, descendants checked transitively, then function_exists() in a running driver, scripts/lpc_verify_handlers.py). The driver walks the actor's sentences from the newest one: when that sentence's function does not exist it raises *Function for verb 'drop' not found. and tries no other handler of the verb (an older dead sentence is passed over silently). Removed 5 registrations in 1 files (scripts/lpc_fix_dead_add_action.py), the verbs are left to their other handlers: /obj/roommak.lpc (do_add, do_map, do_modify, do_set, ...). Most frequent: do_add x1, do_map x1, do_modify x1, do_set x1, do_setup x1.

Combat probe with the seeded administrator (2026-10-22): 56 of the 87 /std/effects/... paths the lib names do not exist

scripts/play_combat_probe.py discworld --account fluffos:Mud@2026 --clone ... --pace 2.5 (Discworld queues one command per heart beat) fights the newbie area's womble: the screen shows *call_other() couldn't find object '/std/effects/ingested/tranquil_sleep'. The whole std/effects/ingested/ directory (drunk 16 references, cum_poison 16, poison 13, basic_healing 6, nausea 5, garlic_breath 4, tranquil_sleep 3, ...), poisons/ (spider_venom 5, poison_gas 3, ...), healing/antiseptic_soap, religious/* and fighting/combat are named by objects but not in the lib; add_effect() / add_eat_effect() of them raises. 31 of the 56 exist in skylib (a Discworld fork): its ingested/ effects load in this tree (basic_healing, blind_protect, calm, cold_cure, garlic_breath, int_bonus, knurd, strength_boost, poison_antidote, ...), the ones that matter most do not as they stand (path.h and the std/shadows/ingested/ shadows are missing here, and skylib's global/standard.h macros ENV / TP / TO / CAP are not defined by these libs). Not changed: a port of about 30 files, queued.

Missing std/effects files adopted from skylib (2026-10-22)

The section above counts 56 /std/effects/... paths that objects of this lib name and the lib does not ship: the womble of the newbie foyer (tranquil_sleep), every drink or herb with an eat effect (drunk, poison, cum_poison, basic_healing, nausea, ...), the scorpion's and the traps' venom (spider_venom, poison_gas), the group command (other/group), consider (it asks PLAYER_DRUNK_EFFECT), the swimming rooms (other/water_effect). libs/skylib is a Discworld fork whose effect core (std/basic/effects.lpc, std/effect_shadow.lpc, the add_effect() / beginning() / merge_effect() / restart() / end() / submit_ee() API, the effect timer's call convention, std/shadows/misc/effects.lpc) is the one of this lib, and it ships most of the missing files (many still carry the original Discworld RCS headers of Ceres, 1998). 51 new files are adopted from it (36 effects, 11 shadows, 4 path.h), all under work/std/; no existing file was edited. Every adopted file starts with // adopted from skylib 2026-10-22: <what was changed>.

Effects adopted unchanged (28): healing/antiseptic_soap, healing/simple_cure, ingested/{basic_charging, basic_healing, blind_protect, calm, cold_cure, cum_poison, garlic_breath, hallucination, int_bonus, knurd, poison, poison_antidote, strength_boost}, object/ward, attached/see_invisible, other/{alertness, dog_urinate, fish_effect, group, ink, meditate_effect, perfume, rubbish_smell, water_effect}, poisons/{constitution, poison_gas}. Shadows adopted unchanged (7): std/shadows/ingested/{blind_prot_shadow, drunk, hallucination, poison}, std/shadows/object/ward, std/shadows/other/fish_effect, std/shadows/attached/see_invisible. New path.h (copies of skylib's): std/effects/ingested/path.h, std/effects/poisons/path.h, std/shadows/ingested/path.h, std/shadows/other/path.h (the std/effects/{attached,object,other}/path.h already existed).

Ports (12 files), each one a token-level edit:

Beyond the first list of 31 (dependencies and live data references, all with a donor in skylib): healing/simple_cure (SIMPLE_CURE of include/disease.h, inherited by antiseptic_soap), poisons/constitution (strength_boost.end() adds it: without it the boost ended in couldn't find object '/std/effects/poisons/constitution'), attached/see_invisible + its shadow (obj/amulets/cat's_head.arm), ingested/vial_healing (obj/food/meals/*.food), object/oak_wand_healing (obj/plants/yarrow.ob, thorny_burr.ob, angelica_root.ob).

Evidence.

Still missing after this (no donor in skylib or in any other lib of this repository; nothing was invented): fighting/combat (named by disease/rabies, room/albatross_attack, living/skills, obj/handlers/taskmaster), poisons/{angry, bloat, con_new, confuse, hp_new, smokebomb} (obj/state_change/base.hdr_dc.lpc and the heat / distill / press .dat files: the cooking and distilling poisons; skylib's poisons/hp is the old potion-space file, not hp_new), religious/{basic_ritual, blind, shield, theological_debate} (debate command, obj/handlers/philosophies, std/living/armour, std/shadows/misc/poker_shadow), magic/{invisibility, virtual_spell} (obj/rings/invisibility, include/virtual_spell.h), disease/{broken_leg, jellyfish_sting} (obj/misc/traps/leg_trap, std/races/jellyfish; skylib has a curses/broken_leg under another path), the faith/*, magic/{magic_circle, grabbing_hands, reduced_weight, witch_eye} and object/rubbery_weapon names that only secure/rcs_handler.o (an RCS file list) carries. Donor exists but only a backup (obj/reactions/base.hdr_dc.c.old) or a doc names them, so not adopted: external/fungus_cure, healing/cold_remedy, healing/weak_hp, ingested/{frog_repellent, potion_spellcheck, weak_poison_cure}, magic/magic_spell. ingested/query_drunk_level() does not exist in skylib's drunk (consider asks it for the considerer's drunkenness): the call returns 0, so the drunk-considerer lines of con_sider.lpc never show.

Fixed on the way (outside the effect files, same day):

Seen on the way, not fixed (outside the effect files):

clone_object() of a shadow the lib never shipped, split over two lines: the -> chain guarded (2026-10-22, KB 06 §7.73 addendum)

std/object.lpc init_dynamic_arg() restores a "special" item with clone_object( "/std/shadows/object/special_item" )->\n setup_shadow( this_object(), player );: the file is not in this archive (std/shadows/object/ has the other shadows), clone_object() of it raises for the object that carries the special dynamic argument. The statement is now { object new_ob = clone_object("/std/shadows/object/special_item"); if (new_ob) new_ob->setup_shadow( this_object(), player ); } (scripts/lpc_guard_carry_object.py --new). The earlier chain-guard pass never reached this lib: the --new regex took exponential time on discworld's source (identifier tokens could be split anywhere) and the run was abandoned; with a word boundary after each identifier the scan of the lib takes 2 seconds. Load check of the changed file, HEAD vs tree: loads in both (the statement sits in a branch that only runs for a saved special item).

log/ subdirectories nobody created: the logging write_file() / log_file() raises (2026-10-22, KB 06 §7.358)

The code of this lib writes logs into 4 subdirectories of log/: log/errors (net/intermud3/services/error.lpc), log/http (net/daemon/http.lpc), log/log/secure/playtesters (obj/handlers/library.lpc) .... Neither the archive, a git clone nor the site pack has them (no local boot wrote into them, so gen_keep_dirs.py never recorded them), and write_file() raises *Wrong permissions for opening file /log/dlog/money for append. "No such file or directory" for a missing directory: the function that logs stops on that line and what came after it (the reward, the flag, the next step) never runs. In the eval probe this lib's log_file() wrote nothing (the probe object has no write access and the sefun does not make the directory itself), so what a real caller gets was not measured. log/errors, log/http, log/log/secure/playtesters, log/secure are now in scripts/wasm_keep_dirs.txt (scripts/lpc_keep_log_dirs.py; the site and the pristine tree create the empty directories from it). Proof (scripts/lpcc_eval.py, an eval object under /adm/daemons or similar writes /log/zzdirprobe/..., fresh processes): the measured table in KB 06 §7.358.