Genesis mudlib (CDlib)

✅ 可玩

Genesis

genesis

更新 0008455 2026-10-20 源码 下载 ZIP 上游 genesismud/mudlib

▶ 开始游玩 · Play Now

The official mudlib of Genesis, one of the oldest and most influential LPMuds still running (source: <https://github.com/genesismud/mudlib>). This is the CD gamedriver-native mudlib itself, not a snapshot of the live game world -- per the repo's own README: "This repo does not contain a standard domain which will need to be implemented to use the mud." There is no `/d/Genesis` domain here: no rooms, no NPCs, no items, no race std files, no wizard guild content. What ships is the engine: master object, login/ character-creation infrastructure, the std/ base classes (living, player, room, container, weapon, armour, ...), the command souls, the board/mail/ bank/donation/report subsystems, and the full wizard toolset.

README

Highlights

Registration flow

new -> character name -> confirm (y/n/q) -> password (min. 8 chars, 2+ non-letter characters) -> confirm password -> in. No race/gender/stat selection screen (that content is missing, see above) -- new characters start as a plain human by default.

Admin account

Registered through the normal registration flow first (as a mortal), then granted rank by adding an entry to config/sys/local.h's DEFAULT_WIZARDS map -- normal new-character registration refuses any name already present there (by design, to stop name-squatting on a wizard's identity), so the name has to exist as a real character first. Verified live: wizard-only soul commands work (update <path> -- the canonical ACL check, since it exercises both read and compile permissions -- successfully recompiled /secure/simul_efun.lpc end-to-end).

Status

Boots clean: zero compile errors and zero uncaught runtime errors in log/debug.log across repeated fresh-driver boots. Full registration, soul-command play (smile, wave, ...), quit/save, and re-login as an existing (including wizard) character all verified end-to-end with a real driver session and a raw socket client. Since no domain ships, a mortal's very first login runs straight into the mudlib's own graceful "your starting locations are corrupt" fallback and disconnects with an explanatory message -- expected, not a bug (see NOTES.md); wizards are exempt from that disconnect and can play normally in the bare bootstrap landing room this port added specifically so login/testing wasn't blocked entirely (secure/login/bootstrap_room.lpc).

A few residual gaps (documented, not blocking): a small number of files are intentionally not standalone-compilable (#include-fragment files that only compile as part of their aggregator, and two read_file()-as-text room-generator templates); one non-fatal "Denied write permission" logged during some wizard-login save/restore-privilege sequences that doesn't block the actual save (both attempts and the resulting file are confirmed correct); lib/link_room.lpc is dead code (never inherited anywhere in this repo). See NOTES.md for the full list.

WASM status: playable. Login as fluffos, soul commands, and quit are verified under the shared WASM driver (NOTES.md WASM status). Play: https://mudlibs.fluffos.info/genesis/

Local run

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

Game port: 40213.

NOTES · 移植与修复记录

Genesis -- porting notes

Source: git clone https://github.com/genesismud/mudlib (commit 92ce6844, cloned 2026-08-25). Slug genesis, number 166, port 40213.

1. What this repo actually is

Per the repo's own README: "This repo does not contain a standard domain which will need to be implemented to use the mud." This is the engine (master, login, std/ base classes, command souls, board/mail/bank/donation/ report subsystems, the wizard toolset) with no /d/Genesis domain at all -- no rooms, no NPCs, no items, no race std files, no wizard guild rooms, no docs directory content that the code references. Every gap documented below as "missing domain content" traces back to this one fact, not to anything this port broke.

2. Compile-sweep: CD-driver -> FluffOS dialect differences

This is a large (225 .lpc/.h files), mature CD gamedriver codebase, so essentially every category of CD-vs-FluffOS incompatibility this project has previously catalogued showed up here too:

- doc/man/efun/m_indexes: "if an integer is given instead of a mapping, 0 is returned" -- FluffOS's keys() hard-errors instead. Fixed in the m_indices() simul_efun (hit for real: secure/master/player.lpc's query_seconds() calls m_indices(m_seconds[first]), legitimately 0 for anyone with no seconds on file, checked on every single login). - doc/man/efun/member_array: "if an integer is given instead of an array, -1 is returned" -- FluffOS's real member_array() efun hard-errors. Shadowed with a simul_efun of the same name (bypassing to the real efun via efun::member_array() for the normal case). Hit for real: std/player/cmd_sec.lpc's check_valid_startloc() calls IN_ARRAY(env, SECURITY->query_list_def_start()) where that function doesn't even exist (a separate, genuine pre-existing bug -- the real functions are FPATH_FILENAME's query_def_start_locations()/ query_temp_start_locations(), not SECURITY's query_list_def_start/ query_list_temp_start, which exist nowhere in this codebase) -- every mortal's very first quit hit this. - doc/man/efun/filter: mapping-filter value-only-callback semantics, see above.

