Documentation - Web API Guide

Red Eclipse game servers and the master server answer HTTP and WebSocket requests with JSON. You can use this to build server lists, live scoreboards, match trackers, Discord bots and moderation tools.

This guide covers what you can call and what comes back. How it is built inside the engine is in src/doc/web-api.md in the base repository.

At a glance

Quick start

All of these work from a browser page on any site (see CORS).

List the servers

const res = await fetch("https://play.redeclipse.net:28888/api/v1/servers");
const list = await res.json();
for(const s of list.servers)
    console.log(s.description, s.host, s.port, s.status ? s.status.map : "(no status)");

See who is playing on one, through the relay

The relay address uses the server’s game port (port), not its webport.

const s = list.servers[0];
const base = `https://play.redeclipse.net:28888/relay/${encodeURIComponent(s.host)}/${s.port}/api/v1`;
const players = await (await fetch(`${base}/players`)).json();

s.relay says what the relay will carry for that server: 0 means nothing, 1 means public routes and the Event feed, and 2 means everything.

Watch a match live

const ws = new WebSocket(`wss://play.redeclipse.net:28888/relay/${encodeURIComponent(s.host)}/${s.port}/api/v1/events`);
ws.onmessage = (e) =>
{
    const ev = JSON.parse(e.data);
    if(ev.event === "frag") console.log(`${ev.killer.name} fragged ${ev.victim.name} with the ${ev.weaponname || ev.weapon}`);
};

Keep a server list up to date

const live = new WebSocket("wss://play.redeclipse.net:28888/api/v1/servers/live");
live.onmessage = (e) =>
{
    const ev = JSON.parse(e.data);
    switch(ev.event)
    {
        case "snapshot": /* ev.servers is the whole list */ break;
        case "add": case "update": /* ev.server is one entry */ break;
        case "remove": /* ev.host and ev.port name it */ break;
    }
};

Reaching a server

Way URL When to use it
Master https://play.redeclipse.net:28888/api/v1/... The server list and its live updates
Relay https://play.redeclipse.net:28888/relay/<host>/<port>/api/v1/... Any game server, from a browser
Direct https://<host>:<webport>/api/v1/... Tools, bots and servers with a CA-signed certificate

Direct connections

A game server’s entry in the master’s list has a webport when its API is on, and a fingerprint when it serves HTTPS. Most servers use a self-signed certificate:

The relay

/relay/<host>/<port>/<path> asks that server for /<path> exactly as given, query string included. <host> is the host from the list and <port> is its game port. They are separate path segments because an IPv6 address has colons of its own. Requests and the Event feed both work.

What the relay passes on:

Errors that come from the relay itself rather than from the server:

Status Meaning
403 The server’s relay level is 0
404 No server with that host and port holds a link to the master
414 The path is over 511 characters
502 The master lost its link to the server
503 The server’s relay bandwidth is used up for now; retry after Retry-After (1 second)
504 The server didn’t answer within 30 seconds

Conventions

JSON and text

Text markup

Strings are plain text, with one exception: a server’s description in the master’s list is stored as the server sent it, and may carry Red Eclipse’s colour codes. Each code starts with a form feed (\f, U+000C). This strips them:

const plain = (s) => s.replace(/\f(?:z..|\[[^\]]*\]|\([^)]*\)|\{[^}]*\}|.)/g, "");

Player names and chat are plain everywhere: in the master’s list, in /status and /players, and on the Event feed. Where the game shows a player with colours and icons, the list gives you the parts as fields of their own (see GET /api/v1/servers).

Errors

Every error has a JSON body of the form {"error": "what went wrong"}, with a matching status:

Status Meaning
400 The request is malformed or a field is missing or invalid
401 The route needs more than Player and you sent no token (WWW-Authenticate: Bearer), or your token is unknown or its session expired (unknown or expired token), so sign in again
403 Your Privilege is too low, the route needs an encrypted connection, the origin isn’t allowed, or your address is banned
404 No such route, player or entry
405 The path exists but not for that method (Allow lists the ones it takes)
408 The request took too long to arrive
409 The action can’t apply, such as banning a player with no address
413 The body is over 1 MB
426 /events was asked for without a WebSocket upgrade
429 Too many failed sign-ins from your address, or too many messages to POST /say; retry after Retry-After
431 Too many headers, or a header line that is too long
503 The server isn’t connected to the master, so it can’t check a sign-in
504 An answer the server waited on, such as a sign-in, took over 30 seconds

CORS

Browsers may call the API from other origins:

The official master, play.redeclipse.net, allows every origin. Any site can use its list, live list and relay from the browser, which is what the Web API is for.

Game servers allow every origin by default too. An operator can limit their own server with httpalloworigin, and then only the origins listed can read its responses from a browser or open its WebSockets. That affects direct connections only, since the relay and the live list follow the master’s setting.

Caching

Compressed responses are sent when you accept gzip, which browsers do by default.

Signing in

Privileges

Every request has a Privilege, from lowest to highest: none, player, supporter, moderator, administrator, developer, founder. An anonymous request is none, as an unauthenticated player is in the game. player means a signed in account with no higher standing.

Each route needs at least a set Privilege. GET /api/v1 lists the routes your Privilege allows, and what each needs.

