Final Realms

✅ 可玩

finalrealms

更新 fdf581a 2026-09-12 源码 下载 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.