3. add_action() bare function-value bug (the headline bug)

add_action(FUNCTION_VALUE, "verb") -- e.g. add_action(quit, "quit"), using the bare function name as a value, the standard idiom throughout this codebase -- compiles cleanly, and the resulting sentence shows up in commands(), but is never actually matched against typed player input on this driver. Confirmed via lpcc/live-driver testing: switching a handful of calls to the string form (add_action("quit", "quit")) made them work immediately, with no other change.

This wasn't a narrow issue: 151 of 158 add_action() call sites in the whole codebase used the bare-value form, including std/living/cmdhooks.lpc's add_action(my_commands, "", 1) -- the catch-all verb ("") that is the entry point into the entire soul-command dispatch system. With that one call broken, no soul command (the emote soul, wizard souls, everything) worked for anyone, wizard or mortal -- while it *looked* like a soul-loading problem (the "Yikes, baaad soul: /d/Genesis/cmd/misc" message, from the genuinely-missing NPC_SOULS second entry, printed on every keystroke) the actual root cause was upstream of the soul system entirely.

Fixed with a tree-wide mechanical sweep (145 more sites, on top of 7 manually fixed earlier while chasing the initial symptom) converting add_action(IDENT, ...) -> add_action("IDENT", ...). One site was not converted: std/room/exits.lpc's ugly_update_action(object player, string cmd, function fun) { add_action(fun, cmd); } takes a genuine runtime function parameter, not a compile-time-known name -- there's no way to recover a string name from an arbitrary function value on this driver (function_name()/function_object() don't exist here either, a previously-documented gap) -- left as-is, already flagged by an existing comment in that file as a pre-existing "de-dup regression" from an earlier, unrelated fix.

4. set_auth-based uid/euid model: "#" placeholder bug

secure/simul_efun.lpc implements set_auth()/query_auth() (and the getuid/geteuid/seteuid/setuid/export_uid efuns built on top of them) as a from-scratch simul_efun shim, because the CD driver's uid model is an arbitrary, freely-settable "uid:euid" string pair per object (via set_auth(ob, "uid:euid")), which doesn't map onto FluffOS's simpler native single-string-uid/single-string-euid model at all.

setuid(), seteuid(), and export_uid() all used a literal "#" string as a placeholder for "the other half of the pair, don't care right now" -- e.g. setuid() did set_auth(previous_object(), CREATOR + ":#") (uid set correctly, euid set to the placeholder). The bug: getuid()/geteuid() never actually resolved "#" back to anything (they only special-case the string "0" as "unset"). So the very next call in this codebase's own standard idiom, setuid(); seteuid(getuid(this_object()));, would read back the *other* field's real value correctly, then immediately overwrite it with "#" when setting the field it was updating -- seteuid(str) did set_auth(ob, "#:" + str), permanently stomping the uid field it should have left alone.

Net effect: every single object that went through this standard idiom (used throughout std/living.lpc's create_container(), and elsewhere) ended up with a uid of the literal string "#" after its very first seteuid() call -- silently breaking any *later* getuid()-based permission check on that object. This is exactly why the new-player bootstrap (\S5) failed with "Denied write permission in save_object()" on its first save attempt: export_uid(), called from secure/master.lpc's save_player() wrapper, read the corrupted "#" uid and propagated it into the object being saved.

Fixed at the source: export_uid(), seteuid(), and setuid() in secure/simul_efun.lpc now always preserve the field they aren't updating (read it first, write it back unchanged), and setuid()'s euid default is "0" (which geteuid() *does* resolve, to unset/none) instead of "#". One direct call site outside the simul_efun file had the exact same bug and needed the same fix: secure/master/fob.lpc's reset_wiz_uid() (called from std/player.lpc's reset_userids(), i.e. every wizard login) did set_auth(wiz, name + ":#") directly.

Every other set_auth(this_object(), "#:root")-style call throughout secure/master.lpc (15+ occurrences) is not the same bug -- those are the master object's own well-understood, intentional self-referential placeholder (master's own uid is never actually checked anywhere in this codebase, only its euid), left untouched.

