Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Omegga

Omegga wraps Brickadia’s server console to provide interactivity and utility via plugins along with a web interface for managing your server.

Join the discord to browse plugins and get support.

Start here

Installinglinux, WSL, or a container
Runningstarting a server and keeping it updated
Configurationomegga-config.yml, field by field
Pluginsinstalling them, and writing your own
APIwhat a plugin can reach
Troubleshootingwhen it does not start

What omegga can do

  • Automatically update/restart your server
  • Manage your worlds from a web interface and load a world on startup/restart
  • Chat with players while not on the server
  • Read chat history with timestamps
  • See kick and ban history
  • Configure plugins from a web interface
  • Manage permissions and multi-user role based access to the above features on a web ui

What plugins can do

  • Interface with in-game wires and react to in-game wire events
  • Add custom chat !commands and /commands
  • Respond to and send chat messages
  • Load bricks onto a player’s template
  • Load/Clear regions of bricks, entities
  • Damage/heal players
  • Give/remove weapons to players
  • Change the environment
  • Teleport players, detect player’s positions
  • Grant players roles
  • Detect when a brick with an interact component is clicked

Installing Omegga

Omegga runs on linux. Pick the one that matches where you are starting from:

Linuxa linux machine or VPS you already have a shell on
Windows (WSL)Windows, through the Windows Subsystem for Linux
Containersdocker or podman, with node and omegga already in the image

The container image is the only option that does not need node on the host. The other two are the same install once you have a shell, so the WSL page is just the extra steps to get one.

Do not install omegga or run brickadia/omegga as root/superuser:

  • running whoami should NOT print “root”
  • your terminal prompt should NOT end with #
  • you should NOT be typing sudo npm i -g omegga
  • running echo $EUID should NOT print “0”
  • if you type pwd it should NOT print “/root” (type cd to navigate to your user’s home dir)

If any of the above are true, create a new user and continue from there.

If you need to run omegga as root, make sure your branch is main-server or unstable-server, as main will not work as root.

Once it is installed, head to Running.

Installing on Linux

Quick Setup

  1. Install linux if you haven’t already (Windows Install is not that bad)

  2. If you type whoami and it says “root”, create a new user and come back. This step is usually only necessary for people using a VPS.

  3. Update the packages you already have, then install the ones omegga needs. Skipping the update step causes several of the errors below:

    sudo apt update && sudo apt upgrade
    
    sudo apt install curl git build-essential python3 wget tar openssl lib32gcc-s1
    

    What each of those is for. On non-Debian distros the names differ; on Arch, lib32gcc-s1 is lib32-gcc-libs and needs multilib enabled.

  4. Run these commands (Installs a node installer, installs node, installs omegga):

    # download nvm
    curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
    
    # activate nvm
    . ~/.nvm/nvm.sh
    
    # install node version 24
    nvm install 24
    
    # install omegga
    npm i -g omegga
    
  5. Head over to Running Omegga or troubleshoot below. To run it in a container instead of installing it, see Containers.

Install Troubleshooting

If you are having issues running omegga, see Troubleshooting for a potential fix. This section is for issues with installing.

  • If you are on Ubuntu and the output of which npm is /bin/npm

    sudo apt purge nodejs # uninstall old version of nodejs
    # restart install instructions from this point
    nvm install 24 # install node version 24 via nvm
    
  • If you get an error like “sh: 28: cd: can't cd to .”, you need to be in bash (and probably type cd to navigate out of root directory):

    bash # use bash instead of sh
    cd # navigate home
    
  • If you get an error like “gyp info find Python using Python version 3.8.10 found at /usr/bin/python3” you need to install python3:

    sudo apt install python3
    npm i -g omegga
    
  • If you get an error like “gyp ERR! stack Error: not found: make” you need to install build-essential:

    sudo apt install build-essential # install make
    npm i -g omegga # re-run omegga install
    
  • If you get an error like “Unable to fetch some archives, maybe run apt-get update” you need to run this before running your original command:

    sudo apt update && sudo apt upgrade
    
  • If you are having trouble installing with nvm and are running Ubuntu/Debian, run the following commands (installs node, installs omegga) instead or install node&npm from NodeSource Binary Distributions.

    curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
    sudo apt-get install -y nodejs
    npm i -g omegga
    

Manual Setup (you install stuff)

Omegga depends on:

Packages

sudo apt install curl git build-essential python3 wget tar openssl lib32gcc-s1 covers all of these on Debian and Ubuntu.

PackageNeeded for
curldownloading the nvm install script
build-essential, python3node-gyp, which builds omegga’s native modules on install
wget, tardownloading and extracting steamcmd
lib32gcc-s1steamcmd itself, which is a 32-bit binary
gitomegga install and omegga update for plugins
opensslthe web UI’s https certificate. Without it the web UI falls back to http

Omegga is installed as a global npm package

npm i -g omegga

Alternatively, you can use a development/local omegga.

# clone omegga
git clone https://github.com/brickadia-community/omegga.git && cd omegga

# install dependencies
npm i

# point development omegga to global npm bin
npm link

# build the web ui, build omegga's typescript, and the plugin omegga.d.ts
npm run dist

If you accidentally install both from Github and npm i -g omegga, you can run npm unlink omegga to stop npm from using the git one.

Any errors, see Troubleshooting for a potential fix.

Creating a New User