A Privilege can be local: standing the server’s operator gives of their own, rather than the master’s, as for players in the game. Local Privileges go no higher than administrator, and wherever a Privilege is given, a local one comes with "local": true, so it can be drawn with the game’s local icon.

Getting a Privilege

There are two ways:

Send a token as Authorization: Bearer <token>. On the Event feed, where browsers can’t set headers, use ?token=<token> on the URL or a message (see Signing in on the feed).

Encryption is required

Tokens and sign-ins only count over an encrypted connection. That means one of:

Over plain http a token is ignored: the request is answered as anonymous, and routes that need an account return 403. This keeps tokens and the addresses moderators can see from crossing the network in the clear.

Sessions from an account

A server’s session comes from the master, which vouches for the account to that server:

  1. Sign in on the master, as in Remembered sign-ins.
  2. POST /api/v1/signin/session/<address>/<port> on the master, with that sign-in, as in Sessions from a remembered sign-in. The answer is:
{"token": "4f3a...", "name": "someone", "privilege": "moderator", "local": true, "expires": 1800}

About the session:

Servers take no challenges themselves. An answer given to one server could be passed on by it to sign in somewhere else as you, so only the master asks for one, and its answer is made so that it counts nowhere else.

Remembered sign-ins

A session lasts half an hour unused and ends when its server restarts, and a site that talks to many servers would need one on each. A remembered sign-in lasts weeks instead. It lives on the master, which gets you a session on whichever server you need, so neither your site nor the person’s browser has to keep their private key.

Signing in on the master is a challenge and response:

  1. POST /api/v1/signin with {"name": "<account name>"} gives {"id": <number>, "challenge": "<string>"}.
  2. Compute the answer to the challenge with the account’s private key, as below.
  3. POST /api/v1/signin with {"id": <number>, "answer": "<string>", "remember": true}, from the same address as step 1 and within a minute, gives:
{"id": "5cbf5ba653fbe075", "name": "someone", "flags": "u", "expires": 1810350612}

Computing the answer:

Options on the last step:

How long it lasts:

Account At most Unused
player or supporter 26 weeks 13 weeks
moderator and above 4 weeks 2 weeks

Each account holds at most 10; signing in an 11th time ends the one used longest ago. The master’s operator can change these, or turn remembered sign-ins off for moderators and above, who then get 403 and can’t sign in through the Web API. expires is when it ends at the latest, as a Unix time.

Where the token goes:

Routes, all below /api/v1/signin, each needing the sign-in:

Route Does
GET /api/v1/signin Lists the account’s own sign-ins: {"name", "flags", "signins": [...]}, each with id, label, created, lastused, expires, firstaddr, lastaddr, remember, and current for the one you used
DELETE /api/v1/signin Signs out: ends the one you used, and clears the cookie
DELETE /api/v1/signin/<id> Ends one of the account’s others
DELETE /api/v1/signin/all Ends all of them, everywhere
POST /api/v1/signin/session/<address>/<port> A session on that server, below
POST /api/v1/signin/session A session on the master itself, below

Each DELETE answers {"ok": true, "ended": <count>}. A sign-in also ends when the account’s key or standing changes on the master.

Sessions from a remembered sign-in

POST /api/v1/signin/session/<address>/<port>, with the address and port as the master’s list gives them, answers as signing in on that server would:

{"token": "4f3a...", "name": "someone", "privilege": "moderator", "local": true, "expires": 1800}

Errors:

Status Error
401 unknown or expired token: the sign-in ended, so sign in again. A cookie that ended is cleared
403 The server’s link to the master isn’t encrypted, or the server has no certificate and its relay level is under 2, so the session couldn’t be used anywhere
404 No such server holds a link to the master
429 Too many failed attempts, or more than 8 sessions waiting on servers for one sign-in
501 this server doesn't support sessions from the master: an older server, which can’t be signed in on

POST /api/v1/signin/session, with no server named, gives a session on the master’s own Web API, in the same form without local. Open the live list with it to queue as the account in the Matchmaking Queue. It ends with the remembered sign-in that made it, and lapses unused like any other session. | 502, 504 | The server’s link closed, or it didn’t answer within 10 seconds |

Failed attempts

Each unknown token, wrong answer or expired session counts as a failure, even on public routes. So drop a token once it stops working rather than resending it.

Game server routes

The paths below leave out /api/v1. All GET routes also answer HEAD.

Method and path Privilege What it does
GET / (no /api/v1) none Says where the API is: {"name": "Red Eclipse", "api": "/api/v1"}
GET /api/v1 none What the server is and the routes you may use
GET /status none The match: map, mode, counts, time left
GET /players none Everyone connected, with scores
GET /teams none Team scores
GET /mapshot none The map’s preview, a PNG
GET /matches none The results of the last few matches
GET /objectives none Flags, points and the ball, as every player’s HUD shows them
GET /events none The Event feed (WebSocket)
GET /controls player Bans, mutes and the like
POST /controls player Kick, or add a ban, mute, limit, allow or except
DELETE /controls/<id> player Remove a Control entry
POST /map player Change the map and mode
POST /say player Send a message to everyone in the game

The Privileges listed are the minimum to reach the route. Each action then checks the server’s own in-game minimum for it, the same as a player in the game would face. For example, banning needs at least the server’s banlock.