Residual, non-blocking: one "Denied write permission in save_object()" is still logged during some wizard-login save/quit euid-transition sequences (traced to std/player/cmd_sec.lpc's save_me(), which explicitly does seteuid(0) immediately before calling SECURITY->save_player() as part of a "drop privileges, let master re-grant them" pattern) -- but the actual save consistently succeeds regardless (the resulting playerfile is correct both times it was checked, and the user-visible "Saving <name>." + clean disconnect always appears). Not fully root-caused given the time budget for this pass; flagged for anyone doing deeper testing on this lib later.

5. Missing character-creation bootstrap (secure/login/ghost_player.lpc)

config/sys/local.h's LOGIN_NEW_PLAYER/LOGIN_TEST_PLAYER macros point at /d/Genesis/login/ghost_player -- unlike most of this repo's other missing-domain-content gaps (narrative/world content with a graceful fallback already built into the surrounding code, left undefined/empty and documented, see \S6), this file never existed anywhere in the upstream repo at all, and it's genuinely load-bearing *infrastructure*: secure/login.lpc clones it and calls open_player()/enter_new_player() on it to create every single new character, and secure/master.lpc's load_player()/save_player() gate every player save/load on LOGIN_NEW_PLAYER->legal_player(). Without some real implementation here, character creation -- and therefore save/load entirely -- was impossible.

Added a minimal, functional bootstrap implementation at secure/login/ghost_player.lpc (inheriting /std/player, config updated to point at it) rather than a recreation of whatever richer race/stat- selection wizard the original surely had (that content is lost along with the rest of the missing domain). It:

A related, previously-dead piece of functionality got wired up in the same pass: std/living/cmdhooks.lpc's load_wiz_souls() (loads a wizard's rank-appropriate command souls, e.g. update, into wiz_souls) was never called from anywhere in this codebase -- genuinely disconnected functionality as shipped, not something this port broke, presumably originally wired into the same missing character-creation/promotion flow. The soul *files* themselves (cmd/wiz/apprentice.lpc, cmd/wiz/wizard.lpc, etc.) are all present and compile cleanly in this repo, so this was a "reconnect existing working code" fix, not content invention: called from std/player.lpc's setup_player(), in the existing if (query_wiz_level()) branch.

6. Missing domain-content symbols (compile-blocking, left content-neutral)

A large number of symbols are referenced throughout std/, cmd/, and secure/ but were never actually #defined anywhere in this repo -- they are normally domain content. Left undefined, each is a hard "Undefined variable/function" compile error (not just a missing-content gap), so a real definition was needed just to get the mudlib to compile at all. Defined in secure/auto.h (this project's chosen "global include file") or the relevant local file, each with a FluffOS PORT NOTE explaining the choice:

7. ARMAGEDDON stale path + bootstrap landing room

Unlike the missing-content symbols in \S6, ARMAGEDDON (the world-shutdown-orchestrator singleton, used pervasively -- checked unconditionally on every single login attempt by secure/login.lpc) is not missing content: its real implementation ships in this repo at secure/armageddon.lpc, exactly like SECURITY/DOCMAKER/MAP_CENTRAL/ etc. (all defined in sys/files.h pointing at their real /secure/... locations) -- but the ARMAGEDDON macro itself, in config/sys/local.h, was still pointing at the original domain path /d/Genesis/obj/armageddon, never updated to match. This turned every login into a hard, uncaught call_other() couldn't find object crash. Fixed by pointing the macro at /secure/armageddon instead.

