Final Realms

✅ 可玩

finalrealms

更新 d185eaf 2026-10-28 源码 下载 ZIP 上游 quixadhal/fr

▶ 开始游玩 · Play Now

A classic, from-scratch English-language MudOS mudlib whose own commit history dates to 1998 -- the oldest-vintage lib onboarded into this collection so far. A full game world, not a bare engine: character creation drops new arrivals into the "Hall of Creation," where a one-time `become <race>` command permanently picks one of 12 playable races (Drow, Duergar, Dwarf, Elf, Gnome, Goblin, Half-Elf, Half-Orc, Halfling, Human, Lizard-Man, Orc) before sending them to that race's own dedicated newbie zone -- real per-race taverns and shops, not a shared generic starting room. Five character guilds (Cleric, Fighter, Mage, Rogue, Warrior) round out the RPG progression, with the Mage guild alone branching into nine D&D-style specialist schools (Abjurer, Conjurer, Diviner, Enchanter, Illusionist, Invoker, Necromancer, Transmuter, and a general path), plus a full wizard-building toolset for creators.

README

Final Realms (source: <https://github.com/quixadhal/fr>) is a classic, from-scratch English-language MudOS mudlib -- the last commit in its own history dates to 1998, making it the oldest-vintage lib onboarded into this collection so far. It's a full game mudlib, not just an engine: race selection (12 playable races -- Drow, Duergar, Dwarf, Elf, Half-Elf, Gnome, Goblin, Halfling, Human, Lizard-Man, Orc, Half-Orc), a real starting-zone "newbie" world per race, guilds, shops, taverns, and a full wizard toolset.

Highlights

Registration flow

name -> confirm (y/n) -> password (real validation, hit live during testing) -> confirm password -> gender (male/female) -> drops into the "Hall of Creation" -> become <race> (one-time, permanent) -> the actual game world. Name rules: 3-11 characters, letters only (no digits).

Admin account

Registered through the normal registration flow first (as a mortal, so a real player save file exists), then promoted via the in-game promote command (which itself only works because of the gods.lpc fix above). Verified live: update /secure/simul_efun (the canonical ACL check, since it exercises both read and compile permission) succeeds for fluffos.

The archive's own original admin account, god / password god (per the game's own first-boot banner), also still works and was used to perform the promotion above -- both accounts have god-level access.

Status

Boots clean: zero compile errors and zero uncaught runtime errors in log/errors/*.err / the per-domain d/*/log/debug.err files across repeated fresh-driver boots. Full registration (a brand-new character all the way through race selection into the actual game world), look/ score/inventory/quit, re-login as an existing character, and wizard- level update/promote all verified end-to-end with a real driver session and a raw socket client.

A large compile-sweep tail remains beyond the fixes above (see NOTES.md \S3 for the full breakdown) -- the overwhelming majority is either genuinely dead/unreferenced legacy content shipped in the archive (confirmed via repo-wide reference search before excluding, not guessed), template files read as raw text rather than compiled, #include-fragment files that only compile as part of their aggregator, or a repeatable lpcc-batch-harness-only artifact (a per-monster call_out nesting counter that only accumulates because lpcc --batch loads hundreds of objects with no event-loop yielding between them -- confirmed absent on a real driver boot). None of it blocks a real boot or blocks play.

WASM status: playable. Login as god, look, and score are verified under the shared WASM driver (NOTES.md WASM status update). Play: https://mudlibs.fluffos.info/finalrealms/

Local run

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

Game port: 40216.

NOTES · 移植与修复记录

Final Realms -- porting notes

Source: git clone https://github.com/quixadhal/fr (commit 16308bd, cloned 2026-08-25). Slug finalrealms, number 168, port 40216. Mudlib root is the clone's mudlib/ subdirectory (mudos/ alongside it is a bundled v21.7b21 MudOS driver source tree, ignored -- this project uses its own driver). Original repo history stops in 1998 (git log -1 on the clone shows a 1998-11-10 commit date), making this the oldest-vintage lib onboarded this session -- a genuine late-generation classic MudOS mudlib (not a CD-driver codebase like genesis), so the compat gap with FluffOS was narrower than genesis's in kind (no &operator/@ composition, no uid:euid string-pair model) but the archive itself had more incidental bitrot/lost-content gaps.

1. Conversion

scripts/convert_lib.sh on raw/mudlib -> work: all-ASCII source (0 lossy conversions except 4 pre-existing corrupted bytes in two duplicate ave8.c files and two already-empty intermud.o save files), 3273 files renamed .c->.lpc, 2933 literal .c" references fixed, 6 local angle-bracket includes converted to quotes, 7 files static->nosave.