Operators can add routes of their own outside /api, and those appear in the index.

GET /api/v1

{
    "name": "Red Eclipse",
    "version": "Red Eclipse 2.0.9 ...",
    "type": "server",
    "routes": [
        {"path": "/api/v1/status", "methods": {"GET": "none"}},
        {"path": "/api/v1/events", "methods": {"WS": "none"}},
        {"path": "/api/v1/controls", "methods": {"GET": "player", "POST": "player"}}
    ]
}

GET /status

{
    "description": "My Server",
    "version": "Red Eclipse 2.0.9 ...",
    "url": "https://example.org/",
    "motd": "Welcome! Match nights are on Saturdays.",
    "map": "bloodlust",
    "mode": "Deathmatch",
    "modeid": 2,
    "mutators": 0,
    "players": 6,
    "spectators": 1,
    "bots": 4,
    "maxplayers": 16,
    "playerslots": 16,
    "reserved": 0,
    "connections": 4,
    "mastermode": "open",
    "state": "Playing",
    "timeleft": 412,
    "relay": 1,
    "bridge": 1
}
Field Meaning
url The operator’s web address, such as their community’s site, present when set; always http:// or https:// with no spaces, quotes or angle brackets, but open it as a link away from your page all the same
motd The server’s message of the day as plain text, present when set
maptitle, mapauthor The map’s title and author, present when the map has them and the server has its files
mapcrc The map’s crc, present when the server has its preview (see GET /mapshot)
mode The mode’s display name, with mutators
players, spectators, bots Human players in the match, spectators, and bots
maxplayers The player slots and the spectator slots the server sets aside, together; one with no spectator slots set aside still lets a few more connect to watch
playerslots How many may play at once; spectators don’t take a player slot. Older servers don’t send it
reserved Player slots the server is keeping for people the Matchmaking Queue sent there, which nobody else can play in. Older servers don’t send it
connections How many more people can connect right now, to play or watch. Older servers don’t send it
modeid, mutators The mode’s number, and its mutators as bit flags (see Modes and mutators)
mastermode One of open, veto, locked, private, password
state A game state
timeleft Seconds left in the match, or -1 with no time limit
relay The server’s relay level
bridge 1 when the server takes chat bridged from outside the game (see Bridging chat), 0 when its operator turned that off; older servers don’t send it
you Your Privilege, present only when signed in, with local when it is local

GET /players

An array with one entry for each player, bot and spectator. The map’s own enemies and hazards are left out here and on the Event feed:

[
    {
        "cn": 0, "name": "someone", "team": "alpha", "state": "Alive", "bot": false,
        "privilege": "player", "colour": 3368703, "handle": "someone", "points": 12, "frags": 10, "deaths": 4, "ping": 38
    }
]
Field Meaning
cn The client number, which POST /controls takes
team A team
state A player state
handle The player’s account, present only when signed in
privilege, local Their Privilege, and local present and true when the server’s operator gave it
colour The colour they chose, as 0xRRGGBB in a number, or 0 for none. The game draws a player’s Privilege icon in it before their name in their team’s colour, and so should you
ip Present only at the server’s iphostlock and above (moderator by default), and only for human players

GET /mapshot

The current map’s preview, as a PNG. Add ?crc= with the mapcrc from /status or the master’s list: the answer is then cached for a day by browsers and the master’s relay, and is 404 once the map has changed. Without it the answer is no-cache with an ETag, so send If-None-Match to get 304 Not Modified.

It’s 404 when the server doesn’t have the map’s files, which is the case in editing, or until a player has sent a map the server didn’t have. The previews of the official maps are on the website too, so try those first.

const img = new Image();
img.src = `https://play.redeclipse.net:28888/relay/${encodeURIComponent(s.host)}/${s.port}/api/v1/mapshot?crc=${s.status.mapcrc}`;

GET /matches

The results of the last matches played there, newest first, kept until the server stops (httpmatches of them, 10 by default). A match is kept when play ends with a winner, by the time or score limit, and only when a human player was playing at the end; demos and editing aren’t matches.

[
    {
        "ended": 1762201234, "duration": 600, "map": "bloodlust", "maptitle": "Bloodlust", "modeid": 2, "mutators": 0,
        "teams": [{"team": "alpha", "score": 5}, {"team": "omega", "score": 3}],
        "players": [
            {"name": "someone", "handle": "someone", "team": "alpha", "points": 12, "frags": 9, "deaths": 4},
            {"name": "Ken", "team": "omega", "points": 7, "frags": 6, "deaths": 8, "bot": true}
        ]
    }
]
Field Meaning
ended When play ended, a unix time
duration How long it was played, in seconds
map, maptitle The map, and its title when it has one
modeid, mutators The mode and mutators
timed Present and true in a timed speedrun, where scores are lap times in milliseconds and the shortest wins
draw Present and true when the first two teams, or players in a mode without teams, are level
teams Team modes only: every team’s score, best first, so the winner is the first
players Everyone who played, bots too, best first as the scoreboard has them; team is present in team modes

GET /objectives

What every player in the match already sees of its objectives: who carries each flag or ball, and who holds each point and who is taking it. It’s {} in modes without objectives and outside play, and only the mode’s own list is present.