Separately, secure/login/bootstrap_room.lpc was added: a deliberately bare, purely-functional landing room (std/room base, no exits, no decoration -- explicitly documented in its own header as infrastructure, not game content) used as WIZ_ROOM and every RACESTART entry (\S6). Without *any* real room anywhere in this repo, character registration could never complete at all (see \S5/\S6's circular-dependency discussion) -- this unblocks login/testing while making zero claim to represent anything in an actual game world.

A handful of OWN_STATUE (/d/Genesis/obj/statue, another missing-content path, used for a "linkdead players become a statue" feature) call sites that skipped the objectp(find_object(...)) guard most other call sites in this codebase already use were fixed the same way (secure/login.lpc, std/player.lpc x4, secure/armageddon.lpc) -- this crashed a wizard's very first quit/password-check flow uncaught.

8. std/container.lpc's add_subloc() foreach-on-int crash

add_subloc(string sloc, mixed resp, mixed ids) -- ids is varargs; every 2-arg call site (the overwhelming majority, including std/living/inventory.lpc's wear_reset()/wield_reset()/hold_reset(), called unconditionally during every single living's creation, players and NPCs alike) leaves it at its default mixed value, 0. The subsequent foreach (string aid in ids) hard-errors on this driver ("Bad argument 2 to foreach: Expected array Got: 0") -- the CD driver evidently tolerated a 0-iteration loop over a non-array. This crashed every single living object's create(), silently truncating it partway through (before enable_commands()/cmdhooks_reset() ever ran) -- the underlying cause of "no commands work" being observable well before the \S3 add_action() bug was even found. Fixed with a pointerp(ids) guard defaulting to ({ }).

9. Miscellaneous fixes

10. Fragment/non-standalone files (false positives in a naive lpcc

sweep)

~45 files fail a flat, one-file-at-a-time lpcc --batch sweep but are not bugs -- they are intentionally not standalone compilation units:

A handful of additional files (std/corpse, std/workroom, std/herb, std/board, doc/workroom, secure/armageddon, secure/mbs_central) show as lpcc failures purely from a create()-time runtime crash inside lpcc's isolated single-object test harness (e.g. a restore_object() permission error in a context with no real filesystem/euid setup) with zero actual compile errors -- a previously-documented "lpcc-vs-live- driver artifact" pattern for this project; the real driver boot (which exercises the actual preload chain in the actual runtime environment) shows these compiling and loading cleanly.

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

Promoted wasm_status from "" to playable. pack_lib_for_web.sh initially failed outright (not a driver/compile issue): doc/examples and doc/man/Genesis were dangling symlinks pointing at ../d/Genesis/doc/... -- the very game-world domain this archive never shipped (see above) -- and emscripten's file_packager errors out trying to stat() a broken symlink's target. Removed both (pure documentation symlinks, irrelevant to boot/play) rather than fixing their targets, since the domain they point at doesn't exist in this repo at all. Once packed, the driver booted clean under WASM with no further fixes needed. Verified with a real scripted WASM session: login as fluffos/Mud@2026, the documented start-location fallback firing correctly (no /d/Genesis domain to land in), a soul command (smile) dispatching correctly (confirming the add_action() bare- function-value fix carries over to the WASM build), and a clean quit ("Saving Fluffos. Goodbye. Until next time."). One caught-and-logged (non-fatal) error during quit -- call_other() couldn't find object '/d/Web/stats/webstats' inside a catch() -- is the same class of graceful missing-domain-content fallback as the start-location one, not a new bug.

Two sibling bugs ported over verbatim from arkadia's §10.7 pass (2026-08-27)

arkadia (libs/arkadia) is a direct Polish-localization fork of this exact engine, and its own round-two deep functional test (a real, played session, unlike this lib's own short WASM smoke test above) found two live crashes in files byte-identical between the two ports. Both are confirmed present here via direct code reading and a real boot-and-kill compile check (no domain content exists in this repo to drive a full interactive repro the way arkadia's own NOTES.md does):

1. secure/master/fob.lpc's do_decay() was declared to take a single mixed *darr parameter, but decay_exp() (called periodically from check_memory(), itself on a ~900-second reset_master() alarm) invokes it via map(m_domains, do_decay) -- this driver's map() over a MAPPING always calls its function with (key, value), not value alone, so the one declared parameter silently bound to the domain NAME (a string) instead of its data array, corrupting the domain name character-by-character on every decay tick and eventually crashing with "*String index out of bounds" once the walk ran past the string's own length. Fixed by adding the missing leading string dname parameter, exactly as in arkadia. See libs/arkadia/NOTES.md and AGENTS.md §7.132 for the full live repro (reproduced there against real bootstrap domain data, since this repo's own m_domains never gets populated with anything given it ships no domain content at all). 2. std/room/description.lpc's room_descs global was declared nosave mixed room_descs; with no initializer (defaulting to 0), but long() unconditionally calls member_array(0, room_descs) on every single room look/glance -- for any room that never called add_my_desc() (the vast majority), this threw "*Bad argument 2 to member_array() Expected: string or array Got: 0" uncaught, silently truncating that room's entire description. Fixed with a = ({}) initializer, identical to arkadia's fix. See AGENTS.md §7.133.

Neither fix was exercised through a live interactive session on THIS repo specifically (there is no real room/domain content here to walk into), only via a real driver boot-and-kill compile check confirming both files still compile clean; a future deep-functional pass on any lib that eventually builds real content atop this engine (or a WASM re-test of this repo itself) should re-verify live.

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

One continuous session per AGENTS.md §10.7. This lib had never had a full round-two playthrough before (confirmed via this file having no "深度功能测试"/dated round-two heading), only the original onboarding verification plus the two sibling-fix ports above (immediately re-verified live, see below). Registered real English-named characters (Tamsworth, Zebulon, Wendlebury, each deleted before committing) through the full new -> name -> confirm -> password -> confirm flow, exercised look/stats/vitals/health/i/smile at every state change, tested quit with a debug.log grep before and after every session, and reconnected both immediately and after a real wall-clock gap. Admin login re-verified: fluffos/Mud@2026 (no x-suffix quirk needed here, unlike arkadia) still authenticates and shows WIZ_KEEPER rank. No newbie-help file exists to read first (this repo's own doc/man/general/INTRO is a wizard LPC-coding tutorial, not player-facing help — expected, given no domain content ships at all); the intended "test path" is simply the bootstrap landing room itself.

Six new, previously-undetected, live-reproduced bugs found and fixed this pass, plus the two already-ported sibling fixes re-confirmed live where this repo's content-free nature allows it, plus one genuinely unresolved observation documented honestly rather than guessed at.

Re-verification of the two already-ported sibling fixes

1. check_valid_startloc() calling two SECURITY functions that don't exist anywhere in this codebase — every mortal quit silently treated as "not a recoverable location" (AGENTS.md §7.135's write-up, different architecture than the arkadia original)

