Protocol reference

Plugin socket

Every message between a game server's plugin and its agent, both ways.

The plugin in each game server streams its gameplay events and status to the node's agent, which stores and uploads the events and acknowledges them, and relays console commands back to the game. The socket listens only on the node's loopback, and the agent image and the CS2 image implement its two ends, so a controller never talks to it. It is here for reference.

Connecting

Each game server's plugin dials its agent on the node's loopback, at the URL its launch gives it in ANKOR_WS_URL. Its events carry the launch's allocation_id, which the launch gives it in ANKOR_ALLOCATION_ID; a plugin without one does nothing.

ws://127.0.0.1:4500/ws/plugin?slot_id=<slot id>&key=<relay key>
  • slot_id is the slot the game runs in.
  • key is the launch's relay key: 8 random bytes in hex, which the agent makes for each launch. It is in that game's container only, so no game of another slot can connect as it. The agent keeps it in a label of the container and reads it back when it adopts the game after a restart.
  • The dial sends Ankor-Protocol with the plugin's protocol version.

The agent refuses a dial it cannot accept with an HTTP status:

StatusWhen
404The slot is not on the node, or the path is wrong.
401The key is not the launch's.
426Ankor-Protocol is missing or has another version.
  • A slot has one plugin connection. A new one replaces the old.
  • A binary frame ends the socket, and a frame over 2 MiB closes it with 1009.

Plugin → agent

What a plugin sends its agent: sequenced event frames, its server_status, phase_change, score, backup, command_result and quitting.

backup

plugin → agent

The round_backup of a match that sends no statistics, which the plugin sends like phase_change, and the agent forwards as backup. backup holds the complete event, unchanged, as the event stream would carry it.

  • type"backup"
  • backupround_backup

    The round_backup event, as the event stream would carry it.

Example
{
  "type": "backup",
  "backup": {
    "type": "round_backup",
    "timestamp": 1700001214000,
    "tick": 180000,
    "round": 14,
    "team1_side": "t",
    "team1_score": 7,
    "team2_score": 6,
    "rules": {
      "max_rounds": 24,
      "overtime": true,
      "overtime_max_rounds": 6,
      "friendly_fire": false
    },
    "file": "\"SaveFile\"\n{\n\t\"round\"\t\t\"13\"\n\t…\n}\n",
    "roster": {
      "team1": {
        "name": "Blue",
        "players": [
          "76561198000000001",
          "76561198000000002"
        ]
      },
      "team2": {
        "name": "Orange",
        "players": [
          "76561198000000011",
          "76561198000000012"
        ]
      },
      "spectators": []
    }
  }
}

command_result

plugin → agent

The plugin's answer to a command that carries a request_id.

  • type"command_result"
  • request_idstring ≤ 64 chars

    The request_id of the command this answers.

  • errorCommandRefusal | null

    Why the plugin refused the command. Null when the plugin carried it out.

    CommandRefusal

    Why a plugin did not carry out a command, as an AgentErrorCode.

    string enum

    • "no_match"
    • "not_in_warmup"
    • "team_empty"
    • "no_backup"
    • "not_live"
    • "at_halftime"
    • "command_blocked"
    • "unsupported_request"
Examples
{
  "type": "command_result",
  "request_id": "4f0c9d2e-7b1a-4c6e-9f3d-8a2b5e1c7d90",
  "error": null
}
{
  "type": "command_result",
  "request_id": "4f0c9d2e-7b1a-4c6e-9f3d-8a2b5e1c7d90",
  "error": "team_empty"
}

event

plugin → agent

One sequenced event, which the plugin sends and the agent spools without its type, then uploads as an EventBatchFrame of an event_batch. seq runs from 0 without gaps. The frame names no server, because the connection's key already proves which one it is.

A frame carries at most 2 MiB, which is no more than 2 MiB, so every event the agent accepts fits an upload batch once stored. The plugin drops a larger event before sending it, and the agent ends a socket that sends one.

After a gap past the events it stored, or with more than 512 events or 4 MiB of them waiting to be stored, the agent accepts no more history and ends the socket with code 1000 once the rest is stored. The plugin reconnects and resends from its last ack.

Example
{
  "type": "event",
  "seq": 128,
  "event": {
    "type": "round_start",
    "timestamp": 1700000200000,
    "tick": 110000,
    "round": 2,
    "round_time": 0,
    "team1_side": "ct",
    "players": [
      {
        "player": {
          "steam_id": "76561198000000001",
          "name": "alpha",
          "side": "ct",
          "team": 1
        },
        "alive": true,
        "equipment_value": 1000,
        "cash": 2400,
        "start_cash": 2400,
        "cash_spent": 0
      }
    ],
    "carryover": {
      "from_round": 1,
      "sampled_tick": 109500,
      "players": [
        {
          "player": {
            "steam_id": "76561198000000001",
            "name": "alpha",
            "side": "ct",
            "team": 1
          },
          "alive": true,
          "equipment_value": 1000,
          "cash": 2400,
          "start_cash": 800,
          "cash_spent": 650,
          "inventory": {
            "items": [
              {
                "weapon": 32,
                "quantity": 1,
                "unit_value": 200
              }
            ],
            "armor": 100,
            "helmet": false,
            "defuse_kit": false
          }
        }
      ]
    }
  }
}

phase_change

plugin → agent

The match moved to another phase, which the plugin sends whether or not the match sends statistics, and the agent forwards as phase_change. Like server_status, it is sent once and lost without a connection.

  • type"phase_change"
  • phaseMatchPhase

    The phase the match is in now.

    MatchPhase

    Where a match is.

    string enum

    • "loading"

      its map is loading.

    • "warmup"

      warmup.

    • "pending_restore"

      a launch with a restore went here from warmup while the engine loads the backup, and goes to live once the restored round starts. A load that fails leaves the match here, where the plugin reports nothing, until a load is tried again or the server is stopped.

    • "knife"

      the knife round.

    • "side_choice"

      the side choice.

    • "live"

      live play.

    • "post_live"

      after the last round.

Example
{
  "type": "phase_change",
  "phase": "knife"
}

quitting

plugin → agent

Tells the agent that the plugin is about to run quit after its match, with every event acknowledged and the demo finished. The agent stops a server that is still running 10 seconds later. Like server_status, it is not resent, so one sent without a connection is lost.

  • type"quitting"
Example
{
  "type": "quitting"
}

score

plugin → agent

The match's score changed, which the plugin sends like phase_change, and the agent forwards as score.

  • type"score"
  • team1_scoreinteger ≥ 0

    Team 1's score.

  • team2_scoreinteger ≥ 0

    Team 2's score.

Example
{
  "type": "score",
  "team1_score": 7,
  "team2_score": 5
}

server_status

plugin → agent

A game server's state, which the plugin sends in answer to every get_server_status. The plugin also sends it unasked once when it connects and once when it turns ready.

  • map_namestring ≤ 64 chars

    The map the server is on, up to 64 characters.

  • matchobject | null

    The loaded match and its team state. Null on a server without a match or after a refused config.

    • match_idMatchId | null

      The match_id the server was launched with. Null for a match without one. The launch's config is the controller's to keep, so it is not echoed.

      MatchId

      The id of one map's match, which the controller chooses. A series of maps is a grouping that the controller keeps. The id is a lowercase UUID, because demo file names carry it and case matters there.

      string

    • phaseMatchPhase

      Where the match is.

      MatchPhase

      Where a match is.

      string enum

      • "loading"

        its map is loading.

      • "warmup"

        warmup.

      • "pending_restore"

        a launch with a restore went here from warmup while the engine loads the backup, and goes to live once the restored round starts. A load that fails leaves the match here, where the plugin reports nothing, until a load is tried again or the server is stopped.

      • "knife"

        the knife round.

      • "side_choice"

        the side choice.

      • "live"

        live play.

      • "post_live"

        after the last round.

    • team1_sidePlayingSide

      Team 1's current side.

      PlayingSide

      A playing side, for a field that never holds spec.

      string enum

      • "t"

        terrorists.

      • "ct"

        counter-terrorists.

    • team2_sidePlayingSide

      Team 2's current side, always the other side, sent so a reader shows each team without deriving it.

      PlayingSide

      A playing side, for a field that never holds spec.

      string enum

      • "t"

        terrorists.

      • "ct"

        counter-terrorists.

    • team1_scoreinteger ≥ 0

      Team 1's score.

    • team2_scoreinteger ≥ 0

      Team 2's score.

  • playersobject[], ≤ 64 items

    Everyone connected, up to 64.

    • steam_idSteamId64

      The player's SteamID64.

      SteamId64

      A SteamID64 as 17 decimal digits.

      string

      Must match ^\d{17}$

    • namePlayerName

      The player's name.

      PlayerName

      A player's name, cut to up to 64 characters, without control characters. Rejected when it holds a lone UTF-16 surrogate.

      string ≤ 64 chars

      Must match ^[^\p{Cc}]*$

    • sideSide

      The player's side.

      Side

      The side a player is on.

      string enum

      • "t"

        terrorists, a playing side.

      • "ct"

        counter-terrorists, a playing side.

      • "spec"

        spectators and players on no team.

  • config_errorstring ≤ 512 chars | null

    Why the plugin could not read or use the match config it was launched with, up to 512 characters. Null when the server has no match config or the plugin accepted it.

  • readyboolean

    Whether players can join. When true, the server is logged on to Steam and has loaded the map that its launch asked for, the workshop map when it has one. It stays true once set.

    CS2 does not fall back to an anonymous logon when Steam refuses its token, so true also indicates that Steam accepted the GSLT and that its last use can be updated.

  • server_steam_idSteamId64 | null

    The SteamID64 that Steam gave the server when it logged on with its GSLT. Each token keeps one SteamID across logons, so a controller can look the server up in Steam's server list by it. The plugin also adds the allocation_id without its dashes to sv_tags, which that list shows. Null until ready.

    SteamId64

    A SteamID64 as 17 decimal digits.

    string

    Must match ^\d{17}$

  • type"server_status"
  • request_idstring ≤ 64 chars | null

    The request_id of the get_server_status this answers. Null when the request had none or the status was sent unasked.

Examples
{
  "type": "server_status",
  "request_id": "s1",
  "map_name": "de_mirage",
  "match": null,
  "players": [
    {
      "steam_id": "76561198000000001",
      "name": "alpha",
      "side": "t"
    }
  ],
  "config_error": null,
  "ready": true,
  "server_steam_id": "85568392936840254"
}
{
  "type": "server_status",
  "request_id": null,
  "map_name": "de_mirage",
  "match": {
    "match_id": "0199a0b4-5c00-7000-8000-00000000abc1",
    "phase": "warmup",
    "team1_side": "ct",
    "team2_side": "t",
    "team1_score": 0,
    "team2_score": 0
  },
  "players": [
    {
      "steam_id": "76561198000000001",
      "name": "alpha",
      "side": "t"
    }
  ],
  "config_error": null,
  "ready": true,
  "server_steam_id": "85568392936840254"
}

Agent → plugin

What the agent sends a plugin: commands, and an ack once events are stored on the node.

When the agent ends a plugin's socket, it sends a close frame whose reason says why: 1001 when the agent shuts down, and 1000 when the plugin must replay its history, when a newer connection of the same server takes its place, or when the server's launch has ended. The plugin reconnects after each and resends from its last ack.

ack

agent → plugin

Acknowledges that every frame with seq <= to_seq is stored on the node, so the plugin may discard them.

The agent sends it as frames are stored, and on every connect once the server has events on the node, so a reconnecting plugin resends only what follows. An ack past the last frame the plugin sent breaks the stream, and the plugin drops the connection.

  • type"ack"
  • to_seqEventSeq

    EventSeq

    An event's position among its server's events, from 0.

    integer ≥ 0

Example
{
  "type": "ack",
  "to_seq": 128
}

exec

agent → plugin

Runs a server console command. The plugin answers with a command_result once it has handed the command to the engine, or refuses a line that names a command of mp_restartgame, mp_warmup_start, mp_warmup_end, mp_warmup_pausetimer, mp_warmuptime, mp_swapteams, mp_switchteams, mp_scrambleteams, mp_backup_restore_load_file, mp_backup_round_file, mp_backup_round_auto, restart, changelevel, map, host_workshop_map, ds_workshop_changelevel, tv_record, tv_stoprecord and alias while it runs a match.

  • type"exec"
  • commandstring

    The console command line.

  • request_idstring ≤ 64 chars

    An id that the answering command_result carries back.

Example
{
  "type": "exec",
  "command": "mp_pause_match",
  "request_id": "c81d4e2a-6f3b-4a97-8e5d-2b9f0a7c3e61"
}

get_server_status

agent → plugin

Asks the plugin for its server's state, which the plugin answers with a server_status.

  • type"get_server_status"
  • request_idstring ≤ 64 chars | null

    An id that the answering server_status carries back, to match it to this request. Null for none.

Example
{
  "type": "get_server_status",
  "request_id": "s1"
}

restore_round

agent → plugin

Loads the server's backup of a round, as restore_round from the controller asks. The plugin runs mp_backup_restore_load_file on the file the engine wrote for it and answers with a command_result.

  • type"restore_round"
  • roundRoundNumber

    The round to play on from, one-based.

    RoundNumber

    A one-based round number. A map's history holds far fewer than the 1000 it allows.

    integer 1–1000

  • request_idstring ≤ 64 chars

    An id that the answering command_result carries back.

Example
{
  "type": "restore_round",
  "round": 14,
  "request_id": "9a3e6c1f-2d7b-4e85-b0c4-6f1a8d2e3b57"
}

start_match

agent → plugin

Starts a match in warmup whatever its ready_mode, as start_match from the controller asks. The plugin answers with a command_result.

  • type"start_match"
  • request_idstring ≤ 64 chars

    An id that the answering command_result carries back.

Example
{
  "type": "start_match",
  "request_id": "4f0c9d2e-7b1a-4c6e-9f3d-8a2b5e1c7d90"
}