{
    "flags": [
        {"team": "alpha", "state": "taken", "carrier": {"cn": 3, "name": "someone", "team": "omega"}},
        {"team": "omega", "state": "home"}
    ],
    "points": [
        {"id": "A", "name": "Point", "owner": "alpha", "progress": 100},
        {"id": "B", "name": "Courtyard", "owner": "neutral", "enemy": "omega", "progress": 45}
    ],
    "balls": [{"state": "dropped"}]
}
Field Meaning
flags Capture the Flag: each flag and its team
balls Bomber Ball: the ball
state A flag’s or ball’s home, taken or dropped
carrier Who has it, when taken: their cn, name and team
points Defend and Control: each point
id The letter the game’s HUD shows the point by, A onwards
name The map’s name for it, or Point when it has none
owner The team holding it, neutral for none
enemy The team taking it, present only while one is
progress How far the point is taken, 0 to 100: 100 for one held and not under attack

The Event feed sends an objectives event with the same fields whenever any of this changes, at most once a second.

GET /teams

[{"team": "alpha", "score": 3}, {"team": "omega", "score": 5}]

The array is empty in modes without teams. A team only appears once it has a score.

GET /controls

The Control entries your Privilege may see:

[
    {"id": 7, "type": "ban", "scope": "local", "source": "player", "name": "someone", "by": "admin", "reason": "cheating", "ip": "1.2.3.4"}
]
Field Meaning
type allow, ban, mute, limit or except
scope temporary, internal, local or global. Global entries come from the master and can’t be removed here
source server, player, automatic or master
name, handle, by, reason Present when known
ip, mask At the server’s iphostlock and above (moderator by default). mask is the prefix length, and is present only for ranges

POST /controls

{"cn": 3, "type": "ban", "reason": "cheating"}

DELETE /controls/<id>

Removes the entry. The answer is {"ok": true}, or 404 when there is no such entry.

POST /map

{"map": "bloodlust", "mode": 2, "mutators": 0}

POST /say

{"text": "Server restarting in 5 minutes"}

The message is shown in game under your account’s name, marked as from the web. It needs the server’s httpsaylock (player by default) and follows the server’s rules for chat: muted addresses and Privileges below messagelock get 403, text is censored and cut to fit messagelength (256 by default) ending with ..., in which case the answer is {"ok": true, "cut": <characters kept>} so you can tell whoever wrote it, and sending more than the server’s flood limit (5 messages in 10 seconds by default) gets 429. Repeated flooding mutes your address, as it would in the game. Add "action": true to send it as an action, as /me does in the game.

Bridging chat

A bridge carries chat between a server and somewhere outside the game, such as a Discord channel, so a message can name the person who wrote it there rather than your account:

{"text": "anyone up for a duel?", "from": "Someone", "via": "discord"}

The Event feed

Open a WebSocket to /api/v1/events, directly or through the relay. Each message is a JSON object with an event name and a time.

Choosing events

?events= takes event names separated by commas, and default, which stands for every event except ping. The game’s broadcast messages are chosen by their own names, such as servmsg or capture.

The server works out an event only when some feed asked for it, so asking only for what you use spares the server and the relay.

Signing in on the feed

A feed starts as none. To see more, such as Control changes, sign in with one of:

with a session on that server, which the master vouches for (see Sessions from an account). A message with a name or answer instead is refused, as servers take no challenges.

The answer is one of:

{"event": "auth", "privilege": "moderator", "local": true, "name": "someone"}
{"event": "auth", "error": "unknown token"}

How long it lasts:

Chatting on the feed

A feed signed in as an account can chat, under the same rules as POST /say: httpsaylock, messagelock, mutes, censoring, cutting and the flood limit. Send:

{"say": "anyone for a duel?", "id": 7}

Add "action": true to send it as an action. id is optional, and comes back so you can match the answer:

{"event": "said", "ok": true, "id": 7}
{"event": "said", "ok": true, "cut": 255}
{"event": "said", "status": 429, "error": "too many messages, slow down"}

status and error are what POST /say would have answered. The message itself comes back as a chat or action event, as everyone’s does. The game’s server browser and the website chat this way.

Events

Players in events are described the same way throughout:

{"cn": 3, "name": "someone", "team": "alpha", "bot": false, "handle": "someone", "privilege": "player"}
Event Fields When
join client, players A player or bot joined; players is how many people are on the server now, spectators included and bots left out
leave client, reason, players A player or bot left; reason is one of the leave reasons, missing for a bot, and players how many people are still on
team client A player changed team
identify client, text A player signed in to an account; client has its handle and the privilege it gave them, and text is the game’s line saying so
frag victim, killer, weapon, weaponname, flags One player fragged another
suicide victim, weapon, weaponname, flags A player died by their own hand or the world
chat client, text Public chat, from the game or POST /say; team chat and whispers are never sent
action client, text A /me action
map map, mode, modeid, mutators A new match started
result match A match ended with a winner; match is as in GET /matches
objectives flags, points, balls The objectives changed, as in GET /objectives; compared once a second
score teams, players Scores changed
ping players Pings changed; only for feeds that ask for it (see Choosing events)
control action, type, id, name, reason, by, ip, mask A Control entry was added or removed

The feed also passes on the game’s broadcast messages, the ones every player sees in their console. These are named for their kind, such as match, balance, duel, capture, defend, bomber, speedrun and servmsg. Each has:

Use text for display, and the events above for anything you compute.

Positions, health, loadouts and anything else a player couldn’t already see in the match are never sent.

The master server

The master also answers the game server routes above for itself, and its /api/v1 index says "type": "master".

GET /api/v1/servers

{
    "time": 1762201000,
    "version": {"main": 209, "game": 289},
    "servers": [
        {
            "host": "1.2.3.4", "port": 28801, "priority": 0,
            "description": "My Server", "handle": "someone", "flags": "s", "branch": "stable",
            "version": 209, "webport": 28888, "relay": 1, "bridge": 1, "fingerprint": "9f86d0...",
            "status": { ... }
        }
    ]
}
Field Meaning
time The master’s clock when it sent the list, a unix time; see below
version (top level) The versions the master expects of current clients: main as in 209 for 2.0.9, and game, the game protocol
script Present when the master has a script for game clients; you can ignore it
host, port The server’s address and game port, which are also its relay address
description May contain markup
handle, flags The account that registered the server and its standing, when there is one
version The server’s version, such as 209 for 2.0.9
webport Present when the server’s API is on
relay 0 when the server isn’t linked to the master
bridge As in GET /status, present only for linked servers new enough to say; treat it missing as 0
fingerprint Present when the server serves HTTPS
channel Present when the server takes Matchmaking Queue pops: devel or release
status What the server is playing, when the master knows; see below

For servers linked to the master, status is what the server reports about its match:

{
    "gameversion": 289, "map": "bloodlust", "maptitle": "Bloodlust", "mapauthor": "someone", "mapcrc": 1324137294,
    "modeid": 2, "mutators": 0,
    "ends": 1762201234, "maxplayers": 16, "playerslots": 16, "reserved": 0, "connections": 4, "mastermode": 0, "gamevars": 0, "gamemods": 0,
    "build": [2, 0, 9, 0, 1], "state": 5, "stateends": 1762201234,
    "spectators": 1, "bots": 4,
    "lastmatch": {"ended": 1762200634, "duration": 600, "map": "bloodlust", "maptitle": "Bloodlust", "modeid": 2, "mutators": 0,
        "teams": [{"team": "alpha", "score": 5}, {"team": "omega", "score": 3}], "players": [{"name": "someone", "team": "alpha", "points": 12, "frags": 9, "deaths": 4}]},
    "players": [
        {"cn": 0, "name": "someone", "handle": "someone", "team": "alpha", "privilege": "moderator", "local": true, "colour": 3368703},
        {"cn": 1, "name": "watcher", "team": "neutral", "privilege": "none", "spectator": true, "colour": 0}
    ]
}

Differences from /status on the server itself:

Field Meaning
cn The client number
name Plain text
handle The player’s account, present only when signed in
team A team
privilege A Privilege
local Present and true when that standing was given by the server’s operator rather than the master
spectator Present and true when the player is spectating
colour The colour the player chose, as 0xRRGGBB in a number, or 0 for none (the game then uses the team’s)

Polled status

Servers that aren’t linked, including older versions of the game, don’t report anything. The master asks them itself every 30 seconds, the way the game’s server browser does, and lists what they answer as a status with "polled": true:

{
    "polled": true, "gameversion": 230, "map": "enyo", "modeid": 2, "mutators": 2,
    "ends": 1762201234, "maxplayers": 16, "mastermode": 0, "gamevars": 0, "gamemods": 0,
    "build": [1, 6, 0, 2, 64], "state": 5, "stateends": 1762201234,
    "players": [
        {"name": "someone", "handle": "someone", "privilege": "moderator", "local": true, "colour": 6316287, "teamcolour": 6252287},
        {"name": "watcher", "privilege": "none", "colour": 16731972, "teamcolour": 9478288}
    ]
}
Field Meaning
privilege The word on the player’s Privilege icon in the server’s version. It is mostly a Privilege, but every version says founder for founder, and v1.6 also has operator, between moderator and administrator
local Present and true when that standing was given by the server’s operator
colour The colour the game shows the player in. Unlike a reported status, this is never 0: a player who chose none gets their weapon’s colour
teamcolour Their team’s colour. No colour has ever been the default for two teams, so a default names the team. Game mods can change them, which is rare

Any of these is left out when the server’s answer doesn’t have it.

A server that has never answered has no status, and you only have what the list gives.

GET /api/v1/servers/<address>/<port>/history

How busy a server has been: the most players it had in each 15 minutes, oldest first, over the last days (default 7, up to a year).

{"bucket": 900, "start": 1762128000, "peaks": [0, 0, null, 3, 8, 12]}

GET /api/v1/history

The same for the whole game: every listed server’s players added together, then the most in each 15 minutes. That’s more than adding up each server’s peaks would say, as their busiest moments needn’t be at the same time. null is a time no listed server said how many were on.

To show when people usually play, average the peaks for each weekday and hour, in the viewer’s own time zone. A small game is quiet on weekdays, so eight weeks give each weekday enough to go on:

const res = await fetch("https://play.redeclipse.net:28888/api/v1/history?days=56");
const h = await res.json();
const sums = [...Array(7)].map(() => new Array(24).fill(0)), counts = [...Array(7)].map(() => new Array(24).fill(0));
h.peaks.forEach((p, i) => {
    if(p === null) return;
    const t = new Date((h.start + i * h.bucket) * 1000), day = (t.getDay() + 6) % 7; // Monday first
    sums[day][t.getHours()] += p; counts[day][t.getHours()]++;
});
const usual = sums.map((row, d) => row.map((s, hr) => counts[d][hr] ? s / counts[d][hr] : null));

WebSocket /api/v1/servers/live

The live list sends these messages:

Changes are checked once a second. All you can say to it is about the Matchmaking Queue, below; anything else is ignored. A token, as ?token= on the URL, is read as the socket opens and sets, for as long as it stays open, which account the socket queues as and what it sees of the queue. A session on the master, from a remembered sign-in, is one.

If your visitors’ networks block port 28888, fall back to polling GET /servers with If-None-Match, no more often than every 5 seconds.

A join link, redeclipse://<host>:<port>, opens the game and joins that server, or hands the server to the game that’s already open. The game registers the scheme itself whenever it starts. Join links never carry a password. Anything after the port is ignored, so later links can carry more. Link a listed server’s host and port from GET /servers straight to its join link when your page is in a browser.

Some places, such as Discord, only make https links clickable. For those, link the master’s bounce link instead:

https://play.redeclipse.net:28888/join/<host>/<port>

It’s a web page, not part of /api/v1. For a server on the list it shows the server’s description, sends the browser on to the join link, and offers a download link for anyone without the game. For anything else it answers 404 with a page that says so, so the master’s address can’t be used to send people to an unlisted server.

The Matchmaking Queue

The Matchmaking Queue is for people who’d play if enough others turned up. It has a channel for each version the master’s operator names, devel with masterverdevel and release with masterverrelease, and a channel set to "" doesn’t exist. A channel’s servers are the linked ones on its version that run the newest protocol any of them does, which is what an up to date game of that version plays, and each lists that channel. Everything below happens within one channel. Each entry has a Threshold: the fewest people it will play with, 2, 4, 6 or 8, counting everyone already on the server it would be sent to, spectators included. Once some group’s Thresholds are all met, the master pops them: it sends them all to one server and takes them out of the queue.

Every entry belongs to an account, and queuing needs one: there is one entry for each account, however many places it joined from. Linked servers report the handle of each player the master signed in there, which is how the master knows:

So a player who joins without signing in to the server isn’t seen there. The game signs in by itself once its auth.cfg holds the account’s key.

Every entry lasts 60 minutes from when it last joined or was renewed, wherever from, whether or not any socket stays open. A renewal adds 30 minutes, up to 90 from now. An entry also ends when its account is deleted.

GET /api/v1/queue

{"time": 1762201000, "queue": {
    "channels": [{"name": "devel", "version": "2.0.9", "servers": 2, "ready": 3, "held": 1, "travelling": 0, "next": {"threshold": 4, "needed": 1}}],
    "ready": 3, "held": 1, "travelling": 0, "thresholds": [2, 4, 6, 8], "next": {"threshold": 4, "needed": 1},
    "entries": [{"handle": "someone", "displayname": "Some One", "privilege": "player", "channel": "devel", "threshold": 4,
        "state": "ready", "added": 1762200400, "expires": 1762204000}]}}

channels gives each channel’s counts, its version and how many servers it has; the fields beside it add every channel up, with the next closest to a pop of them all. A page or bot shows each channel, as a player can only join the servers of their own version.

Field Meaning
ready Entries waiting to be popped, neither held nor on their way
held Entries whose account is on a server
travelling Entries popped and not yet arrived
thresholds The Thresholds an entry may have
next How close the queue is: the Threshold that needs the fewest more people, and how many, on the server they’d go to; null when nobody is waiting or no server could take them
entries Everyone queued, ready ones first by channel and Threshold, then held, then travelling

Show it as the game does: “3 waiting, 1 more needed for a 4+ match”.

Each entry has its account’s handle, displayname ("" for none) and privilege (a name from the privilege table), its channel, threshold, state (ready, held or travelling), added and expires, and when they apply held or popped (host, port and, for popped, time). Show a player as the game does: the privilege’s icon, then the display name, or the handle when there’s none. At masterqueuelock and above (moderator by default) each also has sources (web, game, discord) and a game’s protocol, and at masterqueuediscordlock (developer) the account’s linked discord ID.

Queuing over the live list

Open the live list with a session on the master as ?token=, from a remembered sign-in, and send text messages on it for that account:

Message What it does
{"queue": "join", "source": "web", "channel": "devel", "threshold": 4} Joins that channel, or changes the account’s Threshold or channel
{"queue": "renew"} Adds 30 minutes, up to 90 from now
{"queue": "leave"} Leaves, wherever the account joined from

A socket may send one queue message every 250 ms. Without an account, offer the buttons greyed out with a way to register one, as the master answers sign in with an account to queue.

What comes back:

Event Fields When
queue queue What anyone sees of the queue changed; viewers at masterqueuelock get every change
queued threshold, channel, expires, host and port once popped, or error The account’s entry was taken or changed, from anywhere, and on connecting when it has one; or why a message wasn’t taken
unqueued reason The account’s entry ended: left, arrived, satisfied (held where enough are on), popped (sent but didn’t arrive in time), expired, deleted (the account was) or closed (its channel was taken away)
pop host, port, count, entries, and "mine": true when it’s yours A group was sent to a server