If you are running as root (terminal prompt ends with ‘#’ instead of ‘$’ or running whoami says “root”), create a new user.

The following commands will create a user named brickadia. Feel free to replace it to user or your own name.

# create the user
useradd -m brickadia
# set the new user's password
passwd brickadia
# allow "sudo apt install ...." to work in this user
usermod -aG sudo brickadia

# become this user, navigate to user's home, and run bash
su brickadia -c "cd && bash"

# if you were root, you would be in /root (root's home) instead of /home/brickadia
# this fixes some issues when installing omegga on a VPS

Installing on Windows (WSL)

These are simple instructions to get Windows Subsystem for Linux installed. Once you have a shell in it, follow Installing on Linux.

Note: WSL 2 at the moment requires the wsl2binds plugin. You can install it with omegga install gh:meshiest/wsl2binds

To enable WSL, run this in powershell as an administrator:

dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart

Then in the Microsoft Store, download a linux:

More Advanced Instructions here if the above is not sufficient.

To set WSL version from 2 to 1:

  1. Check WSL version with wsl -l -v in cmd
  2. In Administrator cmd, run wsl --set-version <distribution name> 1 where <distribution name> is Ubuntu, Debian, etc. (From the NAME section of the previous command)

WSL2 Networking

WSL2 has its own network, so the game server is not reachable from outside the machine without forwarding. The wsl2binds plugin forwards UDP traffic for you and prints the netsh command to run for the web UI:

omegga install gh:meshiest/wsl2binds

EACCES on npm i -g omegga

Under WSL 1 the global install sometimes fails with EACCES. Try the npm fix first. Failing that, switch to WSL 2, run npm i -g omegga there, and switch back to WSL 1 if that is where you want to run.

Containers

Release tags publish an image to ghcr.io/brickadia-community/omegga.

docker run -it --rm \
  -v omegga-home:/home/steam \
  -v "$PWD/server:/server" \
  -p 8080:8080 -p 7777:7777/udp \
  -e BRICKADIA_TOKEN -e OMEGGA_PORT=8080 -e BRICKADIA_PORT=7777 \
  -e PUID="$(id -u)" -e PGID="$(id -g)" \
  ghcr.io/brickadia-community/omegga

Generate a hosting token at https://brickadia.com/account/tokens; without one omegga stops at an interactive auth prompt on first start. Arguments are passed through, so omegga --debug and bash both work in place of the default command. Omegga handles SIGINT and SIGTERM, so ctrl+c and docker stop shut down gracefully - add --init if you also want zombie reaping.

Omegga is baked into the image, so it updates by pulling a new one rather than through npm:

docker pull ghcr.io/brickadia-community/omegga:latest
docker compose pull && docker compose up -d # or, under compose

latest moves with every release, so pin a version like :1.14.0 if you would rather choose when that happens. Nothing pulls on its own - podman auto-update and watchtower are the usual ways to automate it.

The game is not in the image. It updates in place inside the volume with omegga --update or /update, and steamcmd keeps itself up to date. Setting the container’s command to omegga --update (command: omegga --update under compose) checks on every start, at the cost of not starting at all when Steam is unreachable, rather than running the version already installed.

Compose

services:
  omegga:
    image: ghcr.io/brickadia-community/omegga:latest
    restart: unless-stopped
    stdin_open: true
    tty: true
    env_file: .env
    ports:
      - '${OMEGGA_PORT}:${OMEGGA_PORT}/tcp'
      - '${BRICKADIA_PORT}:${BRICKADIA_PORT}/udp'
    volumes:
      - home:/home/steam
      - ./server:/server

volumes:
  home:
# .env - read for the ${...} above, and passed to the container
BRICKADIA_TOKEN=...
OMEGGA_PORT=8080
BRICKADIA_PORT=7777
PUID=1000
PGID=1000

compose run --rm --service-ports omegga attaches a tty, so ctrl+c goes to omegga. With compose up it goes to compose, which stops the container instead.

Volumes

  • /home/steam - the Brickadia install, steamcmd, and auth files. Keep it on a named volume or the game is downloaded again every time the container is recreated.
  • /server - the omegga working directory: omegga-config.yml, data, and plugins. Bind mount it to edit them from the host.

Ports

Set them with OMEGGA_PORT and BRICKADIA_PORT rather than editing the config file, so one value drives both the server and the port mapping. They override the config, though server.port still has to be present in it. See Environment Variables for the rest.

Brickadia reports its configured port to the master server, so a published port has to be the same number the server runs on. Never map -p 7778:7777/udp.

Metrics

METRICS_ENABLED=true with METRICS_BIND=0.0.0.0 serves the Prometheus endpoint on the container’s own network interface, where a scraper on the same compose network reaches it by service name without the port being published at all. metrics.token has no environment variable, so it belongs in omegga-config.yml on the mounted /server.

Metrics has a compose file with omegga, Prometheus, and VictoriaMetrics together, and the config files that go with it.

File Ownership

PUID and PGID decide who owns what omegga writes into a bind-mounted /server; set them to your own id -u and id -g. PUID=0 runs as root instead and leaves root-owned files behind on the host.

Building

The Dockerfile builds omegga on top of gameservermanagers/steamcmd, with node from nvm.

# the omegga in this checkout
docker build -t omegga .

# or a release off npm, where OMEGGA_VERSION is an npm version
docker build -t omegga --target npm --build-arg OMEGGA_VERSION=1.14.0 .

OMEGGA_VERSION is required and has to be an exact version. There is deliberately no latest default: that layer is cached on its command text, so latest would never invalidate it - rebuilds would keep reinstalling whichever version was current the first time, and only --no-cache would get past it.

Podman

podman build, podman run, and podman compose take the same arguments and read the same compose file. Two things differ:

  • Rootless podman maps the container’s root to your own user, so PUID=0 is what leaves bind-mounted files owned by you - the opposite of the advice above. Keeping the container’s unprivileged user maps it to a subuid instead, and /server ends up owned by an id you need podman unshare to touch. To stay unprivileged inside the container, --userns=keep-id:uid=1000,gid=1000 maps you onto the image’s steam user.
  • On SELinux systems bind mounts need a relabel: -v "$PWD/server:/server:Z".

Running

It’s recommend to create a folder first before starting your server:

# change "myServer" to "brickadia" or "server" or whatever you want
mkdir myServer && cd myServer

# this will place a folder called "myServer" in your home (cd ~)

To start a server, simply type the following in a linux shell after install:

omegga

Omegga will prompt for credentials as necessary and only stores the auth tokens brickadia generates on login. Omegga does not store your password.

Omegga runs in the current working directory. To have it always use the same folder regardless of where you start it, run omegga config default $(pwd).

Once it is up, the web UI is at https://127.0.0.1:8080 unless you changed omegga.port. See Configuration for what else the server reads on startup.

Updating

Omegga will tell you when it’s out of date. You can update with this command:

npm i -g omegga

In a container, pull a new image instead - see Containers.

If don’t have automatic update enabled, you can start update the Brickadia server by starting omegga with the --update flag:

omegga --update

Or you can run the /update command in the Omegga console, or even update from the Server menu in the web UI.

Configuration

  • CLI config via omegga config
  • Omegga config is located in a generated omegga-config.yml
  • Plugin config is managed inside the web-ui’s plugins tab.
  • Plugin config can also be set with omegga set-config pluginName configName configValue
  • Plugin config can be fetched with omegga get-config pluginName

Every field can also be set with an environment variable, which wins over the file.

Example available omegga-config.yml fields

omegga:
  port: 8080
  webui: true
  https: true
  debug: false
credentials:
  token: # hosting token can go here instead of global config
  # if you are hosting servers for multiple people
server:
  port: 7777
  map: Plate
  # Specifying a branch will use the old launcher instead of SteamCMD
  # This does not have full auto-updater support yet, though the game will update every time it is restarted
  # branch: release:release-server
terminal:
  # prepend timestamps to terminal output using dateformat syntax
  # see https://www.npmjs.com/package/dateformat for format options
  # timestamp: "HH:MM:ss"             # 14:05:30
  # timestamp: "yyyy-mm-dd"           # 2026-03-10
  # timestamp: "yyyy-mm-dd HH:MM:ss"  # 2026-03-10 14:05:30
  # timestamp: "HH:MM"                # 14:05
  # timestamp: "hh:MM:ss TT"          # 02:05:30 PM
  # timestamp: "[HH:MM:ss]"           # [14:05:30]

Note: BRANCH-server branches download only server data

Every field

Default config values, including the ones the generated file leaves out:

omegga:
  port: 8080 # web-ui port
  webui: true # enable web-ui
  plugins: true # enable plugins
  singleUser: false # disable web-ui auth users
  https: true # enable https for web-ui
  debug: false # debug logging
server:
  port: 7777 # game server port
  map: Plate # map name
  # when false, the server launches with -NoRemoteFileAccess
  remoteFiles: true
  # When branch is present, steamcmd is not used. This is for ALPHA only, and Omegga may not work for Brickadia A5 anymore.
  #branch: release:release-server # branch alias:branch name
  steambeta: public # try `unstable`
terminal:
  # prepend timestamps to terminal output (see https://www.npmjs.com/package/dateformat)
  #timestamp: "HH:MM:ss" # e.g. 14:05:30, "[HH:MM:ss]" for [14:05:30]
metrics:
  enabled: false # serve a prometheus metrics endpoint
  bind: 127.0.0.1 # address to bind (the endpoint is unauthenticated by default)
  port: 9000 # metrics port
  path: /metrics # url path to serve on
  #token: hunter2 # when set, scrapes must send `Authorization: Bearer <token>`
  defaultMetrics: true # export standard process_/nodejs_ metrics for omegga
  statusMaxAge: 15 # seconds before a scrape refreshes the cached server status
  plugins: true # let plugins register their own metrics

The [metrics](metrics.md) section is documented in full on its own page.

Environment Variables

These can be set in your shell or in a .env file the same directory as a omegga-config.yml file.

omegga accepts the following environment variables:

  • BRICKADIA_TOKEN - Specify hosting token instead of using config (generate one at https://brickadia.com/account/tokens)
  • BRICKADIA_USER - Brickadia auth username (on first start)
  • BRICKADIA_PASS - Brickadia auth password (on first start)
  • BRICKADIA_PORT - Brickadia server port (default 7777); overrides server.port from the config
  • OMEGGA_PORT - omegga webserver port (default 8080); overrides omegga.port from the config
  • OMEGGA_UI_HOST - host shown in the “Web UI available at” log message (default 127.0.0.1)
  • METRICS_ENABLED - Serve the prometheus metrics endpoint; overrides metrics.enabled from the config
  • METRICS_BIND - Address the metrics endpoint binds to (default 127.0.0.1); overrides metrics.bind
  • METRICS_PORT - Metrics endpoint port (default 9000); overrides metrics.port
  • BRICKADIA_DIR - Override the need to use steamcmd and point to a Brickadia install directory (eg. /home/<USER>/.config/omegga/steam_installs/main/Brickadia)
  • STEAM_INSTALLS_DIR - Set where omegga installs brickadia via steamcmd (default ~/.config/omegga/steam_installs)
  • STEAM_APP_ID - Set the Steam App ID for Brickadia (default 3017590)
  • STEAM_USERNAME - Set the Steam username for downloading Brickadia via steamcmd. Run omegga steamlogin to authenticate with Steam Guard
  • STEAM_PASSWORD - (Optional) Steam password; if not set, you will be prompted interactively
  • VERBOSE - Set to true to enable verbose logging (default false)
  • PACKAGE_NOTIFIER - When set to false, disables the npm update notifier
  • STEAM_NOTIFIER - When set to false, disables the SteamCMD update notifier
  • SKIP_STEAMCMD_PROMPT - When set to true, agrees to installing SteamCMD without prompting
  • BRICKADIA_DEBUG - When set, enables debug logging (equivalent to the --debug flag)

Troubleshooting

Narrow down where the issue might be with the following options:

  • If you forgot your server’s password:
    • terminal: cat data/Saved/Config/LinuxServer/ServerSettings.ini | grep Password
  • If your brickadia is crashing and omegga works:
    • omegga console: /debug
    • terminal: omegga --debug
  • If your omegga isn’t starting
    • terminal: omegga --verbose
  • If a plugin is crashing, message the plugin developer
    • discord: #plugin-bugs
  • If the web UI is blank and you installed from git clone
    • terminal: npm run dist
  • If the web UI is crashing, open the browser developer console and send the error to the #omegga-help discord channel
  • If a plugin fails to update and it has a package-lock.json, ask the plugin developer to update that file before pushing
  • If you are on Ubuntu and the output of which npm is /bin/npm
    • terminal: sudo apt purge nodejs and restart install instructions from nvm install 24.
  • If you’re getting an EACCES error when running npm i -g omegga:
    1. First, try this.
    2. If that doesn’t work, try this horrible bodge method for WSL:
      1. Set your WSL to WSL 2
      2. npm i -g omegga
      3. Set your WSL back to WSL 1 (assuming you want wsl1)
  • If you’re getting a “gyp ERR! stack Error: not found: make

For problems during npm i -g omegga rather than after it, see Install troubleshooting.

Uninstalling

# uninstall omegga
npm uninstall -g omegga

# remove omegga config
rm -rf ~/.config/omegga

# remove brickadia installs
rm -rf ~/.local/share/brickadia-launcher

# potentially remove extra brickadia config
rm ~/.config/Epic

~/.config/omegga is also where steamcmd installs the game, so removing it reclaims the Brickadia download too.

You will have to delete the server folders you created manually.

If you ran omegga in a container instead, none of the above applies: remove the container, its home volume, and the server directory you bind mounted. See Containers.

Plugins

Plugins are located in the plugins directory in an omegga config folder.

A plugin is a module that adds functionality to Omegga or a Brickadia server. An example is the autosaveez plugin, which lets users create and manage autosaves. More can be found in the #finished-plugins channel of the discord.

Omegga and plugins are not officially supported by Brickadia and may cease to function after any update.

Plugin types

Plugins can be created manually using the file structure described below, or initialized automatically with omegga init-plugin. Follow the prompts and your plugin will be generated for you.

init-plugin typeMain fileNotes
safe (default)omegga.plugin.js / .tsThe standard Node VM plugin. Runs in a VM inside a worker, so a crash does not take omegga down
unsafeomegga.main.jsRaw access to internal Omegga APIs, and the ability to crash them
rpcomegga_pluginAny language that runs from one executable, over JSON-RPC on stdin/stdout
rustomegga_pluginAn RPC plugin built on the omegga-rs Rust interface

Javascript is the easiest of these to develop in. Use JSON RPC to write plugins in other languages.

Everything below applies to all four.

Plugin Structure

All plugins are located in a plugins directory where you are running Omegga:

  • plugins/myPlugin - plugin folder (required)
  • plugins/myPlugin/doc.json - plugin information (required)
  • plugins/myPlugin/plugin.json - plugin version information, validated with omegga check (optional, for now)
  • plugins/myPlugin/setup.sh - plugin setup script, run after installed by omegga install (optional)
  • plugins/myPlugin/disable.omegga - empty file only present if the plugin should be disabled (optional)

Every plugin requires a doc.json file to document which briefly describes the plugin and its commands.

doc.json (example)

{
  "name": "My Plugin",
  "description": "Example Plugin",
  "author": "cake",
  "config": {
    "example-text": {
      "description": "This is an example text input",
      "default": "default value",
      "type": "string"
    },
    "example-password": {
      "description": "This is example text input hidden as a password",
      "default": "hidden password value",
      "type": "password"
    },
    "example-number": {
      "description": "This is an example numerical input",
      "default": 5,
      "type": "number"
    },
    "example-bool": {
      "description": "This is an example boolean input",
      "default": false,
      "type": "boolean"
    }
  },
  "commands": [
    {
      "name": "!ping",
      "description": "sends a pong to the sender",
      "example": "!ping foo bar",
      "args": [
        {
          "name": "args",
          "description": "random filler arguments",
          "required": false
        }
      ]
    },
    {
      "name": "!pos",
      "description": "announces player position",
      "example": "!pos",
      "args": []
    }
  ]
}

Plugin Config

This is an example config section of a doc.json. The web ui provides an interface for editing these configs.

{
  "config": {
    "example-text": {
      "description": "This is an example text input",
      "default": "default value",
      "type": "string"
    },
    "example-password": {
      "description": "This is example text input hidden as a password",
      "default": "hidden password value",
      "type": "password"
    },
    "example-number": {
      "description": "This is an example numerical input",
      "default": 5,
      "type": "number"
    },
    "example-bool": {
      "description": "This is an example boolean input",
      "default": false,
      "type": "boolean"
    },
    "example-list": {
      "description": "This is an example list input. List type can be string, password, number, or enum",
      "type": "list",
      "itemType": "string",
      "default": ["hello"]
    },
    "example-enum": {
      "description": "This is an example enum/dropdown input",
      "type": "enum",
      "options": ["foo", "bar", "baz", 1, 2, 3],
      "default": "foo"
    },
    "example-enum-list": {
      "description": "This is an example list of enums.",
      "type": "list",
      "itemType": "enum",
      "options": ["foo", "bar", "baz"],
      "default": ["foo"]
    },
    "example-players-list": {
      "description": "This is an example list of players.",
      "type": "players",
      "default": [
        {
          "id": "fa577b9e-f2be-493f-a30a-3789b02ba70b",
          "name": "Aware"
        }
      ]
    },
    "example-role": {
      "description": "This is an example role dropdown",
      "type": "role",
      "default": "Admin"
    }
  }
}

That config section would generate the following default config:

{
  "example-text": "default value",
  "example-password": "hidden password value",
  "example-number": 5,
  "example-bool": false,
  "example-list": ["hello"],
  "example-enum": "foo",
  "example-enum-list": ["foo"],
  "example-players-list": [
    { "id": "fa577b9e-f2be-493f-a30a-3789b02ba70b", "name": "Aware" }
  ]
}

This is provided to plugins in the constructor or the RPC init function.

Plugin File

This is an example plugin.json, located inside a plugin folder. The plugin file helps omegga know if the plugin is compatible with the current installation. Plugin files can be validated with the omegga check command.

{
  "formatVersion": 1,
  "omeggaVersion": ">=0.1.32",
  "emitConfig": "config.json",
  "dependencies": {
    "otherPlugin": "https://github.com/owner/repo",
    "requiredPlugin": { "optional": false },
    "optionalPlugin": { "optional": true, "repo": "https://github.com/owner/repo" }
  },
  "loadPriority": 0,
  "loadBefore": ["pluginToLoadAfterThis"],
  "loadAfter": ["pluginToLoadBeforeThis"]
}
  • formatVersion - indicates the plugin file format version
  • omeggaVersion - indicates compatible omegga versions (semver cheatsheet)
  • emitConfig - optional, a path to a json file where plugin config will be saved to before the plugin starts.
  • dependencies - optional, declares dependencies on other plugins
    • Can be a string specifying the GitHub repository URL (e.g., "otherPlugin": "https://github.com/owner/repo")
    • Can be an object with optional properties:
      • optional - if true, the plugin will load even if this dependency is missing
      • repo - GitHub repository URL where the dependency can be found (e.g., "https://github.com/owner/repo")
    • Dependencies are automatically loaded before the dependent plugin
  • loadPriority - optional, numeric priority for load order (lower/negative numbers load earlier, higher/positive numbers load later, undefined loads in the middle)
  • loadBefore - optional, array of plugin names that should load after this plugin
  • loadAfter - optional, array of plugin names that should load before this plugin

Note: Omegga will automatically resolve the correct load order based on dependencies, loadPriority, loadBefore, and loadAfter constraints. If there’s a cyclic dependency or conflicting constraints, plugins may fail to load.

Plugin Store

All plugins have the capability to get/set values in a very lightweight “database”

The following asynchronous methods are provided:

MethodArgumentsDescription
store.getkey (string)Get an object from plugin store
store.setkey (string), value (any)Store an object in plugin store
store.deletekey (string)Remove an object from plugin store
store.wipenoneRemove all objects from plugin store
store.countnoneCount number of objects in plugin store
store.keysnoneGet keys for all objects in plugin store

Example usage:

// simple add function
async function add() {
  const a = await store.get('foo');
  const b = await store.get('bar');
  await store.set('baz', a + b);
  await store.delete('foo');
  await store.delete('bar');
}

(async () => {
  // store foo and bar in the plugin store
  await Promise.all([store.set('foo', 5), store.set('bar', 2)]);

  // add foo and bar
  await add();

  // baz should be equal to 7
  console.log('assert', (await store.get('baz')) === 7);

  // demo of storing an object
  await store.set('example object', {
    foo: 'you can store objects in the store too',
    bar: "just don't expect it to work with anything recursive (cannot serialize)",
  });
})();

For Node Plugins, the store is the third argument passed into the constructor. For JSONRPC Plugins, the "store.get"/etc. methods can be used.

JSONRPC Note: store.set has an array of arguments ([key, value])

Plugin Metrics

Plugins get a metrics object as a fourth constructor argument, alongside store. Counters, gauges, and histograms registered on it are exported from omegga’s metrics endpoint as omegga_plugin_<plugin>_<metric>. Handles are returned whether or not the endpoint is enabled, so no guards are needed.

Plugin metrics covers the full API, the RPC metrics notification, and the per-plugin limits.

What plugins can reach

Installing Plugins

CLI Installation

You can install plugins with the omegga install https://github.com/user/repo command.

You can install plugins using a shorthand omegga install gh:user/repo which will install the plugin located at https://github.com/user/omegga-repo (note the inserted omegga- prefix).

The available shorthands are gh for github.com and gl for gitlab.com

This is the recommended way of installing plugins as it automatically runs a setup script when present.

Manual Installation

You can clone a plugin’s github repo inside the plugins folder (created when you run omegga for the first time):

  • cd plugins to navigate to plugins folder
  • git clone https://github.com/user/repo to download the plugin
  • Make sure to read the plugin’s README file for after-install instructions

Updating Plugins

Plugins can be updated with omegga update:

# update all plugins
omegga update

# update plugins named "pluginName" and "anotherPluginName"
omegga update pluginName anotherPluginName

Plugins may also need to be updated based on the project’s README file.

Uninstalling Plugins

Plugins can be installed by deleting the plugin’s respective folder:

rm -rf plugins/PLUGIN_NAME

Node VM Plugins

Node VM Plugins are what you should be using. They are run inside a VM inside a Worker. This means when they crash, they do not crash the whole server, and they can in the future have locked down permissions (disable filesystem access, etc.).

These plugins receive a “proxy” reference to omegga and have limited reach for what they can touch.

Register custom /commands by returning {registeredCommands: ['foo', 'bar']} (registers command /foo and /bar) in the async init() method.

By defining an async pluginEvent(event, from, ...args) method in your plugin class, you can respond to events from other plugins, where from is the name of the other plugin, event is the name of the custom event, and args is an array of any passed arguments.

Globals

  • OMEGGA_UTIL - access to the src/util/index.js module
  • Omegga - access to the “proxy” omegga
  • console.log - and other variants (console.error, console.info) print specialized output to console

Folder Structure

In a plugins directory create the following folder structure:

  • plugins/myPlugin - plugin folder (required)
  • plugins/myPlugin/omegga.plugin.js - js plugin main file (required)
  • plugins/myPlugin/doc.json
  • plugins/myPlugin/access.json - plugin access information (required, but doesn’t have to have anything right now). this will contain what things the vm will need to access

access.json (examples)

Access to any builtin modules (fs, path, etc.)

["*"]

Access to nothing - only the code in the omegga.plugin.js

[]

Access to only fs, (const fs = require('fs');)

["fs"]

omegga.plugin.js (example)

class PluginName {
  // the constructor also contains an omegga if you don't want to use the global one
  // config and store variables are optional but provide access to the plugin data store
  constructor(omegga, config, store) {
    this.omegga = omegga;
    this.config = config;
    this.store = store;
    console.info('constructed my plugin!');
  }

  async init() {
    Omegga.on('chatcmd:ping', (name, ...args) => {
      Omegga.broadcast(`pong @ ${name} + ${args.length} args`);
    }).on('chatcmd:pos', async name => {
      const [x, y, z] = await Omegga.getPlayer(name).getPosition();
      Omegga.broadcast(`<b>${name}</> is at ${x} ${y} ${z}`);
    });
  }

  async stop() {
    // any remove events are not necessary because the VM removes the code
  }
}

module.exports = PluginName;

omegga.plugin.ts (example)

Be sure to put .build/ and node_modules/ in your .gitignore

Requires a tsconfig.json:

{
  "compilerOptions": {
    "noEmit": true,
    "esModuleInterop": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "target": "es2020",
    "baseUrl": ".",
    "paths": {
      "omegga/*": ["node_modules/omegga/dist/*"]
    }
  }
}

omegga.plugin.ts:

import type { OmeggaPlugin, OL, PS, PC } from 'omegga/plugin';

type Config = { foo: string };
type Storage = { bar: string };

export default class Plugin implements OmeggaPlugin<Config, Storage> {
  omegga: OL;
  config: PC<Config>;
  store: PS<Storage>;

  constructor(omegga: OL, config: PC<Config>, store: PS<Storage>) {
    this.omegga = omegga;
    this.config = config;
    this.store = store;
  }

  async init() {
    // Write your plugin!
    this.omegga.on('cmd:test', (speaker: string) => {
      this.omegga.broadcast(`Hello, ${speaker}!`);
    });

    return { registeredCommands: ['test'] };
  }

  async stop() {
    // Anything that needs to be cleaned up...
  }
}

See also

Node Plugins

Node plugins are effectively require’d into omegga. They have the potential to crash the entire service through uncaught exceptions and also can be insecure. Develop and run these at your own risk - your server stability may suffer.

These plugins receive a direct reference to the omegga that wraps the brickadia server. As a result, they can directly modify how omegga runs.

Cleanup is important as code can still be running after the plugin is unloaded resulting in strange and undefined behavior. Make sure to run clearInterval and clearTimeout

Register custom /commands by returning {registeredCommands: ['foo', 'bar']} (registers command /foo and /bar) in the async init() method.

By defining an async pluginEvent(event, from, ...args) method in your plugin class, you can respond to events from other plugins, where from is the name of the other plugin, event is the name of the custom event, and args is an array of any passed arguments.

Globals

  • OMEGGA_UTIL - access to the src/util/index.js module

Folder Structure

In a plugins directory create the following folder structure:

  • plugins/myPlugin - plugin folder (required)
  • plugins/myPlugin/doc.json
  • plugins/myPlugin/omegga.main.js - js plugin main file (required)

omegga.main.js (example)

class PluginName {
  // config and store variables are optional but provide access to the plugin data store
  constructor(omegga, config, store) {
    this.omegga = omegga;
    this.config = config;
    this.store = store;
  }

  async init() {
    this.omegga
      .on('chatcmd:ping', (name, ...args) => {
        this.omegga.broadcast(`pong @ ${name} + ${args.length} args`);
      })
      .on('chatcmd:pos', async name => {
        const [x, y, z] = await this.omegga.getPlayer(name).getPosition();
        this.omegga.broadcast(`<b>${name}</> is at ${x} ${y} ${z}`);
      });
  }

  async stop() {
    this.omegga
      .removeAllListeners('chatcmd:ping')
      .removeAllListeners('chatcmd:pos');
  }

  // optional: respond to events from other plugins
  async pluginEvent(event, from, ...args) {
    if (event === 'greeting') {
      return `Hello from ${from}!`;
    }
  }
}

module.exports = PluginName;

See also

JSON RPC Plugins

JSON RPC Plugins let you use any language you desire, as long as you can run it from a single executable file. They follow the JSON-RPC 2.0 Specification

The server communicates with the plugin by sending messages to stdin and expects responses in stdout. All stderr is printed to the console.

Register custom /commands by returning {registeredCommands: ['foo', 'bar']} (registers command /foo and /bar) in the init method.

Omegga Methods (You can access these)

MethodArgumentsDescriptionReturns
logline (string)Prints message to omegga console
errorline (string)Same as log but with different colors
infoline (string)Same as log but with different colors
debugline (string)Same as log but with different colors
warnline (string)Same as log but with different colors
traceline (string)Same as log but with different colors
store.getkey (string)Get an object from plugin storeObject
store.set[key (string), value (any)]Store an object in plugin store
store.deletekey (string)Remove an object from plugin store
store.wipenoneRemove all objects from plugin store
store.countnoneCount number of objects in plugin storeInteger
store.keysnoneGet keys for all objects in plugin storeList of Strings
execcmd (string)Writes a console command to Brickadia
writelncmd (string)Same as exec
broadcastline (string)Broadcasts a message to the server
whisper{target: string, line: string}Sends a message to a specific client
middlePrint{target: string, line: string}Sends a middle print message to a specific client
getPlayersnoneOnline playersList of Players
getAllPlayerPositionsnoneAn array of objects with fields pos and player.List of { Player Object(…), Position(…), isDead(bool) }
getRoleSetupnoneServer rolesJSON Data
getBanListnoneList of bansJSON Data
getSavesnoneSaves in the saves directoryList Strings
getSavePathname (string)The path to a specific saveString
getSaveDatanoneCurrent save as brs-js dataBRS Object
clearBricks{target: string, quiet: bool}Clears a specific player’s bricks
clearAllBricksquietClears all bricks on the server
saveBricksname (string)Save bricks to a save named name
loadBricks{name: string, offX, offY, offY, quiet: bool}Load bricks of save named name
loadBricksOnPlayer{name: string, player: string, offX, offY, offY}Load bricks of save named name on player clipboard
readSaveDataname (string)Parses save into a brs-js save object, returns the objectBRS Object
loadSaveData{data: object, offX, offY, offY, quiet: bool}Builds brs file from data, loads the file
loadSaveDataOnPlayer{data: object, player: string, offX, offY, offY}Builds brs file from data, loads the file onto a player’s clipboard
changeMapmap (string)Change map to specified map name, returns successBoolean
player.gettarget (string)Gets the player by their name or UUID.{name, id, controller, state, host: bool}
player.getRolestarget (string)Target’s rolesList of Strings
player.getPermissionstarget (string)Target’s permissionsRecord<string, boolean>
player.getNameColortarget (string)Target’s name colorRGB Hex String
player.getPositiontarget (string)Target’s position[number, number, number] or null
player.getPawntarget (string)Target’s pawn namestring or null
player.getGhostBricktarget (string)Target’s ghost brick{targetGrid, location, orientation}
player.getPainttarget (string)Target’s current paint selection{materialIndex, materialAlpha, material, color}
player.isCrouchedtarget (string)Check if target is crouchedboolean
player.isDeadtarget (string)Check if target is deadboolean
player.getTemplateBoundstarget (string)Target’s template/selection bounds{minBound, maxBound, center}
player.getTemplateBoundsDatatarget (string)Target’s template/selection as brs-js save dataBRS Object
player.clearBricks{target, quiet}Clears target’s bricks
player.loadBricks{target, saveName}Loads save file to target’s clipboard
player.loadSaveData{target, data, offX, offY, offZ}Loads brs-js save data to target’s clipboard
player.loadDataAtGhostBrick{target, data, rotate=true, offX, offY, offZ, quiet}Loads brs-js save data at target’s selection bounds
player.killtarget (string)Kills the target player
player.damage{target, amount}Damages target by amount
player.heal{target, amount}Heals target by amount
player.giveItem{target, item}Gives target an item
player.takeItem{target, item}Removes item from target
player.setTeam{target, teamIndex}Sets target’s team
player.setMinigame{target, index}Adds target to minigame at index
player.setScore{target, minigameIndex, score}Sets target’s score in minigame
player.getScoretarget (string)Gets target’s score in minigamenumber
player.setLeaderboard{target, key, value}Sets leaderboard value for target
player.getLeaderboardtarget (string)Gets leaderboard value for targetnumber or null
plugin.gettarget (string)Gets info on the target pluginObject
plugin.emit[target (string), event (string), …args (any)]Emit a custom event to the target plugin

Plugin Methods (You implement these)

MethodArgumentsDescriptionRequired
initconfig objectReturns a start result, called on plugin start
stopnoneReturns something, called on plugin stop
bootstrap[{ omegga info (host, version, etc) }]Run when plugin is started for base data
plugin:players:raw[[… [player username, displayName, id, controller, state]]]Lists players on the server
plugin:emit[event, from, …args]Fired when another plugin sends an event
line[brickadiaLog string]A brickadia console log
start[{map}]On brickadia server start
host[{name, id}]When the host is detected
version[-1 or the CL number]When the version is detected
unauthorizednoneOn brickadia server fails an auth check
join[{name, id, state, controller}]Run when a player joins
leave[{name, id, state, controller}]Run when a player leaves
cmd:command[playerName, …args]Runs when a player runs a /command args
chatcmd:command[playerName, …args]Runs when a player runs a !command args
chat[playerName, message]Runs when a player sends a chat message
interact{brick_asset: string;player: { id: string; name: string; controller: string; pawn: string };position: [number, number, number];}Runs when a player clicks a brick with an interact component. data is parsed JSON if line (from interact component) starts with “json:{“your”: “json”}`. Uses interact log field
event:NAME[>player from click<, …args]Runs when an interact component has `event:NAME: arg1,arg2,arg,3,
mapchange[{map}]Runs when the map changes
autorestart[autorestart config]Runs server has an autorestart scheduled
minigamejoin{player: {name, id}; minigameName: string}Deprecated as of EA3. Runs when a player joins a minigame. Note that minigameName is not unique between minigames. minigameName will be null if player leaves all minigames. This will run before join
wirelog[raw string]Runs when a [Wire Graph] log line is emitted. raw is the text after the [Wire Graph] prefix
wirecmd:command[…args]Runs when a [Wire Graph] log starts with command args. command is lowercased; args are the remaining space-separated words

Folder Structure

In a plugins directory create the following folder structure:

  • plugins/myPlugin - plugin folder (required)
  • plugins/myPlugin/doc.json
  • plugins/myPlugin/omegga_plugin - executable plugin file (required)

omegga_plugin (example, node javascript)

#!/usr/bin/env node

const readline = require('readline');
const { EventEmitter } = require('events');
const {
  JSONRPCServer,
  JSONRPCServerAndClient,
  JSONRPCClient,
} = require('json-rpc-2.0');

// events
const ev = new EventEmitter();

// stdio handling
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
  terminal: false,
});

