Skip to content
You are reading the docs for Melange 0.9.0. Read the latest.

Oasis

Oasis is Melange’s local web app: a page in your browser that talks to the running game. It is served by melange.asi itself, on 127.0.0.1 only, and nothing listens until you open it.

  • Open it from the overlay’s Oasis panel (“Open Oasis”), the Ctrl+Shift+O hotkey, or the Oasis/Open menu item; AutoStart=1 starts the server without any of that. The link has the form http://127.0.0.1:8765/?k=....
  • The link carries a secret token. The first visit swaps it for a session cookie and removes it from the address bar. Anyone with the link can use Oasis while the game runs, so do not share it. A new game session makes a new token. The overlay panel hides it behind a “Show” checkbox and has “Copy URL”; it is also written unmasked to Documents\Melange\oasis_url.txt for test scripts (deleted at the next start) and only ever masked (k=abcd...) in Melange.log.
  • Nothing outside this computer can connect. The server binds 127.0.0.1 only, answers only requests whose Host is 127.0.0.1:<port> or localhost:<port>, and accepts a WebSocket only from its own page’s origin. It never sends CORS headers, so other web sites you visit cannot read from it.
  • Repeated bad tokens are slowed down. After 10 failed requests in a minute, every further bad request in that window waits a second before it is answered, and one warning line a minute records the count. This is for visibility only; the token’s 128 bits of entropy are the real control.
  • If port 8765 is taken, the next free port up to Port + PortRange - 1 is used.
  • The overlay panel also lists connected clients with a Kick button, and the server’s own stats (clients, channels, methods, RPC calls, auth failures).
Panel What it does
Logs The live log (the log stream) in a virtualised table of up to 50 000 records: filter by level, category and text, pause, follow the newest, open a record as a JSON tree. The source menu opens a past session’s events.jsonl.
Events Pick bus messages by name or Prefix.* and watch them arrive with their decoded payloads. Nothing is streamed until you pick something. The Counts view shows every message’s rate from bus.counts.
Console Lua, as the overlay console: the client environment, a mod’s environment or the match. Enter runs, Shift+Enter adds a line, Tab completes, Up and Down recall your history (kept in the browser). Match code follows the console’s rule: refused online unless [LuaConsole] MatchConsoleOnline=1.
Mods Enable and disable mods (content mods take effect after a restart), see load errors, and revoke Deep Desert. Deep Desert is never granted from the browser: a mod waiting for consent asks in the game’s overlay.
Store Browse the plugin store: search, categories, details with screenshots and changelogs, and install, update or remove with progress. The game does the downloading and checking; the page never contacts GitHub itself. Installs, updates and removes are refused in a lobby, a network game or a match. Needs the game.
Settings Every Melange.ini key a module declares, with its default and whether it applies live or after a restart, plus the file as raw text. Saving changes that one line in place and keeps every comment and other byte. [Thumper] GrantSalt is hidden and cannot be changed, and [Thumper] AutoGrantDeepDesert can only be set to 0. This hides the salt from the Settings page; it is not a defense against RawInspect (below), which can already read it as part of the process.
Erg The map editor: open a copy of a multiplayer level, move spawn knots and objects, place oil drums and mines, set water, theme and time of day, and save the project. Needs the level service; the 3D view (three.js) loads only when a project opens.
About Versions, the protocol, and the channels and methods the server offers.

Tabs can be opened side by side (the ⧉ button next to a tab, or the command palette, Ctrl+K). The layout and the colour theme (system, light or dark) are remembered by the browser. With ReadOnly=1 every control that changes something is disabled, the console included.

The Logs panel showing a past session opened from the source menu, with one record expanded as a JSON tree

The Mods panel listing client and content mods, a pending Deep Desert consent, and a Revoke button on a mod that already has it granted

The Settings panel editing a declared Melange.ini key in place, with the grant salt masked

The capture viewer’s Summary tab for an opened .mcap file: the captured frame, scene and GPU facts, and call/texture/program counts

Key Default Meaning
Enabled 1 Load the Oasis module. With 0, nothing of Oasis runs.
AutoStart 0 Start the server when the game starts instead of on first open.
Port, PortRange 8765, 10 The first port to try and how many to try.
MaxClients 4 Browser tabs (WebSocket clients) at once.
ReadOnly 0 Refuse every call that changes something (ini, mods, captures).
RawInspect 1 Allow the read-only memory view in the entity inspector. Anyone with the session cookie can read any address the process can, including Melange’s own memory; set to 0 to turn it off.
WebRoot (empty) Serve the web app from this folder instead of the copy built into melange.asi (development; relative to the game folder).
AutoOpen 0 Also open a browser tab the moment the server starts (otherwise only the overlay button, hotkey and menu item open one).
Hotkey Ctrl+Shift+O Opens Oasis (starts the server if needed and opens exactly one browser tab); also Oasis/Open in the overlay menu.

The web app lives in web/ (TypeScript and Preact) and is built into melange.asi as a zip. The toolchain is portable and needs no npm:

Terminal window
.\scripts\web\fetch.ps1 # once: Node.js and esbuild into tools\, the packages of web\web.lock.json into web\node_modules
.\build.ps1 # type-checks, bundles and embeds the web app
.\scripts\web\test.ps1 # unit tests; add -Url <launch url> for the browser tests against a running game
.\scripts\web\test.ps1 -Panels # plus every panel in headless Edge against a mock server (no game needed)
.\scripts\web\test.ps1 -Erg # plus the Erg map editor in headless Edge (WebGL by SwiftShader) on synthetic levels

web/test/e2e/mock-server.mjs is a stand-in for the game’s server that serves a built app and fakes the streams and methods; node web/test/e2e/mock-server.mjs --root web/dist prints a link you can open by hand.

  • web/toolchain.lock.json pins Node.js and esbuild by SHA-256. web/web.lock.json lists every package, transitive ones included, with its registry tarball, sha512 integrity and licence.
  • fetch.ps1 downloads the tarballs straight from the registry, checks each against the lock, checks every licence against the allowlist (MIT, ISC, BSD-2-Clause, BSD-3-Clause, Apache-2.0, 0BSD), and unpacks them with the system tar.exe. No package manager runs and no install script executes. It also writes THIRD_PARTY.md.
  • scripts/web/lock.mjs (under tools\node\node.exe) resolves new versions from the registry and rewrites web.lock.json; updating a dependency is a reviewed diff of that file.
  • Without the toolchain, or with -DMELANGE_WEB=OFF, melange.asi embeds a one-page placeholder instead.
  • The build is deterministic: the same sources and lock give the same bundle, and the bundle’s build id is a hash of its inputs.

Melange.exe ships next to melange.asi and serves the same app and protocol with the game closed: past session logs, captures, and the mods and settings of the game folder it sits in.

Melange.exe [--game <dir>] [--browser] [--serve] [--web-root <dir>]
  • It opens its own window (WebView2) on the game folder it was installed into, or --game. --browser opens the page in the default browser instead, and --serve runs the server without a window.
  • welcome.server is "standalone"; state, entities and the other game-only channels and methods are absent (a live panel that needs them shows “game not running”).
  • While a real Melange instance is running (detected by the same Local\Melange-<hash> mutex the .asi holds), mods.setEnabled and ini.set are refused with -32003: the running game owns those files. With the game closed, Melange.exe writes them itself (Mods\thumper-state.json, Melange.ini) the same way the overlay would.
  • --web-root <dir> serves a folder instead of the embedded bundle (development); --no-open skips the browser (scripted or headless use).
  • mods.list reads every Mods\<id>\spice.json and resolves them exactly as the game would (spice::Resolve); a Deep Desert mod already granted in-game still shows Enabled, but granting it is not possible from here (Decision 4: the browser can revoke Deep Desert but never grant it, and standalone has no consent dialog to show).

A C++ module (melange::oasis::AddWebPanel) or a client mod (wum.web.panel, see lua-api.md) can add a page of its own, served at /ext/<id>/ and embedded by the shell in <iframe sandbox="allow-scripts">. That frame has an opaque origin: no cookie, and no same-origin fetch, so it cannot reach /ws or anything else in the app directly (/ext/* therefore does not itself require the token or cookie either — the frame could never present them, and its content is the same non-secret bundle already inside melange.asi; the same goes for the bridge script /app/ext.js). Its own JSON API goes through that script and postMessage to the shell, which allows only the panel’s own mod.<id>.* channels and methods plus read-only state and log, and enforces that from the trusted side, not from inside the frame.

Panels are TypeScript modules under web/src/panels/<name>/. Each registers itself and is loaded on first open:

import { registerPanel } from "../../sdk/panels";
registerPanel({ id: "hello", title: "Hello", order: 50, needs: ["game"], load: () => import("./Hello") });
Hello.tsx
import { render } from "preact";
import type { Client } from "../../sdk/client";
export function mount(el: HTMLElement, c: Client) {
const off = c.subscribe<{ n: number }>("mod.hello.ticks", undefined, (m) => { el.textContent = `tick ${m.n}`; });
return () => { off(); render(null, el); };
}

A panel imports only from web/src/sdk/ and its own folder. sdk/ui has a virtualised table, a JSON tree, a filter bar and a split pane.

melange/oasis.h:

#include "melange/oasis.h"
namespace oasis = melange::oasis;
oasis::ChannelId g_ticks = oasis::AddChannel("mod.hello.ticks");
void OnFrame() {
if (oasis::HasSubscribers(g_ticks)) oasis::Publish(g_ticks, "{\"n\":42}"); // costs one atomic load when unwatched
}
void Echo(const oasis::Call& c, oasis::Result& r, void*) { r.json = std::string(c.paramsJson); }
oasis::AddMethod("hello.echo", &Echo, nullptr); // runs on the main thread, in the Frame event
  • Publish copies the message into each subscriber’s queue and never blocks. Each client has a bounded queue per channel: DropOldest drops from the front and tells the client how many it missed; Coalesce keeps only the latest.
  • Methods run on the main thread by default, at most 0.5 ms of them per frame; kRpcServerThread runs one on the server thread instead (it must not touch the game). A method that faults three times is disabled.
  • kRpcMutating methods are refused when ReadOnly=1.

HTTP/1.1 with keep-alive, GET and HEAD only. Requests have no body; Transfer-Encoding, folded header lines, a duplicate Host, control characters and non-ASCII bytes in the target are refused with 400, more than 64 headers or 16 KB of them with 431 or 413.

Route Auth Response
GET /?k=<token> token sets the session cookie, 303 /
GET /, /app/* cookie (/app/ext.js: none) the app; ETag, Cache-Control: no-cache; .gz copies sent as-is to clients that accept gzip
GET /ws (upgrade) cookie + Origin, or ?k= with no Origin WebSocket
GET /captures/<name>.mcap * cookie a capture file, Range supported
GET /ext/<panel>/* * none (see below) a module’s or mod’s web panel, sandboxed
GET /logs/<session>/<file> * cookie past session logs
GET /erg/assets/<key>.glb|.png * cookie an Erg preview (a detail’s mesh or a theme’s material atlas); key only from level.palette or level.load, ETag = the cache key, Cache-Control: private, max-age=86400
anything else - 404

Routes marked * are added by the providers that serve them.

Every response carries Content-Security-Policy (default-src 'self', frame-ancestors 'none', …), X-Content-Type-Options: nosniff and Referrer-Policy: no-referrer, and never any Access-Control-* header. The session cookie is oasis_s_<port> (named per port, since 127.0.0.1 has no per-port cookie jar and two Oasis processes would otherwise overwrite each other’s cookie), HttpOnly; SameSite=Strict; Path=/.

RFC 6455 version 13. Client frames must be masked; no extensions; text messages must be valid UTF-8; fragmented messages are reassembled up to 1 MB; control frames are at most 125 bytes. The server pings after 20 s of silence and closes after 60 s without an answer.

Every message is one JSON object with a t member:

t Direction Members
hello client -> server proto (1), build (the page’s build id), client
welcome server -> client proto, build, server (game or standalone), caps (feature set: ["launcher"] in Melange.exe), game (exeBuild, melange), channels, methods, panels, limits
bye server -> client reason, want: sent before a protocol refusal
sub client -> server ch, filter (object, optional), id (optional; acknowledged by res)
unsub client -> server ch, id (optional)
ev server -> client ch, seq, then d (one value) or b (a batch; the items have seq, seq+1, …)
drop server -> client ch, n (messages this client missed), why (queue or rate)
call client -> server id (integer chosen by the client), m (method), p (object, optional)
res server -> client id, r (the result)
err server -> client id (or null), code, msg, data (optional)

The client sends hello first; anything else first closes the socket with 4002. seq counts every message a client was due on a channel, dropped ones included, so a gap equals the n of the preceding drop. Batches are flushed at most every 50 ms per channel. res and err are never dropped.

proto rises only for a breaking change to this envelope. New channels, methods and members are additive: readers ignore members they do not know, and clients check welcome.channels and welcome.methods before using a feature. A page whose build differs from the server’s reloads once.

Error codes: -32600 bad envelope, -32601 unknown method, -32602 bad parameters, -32000 refused by policy (msg says why), -32001 not in a match or game not running, -32002 busy (at most 16 queued calls per client), -32003 read-only, -32004 the handler faulted.

Close codes: 4000 closed by the game (the page reconnects), 4001 protocol version, 4002 no hello, 4003 auth, 4008 the client stopped reading, 4029 too many clients.

Method Result
sys.ping {frame, ms}: the game’s frame counter and the server’s clock
bus.names [{id, name, posts, deliveries}]: every registered engine message name and its counts
log.sessions [{id, files, bytes}]: the 20 most recent log sessions, newest first, the current one included; their files are served at /logs/<id>/<file>

These methods are available only when Melange.exe is running (the welcome message includes "caps": ["launcher"]). The launcher runs on its own threads; methods are tagged with their thread: M = main (UI thread), S = server, W = setup worker (I/O and hashing). ! = mutating (refused with ReadOnly=1). A new setup channel publishes status changes and progress during transactions.

Method Params → result Notes
launcher.state {} → LauncherState LauncherState: {version, firstRun, gameDir | null, theme: "system"|"light"|"dark", webview: bool, elevated: bool, protected: string[], resume?}; protected lists folders where writes are forbidden (test safety); resume is the setup.elevate action this elevated run should pick up.
launcher.quit ! {} → {} Closes Melange.exe (the button on Restore vanilla’s last screen). Files Restore vanilla couldn’t delete because this process uses them (its own Melange.exe in the game folder) are deleted once it has exited.
launcher.setTheme ! {theme: "system"|"light"|"dark"} → {} Saves to launcher.json and updates the window caption’s dark mode.
launcher.launch {} → {how: "steam"|"exe"} Starts the game via steam://rungameid/70600 (Steam installs) or WormsMayhem.exe directly (GOG/other).
launcher.openPath {what: "game"|"logs"|"backup"|"export"|"incompatible", id?, folder?} → {} Opens a folder in Explorer: the game folder, the Melange logs folder, a backup by id, or Mods\.incompatible (with folder, a sweep notice’s .incompatible\<name>). export shows the zip the last launcher.exportLogs wrote, selected.
launcher.exportLogs {reveal?: bool} → {path, bytes, entries, onDesktop, sessionId | null, pid} Zips the last game’s logs (the newest session folder, the recordings and desync bundles its pid wrote, dumps and engine logs from its time window, launcher.log) to the Desktop, else Documents\Melange\exports, with the same redaction as the in-game export, then shows it selected in Explorer unless reveal is false. -32002 while another export runs.
launcher.shortcuts ! {startMenu: bool, desktop: bool} → {} Creates Start menu and/or desktop shortcuts to Melange.exe in the game folder.
setup.detect {} → {candidates: Candidate[]} Locates game folders (Steam, GOG, saved, or manually browsed). Each Candidate: {path, source: "saved"|"self"|"steam"|"gog"|"manual", library?, check: GameCheck}. GameCheck includes the exe’s size, SHA-256 hash (if readable), and verdict: "ok", "wrongBuild", "noExe", "notFound" or "unreadable".
setup.browse {start?: string} → {path | null, hint?: "child", child?: string} Shows a folder picker (Windows native dialog, UI thread). hint: "child" when the user picked a parent folder.
setup.validate {path} → GameCheck Checks a game folder: exe presence, size, timestamp, SHA-256, and build verdict.
setup.select ! {path, save: bool} → SetupStatus Selects a game folder for this run (and saves it if save: true). Refused unless verdict is ok.
setup.status {} → SetupStatus Game folder state: loader identity and version, Melange state and last-load time, duplicates, legacy files, backups. SetupStatus: {game: GameCheck | null, running, melangeLoaded, loader: {state: "none"|"ual"|"other", dll?: DllInfo}, otherLoaders: DllInfo[], melange: {state, version?, path?, duplicates: string[], lastLoad?: {at, version}}, ini: {present, missingKeys}, legacy: string[], payload: {ok, version, missing: string[], fromGameFolder}, backups: [{id, created, action, files: [{path, op, description?}]}], install?: {...}}.
setup.plan {action: "install"|"repair"|"uninstall", replaceLoader?, allowDowngrade?, removeData?} → Plan Previews changes: the steps that would be taken, their reason, and any choice needed (e.g. which loader to replace). Plan: {planId, steps: [{op, path, detail}], needsChoice?: "loader", refused?: string}.
setup.apply ! {action, ..., planId} → ApplyResult Executes the plan if unchanged. Reports progress on the setup channel. ApplyResult: {ok: true, backupId?, status: SetupStatus} on success, or an error code (-32010 needs elevation, -32012 rollback failure).
setup.restore ! {backupId} → ApplyResult Restores a previous installation from a backup as a transaction.
setup.deleteBackup ! {backupId} → {} Deletes a backup (no undo).
setup.setMelangeEnabled ! {on: bool} → SetupStatus Disables Melange by renaming melange.asi ↔ melange.asi.off (the game runs vanilla).
setup.vanillaPlan {} → VanillaPlan Restore vanilla’s preview: every file in the game folder not in the stock #1077 list (embedded in Melange.exe), minus game-written files (local.cfg, Default.cfg, steam_appid.txt, user.cfg, *.csh, XOM*-*.log, Net_*.log, Redist\). VanillaPlan: {planId, refused?, groups: [{id: "melange"|"renewation"|"wumpatch"|"loader"|"reshade"|"specialk"|"dgvoodoo"|"mousefix"|"asi"|"other", label, files}], files, bytes, sample: string[], replays: string[], replaysDir, modified: string[], modifiedCount, missing: string[], missingCount, overwrites, verify, store, selfInGame}. modified/missing: stock files whose size differs or that are gone; verify when there are any, or a framework that overwrites stock files (Renewation HD, WUMPatch) was found. Refused (with refused) like setup.plan: wrong build, game running, protected folder.
setup.vanillaApply ! {planId} → VanillaResult Moves *.wsr and desync-*.zip to Documents\Melange\replays (never overwriting), then deletes the planned files permanently (no backup) and the non-stock folders left empty. Then launcher.json forgets the game folder, first run, default plugins and the last update check (theme, window and other preferences stay), staged updates are deleted, and with verify on a Steam install steam://validate/70600 is opened. VanillaResult: {ok, deleted, dirsRemoved, moved: [{from, to}], replaysDir, failed, modified, missing, verify, verifyStarted, store, selfPending}. Errors: -32013 plan changed, -32010 needs elevation (setup.elevate {resume: "vanilla"}), -32012 some files could not be deleted, -32002 busy.
setup.elevate {resume: "install"|"repair"|"uninstall"|"restore:<id>"|"vanilla"} → {} (then exits) On Windows, elevates to administrator, re-runs the launcher with --game <dir> --resume <action>, and exits the current process.
plugins.settings {id} → {decl: Setting[], values, defaults} Declaration and current values of a plugin’s settings. Setting: {key, type: "bool"|"int"|"float"|"string"|"enum", label, default, min?, max?, options?: string[], help?, optionLabels?: Record<string, {label, help?}>}.
plugins.setSettings ! {id, values: Record<string, value>} → {values} Writes settings to Melange.ini [Mod.<id>], with validation. Errors: -32602 {key, why} for type/range/enum failures.
plugins.resetSettings ! {id} → {values} Resets to the user’s default or the declared default.
defaults.get {} → Defaults The user’s default set of plugins and their settings. Defaults: {plugins: [{id, enabled, settings: Record<string, value>}], seeded: bool}.
defaults.set ! Defaults → Defaults Saves the user’s defaults.
recommended.get {} → {source: "index"|"builtin", items: Recommended[]} The recommended plugins (from the store index, or a built-in fallback if offline). Recommended: {id, name, description, why, settings, installed, compatible, reason?, decl?: Setting[]}.
recommended.apply ! {items: [{id, settings}], saveAsDefaults: bool} → {queued: string[]} Installs and enables the selected recommended plugins. Reports progress on the store channel. queued lists the plugin ids being downloaded.

Channel setup: {status: SetupStatus} on every change (folder selection, game start/stop, file changes), and {progress: {action, step, of, label}} during a transaction (each step’s name, current step number, total steps).

Melange updating itself (developer guide):

Method Params → result Notes
update.status {} → UpdateStatus UpdateStatus: {current, phase: "idle"|"checking"|"downloading"|"ready"|"current"|"error", latest?, htmlUrl?, progress?: {got, total}, error?, lastCheck?, applied?: {ok, gameUpdated, version, message, at, warnings}, auto}. auto: Check for updates automatically (update.setAuto). ready: a newer release is downloaded and verified. error only follows a check you asked for; the automatic one at start fails silently. applied is what the last Restart to update did, reported once by the launcher that started after it.
update.check {} → UpdateStatus Looks at GitHub’s latest release now, and downloads it in the background if it is newer (a no-op while a check runs). Works with automatic checks off.
update.setAuto ! {on: bool} → UpdateStatus Turns automatic checks on or off: the launcher’s at start (autoUpdate in launcher.json) and the game’s daily one ([Update] CheckInGame in the game folder’s Melange.ini, written when it disagrees and the file exists; a running game reads it at its next start). On also checks now. Publishes on update. -32002 while another setup change runs.
update.apply ! {} → {} (then exits) Starts the downloaded Melange.exe with --apply-update and closes this one. Refused (-32000) with nothing ready or while the game runs (“Close the game to update.”), -32002 while another setup change or an import runs.

Channel update: {status: UpdateStatus} on every change, including download progress (at most 4 a second).

Settings › Display (developer guide):

Method Params → result Notes
display.get {} → DisplayState DisplayState: {monitor: {w, h}, modes: [{w, h}], windowed: {w, h} | null, source: "local"|"default"|"none", localCfg, exclusive, fullscreen, enabled, hotkey, melangeIni, running, refused?}. monitor: the primary monitor in pixels; modes: its display modes and the common 16:9 sizes no larger than it, largest first; windowed: the /W /H the game opens at (local.cfg over Default.cfg, source says which); exclusive: /FS is in local.cfg; fullscreen, enabled, hotkey: [Display] in Melange.ini; refused: why display.set would be refused now (the game runs, no folder).
display.set ! {fullscreen: bool, width, height} → DisplayState (+ removedFs: true) Writes /W /H into local.cfg (everything else in it kept; created when missing) and [Display] Fullscreen into Melange.ini (and Enabled=1 when turning it on). Turning fullscreen on removes /FS from local.cfg, and the result says so with removedFs. Refused (-32000) while the game runs, and for fullscreen without a Melange.ini; -32602 for a size under 640x480 or not whole pixels; -32002 while another setup change runs.
Method Params → result Notes
lua.eval {target: "client"|"match"|"mod", mod?, code} → {ok, text} Main thread, one per frame across clients; code at most 64 KB; =expr is shorthand for return expr. A Lua error is ok:false with the message. match outside a match is -32001, and refused online (-32000, the console’s reason) unless [LuaConsole] MatchConsoleOnline=1. Refused with ReadOnly=1.
lua.complete {target, mod?, prefix} → [string] The names that can follow the last . or : of prefix.
mods.list {} → [ModInfo] ModInfo: {id, name, version, authors, dir, kind: "client"|"content", state, reason, on, restartRequired, implicitManifest, hasClient, hasSim, deepDesert: {declared, granted}, order, source: "store"|"local", sandbox?: {loaded, error, callbacks, disabledCallbacks, faults, bytes}}. state is enabled, disabled, blocked, pending-consent, incompatible or restart-required (Melange.exe uses the same names). source is store for a plugin with a record in Mods\.store\installed.json or a map pack a Store importer made. Melange.exe also lists a folder whose spice.json does not parse, as incompatible.
mods.setEnabled {id, on} → ModInfo As the overlay’s checkbox: persisted; client-only mods apply at once, content mods at the next launch. Enabling a Deep Desert mod does not grant it: the consent prompt appears in the game.
mods.revokeDeepDesert {id} → ModInfo There is no method to grant Deep Desert.
mods.view {} → {showLocal, notices: [Notice]} What the Mods pages show beside the list: whether local plugins are listed (thumper-state.json showLocal, off by default) and the compatibility sweep’s notices, oldest first. Notice: {key, id, name, version, action: "quarantined"|"removed"|"updated"|"failed", reason, melange, at, folder, detail, text} (folder is .incompatible\<name> for a quarantined plugin; text is the line to show). Also in Melange.exe.
mods.setShowLocal {on} → mods.view result Display only: a hidden local plugin keeps its switch and still loads. Melange.exe refuses it while the game runs.
mods.dismissNotice {key?} → mods.view result Without key, every notice. Also in Melange.exe.
ini.get {} → {path, encoding, text, keys} keys: [{section, key, def, live, declared, current, line?}], every declared key plus the undeclared ones in the file; def is null for undeclared keys and current is null for keys missing from the file. The grant salt reads ********.
ini.set {section, key, value} → {live, restart, changed} One line is changed in place (indentation, spacing, key spelling and an inline ; comment kept) or added after the section’s last key, and the file is replaced atomically in its own encoding. The key must be declared, present in the file, or in a [Mod.<id>] section. Values cannot contain line breaks or ; or start or end with a space. [Thumper] GrantSalt, and [Thumper] AutoGrantDeepDesert other than 0, are refused (-32000).

The mods channel sends the whole mods.list result to a new subscriber and again on every change (Coalesce).

The plugin store’s methods are game-only. Each mutating method returns at once and the work runs on the Store’s own thread, reporting on the store channel; the mods channel reports what changed in Mods\ afterwards. Install, update and remove are refused (-32000, with the reason) in a lobby, a network game, a match, while a level loads or an Erg Test runs, and with -32002 while another one is running. Mutating methods are refused with -32003 under ReadOnly=1.

Method Params → result Notes
store.status {} → Status Status: {enabled, indexUrl, customIndex, fetchedAt, offline, serial, plugins, job: {phase, id, version, bytes, total, message}, gate, fetching, haveIndex, rollback, busy, error, pending, notices}. gate is "" or why changes are refused right now.
store.refresh {} → {started} Fetches the list (a no-op while a fetch runs). The panel calls it when it opens; nothing is fetched before that.
store.list {query?, category?, filter?: "all"|"installed"|"updates", incompatible?} → [Item] Item: {id, name, authors, description, categories, kind: "client-only"|"content", unsafe, licence, latest, compatible, size, installed: {version, managed, state, enabled} | null, action: "install"|"update"|"remove"|"none", canRemove, state, reason, error}. Updates first, then by name.
store.details {id} → Item + {homepage, permissions: {unsafe, filesystem}, content, dependencies, conflicts, screenshots: [{n, caption, ready}], versions: [{version, released, melange, size, changelog, yanked, compatible}], dependants, conflictsEnabled, plan: [{id, version}], planError} Starts fetching the plugin’s screenshots; each one arrives as a store event.
store.install {id, version?, enable?: true, replaceManual?: false} → {started} Dependencies in plan are installed first. A copy of the mod put in Mods\ by hand is replaced only with replaceManual.
store.update {id} → {started} Store-installed plugins only.
store.remove {id, deleteData?: false} → {started} deleteData also removes [Mod.<id>] from Melange.ini and the mod’s saved data.
store.cancel {} → {cancelled} Download phase only.
store.openHomepage {id} → {} Opens the listing’s https:// homepage in the default browser on the game’s PC.

The store channel (Coalesce) sends {phase, id, version, bytes, total, message, pending, fetching, busy, gate, serial, shots, shot?}; phase is idle, fetching, downloading, verifying, installing, removing, done or error. GET /store/shots/<id>/<n> serves a screenshot the game has already fetched and verified (404 before that), with the same cookie as the app.

Plugins can declare a recipe to import content (such as maps) from a download or local file on the player’s machine. Every importer is driven through these RPC methods. See importers.md for the recipe schema and safety model.

Method Params → result Notes
import.list {} → {importers: Importer[]} Every installed plugin with a valid recipe
import.status {plugin} → Importer Status of one importer
import.browse {plugin} → {path, name, size} or {cancelled: true} Native file picker for .zip files
import.start {plugin, source: {kind: "download", id} | {kind: "file", path}, keepZip: bool, accepted: true} → {started: true} Start an import; progress on the import channel. accepted: true confirms the disclosure. File paths come only from import.browse.
import.cancel {plugin} → {cancelling: bool} Stop the current job
import.maps {plugin} → {maps: Map[], packs: Pack[]} Maps and packs from the current import, filtered and categorized per the recipe
import.setHidden {plugin, files: string[], hidden: bool} → {hidden: number} Show or hide maps by file name
import.setPacks {plugin, packs: [{id, enabled}]} → {packs: Pack[]} Enable or disable packs (changes online matching)
import.uninstall {plugin, deleteZip?: bool} → {removed: string[]} Remove all imported packs
import.deleteZip {plugin} → {freed: bytes} Delete the cached download

The import channel (Coalesce) publishes {importers: [Importer]} on subscribe and {job: Job} on every phase change and at most every 200 ms during transfer/building; see importers.md for the Job shape.

The streams (log, bus, state, …) and the other methods are listed in welcome as their providers load.

Channel Payload (d/b) Filter Overflow
log {seq, lvl, cat, ts, j} (j: the raw jlog line); backlog of the last 500 matching records on subscribe minLevel, cats (list), text (substring, matched against the raw line) DropOldest + drop
net as log, restricted to the net and handshake categories as log DropOldest + drop
bus {seq, frame, name, cls, path, handle, d?} (d: bus::Decode’s fields, when the filter asks for it and a decoder exists) names (required: exact name or Prefix.*), path ("post"; only Post is hooked), decode DropOldest + drop
bus.counts {name: delta} for every name whose post+deliver count changed, once a second - Coalesce
lobby {inLobby, local: {hash, contentMods, modMessages, vanilla}, peers: [{steamId, name, status, hash16, version}]}, on change and at least once a second - Coalesce
stats oasis::GetStats() plus render::GetTiming() (busyMsP50, busyMsP95, fps, frames), once a second - Coalesce

The bus channel’s hook is installed only while at least one client subscribes to it, and only messages named by some subscriber’s filter reach it at all (Camera.HasUpdated included). bus.counts reads the bus module’s own always-on counters, so it costs nothing extra to keep the hook installed or not.

The Game state panel shows the worms and teams of the current match (the active worm and team highlighted), the live entities by kind (worms, projectiles with their weapon, crates, barrels, everything else by class name), a top-down or side map of their positions, the game’s data variables, and an inspector with the known fields of an entity and a read-only hex view of its memory. Everything is read on the game’s main thread, at most at the rate a client asks for; nothing is ever written. The readers need build #1077 and [GameState] Enabled=1; otherwise the state channel says available: false and the methods answer -32001.

Channel Payload Filter Overflow
state a snapshot: {available, frame, matchSerial, match, teams, worms} hz 1-10 (default 5) Coalesce
entities [{handle, object, vtable, kind, type, label, pos, vel}]; [] outside a match hz 1-5 (default 2), kinds Coalesce
  • match: {inMatch, attract, online, currentTeam, activeWorm, turnMs, turnMsLeft, roundMs, roundMsLeft, windSpeed, windDir, waterLevel, turnsStarted, suddenDeath, theme} (times in ms, -1 for no team or worm, wind direction in radians). turnsStarted and suddenDeath come from the engine’s message counters for this match. attract is true while the menu’s attract demo runs, whose match also reports inMatch.
  • teams[]: {slot, name, active, ai, local, colour, alliance, roundsWon, score}.
  • worms[]: {slot, team, posInTeam, name, active, alive, health, physicsState, weapon, pos, vel}; positions are world units with +Y up, vel is the engine’s raw value in world units per millisecond of game time (Lua’s wum.game.worms() gives it per second), weapon is -1 for none.
  • Outside a match teams and worms are empty. kind is one of Worm, Projectile, Crate, Barrel, Other; pos and vel are {x, y, z} or null when the class’s position is not known.
Method Params → result Notes
state.get {} → a snapshot the same snapshot as wum.game.worms() in the same frame
state.vars {prefix?} → [{name, type, value}] every data variable (about two thousand), value as JSON; containers as {addr, class}; one call per second per client (-32002)
entities.list {kinds?} → the entities payload -32001 outside a match
entity.inspect {handle, len?} or {addr, len?} → {addr, len, vtable, type, kind?, handle?, fields, hex} len 1-4096 (default 256); hex has two characters per byte, ?? where memory is not readable; {addr} and hex need [Oasis] RawInspect=1 (-32000 / null otherwise)

The raw view reads committed, readable pages only (never a guard page) and copies under a fault guard.

See erg.md for the panel; this is the wire surface it and xomtool level use. level.export, level.test and level.build write into Mods\ or the offline Test workspace and are refused (-32003) in Melange.exe while a real Melange instance is running, as mods.setEnabled is. level.test also needs the game running, at the frontend and outside a lobby (-32001 / -32000 otherwise), so it does not exist in Melange.exe at all.

Method Params → result Notes
level.list {} → {bases: [{key, stem, title, source, theme}], projects: [{id, title, stem, base, modified, built}]} every vanilla Level_Type 0 map plus every enabled pack’s, and your own projects
level.new {base, slug, title} → a project an empty patch against base
level.load {project} or {base, surround?} → an erg-scene/1 or /2 result, then one bin frame per blob (surround: true adds the base’s .hmp as an hmp blob) see web/src/sdk/erg/scene.ts for the shape
level.save {project, patch} → {saved, warnings} validates the whole patch (erg-patch/1 or /2) against its pinned base
level.export {project, modId, name, version, mode: "install"|"source", survivor?: bool} → {dir, files, restartRequired} see erg.md; survivor (default false) adds a Survivor copy
level.build {modId} → files written rebuilds a Source-form pack’s map files against this install (what its build.ps1 also does)
level.test {project, tod?} → {key, state} builds into the Test workspace, arms a one-shot override, starts Quick Game itself when the game supports it; tod is DAY, EVENING or NIGHT for this Test only
levels.live {modId, on} → {ok, reason} enables or disables a maps-only pack without a restart: game only, at the main menu, offline (see erg.md)
level.themes {} → themes, times of day, material files from the install’s own Data\Themes
level.palette {theme} → placeable entries {name, resource, role, preview} preview is an /erg/assets/ key
level.objects {} → {crateKinds, weapons, utilities, limits, error} crate contents from the install’s Data\Tweak\WEAPTWK.XOM; error is null when it was read
level.materials {file, base?, source?} → {file, names} the record names of a level material file (Databank.MaterialFile), at most 64, for second-material paint; a pack base’s own Maps\<stem>.txt is read from the pack
level.close {project} → {} frees the server’s parsed copy of the base
level.script.get {project} → {text} the project’s script.lua, "" when it has none
level.script.put {project, text} → {saved, problems: [{line, message}], syntaxChecked} a level script’s rules (≤ 256 KB, UTF-8, no BOM or ESC); "" removes it; not saved when these fail. In the game the script is also compiled (not run) by the engine’s Lua 5.0: a syntax error is saved and listed in problems. Melange.exe checks the bytes only (syntaxChecked: false); the syntax is checked when the level is tested in the game

The erg channel (Coalesce) carries {state, key, detail} from level.test’s progress (idle, registering, registered, armed, starting, playing, ended, failed) and, at the start of a match, {level, water}.