The account’s events go to every socket signed in as it. On a pop that’s mine, open redeclipse://<host>:<port> when the visitor asks, never by itself.

Queuing Discord members

A viewer signed in at masterqueuediscordlock (developer, the Discord bot’s) queues members by their Discord ID, with no source, into the account that ID is linked to:

Message What it does
{"queue": "join", "discord": "<id>", "channel": "devel", "threshold": 4} Queues the member’s account in that channel, or changes its Threshold or channel
{"queue": "renew", "discord": "<id>"} Adds 30 minutes, up to 90 from now
{"queue": "leave", "discord": "<id>"} Takes the account out

Each is answered with queued and the member’s discord, the account’s handle, threshold, channel and expires, or queued with their discord and an error: no account when no account is linked to that ID, so tell them how to register one and link it, or not queued. A leave that takes them out is answered by its unqueued. Those viewers also hear unqueued with handle, the linked discord when there is one, and reason whenever any entry ends.

The queue is saved to masterqueuefile (masterqueue.dat) as it changes, so it outlives a restart.

Accounts

The master holds every account. Anyone can register one, from the game or the website, with a handle and an email address. The master mails a link to activate it, and only an activated account works. All of these routes are on the master and need https (or localhost), like signing in.

Registering

GET /api/v1/accounts says whether registration is open:

{"register": true, "website": true, "days": 7}

website is false when the master has no captcha set up, so only the game can register. days is how long a registration has to be activated.

POST /api/v1/accounts registers one:

Field  
handle 3 to 24 letters, numbers, - or _. It’s stored in lowercase, and no two accounts share one, whatever their case
email Where the link goes. No two accounts, or registrations waiting, share one
captcha From a website: the token Cloudflare Turnstile gave the page. The master checks it with Cloudflare
game From the game: true in place of a captcha

It answers 202 with {"handle": "<handle>", "expires": <Unix time>}, and from the game, also pickup, a secret the game keeps (see The game collects its key).

The registration is now pending. It holds its handle and email so nobody else can take them, but it can’t sign in. The email has two links to the website’s account page:

A registration not activated within days is removed. Registering again with the same handle and email replaces it with a fresh one and a new email, in case the first never arrived; its earlier links stop working. The tokens are after # so they never reach a server’s logs, and a mail scanner fetching the links does nothing, as only the page’s script posts them.

Errors:

Status Error
400 A bad handle or email, or no captcha
403 The captcha wasn’t solved
404 The link is unknown, ran out or was removed
409 The handle or email is taken
410 The account is already activated
429 Too many registrations from your address today (3 by default)
503 Registration is closed, or the website can’t register (no captcha set up)

Lost keys

POST /api/v1/accounts/reset with {"handle": "<handle>"} or {"email": "<address>"}, and a captcha or "game": true as above, asks for a new key. It always answers 202 the same way, so it never says whether an account exists. If one does and has an email address, that address gets a link to /account/reset#<token>, lasting 24 hours. Only the newest link works.

The page posts the token to POST /api/v1/accounts/reset/confirm, which answers like activation, with the new key. The old key stops working, and the account’s remembered sign-ins end.

The game collects its key

A registration or reset from the game gets a pickup secret. The game posts it to POST /api/v1/accounts/pickup as {"pickup": "<secret>"} every few seconds:

The page that followed the link still shows the key too, in case the game was closed.

The owner’s own account

These are below /api/v1/signin, so they take a remembered sign-in, as a cookie or a Bearer token.

Route Does
GET /api/v1/signin/account {"handle", "flags", "email", "steamid", "discord", "displayname"}, and newemail while a change waits
PATCH /api/v1/signin/account {"displayname": "<name>"} sets the display name, once an hour (429 sooner). "" clears it
POST /api/v1/signin/account/email {"email": "<address>"} mails that address a link to /account/email#<token>, and tells the old address, with a link to /account/email-cancel#<token> that stops it. Answers 202
DELETE /api/v1/signin/account {"confirm": "<handle>"} deletes the account (204). Its handle is then free to register again. Accounts at moderator and above are removed by an administrator instead (403)

The email links post their token to POST /api/v1/accounts/email/confirm, which answers {"handle", "email"}, or POST /api/v1/accounts/email/cancel (204).

Display names are what the website shows for an account, always with its handle beside it, as Display Name (handle). They’re free-form and not unique: up to 32 characters, filtered as names in game are, without colour codes or any of (){}<>:. An account that hasn’t chosen one takes the first name it’s seen playing under on a server the master signed it in on, or failing that its Discord name. In game, players are known by their in-game names, and on Discord by their Discord names.

Linking Steam and Discord

An account can link one Steam account and one Discord account, each proven through its own sign-in. A Steam or Discord account links to one account at most (409 otherwise). Links are only shown to the owner, administrators and the Discord bot.

Steam, through Steam’s OpenID:

  1. GET /api/v1/signin/account/steam gives {"url", "state"}. Send the person to url.
  2. Steam sends them back to /account/steam?state=<state> with openid.* fields in the query. Post them all, as they came, to POST /api/v1/signin/account/steam as {"openid": {"openid.ns": "...", ...}}.
  3. The master asks Steam whether it made that answer, and answers with the account as GET /api/v1/signin/account does. The state works once, within 10 minutes.

DELETE /api/v1/signin/account/steam unlinks it.

Discord, two ways:

DELETE /api/v1/signin/account/discord unlinks it.

The bot’s routes need a session at the master’s masterdiscordlock (developer by default), as the bot’s account has:

Route Does
POST /api/v1/accounts/discord/link {"code", "discord": "<member's ID>", "name": "<their Discord name>"} links the member to the code’s account, answering {"handle", "displayname"}
GET /api/v1/accounts/discord/<id> Which account the member is: {"handle", "displayname", "privilege"}, privilege being the account’s own standing, or 404
DELETE /api/v1/accounts/discord/<id> Unlinks the member (204)

Administration

The master has no console, so its administrators manage it here, signed in with a remembered sign-in at the master’s masteradminlock (administrator by default). Nobody can change an account at or above their own standing, or give one, except a founder.

Route Does
GET /api/v1/signin/admin/accounts?q=<text>&limit=<n> Accounts whose handle or email holds q: {"total", "accounts": [{"handle", "flags", "email", "steamid", "discord", "displayname", "pubkey"}]}
PATCH /api/v1/signin/admin/accounts/<handle> {"flags": "<letters>"} changes its standing, which ends its sign-ins. {"displayname": "<name>"} replaces or clears its display name, which takes masterdisplaylock (moderator by default)
DELETE /api/v1/signin/admin/accounts/<handle> Removes it
GET /api/v1/signin/admin/servers Server accounts, the same way
POST /api/v1/signin/admin/servers {"handle", "flags", "pubkey", "email"} adds one
DELETE /api/v1/signin/admin/servers/<handle> Removes one
GET /api/v1/signin/admin/controls The master’s own control entries, which it sends to every linked server: {"controls": [{"id", "type", "address", "reason"}]}
POST /api/v1/signin/admin/controls {"type": "ban", "address": "1.2.3", "reason": "..."} adds one. type is allow, ban, mute, limit, except or trust; address is up to four numbers, fewer for a range
DELETE /api/v1/signin/admin/controls/<id> Removes one
POST /api/v1/signin/admin/mail {"to": "<address>"} sends a test email, to check the master’s mail settings (202)

Flags are letters: u player, s supporter, m or o moderator, a administrator, d developer, c founder.

Limits

Limit Value
Open connections per address 16
Request body 1 MB
Headers per request 64
Header line 8 KB
Idle HTTP connection closed after 15 s
Request arriving must finish within 30 s of its first byte
TLS handshakes per address 30 every 10 s
Messages to the master’s sockets and a server’s Event feed 4 KB
Relayed bandwidth per server 256 KB/s (shared by everyone using that server’s relay)
Session idle lifetime 30 min
Remembered sign-ins 26 weeks, 13 unused; 4 and 2 for moderators and above; 10 per account

These are defaults. Operators can change them, except on the master’s own sockets and the Event feed.

Reference

Privileges

none, player, supporter, moderator, administrator, developer, founder, lowest to highest.

Teams

neutral (no team), alpha, omega, enemy (non-player enemies), environment.

Player states

Alive, Dead, Editing, Spectator, Waiting.

Game states

Numbers are for state in the master’s status, and strings are for /status.

Number String
0 Waiting
1 Getting Map
2 Sending Map
3 Readying
4 Syncing Info
5 Playing
6 Overtime
7 Intermission
8 Voting

Master modes

Number String
0 open
1 veto
2 locked
3 private
4 password

Modes and mutators

modeid Mode
0 Demo
1 Editing
2 Deathmatch
3 Capture the Flag
4 Defend and Control
5 Bomber Ball
6 Speedrun

mutators is a bit field: bit n set means mutator n is on, including any the mode implies (Editing implies Free for All and Arena).

Bit Mutator
0 Free for All
1 Coop
2 Instagib
3 Medieval
4 Kaboom
5 Duel
6 Survivor
7 Classic
8 Onslaught
9 Vampire
10 Resize
11 Hard
12 Arena
13 Dark
14 Game specific 1
15 Game specific 2
16 Game specific 3

The game-specific mutators mean something different in each mode:

Mode Bit 14 Bit 15 Bit 16
Deathmatch Gladiator Old School  
Capture the Flag Quick Defend Protect
Defend and Control Quick King  
Bomber Ball Hold Basket Assault
Speedrun Lapped Endurance Gauntlet

Weapons

claw, pistol, sword, shotgun, smg, flamer, plasma, zapper, rifle, corroder, trident, grenade, mine, rocket, minigun, jetsaw, eclipse, melee.

Leave reasons

End of packet, Invalid client number, Kicked from server, Communication error, Address is banned, Server is in private mode, Incorrect password, AI client removed, Server is at maximum capacity, Server and client are incompatible, Connection timed out, Packet overflow, Server shutting down, Hostname lookup failure, Client with same handle authenticated.

A player who simply quits leaves with no reason.

Versions

Everything here is version 1 of the API, below /api/v1. A future change that breaks clients will move to a new prefix, with /api/v1 kept alongside it for a while. New fields and events may appear in version 1 at any time, so ignore anything you don’t recognise.