// rpc "server and client" for responding/receiving messages
const rpc = new JSONRPCServerAndClient(
  new JSONRPCServer(),
  // the client outputs JSON to console
  new JSONRPCClient(async blob => console.log(JSON.stringify(blob)))
);

// on stdin, pass into rpc
rl.on('line', line => {
  try {
    rpc.receiveAndSend(JSON.parse(line));
  } catch (e) {
    console.error(e);
  }
});

// regexes for matching brickadia console logs
const GENERIC_LINE_REGEX =
  /^(\[(?<date>\d{4}\.\d\d.\d\d-\d\d.\d\d.\d\d:\d{3})\]\[\s*(?<counter>\d+)\])?(?<generator>\w+): (?<data>.+)$/;
const LOG_LINE_REGEX =
  /\[(?<date>\d{4}\.\d\d.\d\d-\d\d.\d\d.\d\d:\d{3})\]\[\s*(?<counter>\d+)\](?<rest>.*)$/;

ev.on('line', line => {
  const logMatch = line.match(LOG_LINE_REGEX);
  if (!logMatch) return;
  const {
    groups: { rest },
  } = logMatch;
  const dataMatch = rest.match(GENERIC_LINE_REGEX);
  if (dataMatch) ev.emit('logData', dataMatch.groups);
  else ev.emit('logLine', rest);
});

// list of players
let players;

// get a player by name
const getPlayer = name => players.find(p => p.name === name);

// watch console logs for a pattern, then remove the listener
function watch(exec, pattern) {
  return new Promise(resolve => {
    function listener(line) {
      const match = line.match(pattern);
      // listener removes itself on a match
      if (match) {
        ev.off('logLine', listener);
        resolve(match.groups);
      }
    }
    // add the listener
    ev.on('logLine', listener);

    // run the console command
    rpc.notify('writeln', exec);
  });
}

// get a player's position
async function getPlayerPos(name) {
  const player = getPlayer(name);
  if (!player) return;

  // get player position from player controller
  const pawnRegExp = new RegExp(
    `BP_PlayerController_C .+?PersistentLevel\\.${player.controller}\.Pawn = BP_FigureV2_C'.+?:PersistentLevel.(?<pawn>BP_FigureV2_C_\\d+)'`
  );
  const { pawn } = await watch(
    `GetAll BP_PlayerController_C Pawn Name=${player.controller}`,
    pawnRegExp
  );

  // get player position from pawn
  const posRegExp = new RegExp(
    `CapsuleComponent .+?PersistentLevel\\.${pawn}\\.CollisionCylinder\\.RelativeLocation = \\(X=(?<x>[\\d\\.-]+),Y=(?<y>[\\d\\.-]+),Z=(?<z>[\\d\\.-]+)\\)`
  );
  const { x, y, z } = await watch(
    `GetAll SceneComponent RelativeLocation Name=CollisionCylinder Outer=${pawn}`,
    posRegExp
  );

  return [x, y, z].map(Number);
}

// emit a console log
const log = (...args) => rpc.notify('log', args.join(' '));

// when available players updates - plugin:players:raw is emitted
rpc.addMethod('plugin:players:raw', ([playerArr]) => {
  // update the players list
  players = playerArr.map(p => ({
    name: p[0],
    id: p[1],
    controller: p[2],
    state: p[3],
  }));
});

// ping command
rpc.addMethod('chatcmd:ping', ([name, ...args]) => {
  rpc.notify('broadcast', `pong @ ${name} + ${args.length} args`);
});

// player position command
rpc.addMethod('chatcmd:pos', async ([name]) => {
  log('player', name, 'requests position');
  const [x, y, z] = await getPlayerPos(name);
  rpc.notify('broadcast', `<b>${name}</> is at ${x} ${y} ${z}`);
});

// pass lines into the event emitter
rpc.addMethod('line', ([line]) => {
  ev.emit('line', line);
});

// receive config object in init
rpc.addMethod('init', async ([config]) => ({ registeredCommands: [] }));
rpc.addMethod('stop', async () => 'ok');

See also

API

What a plugin can reach. The Omegga, Player, Plugin, and Types pages are generated from the declarations in src/plugin.ts, which is also what ships as omegga.d.ts for typescript plugins.

Omeggathe server: players, bricks, saves, chat, minigames
Playerone player: position, roles, paint, clipboard
Pluginwhat a plugin class implements, and its config, store, and metrics
Eventswhat omegga emits as the server runs
Typesthe shapes the above return
Log parsingreading console output omegga does not already parse

JSON RPC plugins reach the same functionality through method names rather than these interfaces; that page has its own table.

Save data is brs-js SaveData.

Omegga API

The Omegga global (and the omegga passed to a plugin constructor) implements OmeggaLike. It is an event emitter, so everything in Events is available on it too.

OmeggaLike

Extends OmeggaCore, LogWrangling, InjectedCommands, MockEventEmitter.

PropertyTypeDescription
versionnumbergame CL version
ConsoleConsoleCommandsversion-resolved Brickadia console command names, nested by namespace. e.g. Omegga.Console.Bricks.Clear resolves to the command string for the running game version (“Bricks.Clear” or “br.Bricks.Clear”)
verbosebooleanverbose logging is enabled
playersOmeggaPlayer[]list of players
host?{ id: string; name: string }server host
startedbooleanserver is started
startingbooleanserver is starting
stoppingbooleanserver is stopping
currentMapstringcurrent map
configPathstringpath to config files
savePathstringpath to saves
worldPathstringpath to worlds
prefabPathstringpath to prefabs
presetPathstringpath to presets
pathstringpath to containing dir
binaryPathstring | nullpath to the directory containing the game server binary - null when the install is launcher-managed and the binary location isn’t known

writeln

writeln(line: string): void

getPlugin

getPlugin(name: string): Promise<PluginInterop | null>

get a plugin’s name, documentation, and loaded status If run in an unsafe plugin, the emitPlugin method sends events from an “unsafe” plugin

Declared in src/plugin.ts.

OmeggaCore

getPlayers

getPlayers(): { id: string; name: string; displayName: string; controller: string; state: string; }[]

get a list of players

Returns list of players {id: uuid, name: name} objects

getPlayer

getPlayer(target: string): OmeggaPlayer | null

find a player by name, id, controller, or state

ParamTypeDescription
targetstringname, id, controller, or state

findPlayerByName

findPlayerByName(name: string): OmeggaPlayer | null

find a player by rough name, prioritize exact matches and get fuzzier

ParamTypeDescription
namestringplayer name, fuzzy

getHostId

getHostId(): string

get the host’s ID

Returns Host Id

broadcast

broadcast(...messages: string[]): void

broadcast messages to chat messages are broken by new line multiple arguments are additional lines all messages longer than 512 characters are deleted automatically, though omegga wouldn’t have sent them anyway

ParamTypeDescription
...messagesstring[]unescaped chat messages to send. may need to wrap messages with quotes

whisper

whisper(target: string | OmeggaPlayer, ...messages: string[]): void

whisper messages to a player’s chat messages are broken by new line multiple arguments are additional lines all messages longer than 512 characters are deleted automatically, though omegga wouldn’t have sent them anyway

ParamTypeDescription
targetstring | OmeggaPlayerplayer identifier or player object
...messagesstring[]unescaped chat messages to send. may need to wrap messages with quotes

middlePrint

middlePrint(target: string | OmeggaPlayer, message: string): void

prints text to the middle of a player’s screen all messages longer than 512 characters are deleted automatically

ParamTypeDescription
targetstring | OmeggaPlayerplayer identifier or player object
messagestringunescaped chat messages to send. may need to wrap messages with quotes

saveMinigame

saveMinigame(index: number, name: string): void

Save a minigame preset based on a minigame index

ParamTypeDescription
indexnumberminigame index
namestringpreset name

deleteMinigame

deleteMinigame(index: number): void

