Skip to content

Repository files navigation

The revenant icon: Elanthia's three moons — black Katamba ringed in miasma, crimson Yavash, blue Xibar, and the shards of Grazhir

Revenant

Python and DragonRealms — a monorepo of hobby projects for the DragonRealms MUD.

Most of the tooling around DragonRealms lives in the Ruby ecosystem (lich, dr-scripts, profanity, Genie plugins). Revenant is an excuse to rebuild some of that in Python, have fun, and see what sticks — docs/why-python.md records what that trade buys and costs, with the evidence. None of these projects are polished or production-ready — they are prototypes, experiments, and works-in-progress.

Shout out to Pylanthia, a great related project.

Getting started

The repo is a uv workspace. client/, chat/, and beholder/ are workspace members with their own pyproject.toml.

# Install uv: https://docs.astral.sh/uv/getting-started/installation/
uv sync                       # create the workspace venv + install everything
uv run pytest --rootdir client   # run the client test suite
uv run -- python -m client.gui.client_gui   # launch the PyQt6 client

Python 3.10+ is required. (The old telnetlib dependency — removed from the stdlib in Python 3.13 — was replaced by client/client/netsock.py, a minimal buffered socket client.)

Projects

Directory What it is
client/ A Python MUD client and engine — aims to be a lich-style middleman with a PyQt6 frontend and a (WIP) terminal frontend.
beholder/ A Dash web dashboard for character experience history, reading the SQLite log the client's bundled xp script writes.
chat/ A standalone LNet chat client, roughly a Python port of rcuhljr's Genie LNet plugin.
launcher/ Small helper scripts for spinning up a headless lich instance alongside ProfanityFE.

client

A rough-draft engine + frontends for playing DragonRealms with Python in the loop.

The PyQt6 frontend in DragonRealms: the title bar names the character, vitals bars and the roundtime counter frame the input line, stream docks route thoughts and arrivals, the compass lights up with the room's exits, and the clocks dock keeps Elanthian time and the moons beside Stockholm and Chicago

  • core — the engine/middleman between the game and whichever frontend is attached. See client/client/core.py.
  • session — a detachable session daemon that logs in, owns the game socket, and serves parsed game text to any number of frontends over localhost, lich-style. Also hosts the Python script engine (;list, ;run, ;stop) — writing your own script is a docstring and a main(s) in scripts/; see docs/scripting.md. See client/client/session.py.
  • gui — a PyQt6 frontend reminiscent of the old AOL-era Gemstone clients, rendering the game's own styling markers: amber room names, blue speech, bold alerts, and clickable command links (<d>north</d> — click to walk). Personal highlight patterns (lich-style) live in ~/.revenant/highlights.json — regexes with a color and boldness, coloring just the matched text; edit them from the View menu. File → Settings covers the autostarts and whether closing the window quits the game (~/.revenant/settings.json). Several characters play side by side, one window each: the launcher's picker (the Start Menu shortcut) lists running sessions to attach and every remembered character on every account to launch, each session on its own port. The Experience dock is a live skill dashboard (rank, percent, mindstate per learning skill), and ;xp logs the same data to ~/.revenant/xp.db for history and analysis — alongside ;sheet's stats, circle, and wealth snapshots and ;wealth's bank-balance sightings, all charted by beholder. The Clocks dock keeps Elanthian time — date, anlas, and the three moons' phases, computed from real time (docs/eltime.md) and calibrated by ;clock (scripts/clock.py) — alongside Stockholm and Chicago wall clocks, with an optional Earth-moon row for fun. The Map dock draws the community map around you, Mudlet-style — rooms and exits laid out on a grid live as you move, your own surveyed rooms outlined in violet; click any room to walk there (;go2 through the shared walker), wheel to zoom, drag to pan. Roundtime (and casttime, for casters) counts down beside the input line, driven by the game's own end-of-roundtime timestamps; vitals bars — health, fatigue, spirit, concentration, and mana when you have it — sit above it beside a status strip (posture, stunned, bleeding, hidden — and an unmissable red DEAD), and the title bar names the logged-in character. The input line keeps shell-style history — Up/Down browse what you typed (your unsent draft survives the browse), and a sent command stays selected so plain Enter repeats it. See client/client/gui/client_gui.py.
  • tui — a non-working draft of a terminal frontend. Profanity is what you actually want for a TUI today; this is just a sketch.

Python 3.10–3.12. Packaged via client/pyproject.toml as a member of the root uv workspace.

The Windows launcher

The character picker: your roster in a list, Play, Switch account, Cancel

tools/install_shortcut.ps1 installs a Start Menu shortcut (pin it to the taskbar from there) that launches windowless: pick a character from your roster and play, or Switch account… to log in as a different account. tools/make_icon.py regenerates the icon from revenant.svg.

Closing the window: quit or detach?

Closing the window (the X, File → Exit, Ctrl+Q) logs your character out: it sends quit, the character leaves cleanly instead of lingering into link-death, and the session ends. To close the window and stay in the game, use File → Detach (Ctrl+D) — the session keeps playing, and the next launch reattaches to it. If you'd rather every close behave like Detach, untick "Quit the game when the window closes" in File → Settings.

Which edits take effect when?

You rarely need to restart anything. From cheapest to dearest:

You edited To pick it up
a script (scripts/) nothing — ;run loads it fresh from disk every time (;stop x, then ;x)
the GUI Detach (Ctrl+D) the window and relaunch; it reattaches, no logout — a plain close would quit the game (see below)
the session/engine ;reexec (below) — or on Windows, where reexec is unsupported, quit and relaunch

A script that is already running keeps its old code until you ;stop and rerun it.

Hot code reload: ;reexec

Type ;reexec in any attached frontend to update the running session to the latest code without logging out — your character never leaves the game, and the connection to the server stays open the whole time.

This exists because a running session loads its code once, at startup: edits on disk don't take effect until the process restarts, and restarting used to mean logging out and back in.

How it works:

  1. The session stops running scripts, marks the game socket's file descriptor inheritable, and stashes any not-yet-parsed game bytes in an environment variable.
  2. It then execs a fresh python -m client.session --game-fd N. The exec closes the listener and every frontend connection (those are per-process); only the game socket survives, adopted by the new process via SocketClient.from_fd.
  3. The new session restores the byte buffer, rebinds the localhost port, and sends a single look to reprime its cold parser state (room title, compass).
  4. Frontends notice the drop and reattach automatically (retrying for up to ~10 s — in practice it's sub-second). In the GUI you'll see session dropped — reattaching ... followed by reattached.

Caveats: running scripts are stopped, not resumed (start them again with ;run); a session older than this feature doesn't know ;reexec, so the first upgrade still needs one old-fashioned QUIT-and-relaunch. On Windows ;reexec is unsupported (WinSock handles can't survive the exec handoff) and says so instead of trying — quit and relaunch there.

beholder

A browser dashboard for character experience history — mindstate and rank over time per character and skill, the historical companion to the GUI's live Experience dock.

The pipeline is pure Python and always on: every session automatically runs the xp script (scripts/xp.py), which snapshots the exp window to ~/.revenant/xp.db every 60s, snapshots the character sheet — stats, circle, TDPs, the full skill roster — every three hours (scripts/sheet.py), keeps a death watchdog running (;deathwatch departs an unattended corpse with the best variant your favors afford before it decays — docs/death.md), and brings the dashboard server up quietly (;stop <name> opts a session out; File → Settings or the REVENANT_NO_* env vars disable durably). Any other script can join the autostarts via Settings — "Also autostart these scripts" — e.g. lnet to be in chat from login. ;circle reads the latest sheet snapshot and reports what gates your next circle — the guildleader's answer with have/need ranks, computed from your guild's requirement table (all eleven circled guilds encoded, docs/circles.md); the dashboard shows the same gates as its Circle-gates table. View it wherever suits: View → Experience History embeds it in the client GUI, View → Beholder in Browser or ;beholder opens it in your browser, and uv run beholder runs it by hand (character dropdown, multi-select skills, mindstate plot with range buttons, refreshing experience table). See beholder/README.md.

chat

A minimal LNet client in pure Python (stdlib ssl only, no dependencies). Connects to lnet.lichproject.org:7155, verifies the server against the pinned CA in chat/LnetCert.txt plus the lichproject.org/LichNet CN check the reference client uses, handles the login XML handshake, answers pings, and parses the incoming XML stream into typed LnetMessages. See chat/chat.py. Run with uv run python chat/chat.py.

LNet names can be password-protected on the server (protocol per lnet.lic 1.15): if a name is protected, login must carry a password attribute or the server answers password required and disconnects. Configure via environment:

  • LNET_NAME — the name to log in as (defaults to Wabbajack, which is currently password-protected — set your own).
  • LNET_PASSWORD — the password for that name; alternatively put it in the git-ignored chat/lnet_password.txt. Never commit it.
  • LNET_DEBUG — set to anything for raw protocol dumps.

To password-protect a name (or change it), log in and call Server.register_password("..."); pass the literal string "nil" to remove protection. Forgotten passwords are reset at https://lnet.lichproject.org.

Typing ;lnet in a revenant frontend brings LNet into the GUI's Thoughts window — chat renders there lich-style ([Channel]-Name: "msg"), and the classic commands work as they always did: ;chat <msg> (default channel), ;chat on <channel> <msg>, ;chat to <name> <msg>, ;reply, ;who [name], ;stats, ;channels [all], ;tune/;untune <channel>. The grammar is a 1:1 port of lnet.lic, and ;chat even starts the connection on demand. ;help lnet shows the manual in-game; ;stop lnet disconnects. Replies to ;who/;stats/;channels render in Thoughts via a minimal Ruby Marshal reader (chat/rmarshal.py).

launcher

launcher/launch.py picks a free port, starts lich headless with --detachable-client, and attaches a Profanity frontend to it. Paths are currently hard-coded for the author's machine — treat it as a template rather than a turnkey tool.

Layout

revenant/
├── beholder/   # Dash/Plotly dashboard over logged experience history
├── chat/       # LNet chat client
├── client/     # PyQt6 client + engine (core/gui/tui)
└── launcher/   # Helpers for launching lich + profanity together

Status

Hobby-grade. Things are in varying states of disrepair — the client is the most actively poked at, beholder is freshly resurrected on the ;xp pipeline, chat works, and launcher is a convenience script. Expect to read code before running anything.

License

MIT (see client/pyproject.toml).

About

Python utilities for DragonRealms

Topics

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages