Query¶
Anyone can ask a SAGL server about itself without joining it. This is how server lists, websites, Discord bots and the game's own server browser find out:
- the server's name, SAGL version and protocol;
- who's on, with their scores and pings, or just how many;
- its branding: colours, and the logo, fetched by its hash;
- the gamemode's own fields: a description, links (website, Discord), tags, and values it changes as it runs.
The query works over UDP on the game port, so it needs nothing else opened. If you turn it on, the same JSON is also served over HTTP for websites.
const server = new Server({
name: "Example RP",
query: {
description: "Los Santos roleplay. Whitelisted; apply on the website.",
gamemode: "Roleplay",
language: "en",
tags: ["roleplay", "whitelist"],
links: { website: "https://example.com", discord: "https://discord.gg/example" },
http: { port: 8080 }, // optional: GET http://host:8080/ for websites
},
});
// Values: the gamemode's own, changed whenever it likes.
server.query.set("map", "Los Santos");
setInterval(() => server.query.set("copsOnDuty", cops().length), 10_000);
To ask a server from Node (a server list, a bot), use @sagl/query. The server SDK also exports the same queryServer.
server.query¶
info¶
server.query.info = { description, gamemode, language, tags, links };
server.query.update({ description: "Event tonight at 8!" }); // change some, keep the rest
| Field | |
|---|---|
description |
A line or two about the server (up to 1000 characters). |
gamemode |
What's played: "Roleplay", "Freeroam", ... |
language |
The players' language(s), e.g. "en". |
tags |
Up to 32 tags, for server lists to filter by. |
links |
Web links by name: website, discord, store, forum, rules, ... Each must be an http(s) URL. |
set(key, value), get(key), delete(key), values¶
Your own values, shown under values. A value is a string (up to 256 characters), a number, a boolean or null, and there can be up to 64 of them. Change them as often as you like: a query always sees the latest.
players¶
How much a query shows of who's on:
"list"(the default): every player's id, name, score and ping."count": only how many are on."none": nothing.
The score is player.score, the same one the in-game player list shows.
result¶
What a query gets right now, as an object.
http(options), closeHttp()¶
Serves the query over HTTP as well, for websites:
| Request | |
|---|---|
GET / or GET /query |
The same JSON a UDP query gets. |
GET /logo |
The logo (PNG or JPEG), with its SHA-256 as the ETag. |
Any site may fetch these (CORS: Access-Control-Allow-Origin: *).
await server.query.http({ port: 8080 }); // or the `query.http` option
await server.query.http({ port: 8080, host: "127.0.0.1" }); // behind your own web server
To serve it over HTTPS, put it behind a web server such as nginx or Caddy.
What a query says¶
{
"query": 1,
"name": "Example RP",
"version": "0.2.0",
"protocol": 102,
"players": {
"online": 2, "max": 50,
"list": [{ "id": 0, "name": "Thomas", "score": 12, "ping": 31 }, { "id": 3, "name": "Sam", "score": 4, "ping": 58 }]
},
"branding": {
"logo": { "sha256": "0ca18ac0…", "size": 48211 },
"primary": "#ff8800", "secondary": null, "showName": false
},
"description": "Los Santos roleplay. Whitelisted; apply on the website.",
"gamemode": "Roleplay",
"language": "en",
"tags": ["roleplay", "whitelist"],
"links": { "website": "https://example.com", "discord": "https://discord.gg/example" },
"values": { "map": "Los Santos", "copsOnDuty": 3 }
}
playersis left out withplayers: "none", and has nolistwith"count". A very long list (thousands of players) is cut short and gets"truncated": true.branding.logois the branding logo's SHA-256 and size, ornull. Fetch it by its hash, and cache it by its hash too: it only changes when the hash does.- Colours are
"#rrggbb", ornullfor SAGL's own. - The gamemode's fields (
descriptionand the rest) are there only if it set them. - A client can join only if its
protocolmatches the server's.
@sagl/query¶
A small Node.js package (no dependencies, no native code) for asking servers:
import { queryServer } from "@sagl/query";
const { info, ping, logo } = await queryServer("play.example.com", 7777, { logo: true });
console.log(`${info.name}: ${info.players?.online}/${info.players?.max}, ${ping} ms`);
queryServer(host, port = 7777, { timeout = 1500, logo = false }) resolves to:
info: the JSON above, typed asQueryResult.ping: the round trip, in ms.logo: the logo's bytes, withlogo: true, checked against its hash.
It rejects if the server doesn't answer.
sagl-query¶
The same from a terminal. It's built with the server, next to sagl-ping:
sagl-query play.example.com 7777 # prints the JSON; the round trip on stderr
sagl-query play.example.com 7777 --logo logo.png
The format, for other languages¶
The query uses UDP on the game port. Integers are little-endian.
Requests are always exactly 64 bytes, zero-padded:
| Offset | ||
|---|---|---|
| 0 | "SAGQ" |
|
| 4 | u8 | version: 1 |
| 5 | u8 | kind: 1 the info, 2 a piece of the logo |
| 6 | u16 | 0 |
| 8 | u32 | token: 0 the first time, then the one from the challenge |
| 12 | u32 | the logo: the offset to read from |
Replies start with "SAGR", u8 version 1, u8 kind, u16 0. What follows depends on the kind:
- Challenge (kind
0): u32 token. The server sends this to a request without a valid token. Send the request again with the token. A token lasts at least 30 seconds, and a new challenge means it has run out. - Info (kind
1): u32 serial, u32 total length, u16 part, u16 parts, then that part's bytes of the JSON. Put the parts with one serial together in order. A new serial means the JSON changed in between, so start again from that one. - Logo (kind
2): u32 total size (0: no logo), u32 offset, the logo's 32-byte SHA-256, then up to 1024 bytes from that offset. Ask for each offset in turn until you have them all, and check the hash.
There are two safeguards against abuse. The challenge is smaller than the request that triggers it, so the query can't be used to flood someone else with spoofed requests. And replies to each IP are limited to 256 KB a second.
Nothing in a reply is sent reliably. If a reply doesn't come, ask again.