Delete a minigame

ParamTypeDescription
indexnumberminigame index

resetMinigame

resetMinigame(index: number): void

Reset a minigame

ParamTypeDescription
indexnumberminigame index

nextRoundMinigame

nextRoundMinigame(index: number): void

Force the next round in a minigame

ParamTypeDescription
indexnumberminigame index

loadMinigame

loadMinigame(presetName: string, owner?: string): void

Load an Minigame preset

ParamTypeDescription
presetNamestringpreset name
ownerstringowner id/name

getMinigamePresets

getMinigamePresets(): string[]

Get all presets in the minigame folder and child folders

resetEnvironment

resetEnvironment(): void

Reset the environment settings

saveEnvironment

saveEnvironment(presetName: string): Promise<void>

Save an environment preset

ParamTypeDescription
presetNamestringpreset name

getEnvironmentData

getEnvironmentData(): Promise<EnvironmentPreset | null>

Save a temporary environment preset and return its contents

readEnvironmentData

readEnvironmentData(presetName: string): EnvironmentPreset | null

Read environment data as json; null when the preset is missing or invalid

ParamTypeDescription
presetNamestringpreset name

loadEnvironment

loadEnvironment(presetName: string): void

Load an environment preset

ParamTypeDescription
presetNamestringpreset name

loadEnvironmentData

loadEnvironmentData(preset: | EnvironmentPreset | NonNullable<EnvironmentPreset['data']>['groups']): void

Load some environment preset data

ParamTypeDescription
preset| EnvironmentPreset | NonNullable<EnvironmentPreset['data']>['groups']preset data

getEnvironmentPresets

getEnvironmentPresets(): string[]

Get all presets in the environment folder and child folders

clearBricks

clearBricks(target: string | { id: string }, quiet?: boolean): void

Clear a user’s bricks (by uuid, name, controller, or player object)

ParamTypeDescription
targetstring | { id: string }player or player identifier
quietbooleanquietly clear bricks

clearRegion

clearRegion(region: { center: [number, number, number]; extent: [number, number, number]; }, options?: { target?: string | OmeggaPlayer; bricks?: boolean; entities?: boolean; }): void

Clear a region of bricks. On EA3 this routes to br.World.ClearRegion and can optionally clear entities too.

ParamTypeDescription
region{ center: [number, number, number]; extent: [number, number, number]; }region to clear
options{ target?: string | OmeggaPlayer; bricks?: boolean; entities?: boolean; }optional settings

clearAllBricks

clearAllBricks(options?: | boolean | { quiet?: boolean; bricks?: boolean; entities?: boolean }): void

Clear all bricks on the server. On EA3 this routes to br.World.ClearAll and can optionally clear entities too. A bare boolean is accepted as the legacy quiet argument.

ParamTypeDescription
options| boolean | { quiet?: boolean; bricks?: boolean; entities?: boolean }quiet (or { quiet, bricks, entities })

saveBricks

saveBricks(saveName: string, region?: { center: [number, number, number]; extent: [number, number, number]; }): void

Deprecated. removed in Brickadia EA3 (no-op on newer servers) - save a prefab with savePrefabRegion instead

Save bricks under a filename

ParamTypeDescription
saveNamestringsave file name
region{ center: [number, number, number]; extent: [number, number, number]; }region of bricks to save

saveBricksAsync

saveBricksAsync(saveName: string, region?: { center: [number, number, number]; extent: [number, number, number]; }): Promise<void>

Deprecated. removed in Brickadia EA3 (no-op on newer servers) - save a prefab with savePrefabRegion instead

Save bricks under a filename, with a promise

ParamTypeDescription
saveNamestringsave file name
region{ center: [number, number, number]; extent: [number, number, number]; }region of bricks to save

loadBricks

loadBricks(saveName: string, options?: { offX?: number; offY?: number; offZ?: number; quiet?: boolean; correctPalette?: boolean; correctCustom?: boolean; }): void

Deprecated. removed in Brickadia EA3 (no-op on newer servers) - load a prefab with loadPrefab instead

Load bricks on the server

loadBricksOnPlayer

loadBricksOnPlayer(saveName: string, player: string | OmeggaPlayer, options?: { offX?: number; offY?: number; offZ?: number; correctPalette?: boolean; correctCustom?: boolean; }): void

Deprecated. removed in Brickadia ~EA2 (no-op on newer servers) - use loadPrefabOnPlayer instead

Load bricks on the server into a player’s clipbaord

getSaves

getSaves(): string[]

Get all saves in the save folder and child folders

getSavePath

getSavePath(saveName: string): string | undefined

Checks if a save exists and returns an absolute path

ParamTypeDescription
saveNamestringSave filename

Returns Path to string, undefined if the save does not exist

getWorlds

getWorlds(): string[]

Get all worlds in the worlds folder and child folders

getWorldPath

getWorldPath(worldName: string): string | undefined

Checks if a world exists and returns an absolute path

ParamTypeDescription
worldNamestringWorld name

Returns Path to string, undefined if the world does not exist

getWorldRevisions

getWorldRevisions(worldName: string): Promise<{ index: number; date: Date; note: string }[]>

Get a list of revisions for a world

ParamTypeDescription
worldNamestringWorld name

loadWorld

loadWorld(worldName: string): Promise<boolean>

Load a world by its name

ParamTypeDescription
worldNamestringWorld name

loadWorldRevision

loadWorldRevision(worldName: string, revision: number): Promise<boolean>

Load a world at a specific revision

ParamTypeDescription
worldNamestringWorld name
revisionnumber

saveWorldAs

saveWorldAs(worldName: string): Promise<boolean>

Save a world as a new name

ParamTypeDescription
worldNamestringWorld name

saveWorld

saveWorld(): Promise<boolean>

Save the current world

createEmptyWorld

createEmptyWorld(worldName: string): Promise<boolean>

Create an empty world with the given name

writeSaveData

writeSaveData(saveName: string, saveData: WriteSaveObject): void

unsafely load save data (wrap in try/catch)

ParamTypeDescription
saveNamestringsave file name
saveDataWriteSaveObjectBRS JS Save data

readSaveData

readSaveData(saveName: string, nobricks?: boolean): ReadSaveObject

unsafely read save data (wrap in try/catch)

ParamTypeDescription
saveNamestringsave file name
nobricksbooleanonly read save header data

Returns BRS JS Save Data

loadSaveData

loadSaveData(saveData: WriteSaveObject, options?: { offX?: number; offY?: number; offZ?: number; quiet?: boolean; correctPalette?: boolean; correctCustom?: boolean; }): Promise<void>

Deprecated. removed in Brickadia EA3 (no-op on newer servers) - use the prefab API (loadPrefab) instead

load bricks from save data and resolve when game finishes loading

ParamTypeDescription
saveDataWriteSaveObjectBRS JS Save data
options{ offX?: number; offY?: number; offZ?: number; quiet?: boolean; correctPalette?: boolean; correctCustom?: boolean; }

loadSaveDataOnPlayer

loadSaveDataOnPlayer(saveData: WriteSaveObject, player: string | OmeggaPlayer, options?: { offX?: number; offY?: number; offZ?: number; correctPalette?: boolean; correctCustom?: boolean; }): Promise<void>

Deprecated. removed in Brickadia ~EA2 (no-op on newer servers) - use the prefab API (loadPrefabOnPlayer) instead

load bricks from save data and resolve when game finishes loading

ParamTypeDescription
saveDataWriteSaveObjectBRS JS Save data
playerstring | OmeggaPlayerPlayer name/id or player object
options{ offX?: number; offY?: number; offZ?: number; correctPalette?: boolean; correctCustom?: boolean; }

getSaveData

getSaveData(region?: { center: [number, number, number]; extent: [number, number, number]; }): Promise<ReadSaveObject | undefined>

Deprecated. removed in Brickadia EA3 (returns undefined on newer servers) - use the prefab API instead

get current bricks as save data

getPrefabs

getPrefabs(): string[]

Get all prefabs in the prefabs folder and child folders (EA3)

getPrefabPath

getPrefabPath(prefabName: string): string | undefined

Checks if a prefab exists and returns an absolute path (EA3)

ParamTypeDescription
prefabNamestringPrefab filename

Returns Path to string

loadPrefab

loadPrefab(path: string, options?: { offX?: number; offY?: number; offZ?: number; atOriginalPosition?: boolean; orientation?: number; rootEntityPersistentIndex?: number; mirrorAxes?: number; overrideUserId?: string; }): void

Load a prefab into the world (EA3). path is a bundle path ref such as Prefabs/Uploads/<hash>.brz.

ParamTypeDescription
pathstringprefab bundle path ref
options{ offX?: number; offY?: number; offZ?: number; atOriginalPosition?: boolean; orientation?: number; rootEntityPersistentIndex?: number; mirrorAxes?: number; overrideUserId?: string; }placement options (offset, orientation, mirror axes, etc)

savePrefab

savePrefab(path: string, options?: { region?: { center: [number, number, number]; extent: [number, number, number]; }; entities?: boolean; rootEntityPersistentIndex?: number; userId?: string; }): void

Save the world (or a region of it) as a prefab (EA3).

ParamTypeDescription
pathstringdestination prefab bundle path ref (e.g. Prefabs/MyPrefab.brz)
options{ region?: { center: [number, number, number]; extent: [number, number, number]; }; entities?: boolean; rootEntityPersistentIndex?: number; userId?: string; }save options; omit region to capture the whole world

savePrefabAsync

savePrefabAsync(path: string, options?: { region?: { center: [number, number, number]; extent: [number, number, number]; }; entities?: boolean; rootEntityPersistentIndex?: number; userId?: string; }): Promise<string | null>

Save a prefab and resolve once the prefab file has been written to disk (EA3).

ParamTypeDescription
pathstringdestination prefab bundle path ref
options{ region?: { center: [number, number, number]; extent: [number, number, number]; }; entities?: boolean; rootEntityPersistentIndex?: number; userId?: string; }same options as savePrefab

Returns absolute path to the written prefab, or null on timeout

givePrefabToPlayer

givePrefabToPlayer(path: string, player: string | OmeggaPlayer, options?: { preserveOwnership?: boolean }): void

Give a prefab to a player’s inventory (EA3).

ParamTypeDescription
pathstringprefab bundle path ref
playerstring | OmeggaPlayerplayer name/id or player object
options{ preserveOwnership?: boolean }give options (preserve ownership)

loadPrefabOnPlayer

loadPrefabOnPlayer(path: string, player: string | OmeggaPlayer, options?: { preserveOwnership?: boolean }): void

Load a prefab onto a player (EA3, replaces loadBricksOnPlayer).

ParamTypeDescription
pathstringprefab bundle path ref
playerstring | OmeggaPlayerplayer name/id or player object
options{ preserveOwnership?: boolean }give options (preserve ownership)

changeMap

changeMap(map: string): Promise<boolean>

Change server map

ParamTypeDescription
mapstringMap name

getRoleSetup

getRoleSetup(): BRRoleSetup

Get up-to-date role setup from RoleSetup.json

getRoleAssignments

getRoleAssignments(): BRRoleAssignments

Get up-to-date role assignments from RoleAssignment.json

getBanList

getBanList(): BRBanList

Get up-to-date ban list from BanList.json

getNameCache

getNameCache(): BRPlayerNameCache

Get up-to-date name cache from PlayerNameCache.json

Declared in src/plugin.ts.

InjectedCommands

getServerStatus

getServerStatus(): Promise<IServerStatus | null>

Get server status

listMinigames

listMinigames(): Promise<IMinigameList>

Deprecated. minigames were replaced by a single gamemode (~CL14000); on modern servers this returns at most one entry with an empty owner. Prefer getGamemode.

Get a list of minigames and their indices

getAllPlayerPositions

getAllPlayerPositions(): Promise<IPlayerPositions>

Get all player positions and pawns

getMinigames

getMinigames(): Promise<ILogMinigame[]>

Get minigames and members (one entry per gamemode on modern servers)

getGamemode

getGamemode(): Promise<IGamemode | null>

Get the single gamemode and its teams/players (modern servers, >=CL14000). Returns null on older servers (use getMinigames).

Declared in src/plugin.ts.

LogWrangling

addMatcher

addMatcher<T>(pattern: IMatcher<T>['pattern'], callback: IMatcher<T>['callback']): void

Add a passive pattern on console output that invokes callback on match

addWatcher

addWatcher<T = RegExpMatchArray>(pattern: IWatcher<T>['pattern'], options?: { timeoutDelay?: number; bundle?: boolean; debounce?: boolean; afterMatchDelay?: number; last?: IWatcher<T>['last']; exec?: () => void; }): Promise<T[]>

Run an active pattern on console output that resolves a match. T is the element type of the resolved matches - RegExpMatchArray for RegExp patterns, the pattern’s return type for function patterns.

watchLogChunk

watchLogChunk<T = RegExpMatchArray>(cmd: string, pattern: IWatcher<T>['pattern'], options?: { first?: 'index' | ((match: T) => boolean); last?: IWatcher<T>['last']; afterMatchDelay?: number; timeoutDelay?: number; }): Promise<T[]>

Run a command and capture bundled output. T is the element type of the resolved matches - RegExpMatchArray for RegExp patterns, the pattern’s return type for function patterns.

watchLogArray

watchLogArray<Item extends Record<string, string> = Record<string, string>, Member extends Record<string, string> = Record<string, string>>(cmd: string, itemPattern: RegExp, memberPattern: RegExp): Promise<{ item: Item; members: Member[] }[]>

Run a command and capture bundled output for array functions

Declared in src/plugin.ts.

Player API

Omegga.getPlayer(target) returns an OmeggaPlayer. The same methods are available as statics on the Player global when all you have is a uuid.

OmeggaPlayer

PropertyTypeDescription
namestringplayer name
displayNamestringplayer display name
idstringplayer uuid
controllerstringplayer controller id
statestringplayer state id

getOmegga

getOmegga(): OmeggaLike

Returns omegga

clone

clone(): OmeggaPlayer

Clone a player

raw

raw(): [string, string, string, string, string]

Get raw player info (to feed into a constructor)

isHost

isHost(): boolean

True if the player is the host

clearBricks

clearBricks(quiet?: boolean): void

Clear player’s bricks

ParamTypeDescription
quietbooleanclear bricks quietly

getRoles

getRoles(): readonly string[]

Get player’s roles, if any

getPermissions

getPermissions(): Record<string, boolean>

Get player’s permissions in a map like {"Bricks.ClearOwn": true, ...}

Returns permissions map

getNameColor

getNameColor(): string

Get player’s name color

Returns 6 character hex string

getPawn

getPawn(): Promise<string | null>

Get the player’s pawn; null when the player has no pawn

Returns pawn

getPosition

getPosition(): Promise<[number, number, number] | null>

Get player’s position; null when the player has no pawn

Returns [x, y, z] coordinates

getGhostBrick

getGhostBrick(): Promise< | { targetGrid: string; location: number[]; orientation: string; } | undefined >

Gets a user’s ghost brick info (by uuid, name, controller, or player object)

Returns ghost brick data

getPaint

getPaint(): Promise< | { materialIndex: string; materialAlpha: string; material: string; color: number[]; } | undefined >

gets a user’s paint tool properties

isCrouched

isCrouched(pawn?: string): Promise<boolean>

gets whether or not the player is crouching

isDead

isDead(pawn?: string): Promise<boolean>

gets whether or not the player is dead

getTemplateBounds

getTemplateBounds(): Promise<BrickBounds | undefined>

Gets the bounds of the template in the user’s clipboard (bounds of original selection box)

Returns template bounds

getTemplateBoundsData

getTemplateBoundsData(): Promise<ReadSaveObject | undefined>

Get bricks inside template bounds

Returns BRS JS Save Data

loadDataAtGhostBrick

loadDataAtGhostBrick(saveData: WriteSaveObject, options?: { rotate?: boolean; offX?: number; offY?: number; offZ?: number; quiet?: boolean; }): Promise<void>

Load bricks at ghost brick location

ParamTypeDescription
saveDataWriteSaveObjectsave data to load
options{ rotate?: boolean; offX?: number; offY?: number; offZ?: number; quiet?: boolean; }

loadBricks

loadBricks(saveName: string): void

Load bricks on this player’s clipboard

ParamTypeDescription
saveNamestringSave to load

loadSaveData

loadSaveData(saveData: WriteSaveObject, options?: { rotate?: boolean; offX?: number; offY?: number; offZ?: number; quiet?: boolean; }): Promise<void>

Load bricks on this player’s clipboard passing save data

ParamTypeDescription
saveDataWriteSaveObjectsave data to load
options{ rotate?: boolean; offX?: number; offY?: number; offZ?: number; quiet?: boolean; }

kill

kill(): void

Kills this player

damage

damage(amount: number): void

Damages a player

ParamTypeDescription
amountnumberAmount to damage

heal

heal(amount: number): void

Heal this player

ParamTypeDescription
amountnumberto heal

giveItem

giveItem(item: WeaponClass): void

Gives a player an item

ParamTypeDescription
itemWeaponClassItem name (Weapon_Bow)

takeItem

takeItem(item: WeaponClass): void

Removes an item from a player’s inventory

ParamTypeDescription
itemWeaponClassItem name (Weapon_Bow)

setTeam

setTeam(teamIndex: number): void

Changes a player’s team

ParamTypeDescription
teamIndexnumberTeam index

setMinigame

setMinigame(index: number): void

Changes a player’s minigame

ParamTypeDescription
indexnumberMinigame index

setScore

setScore(minigameIndex: number, score: number): void

Changes a player’s score

ParamTypeDescription
minigameIndexnumberminigame index
scorenumberScore

getScore

getScore(minigameIndex: number): Promise<number>

Fetch a player’s score

ParamTypeDescription
minigameIndexnumberminigame index

setLeaderboard

setLeaderboard(key: string, value: number): void

Set leaderboard value

ParamTypeDescription
keystringleaderboard key (Score, Kills, Deaths, CorrectGuesses)
valuenumberleaderboard value

getLeaderboard

getLeaderboard(key: string): Promise<number | null>

Get leaderboard value

ParamTypeDescription
keystringleaderboard key (Score, Kills, Deaths, CorrectGuesses)

Returns leaderboard value

Declared in src/plugin.ts.

StaticPlayer

getRoles

getRoles(omegga: OmeggaLike, id: string): readonly string[]

get a player’s roles, if any

ParamTypeDescription
omeggaOmeggaLikeomegga instance
idstringplayer uuid

Returns list of roles

getPermissions

getPermissions(omegga: OmeggaLike, id: string): Record<string, boolean>

get a player’s permissions in a map like {"Bricks.ClearOwn": true, ...}

ParamTypeDescription
omeggaOmeggaLikeOmegga instance
idstringplayer uuid

Returns permissions map

kill

kill(omegga: OmeggaLike, target: string | OmeggaPlayer): void

Kills a player

ParamTypeDescription
omeggaOmeggaLikeOmegga instance
targetstring | OmeggaPlayerPlayer or player name/id

damage

damage(omegga: OmeggaLike, target: string | OmeggaPlayer, amount: number): void

Damages a player

ParamTypeDescription
omeggaOmeggaLikeOmegga instance
targetstring | OmeggaPlayerPlayer or player name/id
amountnumberDamage amount

heal

heal(omegga: OmeggaLike, target: string | OmeggaPlayer, amount: number): void

Heal a player

ParamTypeDescription
omeggaOmeggaLikeOmegga instance
targetstring | OmeggaPlayerPlayer or player name/id
amountnumberHeal amount

giveItem

giveItem(omegga: OmeggaLike, target: string | OmeggaPlayer, item: WeaponClass): void

Gives a player an item

ParamTypeDescription
omeggaOmeggaLikeOmegga instance
targetstring | OmeggaPlayerPlayer or player name/id
itemWeaponClassItem name (Weapon_Bow)

takeItem

takeItem(omegga: OmeggaLike, target: string | OmeggaPlayer, item: WeaponClass): void

Removes an item from a player’s inventory

ParamTypeDescription
omeggaOmeggaLikeOmegga instance
targetstring | OmeggaPlayerPlayer or player name/id
itemWeaponClassItem name (Weapon_Bow)

setTeam

setTeam(omegga: OmeggaLike, target: string | OmeggaPlayer, teamIndex: number): void

Changes a player’s team

ParamTypeDescription
omeggaOmeggaLikeOmegga instance
targetstring | OmeggaPlayerPlayer or player name/id
teamIndexnumberTeam name? index?

setMinigame

setMinigame(omegga: OmeggaLike, target: string | OmeggaPlayer, index: number): void

Changes a player’s minigame

ParamTypeDescription
omeggaOmeggaLikeOmegga instance
targetstring | OmeggaPlayerPlayer or player name/id
indexnumberMinigame index

setScore

setScore(omegga: OmeggaLike, target: string | OmeggaPlayer, minigameIndex: number, score: number): void

Changes a player’s score

ParamTypeDescription
omeggaOmeggaLikeOmegga instance
targetstring | OmeggaPlayerPlayer or player name/id
minigameIndexnumberminigame index
scorenumberScore

getScore

getScore(omegga: OmeggaLike, target: string | OmeggaPlayer, minigameIndex: number): Promise<number>

Fetches a player’s score

ParamTypeDescription
omeggaOmeggaLikeOmegga instance
targetstring | OmeggaPlayerPlayer or player name/id
minigameIndexnumberminigame index

setLeaderboard

setLeaderboard(omegga: OmeggaLike, target: string | OmeggaPlayer, key: string, value: number): void

Set leaderboard value

ParamTypeDescription
omeggaOmeggaLikeOmegga instance
targetstring | OmeggaPlayerPlayer or player name/id
keystringleaderboard key (Score, Kills, Deaths, CorrectGuesses)
valuenumberleaderboard value

getLeaderboard

getLeaderboard(omegga: OmeggaLike, target: string | OmeggaPlayer, key: string): Promise<number | null>

Get leaderboard value

ParamTypeDescription
omeggaOmeggaLikeOmegga instance
targetstring | OmeggaPlayerPlayer or player name/id
keystringleaderboard key (Score, Kills, Deaths, CorrectGuesses)

Returns leaderboard value

Declared in src/plugin.ts.

Plugin API

What a plugin class implements, and the config, store, and metrics handed to its constructor.

OmeggaPlugin

An omegga plugin

PropertyTypeDescription
omeggaOmeggaLike
configPluginConfig<Config>
storePluginStore<Storage>
metrics?PluginMetricsPrometheus metrics for this plugin; no-ops when metrics are disabled. Always supplied by the plugin loader. It is declared optional purely so that plugins written before metrics existed still satisfy implements OmeggaPlugin. Declare metrics: PM on your own class, as the templates do for omegga, config, and store, to get the non-optional type.

init

init(): Promise<void | { registeredCommands?: string[] }>

Run when plugin starts, returns /commands it uses

stop

stop(): Promise<void>

Run when plugin is stopped

pluginEvent

pluginEvent?(event: string, from: string, ...args: any[]): Promise<unknown>

Run when another plugin tries to interact with this plugin

ParamTypeDescription
eventstringEvent name
fromstringName of origin plugin
...argsany[]

Returns value other plugin expects

Declared in src/plugin.ts.

PluginStore

A simple document store for plugins

get

get<T extends keyof Storage>(key: T): Promise<Storage[T]>

Get a value from plugin storage

set

set<T extends keyof Storage>(key: T, value: Storage[T]): Promise<void>

Set a value to plugin storage

delete

delete(key: string): Promise<void>

Delete a value from plugin storage

wipe

wipe(): Promise<void>

Wipe all values in plugin storage

count

count(): Promise<number>

Count entries in plugin storage

keys

keys(): Promise<(keyof Storage)[]>

Get a list of keys in plugin storage

Declared in src/plugin.ts.

PluginConfig

A config representative of the config outlined in doc.json

type PluginConfig<T extends Record<string, unknown> = Record<string, unknown>> = T

Declared in src/plugin.ts.

PluginMetrics

Prometheus metrics for a plugin, exported under omegga_plugin_<plugin>_.

Handles are always returned, so no guards are needed: when the metrics endpoint is disabled every call is a no-op.

PropertyTypeDescription
enabledbooleanWhether the metrics endpoint is enabled; handles work either way

counter

counter(opts: PluginMetricOptions): MetricCounter

gauge

gauge(opts: PluginMetricOptions): MetricGauge

histogram

histogram(opts: PluginMetricOptions & { buckets?: number[]; }): MetricHistogram

Declared in src/plugin.ts.

PluginMetricOptions

Options common to every plugin metric

PropertyTypeDescription
namestringMetric name, exported as omegga_plugin_<plugin>_<name>. Must be lowercase letters, digits, and underscores.
help?stringOne-line description, shown in the scrape output
labels?string[]Label names this metric may use (plugin is added automatically)

Declared in src/plugin.ts.

MetricCounter

A value that only ever increases, such as a number of events

inc

inc(value?: number): void
inc(labels: PluginMetricLabels, value?: number): void

Add to the count (default 1), optionally on a labelled series

reset

reset(): void

Discard every series

Declared in src/plugin.ts.

MetricGauge

A value that can go up and down, such as a queue length

set

set(value: number): void
set(labels: PluginMetricLabels, value: number): void

inc

inc(value?: number): void
inc(labels: PluginMetricLabels, value?: number): void

dec

dec(value?: number): void
dec(labels: PluginMetricLabels, value?: number): void

remove

remove(labels?: PluginMetricLabels): void

Stop exporting one labelled series

collect

collect(fn: () => number | void): void

Produce the value on demand instead of setting it. The callback runs when the metric is collected, which is cheaper than setting it on a timer.

reset

reset(): void

Declared in src/plugin.ts.

MetricHistogram

Bucketed observations, such as durations

observe

observe(value: number): void
observe(labels: PluginMetricLabels, value: number): void

startTimer

startTimer(labels?: PluginMetricLabels): () => number

Start a timer; call the returned function to observe elapsed seconds

reset

reset(): void

Declared in src/plugin.ts.

PluginMetricLabels

Label values attached to a plugin metric

type PluginMetricLabels = Record<string, string | number | boolean>

Declared in src/plugin.ts.

PluginInterop

PropertyTypeDescription
namestring
documentationIPluginDocumentation | null
loadedboolean

emitPlugin

emitPlugin?(event: string, args: any[]): Promise<any>

Declared in src/plugin.ts.

Events

Omegga reads console logs and emits events. Plugins hook these and run their own code in reaction.

EventArgumentsTrigger
*event, ...argsAny event is emitted
linelineA line of Brickadia console output
closednoneThe Brickadia server process closed
exitnoneThe Brickadia server exited
start{map}The server finished auth
host{id, name}The host was detected on server start
versionversionThe game CL was detected
unauthorizednoneThe server failed an auth check
mapchange{map}The map changed
autorestartAutoRestartConfigAn autorestart is scheduled
server:startingnoneOmegga is starting the game
server:stoppingnoneOmegga is stopping the game
server:stoppednoneThe game stopped
joinOmeggaPlayerA player joined
leaveOmeggaPlayerA player left
kickname, kicker, reasonA player was kicked
banname, kicker, reason, durationA player was banned
chatname, messageA player sent a chat message
cmdcommand, name, ...argsA player ran /command args. command is lowercased, args are the remaining space-separated words
cmd:commandname, ...argsSame, with the command built into the event name
chatcmdcommand, name, ...argsSame as cmd, for !command
chatcmd:commandname, ...argsSame as cmd:command, for !command
interactBrickInteractionA player clicked a brick with an interact component
event:NAMEplayer, ...argsAn interact component’s message was event:NAME:arg1,arg2. Escape a literal comma as \,
wirelograwA [Wire Graph] log line, without the prefix
wirecmdcommand, ...argsA [Wire Graph] log that starts with command args
wirecmd:command...argsSame, with the command built into the event name
minigamejoin{player: {name, id}, minigameName}Deprecated as of EA3. A player joined a minigame. minigameName is not unique, and is null when the player leaves all minigames. Runs before join
plugin:players:rawstring[][]Players joined or left. Raw player info, so RPC plugins can track who is online
plugin:statusname, pluginA plugin loaded, unloaded, started, or stopped

Node plugin usage

The Omegga object is an event emitter with an Omegga.on(event, fn) that returns the Omegga, so calls chain.

Omegga.on('start', () => {
  // run on server start
});
Omegga
  .on('chatcmd:ping', (name, ...args) => {
    Omegga.broadcast(`pong @ ${name} + ${args.length} args`);
  })
  .on('chatcmd:pos', async name => {
    const [x, y, z] = await Omegga.getPlayer(name).getPosition();
    Omegga.broadcast(`<b>${name}</> is at ${x} ${y} ${z}`);
  });

Deregister with Omegga.off('name', fn) or Omegga.removeAllListeners('name'). Be careful with removeAllListeners on non-cmd events, as it can deregister omegga’s own.

Node VM plugins do not need to clean up, because the VM goes away with the plugin. Node plugins do:

class Plugin {
  constructor(omegga) {
    this.omegga = omegga;

    // bind, or `this.foo()` inside exampleCallback will throw
    this.exampleCallback = this.exampleCallback.bind(this);
  }

  init() {
    this.omegga
      .on('chatcmd:ping', /* code */)
      .on('chat', this.exampleCallback);
  }

  exampleCallback() { /* code */ }

  stop() {
    this.omegga
      .removeAllListeners('chatcmd:ping')
      .off('chat', this.exampleCallback);
  }
}

RPC plugin usage

RPC plugins receive events as methods of the same name, with all arguments in an array.

// ping command
rpc.addMethod('chatcmd:ping', ([name, ...args]) => {
  rpc.notify('broadcast', `pong @ ${name} + ${args.length} args`);
});

// player position command
rpc.addMethod('chatcmd:pos', async ([name]) => {
  const [x, y, z] = await getPlayerPos(name);
  rpc.notify('broadcast', `<b>${name}</> is at ${x} ${y} ${z}`);
});

Types

Supporting types returned by the Omegga and Player APIs.

IServerStatus

PropertyTypeDescription
serverNamestring
descriptionstring
bricksnumber
componentsnumber
timenumber
maxPlayers?numberplayer slots, from the status’ Players (online/max): line
stats?Record<string, number>every integer-valued line in the status header, keyed by a snake_cased version of its label (bricks, components, …). Reported generically so stats the game adds later are available without a parser change.
players{ name: string; ping: number; time: number; roles: string[]; address: string; id: string; }[]

Declared in src/omegga/types.ts.

IGamemode

The single gamemode that replaced minigames (~CL14000). Owns the teams and the players within them.

PropertyTypeDescription
namestringthe gamemode name (e.g. “Sandbox”)
gamestatestringthe BP_GameStateBase_C object id
membersOmeggaPlayer[]every player across all teams
teams{ name: string; team: string; color: number[]; members: OmeggaPlayer[]; }[]

Declared in src/omegga/types.ts.

ILogMinigame

PropertyTypeDescription
namestring
rulesetstring
indexnumber
membersOmeggaPlayer[]
teams{ name: string; team: string; color: number[]; members: OmeggaPlayer[]; }[]

Declared in src/omegga/types.ts.

IMinigameList

An array of:

PropertyTypeDescription
indexnumber
namestring
numMembersnumber
owner{ name: string; id: string; }

Declared in src/omegga/types.ts.

IPlayerPositions

An array of:

PropertyTypeDescription
playerOmeggaPlayer
pawnstring | nullnull when the player has no pawn (e.g. spectating)
posnumber[] | nullnull when the player has no pawn position
isDeadboolean

Declared in src/omegga/types.ts.

BrickBounds

PropertyTypeDescription
minBound[number, number, number]
maxBound[number, number, number]
center[number, number, number]

Declared in src/plugin.ts.

BrickInteraction

Created when a player clicks on a brick with an interact component

PropertyTypeDescription
brick_namestringBrick name from catalog (Turkey Body, 4x Cube)
brick_assetstring | nullBrick asset name; null when the display name is not recognized
brick_size[number, number, number] | nullBrick size; null when the display name is not recognized
player{ id: string; name: string; controller: string; pawn: string }Player information, id, name, controller, and pawn
position[number, number, number]Brick center position
messagestringmessage sent from a brick click interaction
datanull | number | string | boolean | Record<string, unknown>data parsed from the line (if it starts with json:)
jsonbooleanTrue when there was a json payload
errorbooleanTrue when there was a parse error

Declared in src/plugin.ts.

AutoRestartConfig

AutoRestart options

PropertyTypeDescription
playersboolean
announcementboolean
saveWorldboolean

Declared in src/plugin.ts.

WeaponClass

type WeaponClass = | 'Weapon_APCarbine'
  | 'Weapon_AntiMaterielRifle'
  | 'Weapon_ArmingSword'
  | 'Weapon_AssaultRifle'
  | 'Weapon_AutoShotgun'
  | 'Weapon_Battleaxe'
  | 'Weapon_Bazooka'
  | 'Weapon_BoltActionRifle'
  | 'Weapon_Bow'
  | 'Weapon_BoxPistol'
  | 'Weapon_BullpupRifle'
  | 'Weapon_BullpupSMG'
  | 'Weapon_ChargedLongsword'
  | 'Weapon_CrystalKalis'
  | 'Weapon_Derringer'
  | 'Weapon_Dynamite'
  | 'Weapon_FlintlockPistol'
  | 'Weapon_GrenadeLauncher'
  | 'Weapon_GuardPistol'
  | 'Weapon_Handaxe'
  | 'Weapon_HealthPotion'
  | 'Weapon_HeavyAssaultRifle'
  | 'Weapon_HeavySMG'
  | 'Weapon_HeroSword'
  | 'Weapon_HighPowerPistol'
  | 'Weapon_HoloBlade'
  | 'Weapon_HuntingShotgun'
  | 'Weapon_Ikakalaka'
  | 'Weapon_ImpactGrenade'
  | 'Weapon_ImpactGrenadeLauncher'
  | 'Weapon_ImpulseGrenade'
  | 'Weapon_Khopesh'
  | 'Weapon_Knife'
  | 'Weapon_LeverActionRifle'
  | 'Weapon_LightMachineGun'
  | 'Weapon_LongSword'
  | 'Weapon_MagnumPistol'
  | 'Weapon_MicroSMG'
  | 'Weapon_Minigun'
  | 'Weapon_Minigun'
  | 'Weapon_MissileLauncher'
  | 'Weapon_PDW'
  | 'Weapon_Pickaxe'
  | 'Weapon_PipeBomb'
  | 'Weapon_Pistol'
  | 'Weapon_PlasmaPistol'
  | 'Weapon_PlasmaSMG'
  | 'Weapon_PulseCarbine'
  | 'Weapon_PulseRifle'
  | 'Weapon_QuadLauncher'
  | 'Weapon_Revolver'
  | 'Weapon_RocketJumper'
  | 'Weapon_RocketLauncher'
  | 'Weapon_Sabre'
  | 'Weapon_SemiAutoRifle'
  | 'Weapon_ServiceRifle'
  | 'Weapon_Shotgun'
  | 'Weapon_SlugShotgun'
  | 'Weapon_Sniper'
  | 'Weapon_SodaCan'
  | 'Weapon_Spatha'
  | 'Weapon_SportingShotgun'
  | 'Weapon_StampedSMG'
  | 'Weapon_StandardSubmachineGun'
  | 'Weapon_StickGrenade'
  | 'Weapon_SubmachineGun'
  | 'Weapon_SuperShotgun'
  | 'Weapon_SuppressedAssaultRifle'
  | 'Weapon_SuppressedBullpupSMG'
  | 'Weapon_SuppressedGuardPistol'
  | 'Weapon_SuppressedHeavySMG'
  | 'Weapon_SuppressedMicroSMG'
  | 'Weapon_SuppressedPistol'
  | 'Weapon_SuppressedServiceRifle'
  | 'Weapon_TacticalSMG'
  | 'Weapon_TacticalShotgun'
  | 'Weapon_Tomahawk'
  | 'Weapon_TwinCannon'
  | 'Weapon_TypewriterSMG'
  | 'Weapon_Zweihander'

Declared in src/plugin.ts.

IPluginConfigDefinition

type IPluginConfigDefinition = {
  description: string;
} & (
  | {
      type: 'string' | 'password' | 'role';
      default: string;
    }
  | {
      type: 'boolean';
      default: boolean;
    }
  | {
      type: 'number';
      default: number;
    }
  | {
      type: 'enum';
      options: (string | number)[];
      default: string | number;
    }
  | {
      type: 'players';
      default: {
        id: string;
        name: string;
      };
    }
  | ({
      type: 'list';
    } & (
      | {
          itemType: 'string';
          default: string[];
        }
      | {
          itemType: 'number';
          default: number[];
        }
      | {
          itemType: 'enum';
          options: (string | number)[];
          default: string | number;
        }
    ))
)

Declared in src/plugin.ts.

IPluginCommand

PropertyTypeDescription
namestring
descriptionstring
example?string
argsIPluginCommandArgument[]

Declared in src/plugin.ts.

IPluginCommandArgument

PropertyTypeDescription
namestring
descriptionstring
required?boolean

Declared in src/plugin.ts.

IPluginDocumentation

PropertyTypeDescription
namestring
descriptionstring
authorstring
configRecord<string, IPluginConfigDefinition>
commandsIPluginCommand[]

Declared in src/plugin.ts.

IMatcher

type IMatcher<T> = | {
      pattern: RegExp;
      callback: (match: RegExpMatchArray) => boolean;
    }
  | {
      pattern: (line: string, match: RegExpMatchArray | null) => T;
      callback: (match: RegExpMatchArray) => T;
    }

Declared in src/plugin.ts.

IWatcher

type IWatcher<T> = {
  bundle: boolean;
  debounce: boolean;
  timeoutDelay: number;
  afterMatchDelay: number;
  last: (match: T) => boolean;
  callback: () => void;
  resolve: (...args: any[]) => void;
  remove: () => void;
  done: () => void;
  timeout: ReturnType<typeof setTimeout>;
} & (
  | {
      pattern: WatcherPattern<T>;
      matches: T[];
    }
  | {
      pattern: RegExp;
      matches: RegExpMatchArray[];
    }
)

Declared in src/plugin.ts.

WatcherPattern

type WatcherPattern<T> = (
  line: string,
  match: RegExpMatchArray | null,
) => T | RegExpMatchArray | null | undefined | '[OMEGGA_WATCHER_DONE]'

Declared in src/plugin.ts.

Log parsing

To tackle the issue of parsing brickadia console logs, the LogWrangler was created. It wrangles logs into a manageable form.

It’s important to note that some console commands will take longer to run when there are more players on the server and some watchers may time out or run slower.

Terminology

TermDefinition
PatternA function or Regex that matches a brickadia server log
MatcherGiven a pattern and a callback, a matcher will execute the callback every time a log matches the pattern
WatcherGiven a pattern, a watcher will wait for a log to match the pattern and resolve with the matched line
Log LineA line of the brickadia server log
Log ChunkA block of logs that match the same pattern with incremental indices
Log ArrayA LogChunk with items after each matched chunk line

Watch Log Chunk

When running console commands, it’s helpful to be able to parse large blocks of uniform data. The Omegga.watchLogChunk(cmd, pattern, options) method does just that.

This method is a helper method for a watcher.

If nothing is matched within the configured time, the promise rejects. Otherwise, it resolves with an array of the matched results.

Check out the commandInjector for some example usage of watchLogChunk and watchLogArray.

Omegga.watchLogChunk(cmd, pattern, options) -> Promise<Array<Results>>

Arguments

NameTypeDescription
cmdstringBrickadia console command to run
patternRegExp or functionreturns non-null or matches when a log line is matched. The result is the added to the promise result
optionsobjectOptions to configure the watcher

Options

Options are an object of {optionName: optionValue} based on the below table.

NameTypeDefaultDescription
firststring or functionnoneDetermines if this is the first log line in the log chunk. If first is set to 'index', the chunk will start when the index capture group of the pattern argument is '0'. If first is a function, the chunk will start when the function returns true
lastfunctionnoneSame as last option in addWatcher (below)
afterMatchDelaynumber10Same as afterMatchDelay option in addWatcher (below)
timeoutDelaynumber100Same as timeoutDelay option in addWatcher (below)

Example Preferred Console Logs

The following is the result of the console command: GetAll BRPlayerState PlayerNamePrivate

[2021.02.16-23.52.46:582][320]0) BP_PlayerState_C /Game/Maps/Plate/Plate.Plate:PersistentLevel.BP_PlayerState_C_2147482508.PlayerNamePrivate = cake
[2021.02.16-23.52.46:582][320]1) BP_PlayerState_C /Game/Maps/Plate/Plate.Plate:PersistentLevel.BP_PlayerState_C_2147482402.PlayerNamePrivate = cake
[2021.02.16-23.52.46:582][320]2) BP_PlayerState_C /Game/Maps/Plate/Plate.Plate:PersistentLevel.BP_PlayerState_C_2147482287.PlayerNamePrivate = cake

Watch Log Array

Watch Log Array is useful for parsing console output that is multi dimensional (A result for each player, and each result has multiple entries). This method is an extension of Watch Log Chunk.

If nothing is matched within the configured time, the promise rejects. Otherwise, it resolves with an array of the matched results.

Check out the commandInjector for some example usage of watchLogChunk and watchLogArray.

Omegga.watchLogArray(cmd, itemPattern, memberPattern) -> Promise<Array>

Output Format

[{
  item: /* capture group from the match of itemPattern */,
  members: [
     /* array of capture groups from the match of memberPattern */ 
  ],
}, /* ... */ ]

Arguments

NameTypeDescription
cmdstringBrickadia console command to run
itemPatternRegExpA regex to match the top level items. Must have an (?<index>) capture group. Information is extracted via capture groups.
memberPatternRegExpA regex to match the bottom level logs.

Example Preferred Console Logs

The following is the result of the console command: GetAll BP_Ruleset_C MemberStates

Note: the two spaces before 0: and 1: are tabs in the game output.

[2021.02.16-23.53.27:331][701]0) BP_Ruleset_C /Game/Maps/Plate/Plate.Plate:PersistentLevel.BP_Ruleset_C_2147482516.MemberStates =
[2021.02.16-23.53.27:331][701]  0: BP_PlayerState_C'/Game/Maps/Plate/Plate.Plate:PersistentLevel.BP_PlayerState_C_2147482508'
[2021.02.16-23.53.27:331][701]  1: BP_PlayerState_C'/Game/Maps/Plate/Plate.Plate:PersistentLevel.BP_PlayerState_C_2147482402'
[2021.02.16-23.53.27:331][701]1) BP_Ruleset_C /Game/Maps/Plate/Plate.Plate:PersistentLevel.BP_Ruleset_C_2147482167.MemberStates =
[2021.02.16-23.53.27:331][701]  0: BP_PlayerState_C'/Game/Maps/Plate/Plate.Plate:PersistentLevel.BP_PlayerState_C_2147482287'

The following code will extract Rulesets (minigames) as items with PlayerStates (clients) as members. BP_Ruleset_C no longer exists on current builds; the shape of the parse is what the example is for:

const ruleMembersRegExp = /^(?<index>\d+)\) BP_Ruleset_C (.+):PersistentLevel.(?<ruleset>BP_Ruleset_C_\d+)\.MemberStates =$/;
const playerStateRegExp = /^\t(?<index>\d+): BP_PlayerState_C'(.+):PersistentLevel\.(?<state>BP_PlayerState_C_\d+)'$/;
Omegga.watchLogArray('GetAll BP_Ruleset_C MemberStates', ruleMembersRegExp, playerStateRegExp)
  .then(console.log)
  .catch(console.error)

Matchers

Matchers are used by Omegga to trigger events. You can find the matchers omegga uses in the src/omegga/matchers directory.

These are less useful for plugins that use existing triggers and more useful for adding new core features to Omegga.

Omegga.addMatcher(pattern, callback) -> deregister function

Arguments

NameTypeDescription
patternRegExp or functionreturns non-null or matches when a log line is matched. The result is the arguments to the callback
callbackfunctionFunction to run when the pattern matches

Watchers

Watchers are used when waiting for something to produce console output.

If nothing is matched within the configured time, the promise rejects.

Omegga.addWatcher(pattern, options) -> Promise<Result of Pattern>

Arguments

NameTypeDescription
patternRegExp or functionreturns non-null or matches when a log line is matched. The result is the return value of the promise
optionsobjectOptions to configure the watcher

Options

Options are an object of {optionName: optionValue} based on the below table.

NameTypeDefaultDescription
timeoutDelaynumber50Milliseconds before the watcher rejects
bundlebooleanfalseIf bundle is set to true, it returns all matches after the timeout ends rather than resolving (can’t be used with delay 0)
debounceboolfalse(used with bundle) Waits extra time after each match before timing out
lastfunctionnone(used with bundle) A function run on the log line. if it returns true, the watcher resolves early
execfunctionnoneA function run after the watcher is created

Here is an example options object:

  {
    timeoutDelay: 50,
    bundle: false,
    debounce: false,
    afterMatchDelay: 0,
    exec: () => Omegga.writeln('Chat.Broadcast "Hello"')
  }

Metrics

Omegga can serve a Prometheus scrape endpoint, off by default. It is a standalone HTTP server, so it works with omegga.webui: false and binds to its own address and port.

metrics:
  enabled: true
  bind: '127.0.0.1'
  port: 9000

Or set METRICS_ENABLED=true, METRICS_BIND, METRICS_PORT.

# prometheus.yml
scrape_configs:
  - job_name: omegga
    static_configs:
      - targets: ['localhost:9000']

Security

The endpoint is unauthenticated, hence the loopback default. To expose it, either front it with a reverse proxy or set metrics.token and have Prometheus send it as authorization: { credentials: ... }. Omegga warns at startup if you bind off-loopback without a token.

A Prometheus to scrape it

If you do not already run one, this is enough to stand a scraper up next to omegga. It assumes omegga runs on the host and Prometheus runs in docker; for omegga in a container too, skip to the whole stack in compose.

# compose.yml
services:
  prometheus:
    image: prom/prometheus:v3.14.0
    restart: unless-stopped
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention.time=15d'
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ./omegga.token:/etc/prometheus/omegga.token:ro
      - prometheus:/prometheus
    # omegga runs on the host, not on this network
    extra_hosts:
      - 'host.docker.internal:host-gateway'
    ports:
      - '127.0.0.1:9090:9090'

volumes:
  prometheus:
# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: omegga
    authorization:
      credentials_file: /etc/prometheus/omegga.token
    static_configs:
      - targets: ['host.docker.internal:9000']
        labels:
          instance: server-1

omegga.token holds the same string as metrics.token, and nothing else. Prometheus does not expand environment variables in its config file, which is why the token goes in a file rather than inline.

Three things that are easy to get wrong:

  • The default bind is unreachable from a container. metrics.bind defaults to 127.0.0.1, which is the host’s loopback and not one any container shares. Set METRICS_BIND=0.0.0.0, and set metrics.token in the same change, or the endpoint is on your LAN unauthenticated.
  • Prometheus itself has no authentication. Publishing it on 127.0.0.1:9090 keeps its API off the network while leaving it reachable from the host, which is all the web UI dashboards need.
  • instance is what those dashboards filter on. Whatever you label the target here is what metrics.prometheus.instance has to be set to. Several omeggas is one more targets entry each, with a distinct instance.