2. Compile-time driver-compat fixes

- secure/simul_efun/query_ed_mode.lpc (new): a simul_efun using the in_edit(object) efun (which IS available in this build) to approximate query_ed_mode() -- returns 0 while genuinely editing, -1 otherwise. Loses the driver's finer sub-states (the -2 "more" pagination prompt, per-line insert-mode prompts) but preserves the one behavior actually load-bearing across the codebase: the != -1 gate that routes input to the editor instead of normal commands. - global/new_ed.lpc's begin_editing() rewritten against the real ed(fname, exitfn_string, restricted) efun. ed()'s exit-function argument is a plain string (a function *name*), not a closure, so a new __ed_exit_dispatch() entry point was added as that name, which then invokes the actual function closure callers already pass (preserving every existing caller's API, e.g. global/line_ed.lpc's (: editor_finish_ed :)). One convenience lost: the original used ed_cmd("$a") right after starting to drop mortals/empty-file edits straight into insert mode; there's no LPC-level way to feed the just-started real ed() session a synthetic command, so editing now always starts at ed's normal command prompt. Low-severity: ed is documented in global/line_ed.lpc as an explicit opt-in advanced editor ("Please do not set your editor to ed unless you know what you are doing"), not the default player editing experience (that's a separate, unaffected pure-LPC "menu"/"command" editor in the same file). - global/process_input.lpc's ed_cmd() call site (the branch that forwarded raw input to an in-progress ed session) neutralized to a defensive error message -- under the real ed() efun the driver handles an entire editing session internally (this apply isn't even invoked while genuinely editing), so this branch can't actually fire in practice, but it still needed to compile.

3. Compile-sweep summary

scripts/lpcc_check.sh against the full 3273-file tree: started at 0/3273 (master/simul_efun themselves failed to compile -- the status-type and switch-only-default bugs above), reached 2400/3273 (73%) passing after the fixes above. The remaining ~870 failures break down as:

4. Boot and play test