std/player/cmd_sec.lpc:134 (quit()'s own inventory-drop gate) called IN_ARRAY(env, SECURITY->query_list_def_start()) / SECURITY->query_list_temp_start()) — functions that exist NOWHERE in this codebase (already flagged, but left unfixed, in this file's own §2 "documented CD-driver graceful degradation" section, because the member_array() leniency shim happens to absorb the resulting undefined-function-call fallout as a graceful -1 rather than a crash). Fix: swapped to the real, already-existing VALID_DEF_START_LOCATION()/VALID_TEMP_START_LOCATION() macros (sys/files.h), which wrap sys/global/filepath.lpc's real valid_def_start_location()/valid_temp_start_location() functions. This is the closest genesis analogue to arkadia's own §7.135 finding (both are about quit()'s inventory-drop gate silently misclassifying a legitimate location) — but the ROOT CAUSE differs: arkadia's was a missing lazy-init guard on one sibling accessor out of several; genesis's sys/global/filepath.lpc initializes its equivalent globals eagerly in create() (no lazy-guard shape to be missing at all) and the actual bug was a wrong callee entirely. Verified live: standing in the bootstrap room and quitting no longer silently treats it as non-recoverable.

2. DEF_STARTING_PLACES never updated to include the one real starting room this port has — "SERIOUS PROBLEM with your start location" printed on literally every mortal login

config/sys/local.h's DEF_STARTING_PLACES (used by sys/global/filepath.lpc to validate a mortal's default_start_location) still lists only the five original /d/Genesis/start/<race>/... paths — none of which exist in this repo. std/player/savevars_sec.lpc's set_default_start_location() requires VALID_DEF_START_LOCATION() for any non-wizard, so it silently rejected the only real starting room this port added (/secure/login/bootstrap_room, per RACESTART) on every single mortal login — enter_game()'s first try_start_location() attempt always failed as a result (the accessor never actually got updated), printing "SERIOUS PROBLEM with your start location. You revert to your default racial start location." before falling through to the racial-default fallback (query_def_start(), called directly, bypassing the broken accessor), which happened to still succeed. Fix: added /secure/login/bootstrap_room to DEF_STARTING_PLACES, exactly the same "let login work" rationale already used for WIZ_ROOM/RACESTART in the same file. Verified live: a fresh mortal character's login no longer shows the SERIOUS PROBLEM message at all.

Residual, wizard-only: wizards bypass set_default_start_location()'s validation entirely (query_wiz_level() is checked first), so this specific fix does not change their path — see the unresolved observation below for why the seeded admin account still shows this message every login despite having a correctly-saved default_start_location on disk.

3. Missing basic player command souls — no mortal OR wizard could look, get, drop, say, or use ANY ordinary verb (AGENTS.md §7.136, new entry)

The headline bug this pass. See AGENTS.md §7.136 for the full technical write-up (root cause, the raw/secure/proto_char.o evidence trail, and the fix). Summary: setup_player() strips a mortal's entire cmdsoul_list on every login relying on the (missing) race content to grant the real set back; nothing does, so load_command_souls() fell back to the NPC-shaped NPC_SOULS default, which is missing every ordinary player verb soul. Fixed with a new DEFAULT_PLAYER_SOULS constant (secure/auto.h), sourced directly from this repo's own shipped raw/secure/proto_char.o, reseeded for both mortals and wizards. Verified live: a freshly registered mortal (Tamsworth/Zebulon/Wendlebury, all three tested) and the previously-broken seeded admin account (fluffos) both now have working look/i/stats/smile/every ordinary verb.

4. command("$...") — CD-driver quicktyper-bypass convention with no equivalent on this driver, 13 silently-dead call sites (AGENTS.md §7.137, new entry)

See AGENTS.md §7.137 for the full write-up. Summary: doc/man/efun/command documents a leading "$" as "bypass the quicktyper" — this driver's real command() has no such handling at all, so every command("$verb ...") call site silently dispatched a literal, nonexistent "$verb" and always failed. Found because the very first look after every login (start_player()'s command("$look")) always silently failed. Swept all 13 call sites (std/player.lpc, std/living/possess.lpc, std/living/move.lpc, std/player/cmd_sec.lpc's quit() drop calls, std/launch_weapon.lpc, cmd/live/things.lpc, cmd/std/tracer_tool.lpc, cmd/live/thief.lpc x5, cmd/wiz/arch.lpc), stripping the leading "$" — confirmed safe since this codebase's own quicktyper's alias-interception hook, modify_command(), is itself dead code on this driver (never called, zero references in driver source), so there was no alias-interference risk to preserve. Verified live: the login auto-look now correctly shows the room description immediately, with no manual look needed.

5. move_living()'s magic-map notification: unguarded call_other() on a 0 object on EVERY successful move, project-wide

std/player.lpc's move_living() override notifies a "magic map" item of every successful move (!result means success, per this function's own return-value convention) via magic_map-> notify_new_room(to_dest), where magic_map is set from present(MAGIC_MAP_ID) — but never falls back to anything if no such item is present (true for every move in this content-free repo, since no magic map item ships anywhere). This driver's call_other() hard- errors on a plain int(0) target ("Bad argument 1 to EFUN call_other()") rather than degrading gracefully — unlike several other missing-item call sites elsewhere in this port, this one had no objectp() guard. Confirmed live: logged in debug.log on literally the first move of a brand new character's registration, and every move after, project-wide (the existing catch() suppressed the crash but not the log noise). Fix: added an objectp(magic_map) guard before the call, matching the same pattern already used pervasively elsewhere in this port for other missing-item call sites.

