LumiRealm: RisuAI Compatibility Runtime
Published:
RisuAI is a popular character-driven chat application with its own character-card format (.charx), scripting layer (Lua + a curly-brace macro DSL called CBS, ~179 functions), and a runtime for trigger code, lorebook decorators, and DOM-injected widgets. LumiRealm is a compatibility extension that re-implements that runtime on top of Lumiverse’s host APIs so an existing RisuAI card runs natively, without conversion or re-import.
The work splits cleanly across three stages of a card’s lifecycle:
1. Import & translation. A chunked WebSocket upload feeds bytes into a translator pipeline that does, in order: read the .charx (bare ZIP or polyglot JPEG+ZIP, with per-entity fault isolation and zip-bomb defense), decode Risu’s 512-byte byte-substitution codec (“rpack”), then run a chain of mappers that project Risu shapes onto Lumi’s shapes: character fields (CCSv2/v3 → Lumi character), lorebook entries with every flag preserved, regex scripts with a four-mode substitute-macros picker, “at-actions” (@@emo, @@repeat_back, @@inject, …) compiled into script-pack entries or native regex rows, and Risu triggers compiled by a 96-opcode V1+V2 compiler down to an AsyncFunction body. CSS gets a font-hoist + island-merge wrap so multi-top-level cards don’t shatter across shadow roots. SVGs are classified (simple / theme-reactive / animated / templated / dangerous) and rasterized on the frontend via Image + OffscreenCanvas so the host’s sanitizer doesn’t strip them. Raw card bytes are stashed alongside the translated output so a future schema bump can re-translate without asking the user to re-upload.
2. Generation lifecycle. Inside the worker, every chat event flows through an AsyncLocalStorage-scoped dispatcher (userIdAls, inheritedVarsAls, dispatchAls) so concurrent users never cross-attribute. The Lua bridge uses fengari with a Promise-to-coroutine yield adapter, exposing ~37 host-bound globals (chat / vars / LLM / state / lorebook / regex). Trigger code dispatches by binding (start / request / output / display / manual) with a type-bypass for Risu’s triggerlua / triggercode. A Tier 2/3 lorebook decorator runtime gates entry activation (is_greeting, activate_only_after, keep_activate_after_match, exclude_keys, …) and stages inject_lore / inject_at / pt_* content into per-chat 60-second buffers consumed by the prompt-assembly interceptor. listenEdit chains (editInput / editRequest / editOutput / editDisplay) get a 150ms cross-chain TTL cache so per-trigger IPCs don’t dominate the loop. Self-echo writes are filtered out by a 60-second content-hash window so the runtime doesn’t react to its own state mutations.
3. Render-time pipeline. Every message paint hits a render-origin hook in the host’s message-content processor. The CBS evaluator (a port of Risu’s risuChatParser) runs scoped blocks (risu_if / risu_when / risu_each) with a variable-overlay LRU, then ~179 macro handlers across 16 category files (arrays, assets, chat-context, math, logic, …). HTML islands are extracted by the host and rendered into per-message shadow DOMs; LumiRealm adopts a 143KB Risu CSS bundle rescoped at sheet-build time so .prose / .chattext selectors map to :host, splits @imports because CSSStyleSheet.replaceSync rejects them, and quote-marks dialogue with a MutationObserver so themes can color it. A runtime DOM-clone portal lifter uses getComputedStyle().position === 'fixed' post-cascade to hoist virtualization-trapped fixed widgets into a body-level overlay. A 5-second content-hash-validated LRU caches editDisplay output per bubble, with an in-flight promise table so racing renders share a single scan.
Migrations. A lazy chat-open re-translation runner reads the stashed source bytes and re-runs the current translator whenever stored cards are at an older schema version, replacing regex scripts and refreshing risuPayload without requiring a re-import. Pre-source-storage legacy cards get a one-shot reimport modal instead.
In-app discovery. A built-in browser (“RisuRealm”) hits Risu’s realm.risuai.net (download) and sv.risuai.xyz (search) and, when a card isn’t a .charx, synthesises one in-memory from a PNG/JSON before handing it to the translator.
The repo has two tracked branches (main against released Lumiverse, staging against unreleased host APIs), with extensions auto-switching branch from the UI. ~400+ unique users.