Booted ~/src/fluffos/build-debug/src/driver config.fluffos (memory-capped per this project's own past-incident precaution) repeatedly; zero compile errors on every boot after the fixes above, Initializations complete. and accepting connections on 40216 every time.

Verified via a raw Python socket client (this project's established method), end to end:

Zero uncaught runtime errors logged anywhere (log/errors/*.err, d/{mudlib,vehicle,newbie,heaven}/log/debug.err) across this whole session's boots and play-testing.

WASM status update (2026-08-25, another session)

Promoted wasm_status from "" to playable. Hit a genuine, real (non-test-artifact) bug: secure/login.lpc's logon() has a hardcoded if(uptime() < 20) { ... refuse connection ... } startup-grace gate. This project's own WASM deployment boots a fresh in-browser instance per visitor, so EVERY real player would hit this on EVERY page load (not just an admin reconnecting moments after a manual reboot, which is what the check is actually for) -- per AGENTS.md's standing policy on legacy connection-time gates, bypassed for loopback: query_ip_number(this_object()) != "127.0.0.1" && uptime() < 20 (current WASM builds correctly report 127.0.0.1, confirmed via AGENTS.md's own IP-format section).

Once past that, booted and played clean: login as god/god, look and score both producing correct output matching native testing. Also found (documented, not fixed -- non-blocking, same class as ds386's optional network tools): /net/identd.lpc (an ident-protocol lookup daemon) fails to compile on connection due to socket_error()/ socket_address() being undefined (no sockets package on this driver build) -- caught gracefully, game continues normally, not on the boot/login/play path. quit wasn't recaptured in this WASM transcript but is already verified clean under native testing above and untouched by either fix.

5. Deep functional test (round two, 2026-08-27)

First full §10.7 round-two pass on this lib (confirmed via grep: no prior 深度功能测试/dated round-two heading existed). One continuous session, English-named test characters per this lib's own naming convention (quillfrost, brackenfell, plus throwaway testerbrave/brytewind during earlier diagnosis -- all four deleted from players/ before this commit, keeping only the seeded god account). Booted ~/src/fluffos/build-debug/src/driver config.fluffos repeatedly via a raw Python socket client; killed each instance by exact PID when done.

Newbie help read first: help advance (skill-training syntax) and help fighter (guild locations/requirements) -- confirmed guild training (advance <skill>) is gated on physically being inside a guild hall, not available from the open newbie zone, matching the help text's own "Location: various" framing; not a bug, just meant travel wasn't budgeted this pass (see "not reached" list at the end).

Six real, confirmed, and fixed bugs, found in this order:

5.1 room/raceroom.lpc: every new character of every race was funneled into a generic admin/coding-school hub instead of their own race's fully-built starting room, and permanently got zero starting equipment

do_become()'s success branch had startplace = "/room/entryroom.lpc"; (with the ORIGINAL, correct me->move("/room/start/"+race); commented out immediately above it) followed unconditionally by startplace->add_equipment();. /room/entryroom.lpc (an FR:Illumitech-branded admin/mudlib-coding-instruction hub, exits only to a meeting-room and post office) never defines add_equipment(), so that call_other silently no-op'd -- and since entryroom.lpc has NO exit into any of the 12 fully-built d/newbie/<race>/ zones this project's own onboarding notes describe, EVERY new character was also permanently cut off from all of that content. room/start/<race>.lpc (all 12 races have one) is a real, still-fully-implemented starting room with its own add_equipment() (or, for drow/duergar, add_clone() calls directly in setup()) AND a real exit into that race's own newbie zone -- confirmed this predates the port (byte-identical in raw/mudlib/room/raceroom.c), not something this project's conversion introduced. Fix: restored startplace = "/room/start/"+race; (this project's usual scope: a hardcoded override + wrong call_other target defeating an already-fully-implemented feature, not a content/design call -- the original commented-out line proves the intended behavior). Verified live: a fresh human character now lands in "Human entry room" (a real cottage room with leave/south/west exits, leave reaching d/newbie/human/rooms/t05.lpc) instead of the generic hall, look correctly shows "Torch. Cloak. Dagger." lying on the floor, and get all/i correctly picks them up ("Carrying: Dagger. Cloak. Two Torches." -- the extra torch is leftover from repeat testing in the same shared room instance, not a bug).

5.2 obj/misc/torch.lpc: a broken comment (/ where /* was meant) hard-failed this file's compile, silently breaking every race's starting-equipment torch (and any other torch clone) project-wide

Line 43: / Will try add_action (missing the second *) -- a genuine pre-existing archive bug (confirmed present in raw/mudlib), not a porting artifact; it was already a known FAIL in this lib's own lpcc_fail.log from onboarding but wasn't reachable/prioritized then. This is exactly why 5.1's add_equipment() fix alone wasn't enough: even with routing fixed, clone_object("/baseobs/misc/torch") (called from EVERY race's add_equipment()) returned 0, throwing "Bad argument 1 to EFUN call_other()" the moment it tried boo->move(this_object()) on the failed clone. Fix: restored the missing *. Same missing-star typo also found and fixed in obj/misc/book.lpc (6 sites, an otherwise unreferenced/dead file, fixed for completeness), d/heaven/heaven/ meeting.lpc, and room/admin/site_control.lpc (reachable via d/heaven/heaven/admin3.lpc) -- a corpus-wide grep -n '); / [a-z]' sweep found no further live instances. Added as new AGENTS.md §7.146 (a genuinely new bug class, not covered by any existing entry). Verified live: update /baseobs/misc/torch now compiles clean; the full 5.1 equipment flow above is the live end-to-end proof.

5.3 global/player.lpc heart_beat(): an abrupt disconnect crashed with an uncaught runtime error on every single heartbeat tick, forever (or until a delayed self-quit eventually finished) -- AGENTS.md §7.130, new confirmed instance with an added nuance

The old inline idle-kick else block had been commented out at some point in this codebase's history, but the comment's own boundaries left last_command = time() - query_idle(this_object()); stranded INSIDE the if (!interactive(this_object())) branch instead of removing/relocating it -- an interactive-only efun called unconditionally on a path already known to be non-interactive. Reproduced live via a genuine abrupt TCP close (no quit sent): log/errors/no_object.err immediately began accumulating "*Bad argument 1 to interactive() Expected: object Got: 0." entries (see 5.3a below for why it's interactive(), not query_idle(), in the final fixed version) roughly once per heart_beat tick, indefinitely -- this project's own scattered leftover test-character disconnects from earlier in this exact session had already been silently spamming this error the whole time before it was noticed. Fix: guard with interactive(this_object()), matching AGENTS.md §7.130's established pattern exactly.

5.3a -- second-order crash the naive §7.130 fix exposed: guarding with only interactive(this_object()) was not sufficient here. This lib's quit() calls really_quit() SYNCHRONOUSLY (immediate dest_me()) for a non-fighting player, so the vulnerable line can be reached in the SAME heart_beat() tick as the object's own destruction -- and this_object() reads back as literal int 0 once already destructed, so interactive(0) itself THROWS rather than returning false, reproducing an equivalent crash one line downstream. Fix: added an objectp() guard ahead of interactive(). Verified live: a fresh abrupt-disconnect repro, left running across a 20+ second wait (multiple heartbeat ticks), produced zero further errors, and the netdead body was confirmed reaped (who correctly shows only currently-connected players, the disconnected body no longer lingers). Extended AGENTS.md §7.130 with this nuance.

5.4 baseobs/monsters/healer.lpc / ~20 race raiserooms: the entire death/resurrection ("raise") mechanic was silently broken for every race except the one using this NPC base's own default name -- new AGENTS.md §7.144

healer.lpc's setup() unconditionally self-named via set_name("james") at clone time; /obj/monster.lpc's set_name() is a one-shot setter (no-ops once name is already non-default). ~20 per-race raiseroom files clone this SAME base object once as a persistent "nurse" NPC and then try helper->set_name(<real name>) in their own reset() (e.g. d/newbie/human/rooms/v05.lpc renames it to "june") -- every such rename was a complete no-op, so find_match(nurs, this_object())/find_living() lookups keyed on the intended name permanently failed, even though the NPC was visibly present under that display name (set_short() isn't gated the same way). Root-caused live via debug_message() tracing (log_file() would have needed elevated euid this room's object doesn't have -- see AGENTS.md §7.129's own note on this exact gotcha). Confirmed via grep: 20 raiserooms across the whole newbie-zone corpus clone baseobs/monsters/healer.lpc; at least 12 rename it to something other than "james" (d/newbie/newliz/rooms/raiserm.lpc, newelf/rooms/ raiseroom.lpc, newken/rooms/raiseroom.lpc, elf/rooms/raiseroom.lpc, human/rooms/v05.lpc, half-elf/rooms/castle/d1.lpc, dwarf/{newrooms/ raiserm28,rooms/raiserm25}.lpc, new_halfelf/rooms/h1.lpc, guests/ rooms/raiseroom.lpc, lizard/rooms/raiserm.lpc, halfling/rooms/ raiseroom.lpc) and were all affected. Fix: removed the self-naming set_name("james") call from healer.lpc's own setup(); the one caller that relies on the "james" default without ever renaming it (std/raiseroom.lpc's do_raise(), which clones a disposable, never- looked-up-by-name temporary priest) now explicitly calls priest->set_name("james") itself. Verified live end-to-end on the human zone: a test character (quillfrost) was killed by the "june" NPC, walked (as a ghost) to the raiseroom, and raise me now correctly runs the full sequence -- "The healer raises his hands...", "You reappear in a more solid form.", "Saving...", the full nurse/priest dialogue -- and a subsequent score shows the character alive again with real HP. Before the fix, raise me produced zero output and left the character permanently dead.

5.5 Five race-zone shops: buy/sell were completely dead (silent no-ops) because a broken "is the shopkeeper here" pre-check was wired as a direct verb override instead of through the base class's own dedicated hook -- new AGENTS.md §7.145

std/shop.lpc (the shared shop base) exposes set_open_condition(mixed) specifically so a subclass can gate buy()/sell()/list()/value()/ browse() (all of which check it internally via test_open()) on a custom precondition. d/newbie/human/rooms/v02.lpc (and 4 siblings: newelf/rooms/shop.lpc, newliz/rooms/shop.lpc, newken/rooms/ shop.lpc, halfling/rooms/shop.lpc) each wrote a do_check() matching that exact 0/1 contract, with the correct set_open_condition("do_check") call sitting right there in setup() -- but COMMENTED OUT, replaced with add_action("do_check","buy"); add_action("do_check","sell"); in init(). Since a subclass's own init() registrations are tried before an inherited base class's same-verb registration, do_check() always won and, since it return(1)s unconditionally whenever the shopkeeper is merely present, completely swallowed every buy/sell (silent success, nothing exchanged) -- std/shop.lpc's real buy()/sell() never ran at all. d/newbie/half-elf/rooms/town/b3.lpc has the identical broken shape but already fully commented out (dead, unreachable) -- left untouched. Root-caused live via debug_message() tracing after noticing list worked (not registered via the broken do_check at all) while buy/ sell/value all produced zero output. Fix: deleted the shadowing add_action pair, restored the commented-out set_open_condition ("do_check") call, in all 5 live-affected files.

5.5a -- second, independent bug this one was hiding behind: newelf/rooms/shop.lpc's do_check() checked present("Old woman"), but this room's actual shopkeeper NPC (chars/geldon.lpc) is named "geldon" -- a copy-paste leftover from the generic "Old woman" shop template used elsewhere. Fixed the string to match. newliz/rooms/shop.lpc and newken/rooms/shop.lpc clone/reference a shopkeeper NPC that doesn't actually exist in this archive at all (d/newbie/newliz/npcs/woman.lpc is missing; newken/rooms/shop.lpc never clones anyone) -- a genuine, pre-existing missing-NPC CONTENT gap, left untouched per this project's scope (documented with a code comment at each site); both shops will now correctly and honestly report "shopkeeper not present" instead of silently doing nothing, which is the right outcome for a real content gap.

5.5b -- a second, ALSO independent case-sensitivity bug, found only after 5.5's fix made it newly visible: v02.lpc's (and the other "Old woman" shops') present("Old woman") used the NPC's capitalized DISPLAY name (set_short()), not its actual lowercase id()/name (set_name("old woman")) -- id() on this driver (std/basic/id.lpc) is a plain case-sensitive == compare, so even after 5.5's dispatch fix, EVERY shop command (including list/value/browse, which don't go through do_check directly but do share the test_open() gate) reported "shop closed" until this string was lowercased to match. Fixed in v02.lpc and halfling/rooms/shop.lpc (the two live shops using the real "old woman" NPC). Verified live, full end-to-end transaction on d/newbie/human/rooms/v02.lpc (admin-granted test funds via call adjust_money(50,"silver") @brackenfell): list shows real stock, buy torch -> "You buy a Torch for 7 copper coins." with correct inventory/purse updates, value torch -> "The Torch is valued at 5 copper coins.", sell torch -> "You sell a Torch for 5 copper coins." with correct purse update. Before either fix: all four silently did nothing.

Observed, NOT fixed -- native add_action-registered command failures are silently swallowed somewhere in this lib's custom command-queue dispatch, distinct from and not fully root-caused

While diagnosing 5.4/5.5, repeatedly observed that when a command registered via a NATIVE add_action() call (not a cmds/-directory file dispatched through CMD_HANDLER) fails its own internal check and calls notify_fail("...") before return 0 (e.g. raise bob with no such target, value/sell for an item not carried), NEITHER that notify_fail() message NOR the driver's generic default fail message displays at all -- total silence, no crash, nothing in any error log. Meanwhile notify_fail()-based failures on cmds/-directory commands (cmds/player/kill.lpc's "Alas, your etherealness has little effect..." when dead) display correctly. Traced as far as confirming std/living/action_queue.lpc's custom per-tick dispatcher (command( curr_act), called from aq_add()/action_check() rather than the driver's own top-level process_user_command()) is the architecture involved, and that this driver's own C source (parse_command() / user_parser() / notify_no_command()) SHOULD still display either message given how command_giver is threaded through save_command_giver()/ restore_command_giver() around the raw command() efun -- but could not pin down why it empirically doesn't, within this pass's time budget. Does not block real functionality: every SUCCESS path through this same native-add_action mechanism (5.4/5.5's fixes, kill's actual attack) displays its own output correctly; this is specifically about missing FEEDBACK TEXT on certain failure paths, not a functional blocker. Documented here rather than guessed at or "fixed" blind, per this project's own standing policy -- flagged for whoever next does a deep dive on this lib (or a driver-level command()/command_giver investigation) to pick up.

Standing cross-cutting patterns checked systematically (grep + targeted live tests)

Combat, quit/reconnect, shop -- what was and wasn't reached live

Six files' fixes verified via targeted lpcc single-file/room compiles plus full native driver boots (repeated ~13 times across this session, each killed by exact PID); zero compile regressions, zero new runtime errors in log/errors/* across the final clean boot's full test replay. Throwaway test characters (testerbrave, brytewind, quillfrost, brackenfell) deleted from players/ before commit; only the seeded god account's own incidental save-state drift (last-login timestamp, playtime) and normal daemon/shop aggregate-stat drift (save/ timekeeper.o, d/newbie/human/rooms/v02.o's sales counters, a freshly-created save/death.o from this session's first-ever kills) were left as ordinary collateral of live testing, matching this project's usual convention.

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

scripts/lpc_fix_negative_ranges.py: 3 line(s) in 3 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-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-06)— 技能等级每次登录都丢失(KB 06 §7.233)

std/skills.lpc(技能控制器)声明了存档变量 skills(技能树,create() 里由 STD_SKILLS 生成),std/living/skills.lpc 继承它,又声明了自己的存档变量 skills(玩家的技能等级)。于是玩家存档里有两行 skills;restore_object() 按变量名找第一个匹配的变量(继承链最深最早的那个),两行都写进控制器的技能树,玩家自己的技能等级被清成 0。实测(只含被跟踪文件的树,god 登录):exec 给 str 加 5 级,query_skill("str") 为 5,存档第二行 skills ({"str",5,0,({}),}),退出再登录后为 0。控制器的三个变量(skills、reg_skills、stat_bonus,都在 create() 里重建,没有任何代码保存它们)改为 nosave:存档里只剩玩家的技能等级,旧存档的两行都写进等级变量、后一行为准。复测:dex 加 3 级,退出再登录后仍为 3。档案里所有玩家存档的技能等级都是空的,与此相符。

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

命令文件在第一次使用时才编译,编译警告直接打印到玩家屏幕上(如 look.lpc 的 Number of arguments to 'cmd' disagrees),所以警告就是玩家看得到的问题。

1. scripts/lpc_warnings.py finalrealms --fix:13 个 nosave 函数改 protected,补 213 处 varargs(包括 192 个两参数 cmd() 覆盖三参数基类的情形),4 处缺返回值的 return;,删 454 个未用局部变量;lpc_audit_removed_locals.py 复核 725 个,0 个需要处理。 2. 同名变量(KB 04 §6.10,都是 nosave,不涉及存档):global/more_file.lpc 与 more_string.lpc 各有一套分页状态(fsize、topl、fname、stat_line、last_search、finish_func),前者改 mf_ 前缀;std/living/equip.lpc 的私有 worn_ac 只被置 0(查询走 wear.lpc 的那份),改名 equip_worn_ac;技能控制器的树改名 skill_tree(上一节已改为 nosave);global/wiz_communicate.lpc 未使用的 my_file_name 改名;global/pweath.lpc(个人天气)的昼夜标志 light 与 std/basic/light 的光照值同名,改名 daylight。 3. net/daemon/http 的 protected private 改为 private(FluffOS 不允许两个访问修饰),现在可以编译。global/events.lpc 注释掉的旧 fix_string 结尾行里的 /* 去掉(只留关闭的 */)。 4. 启动警告 202 → 0,错误 0。改动的 .lpc 在只含被跟踪文件的树里 HEAD 与新版各编一次:352 → 353 通过,无回退。实测 god 登录,look、score、i、who 屏幕上不再出现编译警告,技能等级退出再登录后保留。本库未经 §9 格式化,本次也不格式化。 5. std/index.lpc(手册索引)与 net/daemon/remotepost.lpc 里还有 12 处 private protected(早期把 static 一律换成 protected 时,private static 变成了两个访问修饰),整个文件编译不过:函数改为 private,变量改为 private nosave(即原来 private static 的意思)。remotepost 现在可以编译;std/index 编译通过但载入时另有问题,未处理。

深度功能测试(§10.7,2026-10-06)— 巫师 man 命令

巫师命令 man 一直回答「It appears that the manual page displayer is broken.」,三处原因: 1. std/index(文档索引,obj/handlers/man 继承它)和 obj/menu(翻页显示器)都 inherit "/std/basic/wildcard.lpc",而原始存档里这个文件只在 std/basic.old/ 下。两个对象都载入失败。改为继承 /std/basic.old/wildcard.lpc。 2. 索引在 create() 后用 call_out("sort_manual_dirs", 2) 分批建立,而 sort_manual_dirs() 是 private:继承它的 obj/handlers/man 里看不见,驱动拒绝这个 call_out(apply() with insufficient permission ... sort_manual_dirs ... needs: private, has: hidden,KB 07 §8.3),索引永远停在「the system is currently loading」。改 protected。 3. obj/menu 一个没用的局部变量 is_directory:警告会在第一次 man 时直接打到巫师屏幕上,删去。 验证(临时副本,新进程):fluffos 登录,man write_file 显示 efuns/filesystem/write_file 的说明。登录时 secure/login 对 global/creator 调 set_name() 被拒(该函数是 protected,原 MudOS 版是 static,同样不能被外部调用;名字随后由存档恢复时设置),是原样行为,未改。

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

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

深度功能测试(§10.7,2026-10-10)— 基础类里的重复全局变量

分块扫描器重新整库扫描,还有 1565 条 Redeclaration of global variable(634 个文件),根源在四个基础类。这些警告在物件第一次被复制时打印(武器、护甲、怪物都不在预载里,所以启动时看不到)。

四个基础类加上继承它们的 458 个物件,HEAD 与工作树各载入一次:408 -> 408 通过,无回退;这批文件里的重复声明警告 1164 -> 22。剩下的 22 条来自三个叶子文件:d/newbie/gnome/npcs/clones/cave_rat*.lpc 为了 death() 回调又继承了 /std/actions(它继承 /std/room),而注册回调的 add_triggered_action() 早已被注释成空函数;npcs/king*.lpc 继承的 respond_give 有自己的 value。未改。

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

Deep test (§10.7, 2026-10-12): compile warnings, final pass

Warnings in files that compile: 59 -> 0 (lpc_warnings.py rescan; 2461 -> 2469 files compile, and no file gained an error). Part of this was ported back from ninetears (lpc_twin_port.py adopt ninetears finalrealms, 12 twin files: #undef before the standard.h macros redefined in net/ daemons and gopher.h/newudp.h, stalker.lpc's start_player, nameserver.lpc, quest_handler.lpc comment, unused locals).

Real bugs:

Behaviour-preserving cleanups: void overrides where the parent is void and no caller reads the value (monster set_race/set_guild, weapon set_name, six newbie shop/pub dest_me, katuran heart_beat, dankequip attack_by); string query_short_help() in four locate/rwho commands; a duplicate object board; in the duergar guild room; equiptest.lpc's worn_ac renamed equip_worn_ac as equip.lpc already does; pendant.lpc's bitwise | replaced by ||; #undef before W_BASE (armory.h after weapon.h), DEAD (tree.h), LEVEL, and PACIFY_*; a /* inside comments in inetd.lpc, testsoul.lpc and grads/npcs/path.h, rewritten so each comment still covers the same text.

listcheck: 100 objects (changed files, includers of changed headers, inheritors of changed bases), HEAD 86 -> tree 87 PASS. No load regressions; le_grimbrand5 loads newly. The deleted-declaration audit has 0 entries to review.

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. 7 file(s) fixed: /net/daemon/chars/in_finger_demon.lpc, /net/daemon/chars/tell_demon.lpc, /obj/misc/board_tale.lpc, /obj/misc/stat_book.lpc, /room/sacrifice/grim_sac.lpc, /secure/permissions.lpc, /std/vaults/vault_room.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: 4 of them failed before and load now, none regressed.

x == "a" || "b":后面的字符串常量永远为真(2026-10-19,KB 06 §7.281)

if (arg == "yuyou" || "yu you") 里的 "yu you" 是非空字符串,永远为真,整个条件对任何 arg 都成立;作者要的是每个字符串都和变量比较。在每个字符串前补上变量和运算符(scripts/lpc_fix_literal_or.py;forrest2.lpc 是 dir == "north" && "south",手工改成 (dir == "north" || dir == "south") && ...):/d/newbie/goblin/rooms/ngob15.lpc(1 处)、/d/newbie/goblin/rooms/ngob20.lpc(1 处)、/d/newbie/goblin/rooms/ngob23.lpc(1 处)、/d/newbie/goblin/rooms/ngob36.lpc(1 处)。

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: /obj/handlers/weather_room.lpc (1), /std/environ/weather_room.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, 10 terms). Changed: /d/newbie/duergar/rooms/pr.lpc (2), /d/newbie/half-elf/rooms/castle/b5.lpc (1), /d/newbie/half-elf/rooms/secret/c4.lpc (1), /doc/driver/efuns/objects/sand.lpc (1), /global/more_file.lpc (1), /std/living/baldy/equip.lpc (1), /std/living/dankequip/equip.dank.lpc (1), /std/living/dankequip/equip.lpc (1), /std/memcode.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).

apply() with insufficient permission at login (2026-10-20, known, harmless)

Creating a character, secure/login.lpc calls set_name on global/player, which is protected: the console prints apply() with insufficient permission ... function: set_name. The name is set by set_name(bong) inside player.lpc anyway (who shows it), so the line is a dead call from upstream and was left alone.

wer and whoami did not compile: efun::tell_object() outside the allowed directories (2026-10-20, KB 06 7.311)

/cmds/player/wer.lpc:226:9: error: Invalid simulated efunction override (twice) every time wer was typed, and the same in whoami: the master's valid_override() lets only secure/, std/, obj/, simul_efun, global/ and a few more call efun:: for a function the simul_efun object overrides, and a command file is none of them. Both commands now write through the player's do_efun_write(), as the Discworld mudlib they descend from does. dump_socket_status() (a MudOS efun this driver lacks) is a sefun built from socket_status(); a driver without the sockets package (the WASM build) prints the header of the table only.

每次登录控制台一条 apply() with insufficient permission ... set_name(2026-10-21,KB 06 §7.322)

secure/login.lpc 和 secure/new_login.lpc 的 begin2() 里 new_copy -> set_name(name); 调用的 global/player.lpc 的 set_name() 是 protected(原版 MudOS 里是 static,call_other 到不了它,调用一直是空操作);这个驱动对这种调用在控制台打印 apply() with insufficient permission: cob: secure/login#2, ob: global/player#8, function: set_name, origin: call_other, needs: public, has: protected,每个访客登录一次。名字其实由 move_player_to_start() 里本地的 set_name(bong) 设置(who 里是 Xqaab),所以删掉这两处死调用(留一行注释),行为不变。ninetears 的同名函数是 nomask(公开),没有这个问题。扫描时这条错误记在了字母序第一个命令 allinv 上:登录时的控制台输出被第一个命令吃掉,见 KB 08 §10.16。实测:登录后控制台没有这一行,who、score 正常。

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

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

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 22 registrations in 12 files (scripts/lpc_fix_dead_add_action.py), the verbs are left to their other handlers: /d/newbie/elf/rooms/rasrmbkup.lpc (do_raising), /d/newbie/gnome/items/parchment.lpc (give_me, put_me), /d/newbie/gnome/rooms/gnome18.lpc (do_sell), /d/newbie/orc/items/misc/corpse.lpc (get_me), /d/newbie/orc/items/misc/parchment.lpc (give_me, put_me), /global/communicate.lpc (do_echo, do_echo_to, do_emote_all), /obj/misc/board_mas4.lpc (subjects), /std/bank.lpc (close_account), /std/living/dankequip/living.lpc (hold, remove, wear, wield), /std/living/dankequip/living_creator.lpc (hold, remove, wear, wield) and 2 more files (12 files). Most frequent: remove x4, give_me x2, put_me x2, hold x2, wear x2. Renamed: /obj/misc/board_mas2.lpc new -> newx (the file defines newx), /scripts/board_mas2.lpc new -> newx (the file defines newx). Left alone (read): /global/wiz_backup/wiz_file_comm.lpc indent (the object does not compile; the registration waits until it loads).

add_action() registrations of a base whose descendants define the handler: the base itself raises when loaded on its own (2026-10-21, KB 06 §7.349)

A base class (a room, an NPC, a board) registers add_action("fn", "verb") in init() for a function only its descendants define; when the base object itself is loaded (a room one can walk into, an NPC that is cloned) its visitors get *Function for verb 'x' not found. (the first scan passed these over as alive because a descendant defines the handler; scripts/lpc_verify_handlers.py on the base object showed function_exists() MISSING). The registration now stands behind if (function_exists("fn", this_object(), 1)) (flag 1: function_exists() treats protected / private functions as not defined by default); descendants register as before. 8 statements in 1 files (scripts/lpc_fix_dead_add_action.py --guard): /global/wiz_backup/wiz_communicate.lpc (do_channels).

new(MISSING)->move() / carry_object(MACRO(..))->wear() chains guarded (2026-10-22, KB 06 §7.73 addendum)

A statement new(PATH)->move(...); / carry_object(ARMOR_D("x"))->wear(); whose path names no file of this archive: new() answers 0 and ->move() on it raises Bad argument 1 to EFUN call_other(). In create() the object never loads, in reset() every spawn below the first missing one is skipped on every reset. 12 statement(s) in 8 file(s) (clone_object, new) are now { object new_ob = new(PATH); if (new_ob) new_ob->move(...); } (scripts/lpc_guard_carry_object.py --new: the path is resolved with the macros the file itself sees, an ambiguous macro or a virtual-object path is left). Load check of the 8 changed files, HEAD vs tree: 0 load at HEAD, 7 load now (7 newly loading, 0 regress).

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 1 subdirectory of log/: log/save/board (global/line_ed.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/save/board 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.

深度功能测试(2026-10-27,§6.25 one-arg tell_object 残渣)

elf smith / smithr:tell_object("Eldevan…") → tell_object(this_player(), …)。