6. quit()'s own skill-decay alarm outliving the just-destructed player object — a guaranteed dangling-alarm error on every mortal quit

std/player/cmd_sec.lpc's save_me() (called from quit() right before the player object is destructed) schedules set_alarm(1.0, 0.0, decay_skills) whenever query_skill_decay() is true — which it always is after the very first login, since setup_player()'s setup_skill_decay() unconditionally sets do_skill_decay = 1 for every mortal. quit() then immediately calls this_object()->remove_object(), destructing the player one second before that alarm fires — producing "*Owner (.../ghost_player#N) of function pointer is destructed", uncaught, in debug.log, on literally every single mortal quit. Same underlying shape as arkadia's own AGENTS.md-documented dangling-alarm finding (§8.5 there), but on the universal player-quit path instead of a peripheral login helper, and reproducible on every quit rather than an edge case — not yet in AGENTS.md as its own numbered entry, since it's a single narrow instance so far, not (yet) confirmed on a second lib; worth promoting to a numbered cross-cutting entry if it recurs. Fix: added skill_decay_alarm (with query_skill_decay_alarm()/ set_skill_decay_alarm() accessors, needed because std/player/cmd_sec.lpc is a text-#included fragment positioned ABOVE this variable's own declaration in std/player.lpc, so a bare reference to it is "Undefined variable" at compile time) to track the outstanding alarm id, and a remove_object() override that cancels it before deferring to ::remove_object() — a single destruction choke point that also covers any OTHER removal path, not just quit(). Verified live: a full driver restart, fresh mortal registration, quit(), and debug.log grep immediately after shows ZERO dangling- alarm errors (only the two already-documented, benign missing-content catch()-wrapped errors remain: /d/Genesis/cmd/misc at object creation time, /d/Web/stats/webstats on quit).

Unresolved observation: login_time/login_from/(for wizards only) default_start_location never actually restore from a correctly-saved value, despite restore_object() reporting success

Confirmed live, root cause NOT confirmed within this pass's time budget — documented honestly per this project's own testing standard rather than guessed at. Symptom: every single login (fresh or returning, mortal or wizard) prints "Last login at: Wed Dec 31 16:00:00 1969" / "Last login from: 0" — the Unix epoch defaults — even immediately after a session that demonstrably set_login_time()/ set_login_from()'d real values and saved cleanly (confirmed via direct inspection of the .o file on disk: login_time 1787852608/login_from "localhost" are genuinely present and correct). Added temporary write() debug instrumentation directly after load_player()'s own ret = restore_object(PLAYER_FILE(pl_name)) call (removed before committing) and confirmed: restore_object() returns 1 (success) but query_default_start_location()/ query_login_from()/query_login_time() are ALREADY empty/0 immediately afterward, on the SAME line, before any other code has a chance to touch them.

This is NOT visible on mortal accounts for default_start_location specifically, because enter_game()'s own logic unconditionally resets that ONE field for non-wizards regardless of what restore produced (masking the failure) — it stays visible for login_time/ login_from on EVERY account (nothing resets those) and for default_start_location on wizard accounts specifically (the reset path is !query_wiz_level()-gated). This is why the seeded admin account (fluffos) still shows the "SERIOUS PROBLEM with your start location" message on every single login even after fix #2 above — the .o file's default_start_location is correct, but never actually gets restored into the live object before the check runs.

Best working hypothesis (NOT confirmed as the actual mechanism): std/player/savevars_sec.lpc declares default_start_location as the 7th name in a single long comma-separated private string a, b, c, ...; statement, following several fields (player_file, path, mailaddr, adj_desc) that are never set for a plain bootstrapped character and therefore never appear as lines in the saved file at all (this driver's save_object() omits fields still at their compile-time default) — a "sparse" middle of that declaration list. login_from/login_time sit in similarly-shaped later declarations. Every field BEFORE the first gap (name, password) restores correctly; every field tested AFTER a gap does not. This is consistent with (but not proven to be) a #pragma save_binary restore-side quirk on this driver where a sparse/gapped save file causes later name-keyed lines to misalign against the variable table — as opposed to a bug in this port's own code, since the .o file's own content is unremarkable, well-formed, and byte-for-byte what a human would expect to see.

Why this is flagged as an observation rather than "fixed": (a) no gameplay-relevant data loss was ever observed — race_name, cmdsoul_list, learn_pref, acc_exp (all declared in a DIFFERENT file, std/living/savevars.lpc, the base class rather than the player-specific fragment) restore correctly and consistently across every tested session; (b) the affected fields are cosmetic/display-only (a "Last login" banner) except for the wizard-only SERIOUS PROBLEM message, which is itself harmless and self-healing via the existing fallback chain; (c) arkadia (the sibling engine, byte-identical savevars_sec.lpc) does not report this same symptom in its own NOTES.md after its own §7.135 fix, suggesting this may be specific to some other, not-yet-identified difference in genesis's own file layout or save history rather than a generic engine/driver bug — guessing at a fix here risks a worse, harder-to-diagnose regression than leaving it alone. Worth a focused follow-up pass specifically tracing restore_object()'s behavior against a deliberately constructed minimal test object with the same "sparse gap in a multi-variable declaration" shape.

Standing cross-cutting patterns checked systematically

Shop/economy/combat/guild — unreachable, unchanged from onboarding

Same as arkadia's own honest disclosure: this repo ships no domain content at all, so shop/economy, combat, and guild/skill acquisition remain explicitly UNVERIFIED this pass — not something this pass's driver-bugs-only scope permits fixing, and there is no real content anywhere in this repo to reach them through. The bootstrap landing room (secure/login/bootstrap_room.lpc) is the only room; no NPCs, no items, no shops exist to test against.

Verification summary

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

scripts/lpc_fix_negative_ranges.py: 67 line(s) in 36 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)— 主控 author_file()/domain_file()

驱动的 mudlib_stats 包在主控载入时各调用一次 author_file(<主控文件>) 和 domain_file("/")。本库主控没有这两个 apply,所以每次启动都打印 author_file() in the master file does not work, using root_uid as fallback 和 domain_file() in the master file does not work, using bb_ui as fallback 两行,网页版每位访客的终端里都看得到。secure/master.lpc 补上了这两个 apply:作者返回 get_root_uid(),域返回 get_bb_uid(),正是驱动缺少它们时所用的值。域名等于 backbone 域时,驱动让对象沿用创建者的域,与没有这个 apply 时相同(KB 06 §7.222)。本库保留上游代码风格(几乎所有文件都未经 §9 格式化),所以只插入代码,不格式化。用 lpcc config.fluffos /nonexistent.lpc 前后对比,主控载入段只少了这两行。

深度功能测试(§10.7,2026-10-05)— map/filter 语义、mbs 中心守护进程

1. FluffOS 对映射的 map()/filter() 传 (key, value),CD 驱动只传 value;secure_var() 的 map(var, secure_var) 把每个映射变成 key:key,lib/cache.lpc 的读取全部被破坏。secure/simul_efun.lpc 加了 CD 语义的 map/filter 覆盖,sefun 内部的 secure_var 改用 (: secure_var($2) :)(KB 06 §7.223;arkadia 同样修复)。 2. secure/mbs_central 一直载入失败:create() 只调用 seteuid(ROOT_UID) 而不先 setuid(),query_auth() 对未登记的对象返回 "0:0",valid_seteuid 拒绝,随后 restore_object(/data/mbs_central) 报 read permission denied,于是每个 mbs 命令都拿不到数据。CD 驱动会自动给新对象创建者的 uid;照 arkadia 的写法,query_auth() 在没有 set_auth() 记录时返回 creator_object(ob) + ":0"。现在 mbs_central 正常载入,mbs、mbs L 无错误。data/mbs_save/<字母>/ 补了 26 个 .gitkeep(保存路径 SAVE_DIR + 首字母 + "/")。

深度功能测试(§10.7,2026-10-05)— set_alarm 闹钟按对象归属

secure/simul_efun.lpc 用一张全局表模拟 CD 驱动的 set_alarm()。get_all_alarms() 返回所有对象的闹钟,而 std/object.lpc 的 reset() / enable_reset() 会先删掉列表里所有名为 reset 的闹钟再排自己的,结果任何一个对象 reset 都会取消其他所有对象的 reset,房间实际上不再重置(NPC、物品不刷新)。用 lpcc 载入两个房间验证:改前两个房间看到同样 5 个闹钟、只剩一个 reset;改后各自只有自己的 reset。现在每个闹钟记录 previous_object() 为主人,get_all_alarms() / get_alarm() / remove_alarm() 只作用于调用者自己的闹钟,返回 CD 格式;主人已销毁的闹钟到期时直接丢弃(KB 06 §7.224)。genesis 原来的 get_all_alarms() 只返回 id 整数、get_alarm() 返回函数,std/object.lpc 的 reset 扫描以及 torch / corpse / resistance 里的 get_alarm(id)[2] 都会出错,现在与 arkadia 用同一份实现。

深度功能测试(§10.7,2026-10-05)— 编译警告清理、带种子的 random、set_alarm 字符串形式

1. 编译警告:启动时约 690 条 → 0;整树扫描 1489 条诊断 → 305 条,剩下的全是被单独编译的 #include 片段(cmd/wiz/apprentice/*、secure/master/*、std/living/*、std/player/*_sec),在宿主对象里编译时没有警告。scripts/lpc_warnings.py genesis --fix:675 个 nosave 函数改 protected(被别处按名调用的 24 个去掉 nosave 保持 public)、删 236 行 CD 专用 #pragma、删 210 个未用局部变量;lpc_audit_removed_locals.py 复核 349 个被删声明,0 个需要处理。手工:10 处负数下标 [-1]/[-5] 改 [<1]/[<5](这个驱动里是越界错误:item_expiration 的房间、skill_raise 的 max/级数解析、map_central 文件名与行尾空格、master 的内存统计、report_central、herb);master.h 的 add_wizard_to_domain、heart_beat、library 的 done_writing、launch_weapon 的 find_projectiles 原型改成与定义一致;std/object.lpc 的 set_adj 改为返回 int(玩家的覆盖版本返回 int,apprentice 读取它);三处嵌套 /* 注释(master.lpc 一段注释少了 */,没有代码被吞);#if 分支里才用到的局部变量移进分支;spells.lpc、messenger.lpc 两处菱形继承显式写出驱动本来就选用的那个定义。登录、tree、mbs、Dump、finger、compare 全程 0 警告,无 insufficient permission。 2. 带种子的 random(KB 06 §7.225):CD 驱动的 random(range, seed) 对同一种子总给同一个数,compare/估价按两个物件的编号取种子,避免反复比较把受技能限制的估计平均成真值。上线时 5 个文件里 45 处的种子被删掉了(state.lpc 留下 5 个无用的 seed 变量)。按 raw/ 原文把种子放回,simul_efun 加了 random(int range, mixed *seed...):有种子时对种子做固定散列。 3. set_alarm() 接受 CD 的完整形式 set_alarm(float, float, function|string, args...):字符串是调用者自己的函数名,多余参数传给它(std/callout.lpc 的 call_out() 就这样用);get_alarm() 返回参数(KB 06 §7.224)。 4. secure/master.lpc 的 valid_read 对 #include 放行(binaries、data 仍关闭)。驱动拿主控当读者检查每个 include,结果取决于主控当时的 euid,而 preload_boot() 会把它设成被载入文件的创建者。arkadia 清完警告后就因此载入失败,因为以前每条警告都调用 log_error(),顺手把 euid 设回 root(KB 06 §7.226)。genesis 目前没有触发,这里是预防性修改。

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

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

Punctuation the conversion lost, put back (2026-10-19, KB 03 4.6)

1 file(s) lost a semicolon, comma, quote or the closing } at the end of the file and did not load. The file stopped one or two } short (its last byte or two went missing); appended: /lib/std_area/preamble.lpc. Loaded at HEAD and in the tree: all 1 were failing before, none regressed (scripts/lpc_restore_from_twin.py, lpc_fix_eaten_quote.py --comma, lpc_fix_eof_braces.py).

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

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