Local storage is capped by --storage.tsdb.retention.time. To keep more than that, remote_write into something built for it rather than raising the retention; the web UI reads back at most metrics.prometheus.retentionDays regardless. Grafana pointed at the same Prometheus works if you want dashboards beyond the built-in ones.

The whole stack in compose

Running omegga in a container puts it on the same network as the scraper, so the metrics port never has to be published and Prometheus reaches it by service name. This adds VictoriaMetrics behind Prometheus, which keeps the long history while Prometheus stays a 15 day buffer in front of it.

Four files, in one directory:

.
├── compose.yaml
├── .env
├── omegga.token
├── prometheus.yml
└── server/
    └── omegga-config.yml

server/ is the bind mount, so server/omegga-config.yml is what omegga sees at /server/omegga-config.yml. Write it before the first up, or let omegga generate a default one and edit it afterwards.

# compose.yaml
services:
  omegga:
    image: ghcr.io/brickadia-community/omegga:latest
    restart: unless-stopped
    stdin_open: true
    tty: true
    env_file: .env
    ports:
      - '${OMEGGA_PORT}:${OMEGGA_PORT}/tcp'
      - '${BRICKADIA_PORT}:${BRICKADIA_PORT}/udp'
    volumes:
      - home:/home/steam
      - ./server:/server

  prometheus:
    image: prom/prometheus:v3.14.0
    restart: unless-stopped
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--storage.tsdb.retention.time=15d'
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ./omegga.token:/etc/prometheus/omegga.token:ro
      - prometheus:/prometheus

  victoriametrics:
    image: victoriametrics/victoria-metrics:v1.150.0
    restart: unless-stopped
    command:
      - '-storageDataPath=/storage'
      - '-retentionPeriod=2y'
      - '-httpListenAddr=:8428'
    volumes:
      - victoriametrics:/storage

volumes:
  home:
  prometheus:
  victoriametrics:
# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: omegga
    authorization:
      credentials_file: /etc/prometheus/omegga.token
    static_configs:
      - targets: ['omegga:9000']
        labels:
          instance: server-1

remote_write:
  - url: http://victoriametrics:8428/api/v1/write
# server/omegga-config.yml
omegga:
  port: 8080
  webui: true
  https: true
server:
  port: 7777
  map: Plate
metrics:
  enabled: true
  # the container's own network interface, not the host's
  bind: 0.0.0.0
  port: 9000
  # the same string as ./omegga.token
  token: CHANGE_ME
  prometheus:
    enabled: true
    url: http://prometheus:9090
    instance: server-1
    retentionDays: 15
# .env - read for the ${...} in compose.yaml, and passed to the container
BRICKADIA_TOKEN=...
OMEGGA_PORT=8080
BRICKADIA_PORT=7777
PUID=1000
PGID=1000
# the token, with no trailing newline
printf '%s' 'CHANGE_ME' > omegga.token
chmod 600 omegga.token

Then docker compose up -d.

Why the metrics settings are in the config file rather than the environment: metrics.token is the one field with no environment variable, and having half the block in each place is worse than having all of it in one. METRICS_ENABLED, METRICS_BIND, and METRICS_PORT still work if you would rather template them.

A few things this arrangement is doing on purpose:

  • Neither Prometheus nor VictoriaMetrics publishes a port. Neither has any authentication, and VictoriaMetrics’ HTTP API includes both writes and series deletion, so being unreachable is what protects them. Add ports: - '127.0.0.1:9090:9090' to prometheus if you want its own UI on the host; that keeps it off the network while making it reachable locally.
  • The metrics port is not published either, and does not need to be. Only prometheus talks to it, over the compose network. The token is still set because omegga warns about an off-loopback bind without one, and because anything else you later attach to that network could otherwise read it.
  • instance: server-1 appears twice, in the scrape config and in metrics.prometheus.instance. They have to agree or the dashboards query unfiltered and chart every scraped server at once. A second omegga is another service, another targets entry, and a distinct instance.
  • Prometheus keeps 15 days, VictoriaMetrics keeps two years. Losing prometheus’s volume costs nothing durable; the one worth backing up is victoriametrics.

The web UI reads from Prometheus here, so its range picker only reaches back as far as retentionDays. VictoriaMetrics answers the same /api/v1/query and /api/v1/query_range that omegga uses, so pointing url at http://victoriametrics:8428 and raising retentionDays gives the dashboards the full two years instead.

Game metrics

metrictypedescription
brickadia_server_infogaugeBuild and config as labels (version, server_name, map, steambeta, port); always 1
brickadia_upgauge1 when the game is running with a map loaded, from its start/stop log events
brickadia_status_responsivegauge1 when the last status poll was answered; absent until one is attempted
brickadia_server_stategaugeState set: 1 on the active state of starting/running/stopping/stopped/updating
brickadia_starting, brickadia_stopping, brickadia_updatinggaugeThe same states as flat booleans, easier to alert on
brickadia_uptime_seconds, brickadia_start_time_secondsgaugeUptime and last start
brickadia_players_online, brickadia_players_maxgaugeConnected players and slots
brickadia_bricks_count, brickadia_components_countgaugeWorld contents
brickadia_entities_countgaugeEntities, when the game reports them
brickadia_status_age_secondsgaugeAge of the cached server status
brickadia_update_availablegaugeWhether an update was found at the last check
brickadia_update_check_timestamp_seconds, brickadia_last_update_timestamp_secondsgaugeLast update check and last completed update
brickadia_players_joined_totalcounterPlayer joins
brickadia_players_joined_unique_totalcounterDistinct players since omegga started
brickadia_players_left_total, brickadia_players_kicked_totalcounterDisconnects and kicks
brickadia_players_banned_totalcounterBans issued
brickadia_bans_active, brickadia_bans_listedgaugeBans in force, and every ban list entry including expired ones
brickadia_player_playtime_seconds_totalcounterTotal player-seconds connected; accrues live, so in-progress sessions count
brickadia_chat_messages_totalcounterChat messages
brickadia_commands_total, brickadia_unknown_commands_totalcounterChat commands run
brickadia_interactions_totalcounterBrick interactions
brickadia_log_lines_total, brickadia_stderr_lines_totalcounterGame log throughput; a flatline is the earliest sign it has wedged
brickadia_starts_total, brickadia_stops_total, brickadia_crashes_totalcounterLifecycle events
brickadia_map_changes_total, brickadia_autorestarts_total, brickadia_updates_totalcounterMap changes, autorestarts, updates
brickadia_status_poll_failures_totalcounterStatus polls that timed out or failed to parse
brickadia_player_session_secondshistogramHow long players stayed connected
brickadia_player_ping_secondshistogramPlayer ping, sampled once per status poll
brickadia_status_poll_duration_secondshistogramHow long Server.Status took, a good proxy for game hitching
brickadia_process_cpu_seconds_totalcounterCPU consumed by the game process (Linux)
brickadia_process_resident_memory_bytes, brickadia_process_start_time_seconds, brickadia_process_open_fdsgaugeGame process stats from /proc (Linux)

World stats come from a Server.Status console command, cached and refreshed at most once per statusMaxAge seconds however many servers scrape. A scrape serves the cache and refreshes in the background, so an unresponsive game reads brickadia_up 0 instead of stalling the scrape. Any integer line in the status header is exported as brickadia_<name>_count, so stats a future build starts reporting appear on their own.

Omegga metrics

metrictypedescription
omegga_build_infogaugeVersion, node version, platform, containerized; always 1
omegga_up, omegga_start_time_secondsgaugeLiveness and start time
omegga_webserver_upgaugeWhether the web UI is serving
omegga_plugins_scanned, omegga_plugins_enabled, omegga_plugins_loadedgaugePlugin counts
omegga_plugin_info, omegga_plugin_loaded, omegga_plugin_enabledgaugePer-plugin state, labelled by plugin
omegga_uncaught_exceptions_totalcounterExceptions reaching the process handler
omegga_unhandled_rejections_totalcounterPromise rejections reaching the process handler
omegga_plugin_errors_totalcounterPlugin load, unload, and runtime failures, by plugin
omegga_plugin_metrics_seriesgaugeSeries each plugin is exporting
omegga_plugin_metrics_dropped_total, omegga_plugin_metrics_errors_totalcounterPlugin metrics hitting the limits, or whose collect() threw
omegga_metrics_collect_errors_totalcounterOmegga’s own collectors that threw and were skipped
omegga_host_cpu_ratio, omegga_host_memory_*, omegga_host_disk_*gaugeHost utilization (sampled by the web UI heartbeat; absent when it is off)
omegga_host_network_receive_bytes_total, omegga_host_network_transmit_bytes_totalcounterHost network totals (Linux)
omegga_scrapes_total, omegga_scrape_duration_secondscounter, histogramThe endpoint’s own stats

Omegga exits after an uncaught exception, so a scrape usually will not catch omegga_uncaught_exceptions_total incrementing; the target going down is the signal. omegga_plugin_errors_total keeps a series for plugins that have since unloaded, so a crash loop stays visible.

With defaultMetrics: true (the default) the standard process_* and nodejs_* metrics are included under their conventional names.

Plugin metrics

Plugins get a metrics object as a fourth constructor argument, alongside store. Everything registered is exported as omegga_plugin_<plugin>_<metric>, with the plugin’s real name as a plugin label.

import OmeggaPlugin, { OL, PS, PC, PM } from 'omegga';

export default class Plugin implements OmeggaPlugin<Config, Storage> {
  omegga: OL;
  config: PC<Config>;
  store: PS<Storage>;
  metrics: PM;

  constructor(omegga: OL, config: PC<Config>, store: PS<Storage>, metrics: PM) {
    this.omegga = omegga;
    this.config = config;
    this.store = store;
    this.metrics = metrics;
  }

  async init() {
    // omegga_plugin_my_plugin_kills_total{plugin="my plugin",weapon="pistol"}
    const kills = this.metrics.counter({
      name: 'kills_total',
      help: 'Kills by weapon',
      labels: ['weapon'],
    });
    this.omegga.on('cmd:kill', () => kills.inc({ weapon: 'pistol' }));

    // gauges push with set()/inc()/dec() or pull with collect()
    this.metrics
      .gauge({ name: 'queue_size', help: 'Queued' })
      .collect(() => this.queue.length);

    // histograms observe values, or time a block
    const done = this.metrics
      .histogram({ name: 'lookup_seconds', help: 'Lookups', buckets: [0.1, 1] })
      .startTimer();
    await this.lookup();
    done();
  }
}

A plugin’s metrics are dropped when it unloads or crashes, so a dead plugin’s last values are never scraped as if live. A collect() that throws leaves that metric at its last value and bumps omegga_plugin_metrics_errors_total, without affecting the rest of the scrape. Worker and RPC plugin metrics render from the last snapshot the host received and run no plugin code during a scrape, so a hung plugin goes stale rather than delaying it.

Limits. At most 64 metrics per plugin, 1000 label combinations each, 8 label names. Going over yields a no-op handle or a dropped series, counted by omegga_plugin_metrics_dropped_total. Those limits are a backstop, not a licence: never label with a player name, ID, brick, or position. Label with things you can enumerate ahead of time.

RPC plugins push a metrics notification carrying the same snapshot, as often as they like:

{
  "jsonrpc": "2.0",
  "method": "metrics",
  "params": [
    {
      "type": "counter",
      "name": "omegga_plugin_my_plugin_kills_total",
      "help": "Kills by weapon",
      "samples": [{ "labels": { "weapon": "pistol" }, "value": 12 }]
    }
  ]
}

Histogram families also carry buckets (ascending upper bounds); their samples carry counts (one per bucket plus a trailing +Inf slot, not cumulative), sum, and count. Names must start with the plugin’s own prefix.

Dashboards in the web UI

Omegga can also read those metrics back out of a Prometheus that scrapes it, and chart them in the web UI. Off unless configured:

metrics:
  prometheus:
    enabled: true
    url: http://127.0.0.1:9090
    instance: server-1 # the `instance` label identifying this omegga's series

Or METRICS_PROMETHEUS_ENABLED, _URL, _INSTANCE, _TIMEOUT. Also available: timeout (seconds, default 3), cacheSeconds (default 15), and retentionDays (default 15), which limits how far back the range picker reaches.

Set instance to whatever your scrape config relabels this server to. Without it every query runs unfiltered, so a Prometheus scraping two servers charts both at once. Only [A-Za-z0-9_.:-] is accepted, because the value goes into a PromQL label matcher.

A Metrics entry then appears in the nav for users holding any of the metrics.* permissions, with four dashboards: players, server health, plugins, and host health. Each has its own permission, so a moderator can see player activity without seeing the machine. Panels can be hidden per dashboard from the Panels menu; that choice is per browser. Hovering a panel header explains what it measures.

Two things the UI does not do, deliberately:

  • It never accepts PromQL from the browser. Panels are a fixed catalog in dashboards.ts and the client asks for them by name. A Prometheus scraping omegga is usually scraping everything else its operator runs, so a pass-through ?query= would make the web UI a read interface for all of it.
  • It never writes config. The connection is file and environment only.

If Prometheus is unreachable the dashboards say so instead of rendering empty charts, and a panel whose query fails reports it in place without disturbing the rest. “Reachable but holds nothing for this instance” gets its own message, since the usual cause is a scrape config that relabels differently than instance expects.

Web UI Permission System

The web UI uses a hierarchical, purely additive permission model. Permissions can only grant access, never deny it.

Data Model

Each user has a PermissionSet:

interface PermissionSet {
  root: 'all' | 'read' | 'off';
  domains: Partial<Record<Domain, 'all' | 'read'>>;
  scopes: Partial<Record<Scope, boolean>>;
}

A server-wide default PermissionSet serves as a fallback for all users. Roles are additive collections of permissions that can be assigned to users.

Resolution

When checking whether a user has a specific scope, the resolver walks these levels in order. The first level that produces a definite answer wins.

1. Owner bypass    - isOwner grants everything
2. Root level      - 'all' grants everything; 'read' grants read-only scopes; 'off' falls through
3. Domain level    - 'all' grants all scopes in that domain; 'read' grants read-only scopes; absent falls through
4. Scope level     - true grants; absent falls through to role permissions
5. Role perms      - union of all assigned role permissions + default permissions, resolved with the same root/domain/scope logic
6. Not granted     - false

The readOnly flag on each scope definition determines whether “Read Only” mode at root or domain level grants that scope.

Roles

Roles are named, ordered collections of permissions stored as PermissionSet values. A user’s effective permissions are the union of their direct permissions, all assigned role permissions, and the default permissions.

Ordering and Hierarchy

Each role has a numeric order value. Higher order = more powerful. The order is used solely for hierarchy enforcement (preventing privilege escalation), not for permission resolution (which is purely additive/union).

Hierarchy rules for non-owner users:

  • A user can only manage (edit, delete, reorder) roles with order strictly less than their highest role that grants the relevant permission
  • A user cannot grant or revoke roles at or above their own level
  • A user cannot grant permissions they do not possess to a role
  • Reorder requires role.edit from an assigned role (not from default permissions)
  • New roles are created at order 1 (weakest), with existing roles bumped up

The display sorts roles descending by order (most powerful at top).

Default Permissions

Default permissions apply to all users as a baseline fallback. They are edited separately from roles via the role.defaultPermissions scope and appear as the “Everyone” entry at the bottom of the roles list.

Levels

Root

ValueUI LabelBehavior
allAllGrants every scope
readRead OnlyGrants only scopes marked readOnly: true
offManualFalls through to domain and scope checks

Domain

ValueUI LabelBehavior
allAllGrants every scope in this domain
readRead OnlyGrants only readOnly scopes in this domain
(absent)ManualFalls through to individual scope toggles

Domains are purely additive. Setting a domain to “Manual” removes it from the domains map rather than storing a deny value.

Scope

Permissions are purely additive. true grants the scope, absent (or toggled off) falls through to role/default permissions. There is no way to explicitly deny a scope – toggling a scope off simply removes the user-level override so the role/default permissions apply.

Domains and Scopes

Each scope belongs to exactly one domain and is either read-only (R) or read-write (W).

Chat

ScopeR/WDescription
chat.sendWSend messages in the dashboard chat widget
chat.recentRView recent chat on the dashboard
chat.historyRBrowse past chat logs in the history view
chat.calendarRNavigate chat by date in the history view

Player

ScopeR/WDescription
player.listRView the player list in the players view
player.getRInspect player details and history
player.banWBan players from the player inspector
player.kickWKick players from the player inspector
player.unbanWUnban players from the player inspector
player.clearBricksWClear a player’s bricks from the player inspector

Plugin

ScopeR/WDescription
plugin.listRView installed plugins in the plugins view
plugin.getRInspect plugin details and configuration
plugin.configWEdit plugin settings in the plugin inspector
plugin.loadWLoad plugins from the plugin inspector
plugin.unloadWUnload plugins from the plugin inspector
plugin.toggleWEnable or disable plugins in the plugins view
plugin.reloadAllWReload all plugins from the plugins view

Server

ScopeR/WDescription
server.statusRView server status on the dashboard and server view
server.startWStart the server from the server view
server.stopWStop the server from the server view
server.restartWRestart the server from the server view
server.update.checkWCheck for server updates in the server view (runs SteamCMD)
server.update.runWRun server updates from the server view
server.autorestart.getRView auto-restart settings in the server view
server.autorestart.setWChange auto-restart settings in the server view
server.utilizationRView CPU, memory, and disk usage on the dashboard

User

ScopeR/WDescription
user.listRView web UI user accounts in the users view
user.createWCreate new user accounts in the users view
user.passwdWChange other users’ passwords in the user inspector
user.banWDisable or re-enable users in the user inspector
user.deleteWPermanently delete user accounts
user.permissionsWEdit user permissions in the users view
user.grantRoleWAssign and revoke roles to/from users
user.readMfaRView MFA status of other users in the user inspector
user.resetMfaWReset MFA for other users in the user inspector

World

ScopeR/WDescription
world.listRView available worlds in the worlds view
world.activeRSee which world is currently loaded
world.nextRSee which world will load next
world.revisionsRView world save revisions in the world inspector
world.metaRView world metadata in the world inspector
world.loadWLoad worlds from the world inspector
world.useWSet the default world in the worlds view
world.saveWSave the current world from the worlds or server view
world.createWCreate new worlds in the worlds view

Role

ScopeR/WDescription
role.listRView roles in the roles view
role.editWCreate, edit, delete, and reorder roles
role.defaultPermissionsWEdit the default permissions that apply to all users
role.grantPermissionWAdd or remove permissions within roles

Self-Service

All authenticated users can access the /account page regardless of permissions. This page shows the user’s own account info, MFA management (TOTP, passkeys, recovery codes), and password change.

The user.self endpoint (scoped to session.info) returns the current user’s data without requiring user.list. The user.passwd endpoint allows any user to change their own password (requires current password); changing another user’s password requires the user.passwd scope. MFA management endpoints (mfa.*) are scoped to session.info and require password verification for sensitive operations (TOTP setup/disable, passkey removal, recovery code generation).

Enforcement

Backend

Every tRPC endpoint is wrapped with protectedProcedure(scope), which runs requireScope middleware. The middleware:

  1. Rejects unauthenticated requests (UNAUTHORIZED)
  2. Allows owners unconditionally
  3. Resolves the scope against the user’s permissions + role permissions + server defaults
  4. Rejects with FORBIDDEN if not granted

Subscription endpoints (like server.onStatus, chat.onMessage) share the scope of their corresponding query endpoint rather than having separate scopes.

Role management endpoints additionally enforce hierarchy checks via checkRoleHierarchy, which verifies the actor’s highest role granting the required scope has an order strictly greater than the target role’s order.

Frontend

On login, the session response includes resolvedScopes – a flat Record<string, boolean> with every scope pre-resolved (including role and default permissions). The frontend stores this in a nanostore ($resolvedScopes).

  • useHasScope(...scopes) – returns true if the user has all specified scopes
  • useHasAnyScope(...scopes) – returns true if the user has any of the specified scopes
  • useRequireScope(scope) – redirects to / if the user lacks the scope

The permission and scope definitions are shared between frontend and backend via @backend/scopes.

Sidenav Visibility

Each sidenav link is gated by a specific scope:

LinkRequired ScopeAlways Visible
DashboardYes
Worldsworld.listNo
Historychat.historyNo
Pluginsplugin.listNo
Playersplayer.listNo
Serverserver.statusNo
Usersuser.listNo
Rolesrole.list (shown when user lacks user.list)No
AccountYes

The Users sidenav item also highlights when on /roles. If a user has role.list but not user.list, a Roles-only sidenav item appears instead.

View Access

Each view redirects to the dashboard if the user lacks its required scope:

  • Worlds requires world.list
  • History requires chat.history
  • Plugins requires plugin.list
  • Players requires player.list
  • Server requires server.status
  • Users requires user.list
  • Roles requires role.list

The Users and Roles views share a tab bar when the user has both user.list and role.list. The tab bar is hidden when only one permission is held.

Within a view, individual buttons and controls are conditionally rendered based on their specific scopes. The role inspector shows read-only mode for roles at or above the user’s hierarchy level. Admin actions in the user inspector (change password, disable, delete, reset MFA) are shown in an actions widget gated by their respective scopes.

The backend remains the source of truth – frontend checks are for UX only.

Privilege Escalation Prevention

All user-management mutations (passwd, ban, delete, permissions, grantRole, revokeRole, resetMfa) enforce user hierarchy checks via checkUserHierarchy. The actor’s highest role order must be strictly greater than the target’s highest role order. The owner is always protected and cannot be targeted by non-owners.

Self-Action Prevention

Users cannot: change their own permissions, grant/revoke roles to/from themselves, disable themselves, or delete themselves. Self-service password change is allowed but requires the current password.

User Permissions

When a non-owner user edits another user’s permissions, the backend checks that the editor is not granting scopes they don’t have themselves. The mutation resolves all scopes in the proposed PermissionSet against the editor’s effective permissions (direct + roles + defaults) and rejects any scope the editor lacks. The target user must also be below the editor in the role hierarchy.

Role Permissions

When editing or creating a role’s permissions, the user must have both role.edit and role.grantPermission. The hierarchy check ensures the target role is below the user’s level. The escalation check ensures the user has every permission they are granting to the role, using their full effective permissions (direct + roles + defaults).

Default Permissions

Editing default permissions requires role.defaultPermissions. The same escalation check applies – users cannot add permissions to the defaults that they don’t have themselves. Note that removing default permissions is allowed (this can affect all users who rely on defaults).

Role Assignment

Granting a role requires user.grantRole. Three checks are enforced:

  1. Hierarchy: the role being granted must be below the actor’s highest role that grants user.grantRole
  2. Containment: the actor must possess every permission contained in the role being granted (prevents indirect privilege escalation through role assignment)
  3. Target protection: the target must be below the actor in the role hierarchy

Revoking a role requires the same hierarchy check on the role being revoked, plus the target must be below the actor.

Role Reordering

Reorder requires role.edit from an assigned role (not from default/direct permissions, which return Infinity and would bypass all hierarchy). Non-owners must submit all roles below their level (no partial reorders). The reorder preserves the order slots of unmanaged roles, preventing collisions with roles above the actor’s level.

Direct Permissions and Hierarchy

When a user has a scope from direct or default permissions (but not from any assigned role), getActorHighestOrder returns Infinity for that scope. This allows the user to manage any role for create/edit/delete operations but does NOT allow reorder (which explicitly requires a role-based order). Owners should be aware that granting role.edit as a direct permission is equivalent to owner-level role management.

Race Conditions

  • createRole uses a single atomic $inc multi-update to bump all existing role orders before inserting at order 1. The uniform bump preserves relative ordering, so concurrent operations see consistent hierarchy.
  • Reorder assigns orders inline within the endpoint handler. NeDB operations are individually atomic but there are no multi-document transactions. Under concurrent reorder requests, the last write wins, but hierarchy checks prevent escalation because they validate against the current database state at check time.
  • The roles cache is invalidated after every mutation. Between a mutation and cache invalidation, concurrent reads may see stale data, but stale data is always MORE restrictive (lower actor orders), never less.

Storage

User permissions are stored in the users NeDB store as part of each user document. Default permissions are stored in the server NeDB store as a { type: 'defaultPermissions' } document. Roles are stored in the server NeDB store as { type: 'webRole' } documents.

New users are created with EMPTY_PERMISSIONS (root: 'off', domains: {}, scopes: {}) and an empty roles: [] array, which means all access is determined by server defaults until explicitly configured.

Omegga behind a real certificate

Omegga’s web UI serves https with a certificate it generates itself, so browsers warn on every visit. Putting a reverse proxy in front of it gets a certificate browsers trust, at omegga.yourdomain.com instead of an IP and a port.

Replace any instance of OMEGGA.YOURDOMAIN.COM below with your domain (most people use omegga as a subdomain).

Either way, first create an A record in your domain’s DNS settings pointing OMEGGA.YOURDOMAIN.COM at your server’s IP, and make sure ports 80 and 443 are forwarded and open on the firewall. Do not forward 8080 unless you are troubleshooting.

Caddy

Caddy obtains and renews the certificate on its own, so this is the shorter path. Install it from caddyserver.com/docs/install, then put this in /etc/caddy/Caddyfile:

OMEGGA.YOURDOMAIN.COM {
    reverse_proxy https://127.0.0.1:8080 {
        transport http {
            # omegga's own certificate is self-signed
            tls_insecure_skip_verify
        }
    }
}

Then sudo systemctl reload caddy. That is the whole configuration: websockets, the redirect from port 80, and certificate renewal are defaults.

Caddy needs port 80 reachable from the internet to complete the ACME challenge. If it cannot get a certificate, journalctl -u caddy says why.

If you would rather not skip verification, set https: false under omegga: in omegga-config.yml and proxy http://127.0.0.1:8080 instead. The hop is then plaintext over loopback, which does not leave the machine.

Running a second omegga is another block with its own hostname and port:

OMEGGA2.YOURDOMAIN.COM {
    reverse_proxy https://127.0.0.1:8081 {
        transport http {
            tls_insecure_skip_verify
        }
    }
}

nginx

More moving parts, and certbot has to be run separately, but if nginx is already on the box this fits alongside what is there.

These are for people hosting a dedicated server who want other users to reach the web ui, not for the faint of heart.

Replace any instance of OMEGGA.YOURDOMAIN.COM in these instructions with your domain (most users use omegga as a subdomain)

Create an A record in DNS settings for your domain. Point OMEGGA.YOURDOMAIN.COM at your server’s IP.

Generate some temporary ssl keys and move them to /etc/ssl/certs

sudo openssl req -x509 -newkey rsa:4096 -nodes -keyout ./omegga_key.pem -out ./omegga_cert.pem -days 365 -subj '/CN=OMEGGA.YOURDOMAIN.COM'
sudo mv omegga_*.pem /etc/ssl/certs/

Generate a strong dhparam (What is dhparam??)

sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048

Create nginx config by pasting this in /etc/nginx/sites-enabled/omegga.conf and replace OMEGGA.YOURDOMAIN.COM with your domain.

server {
    listen 443 ssl;
    server_name OMEGGA.YOURDOMAIN.COM;

    error_log /var/log/nginx/omegga.log;
    ssl_certificate /etc/ssl/certs/omegga_cert.pem;
    ssl_certificate_key /etc/ssl/certs/omegga_key.pem;
    ssl_dhparam /etc/ssl/certs/dhparam.pem;
    ssl_protocols TLSv1.2;
    ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:20m;
    ssl_session_timeout 180m;

    location / {
        proxy_pass https://127.0.0.1:8080/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Nginx-Proxy true;
        proxy_redirect off;
    }
}

server {
  listen 80;
  server_name OMEGGA.YOURDOMAIN.COM;
  return 301 https://OMEGGA.YOURDOMAIN.COM$request_uri;
}

Delete /etc/nginx/sites-enabled/default if you haven’t already and service nginx restart

Make sure you have ports 80 and 443 forwarded/open on firewall for your server. Do not bother port forwarding 8080 unless you are troubleshooting.

Before you can run certbot, you need to make sure nginx is working. Visit https://OMEGGA.YOURDOMAIN.COM and check if it has insecure certificate.

You can check if nginx has any errors by cat /var/log/nginx/error.log

Follow certbot instructions for nginx and run certbot --nginx when you are ready.

You should be able to access the omegga web ui from https://OMEGGA.YOURDOMAIN.COM!

Running omegga on another machine

Getting at the files and the web UI of an omegga that lives on a VPS or a spare box, rather than the one in front of you.

SSH

If you don’t already have SSH access, it’s easy to setup. These instructions should work on WSL, but additional config may be needed.

  1. Install ssh server: sudo apt install openssh-server
  2. On the computer you will be accessing from:
    1. If you haven’t already, run ssh-keygen
    2. ssh-copy-id user@remoteip (copy the key to the server so you don’t have to type passwords)
    3. ssh user@remoteip (ssh in)
    4. sudo nano /etc/ssh/sshd_config (modify the ssh server config file)

Disable in your /etc/ssh/sshd_config, it’s advised to set PasswordAuthentication no and UsePAM no to prevent people from getting access by guessing passwords. You may need to manually copy future ssh public keys or temporarily disable this to gain access again from another PC.

If you want, change Port 22 to a different port (may need to remove # from the beginning of the line)

Don’t forget to port forward the configured port (default 22) on TCP.

Remote file sharing

  1. Install samba: apt install samba

  2. Configure samba:

In /etc/samba/smb.conf, add a line like this, replace USER with your non-root user and /home/USER/omegga with the path to the omegga folder (or home if you want more freedom)

; Omegga folder path
[omegga]
  path = /home/USER/omegga
  browseable = yes
  valid users = @USER
  writable = yes
  read only = no

Then run sudo service smbd restart (you may need to port forward for TCP 445 or allow sudo ufw allow samba)

  1. Add a samba user for you: sudo smbpasswd -a USER (it will prompt for a password)

  2. Add it to windows by network drive or in the url bar with this url: \\ip-address\omegga

  3. Done!

Multiple Omeggas

If you are running instances of omegga, you will need to edit your omegga-config.yml. Here’s one with non-default ports:

omegga:
  port: 8081
  webui: true
  https: true
server:
  port: 7778

You need to change server.port or it will not post the correct port to the master server. You do not need to use a different omegga.port if you plan to redirect at the port forwarding level.

You may need to port forward the new ports in your VM and in your router.

Reverse proxy (if you own a domain)

This step is not necessary if you do not own your own domain and are okay connecting to the web-ui by IP.

If you are running multiple omegga VMs, this step will have to take place on your main PC and cannot take place on a VM.

Follow the https guide.

If you are running a second omegga, repeat the guide with these modifications (the Caddy route needs none of them beyond a second block)

  • Skip the steps for generating temporary ssl keys and dhparam, these can be re-used
  • DNS Configure OMEGGA2.YOURDOMAIN.COM to be a copy of the OMEGGA.YOURDOMAIN.COM record.
  • Make a second /etc/nginx/sites-enabled/omegga2.conf based on the one in the guide with the following changes:
    • OMEGGA.YOURDOMAIN.COM is a different OMEGGA2.YOURDOMAIN.COM
    • proxy_pass https://127.0.0.1:8080/; becomes proxy_pass https://127.0.0.1:8081/; (new web ui port)
  • Run certbot again to add key for the second domain