目录读出后的后缀截取宽度(2026-10-20,KB 06 §7.296)

.c 改名成 .lpc 之后,get_dir("*.lpc") 读出的文件名还按两个字符截取($1[0..<3]):baguadao.lpc 变成 baguadao.l,用它拼出的路径加载不到对象(故事、战场、事件、帮助、自造物品数据、召唤列表等)。改成 [0..<5](scripts/lpc_fix_getdir_strip.py)。改动:/cmd/wiz/wizard.lpc(1 处)、/secure/map_central.lpc(1 处)。

压缩包没带的可选模块(2026-10-20,KB 06 §7.308)

/d/Genesis/ 和 /d/Web/ 是压缩包里没有的领域内容:armageddon 加载 NPC_SOULS 时对 /d/Genesis/cmd/misc 的 call_other 失败(第一次登录打印 12 段回溯),quit() 里 catch(WEBSTATS_CENTRAL->player_logout(...)) 对 /d/Web/stats/webstats 失败;catch() 吞掉了错误,但本驱动在 master 没有 error_handler 时仍把被捕获的错误打到控制台。config/sys/local.h 的 NPC_SOULS 去掉了缺失的那一项,std/player/cmd_sec.lpc 在调用前检查 file_size(WEBSTATS_CENTRAL + ".lpc")。