asyncapi: 3.1.0
info:
  title: Ankor node protocol
  version: "0.9"
  description: This reference lists every message that passes between an Ankor node and a controller, with its fields and an example. For how the connection works and where to start, read the [protocol overview](https://docs.ankor.sh/protocol/) first.
  tags:
    - name: required
      description: A controller for Ankor agents must implement this frame.
    - name: optional
      description: A controller for Ankor agents may ignore this frame or never send it.
    - name: socket
      x-title: Agent socket
      description: Every message on the agent's WebSocket, both ways.
    - name: uploads
      x-title: Event upload
      description: Gameplay events uploaded over HTTP.
    - name: match
      x-title: Match
      description: The map starting, going live and ending, round backups, and plugin log lines.
    - name: players
      x-title: Players
      description: Players joining, leaving and changing sides.
    - name: knife
      x-title: Knife round
      description: The knife round and side choice.
    - name: rounds
      x-title: Rounds
      description: Round boundaries, economy, shots and results.
    - name: combat
      x-title: Combat
      description: Kills, damage, flashes and grenades.
    - name: bomb
      x-title: Bomb
      description: Plants, defuses and explosions.
    - name: plugin
      x-title: Plugin socket
      description: Every message between a game server's plugin and its agent, both ways.
defaultContentType: application/json
servers:
  socket:
    host: "{controllerHost}"
    protocol: wss
    description: The agent dials `ws://` instead when `controller.tls` is false in its agent.yml.
    variables:
      controllerHost:
        description: "`controller.host` from the node's agent.yml."
    security:
      - $ref: "#/components/securitySchemes/nodeToken"
  http:
    host: "{controllerHost}"
    protocol: https
    description: The agent posts over `http://` instead when `controller.tls` is false in its agent.yml.
    variables:
      controllerHost:
        description: "`controller.host` from the node's agent.yml."
    security:
      - $ref: "#/components/securitySchemes/nodeToken"
  plugin:
    host: 127.0.0.1:4500
    protocol: ws
    description: The agent's port for plugins, on the node's loopback.
channels:
  agentSocket:
    address: /ws/agent
    title: Agent socket
    description: The default path. `controller.socket_path` in agent.yml sets another. One socket per node. Requests carry an `id`, and the agent answers each with one reply whose `reply_to` repeats it and whose `request` names the request. The controller reads messages up to 983040 bytes and closes with `1008` `rate_limited` past a node's budgets; see [Limits](https://docs.ankor.sh/protocol/limits/).
    servers:
      - $ref: "#/servers/socket"
    x-directions:
      - from: agent
        to: controller
        about: |-
          What the agent sends: its [`hello`](#/components/schemas/AgentHello), one [`reply`](#/components/schemas/AgentReply) to each
          request, and reports of the node as it changes. Until a [`welcome`](#/components/schemas/ControllerWelcome) answers the
          [`hello`](#/components/schemas/AgentHello), only replies follow it, and a [`shutdown`](#/components/schemas/AgentShutdown) if the
          agent stops.
      - from: controller
        to: agent
        about: |-
          What the controller sends:

          - The [`welcome`](#/components/schemas/ControllerWelcome) that accepts a [`hello`](#/components/schemas/AgentHello), or the
            [`hello_refused`](#/components/schemas/ControllerHelloRefused) that refuses it.
          - Requests, which carry an `id` and receive one [`reply`](#/components/schemas/AgentReply) each.
          - The watches that start or renew a stream of logs or resource samples.
          - [`check_cs2_build`](#/components/schemas/ControllerCheckCs2Build), which receives no answer.
    x-closes:
      description: |-
        Every close that the controller or the agent makes on purpose, with a code from
        [`CLOSE_CODES`](https://docs.ankor.sh/reference/socket/#closing). Any other code, or none, is a broken connection, and the agent redials.
      closes:
        - code: 1000
          reason: "`SIGTERM`, `SIGINT` or `SIGQUIT`"
          by: agent
          when: The agent is stopping, after its `shutdown`.
          then: The controller closes its side with the same code and reason.
        - code: 1008
          reason: "`hello_refused`"
          by: controller
          when: The controller will not accept the node's `hello`, because the `hello` fails its schema or the controller refuses the node. A `hello_refused` frame just before the close says which.
          then: Logs the frame's reason and detail, and stops dialling until it is restarted.
        - code: 1000
          reason: null
          by: agent
          when: The controller sent `hello_refused` and has not closed within 5 seconds.
          then: Stays stopped until it is restarted.
        - code: 1000
          reason: null
          by: agent
          when: A `set_config` moved the node to another controller address, once its reply has gone.
          then: Dials the new address immediately.
        - code: 1008
          reason: "`hello_timeout`"
          by: controller
          when: No valid `hello` within 10 seconds of the upgrade.
          then: Redials with its backoff.
        - code: 1000
          reason: "`welcome_timeout`"
          by: agent
          when: No `welcome` answered its `hello` within 15 seconds.
          then: Redials with its backoff.
        - code: 1008
          reason: "`duplicate_hello`"
          by: controller
          when: A second `hello` arrived on the same socket.
          then: Redials with its backoff.
        - code: 1008
          reason: "`node_connected`"
          by: controller
          when: The node is already connected on another socket.
          then: Redials with its backoff, and connects once the other socket closes or times out.
        - code: 1008
          reason: "`rate_limited`"
          by: controller
          when: The node went over a [budget](https://docs.ankor.sh/protocol/limits/).
          then: Redials with its backoff. The controller answers `429` for 60 seconds.
        - code: 1008
          reason: "`agent_deleted`"
          by: controller
          when: The node was deleted in the controller.
          then: Stops dialling until it is restarted.
        - code: 1008
          reason: "`resync`"
          by: controller
          when: The controller lost track of the node's state.
          then: Redials and sends a fresh `hello`.
        - code: 1009
          reason: null
          by: controller
          when: A message over 960 KiB.
          then: Redials with its backoff.
        - code: 1009
          reason: null
          by: agent
          when: The controller sent a message over 2 MiB.
          then: Redials with its backoff.
    messages:
      hello:
        $ref: "#/components/messages/hello"
      node_status:
        $ref: "#/components/messages/node_status"
      shutdown:
        $ref: "#/components/messages/shutdown"
      slot_status:
        $ref: "#/components/messages/slot_status"
      server_exited:
        $ref: "#/components/messages/server_exited"
      log_batch:
        $ref: "#/components/messages/log_batch"
      log_index:
        $ref: "#/components/messages/log_index"
      cs2_update_progress:
        $ref: "#/components/messages/cs2_update_progress"
      demo:
        $ref: "#/components/messages/demo"
      demo_removed:
        $ref: "#/components/messages/demo_removed"
      demo_progress:
        $ref: "#/components/messages/demo_progress"
      resource_batch:
        $ref: "#/components/messages/resource_batch"
      event_backlog:
        $ref: "#/components/messages/event_backlog"
      phase_change:
        $ref: "#/components/messages/phase_change"
      score:
        $ref: "#/components/messages/score"
      backup:
        $ref: "#/components/messages/backup"
      reply:
        $ref: "#/components/messages/reply"
      start_server:
        $ref: "#/components/messages/start_server"
      start_server_reply:
        $ref: "#/components/messages/start_server_reply"
      stop_server:
        $ref: "#/components/messages/stop_server"
      stop_server_reply:
        $ref: "#/components/messages/stop_server_reply"
      get_server_status:
        $ref: "#/components/messages/get_server_status"
      get_server_status_reply:
        $ref: "#/components/messages/get_server_status_reply"
      update_cs2:
        $ref: "#/components/messages/update_cs2"
      update_cs2_reply:
        $ref: "#/components/messages/update_cs2_reply"
      check_cs2_build:
        $ref: "#/components/messages/check_cs2_build"
      delete_demo:
        $ref: "#/components/messages/delete_demo"
      delete_demo_reply:
        $ref: "#/components/messages/delete_demo_reply"
      reserve_slot:
        $ref: "#/components/messages/reserve_slot"
      reserve_slot_reply:
        $ref: "#/components/messages/reserve_slot_reply"
      set_config:
        $ref: "#/components/messages/set_config"
      set_config_reply:
        $ref: "#/components/messages/set_config_reply"
      update_agent:
        $ref: "#/components/messages/update_agent"
      update_agent_reply:
        $ref: "#/components/messages/update_agent_reply"
      update_cs2_image:
        $ref: "#/components/messages/update_cs2_image"
      update_cs2_image_reply:
        $ref: "#/components/messages/update_cs2_image_reply"
      watch_resources:
        $ref: "#/components/messages/watch_resources"
      watch_logs:
        $ref: "#/components/messages/watch_logs"
      welcome:
        $ref: "#/components/messages/welcome"
      hello_refused:
        $ref: "#/components/messages/hello_refused"
      console_command:
        $ref: "#/components/messages/console_command"
      console_command_reply:
        $ref: "#/components/messages/console_command_reply"
      restore_round:
        $ref: "#/components/messages/restore_round"
      restore_round_reply:
        $ref: "#/components/messages/restore_round_reply"
      start_match:
        $ref: "#/components/messages/start_match"
      start_match_reply:
        $ref: "#/components/messages/start_match_reply"
    bindings:
      ws:
        headers:
          type: object
          properties:
            Ankor-Protocol:
              type: string
              const: "0.9"
          required:
            - Ankor-Protocol
  eventUpload:
    address: /api/node/events
    title: Event upload
    description: "The default path. `controller.events_path` in agent.yml sets another. `POST` with `Ankor-Protocol: 0.9`. Any 2xx carries a receipt. On any other status, or no answer within 15 seconds, the agent backs off and retries."
    servers:
      - $ref: "#/servers/http"
    messages:
      event_batch:
        $ref: "#/components/messages/event_batch"
      event_batch_receipt:
        $ref: "#/components/messages/event_batch_receipt"
      event_batch_rejection:
        $ref: "#/components/messages/event_batch_rejection"
  gameplayEvents:
    address: null
    title: Gameplay events
    description: The `event` of each frame in an event batch.
    messages:
      map_start:
        $ref: "#/components/messages/map_start"
      player_connect:
        $ref: "#/components/messages/player_connect"
      player_disconnect:
        $ref: "#/components/messages/player_disconnect"
      knife_start:
        $ref: "#/components/messages/knife_start"
      knife_end:
        $ref: "#/components/messages/knife_end"
      side_choice:
        $ref: "#/components/messages/side_choice"
      going_live:
        $ref: "#/components/messages/going_live"
      round_backup:
        $ref: "#/components/messages/round_backup"
      match_restored:
        $ref: "#/components/messages/match_restored"
      round_start:
        $ref: "#/components/messages/round_start"
      round_freeze_end:
        $ref: "#/components/messages/round_freeze_end"
      round_buy_end:
        $ref: "#/components/messages/round_buy_end"
      round_end:
        $ref: "#/components/messages/round_end"
      round_complete:
        $ref: "#/components/messages/round_complete"
      player_death:
        $ref: "#/components/messages/player_death"
      player_hurt:
        $ref: "#/components/messages/player_hurt"
      player_blind:
        $ref: "#/components/messages/player_blind"
      grenade_throw:
        $ref: "#/components/messages/grenade_throw"
      grenade_detonate:
        $ref: "#/components/messages/grenade_detonate"
      grenade_end:
        $ref: "#/components/messages/grenade_end"
      round_shots:
        $ref: "#/components/messages/round_shots"
      bomb_begin_plant:
        $ref: "#/components/messages/bomb_begin_plant"
      bomb_abort_plant:
        $ref: "#/components/messages/bomb_abort_plant"
      bomb_planted:
        $ref: "#/components/messages/bomb_planted"
      bomb_begin_defuse:
        $ref: "#/components/messages/bomb_begin_defuse"
      bomb_abort_defuse:
        $ref: "#/components/messages/bomb_abort_defuse"
      bomb_defused:
        $ref: "#/components/messages/bomb_defused"
      bomb_exploded:
        $ref: "#/components/messages/bomb_exploded"
      round_mvp:
        $ref: "#/components/messages/round_mvp"
      map_end:
        $ref: "#/components/messages/map_end"
      log:
        $ref: "#/components/messages/log"
  pluginSocket:
    address: /ws/plugin
    title: Plugin socket
    description: One socket per game server, on the node's loopback.
    servers:
      - $ref: "#/servers/plugin"
    bindings:
      ws:
        query:
          type: object
          properties:
            slot_id:
              type: string
              description: The slot the game runs in.
            key:
              type: string
              description: "The launch's relay key: 8 random bytes in hex, which the agent makes for each launch."
          required:
            - slot_id
            - key
        headers:
          type: object
          properties:
            Ankor-Protocol:
              type: string
              const: "0.9"
          required:
            - Ankor-Protocol
    x-directions:
      - from: plugin
        to: agent
        about: |-
          What a plugin sends its agent: sequenced [`event`](#/components/schemas/PluginEventFrameView) frames, its
          [`server_status`](#/components/schemas/ServerStatus), [`phase_change`](#/components/schemas/PluginPhaseChange),
          [`score`](#/components/schemas/PluginScore), [`backup`](#/components/schemas/PluginBackup),
          [`command_result`](#/components/schemas/CommandResult) and [`quitting`](#/components/schemas/Quitting).
      - from: agent
        to: plugin
        about: |-
          What the agent sends a plugin: commands, and an [`ack`](#/components/schemas/EventAck) 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`](#/components/schemas/EventAck).
    messages:
      plugin_event:
        $ref: "#/components/messages/plugin_event"
      plugin_server_status:
        $ref: "#/components/messages/plugin_server_status"
      plugin_phase_change:
        $ref: "#/components/messages/plugin_phase_change"
      plugin_score:
        $ref: "#/components/messages/plugin_score"
      plugin_backup:
        $ref: "#/components/messages/plugin_backup"
      plugin_command_result:
        $ref: "#/components/messages/plugin_command_result"
      plugin_quitting:
        $ref: "#/components/messages/plugin_quitting"
      plugin_exec:
        $ref: "#/components/messages/plugin_exec"
      plugin_restore_round:
        $ref: "#/components/messages/plugin_restore_round"
      plugin_start_match:
        $ref: "#/components/messages/plugin_start_match"
      plugin_get_server_status:
        $ref: "#/components/messages/plugin_get_server_status"
      plugin_ack:
        $ref: "#/components/messages/plugin_ack"
operations:
  hello:
    action: receive
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/hello"
    tags:
      - name: socket
  node_status:
    action: receive
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/node_status"
    tags:
      - name: socket
  shutdown:
    action: receive
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/shutdown"
    tags:
      - name: socket
  slot_status:
    action: receive
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/slot_status"
    tags:
      - name: socket
  server_exited:
    action: receive
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/server_exited"
    tags:
      - name: socket
  log_batch:
    action: receive
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/log_batch"
    tags:
      - name: socket
  log_index:
    action: receive
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/log_index"
    tags:
      - name: socket
  cs2_update_progress:
    action: receive
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/cs2_update_progress"
    tags:
      - name: socket
  demo:
    action: receive
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/demo"
    tags:
      - name: socket
  demo_removed:
    action: receive
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/demo_removed"
    tags:
      - name: socket
  demo_progress:
    action: receive
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/demo_progress"
    tags:
      - name: socket
  resource_batch:
    action: receive
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/resource_batch"
    tags:
      - name: socket
  event_backlog:
    action: receive
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/event_backlog"
    tags:
      - name: socket
  phase_change:
    action: receive
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/phase_change"
    tags:
      - name: socket
  score:
    action: receive
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/score"
    tags:
      - name: socket
  backup:
    action: receive
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/backup"
    tags:
      - name: socket
  start_server:
    action: send
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/start_server"
    tags:
      - name: socket
    reply:
      channel:
        $ref: "#/channels/agentSocket"
      messages:
        - $ref: "#/channels/agentSocket/messages/start_server_reply"
  stop_server:
    action: send
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/stop_server"
    tags:
      - name: socket
    reply:
      channel:
        $ref: "#/channels/agentSocket"
      messages:
        - $ref: "#/channels/agentSocket/messages/stop_server_reply"
  get_server_status:
    action: send
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/get_server_status"
    tags:
      - name: socket
    reply:
      channel:
        $ref: "#/channels/agentSocket"
      messages:
        - $ref: "#/channels/agentSocket/messages/get_server_status_reply"
  update_cs2:
    action: send
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/update_cs2"
    tags:
      - name: socket
    reply:
      channel:
        $ref: "#/channels/agentSocket"
      messages:
        - $ref: "#/channels/agentSocket/messages/update_cs2_reply"
  check_cs2_build:
    action: send
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/check_cs2_build"
    tags:
      - name: socket
  delete_demo:
    action: send
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/delete_demo"
    tags:
      - name: socket
    reply:
      channel:
        $ref: "#/channels/agentSocket"
      messages:
        - $ref: "#/channels/agentSocket/messages/delete_demo_reply"
  reserve_slot:
    action: send
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/reserve_slot"
    tags:
      - name: socket
    reply:
      channel:
        $ref: "#/channels/agentSocket"
      messages:
        - $ref: "#/channels/agentSocket/messages/reserve_slot_reply"
  set_config:
    action: send
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/set_config"
    tags:
      - name: socket
    reply:
      channel:
        $ref: "#/channels/agentSocket"
      messages:
        - $ref: "#/channels/agentSocket/messages/set_config_reply"
  update_agent:
    action: send
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/update_agent"
    tags:
      - name: socket
    reply:
      channel:
        $ref: "#/channels/agentSocket"
      messages:
        - $ref: "#/channels/agentSocket/messages/update_agent_reply"
  update_cs2_image:
    action: send
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/update_cs2_image"
    tags:
      - name: socket
    reply:
      channel:
        $ref: "#/channels/agentSocket"
      messages:
        - $ref: "#/channels/agentSocket/messages/update_cs2_image_reply"
  watch_resources:
    action: send
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/watch_resources"
    tags:
      - name: socket
  watch_logs:
    action: send
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/watch_logs"
    tags:
      - name: socket
  welcome:
    action: send
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/welcome"
    tags:
      - name: socket
  hello_refused:
    action: send
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/hello_refused"
    tags:
      - name: socket
  console_command:
    action: send
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/console_command"
    tags:
      - name: socket
    reply:
      channel:
        $ref: "#/channels/agentSocket"
      messages:
        - $ref: "#/channels/agentSocket/messages/console_command_reply"
  restore_round:
    action: send
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/restore_round"
    tags:
      - name: socket
    reply:
      channel:
        $ref: "#/channels/agentSocket"
      messages:
        - $ref: "#/channels/agentSocket/messages/restore_round_reply"
  start_match:
    action: send
    channel:
      $ref: "#/channels/agentSocket"
    messages:
      - $ref: "#/channels/agentSocket/messages/start_match"
    tags:
      - name: socket
    reply:
      channel:
        $ref: "#/channels/agentSocket"
      messages:
        - $ref: "#/channels/agentSocket/messages/start_match_reply"
  event_batch:
    action: receive
    channel:
      $ref: "#/channels/eventUpload"
    messages:
      - $ref: "#/channels/eventUpload/messages/event_batch"
    tags:
      - name: uploads
    bindings:
      http:
        method: POST
    reply:
      channel:
        $ref: "#/channels/eventUpload"
      messages:
        - $ref: "#/channels/eventUpload/messages/event_batch_receipt"
        - $ref: "#/channels/eventUpload/messages/event_batch_rejection"
  gameplay_events:
    action: receive
    channel:
      $ref: "#/channels/gameplayEvents"
    messages:
      - $ref: "#/channels/gameplayEvents/messages/map_start"
      - $ref: "#/channels/gameplayEvents/messages/player_connect"
      - $ref: "#/channels/gameplayEvents/messages/player_disconnect"
      - $ref: "#/channels/gameplayEvents/messages/knife_start"
      - $ref: "#/channels/gameplayEvents/messages/knife_end"
      - $ref: "#/channels/gameplayEvents/messages/side_choice"
      - $ref: "#/channels/gameplayEvents/messages/going_live"
      - $ref: "#/channels/gameplayEvents/messages/round_backup"
      - $ref: "#/channels/gameplayEvents/messages/match_restored"
      - $ref: "#/channels/gameplayEvents/messages/round_start"
      - $ref: "#/channels/gameplayEvents/messages/round_freeze_end"
      - $ref: "#/channels/gameplayEvents/messages/round_buy_end"
      - $ref: "#/channels/gameplayEvents/messages/round_end"
      - $ref: "#/channels/gameplayEvents/messages/round_complete"
      - $ref: "#/channels/gameplayEvents/messages/player_death"
      - $ref: "#/channels/gameplayEvents/messages/player_hurt"
      - $ref: "#/channels/gameplayEvents/messages/player_blind"
      - $ref: "#/channels/gameplayEvents/messages/grenade_throw"
      - $ref: "#/channels/gameplayEvents/messages/grenade_detonate"
      - $ref: "#/channels/gameplayEvents/messages/grenade_end"
      - $ref: "#/channels/gameplayEvents/messages/round_shots"
      - $ref: "#/channels/gameplayEvents/messages/bomb_begin_plant"
      - $ref: "#/channels/gameplayEvents/messages/bomb_abort_plant"
      - $ref: "#/channels/gameplayEvents/messages/bomb_planted"
      - $ref: "#/channels/gameplayEvents/messages/bomb_begin_defuse"
      - $ref: "#/channels/gameplayEvents/messages/bomb_abort_defuse"
      - $ref: "#/channels/gameplayEvents/messages/bomb_defused"
      - $ref: "#/channels/gameplayEvents/messages/bomb_exploded"
      - $ref: "#/channels/gameplayEvents/messages/round_mvp"
      - $ref: "#/channels/gameplayEvents/messages/map_end"
      - $ref: "#/channels/gameplayEvents/messages/log"
    tags:
      - name: uploads
  plugin_event:
    action: receive
    channel:
      $ref: "#/channels/pluginSocket"
    messages:
      - $ref: "#/channels/pluginSocket/messages/plugin_event"
    tags:
      - name: plugin
  plugin_server_status:
    action: receive
    channel:
      $ref: "#/channels/pluginSocket"
    messages:
      - $ref: "#/channels/pluginSocket/messages/plugin_server_status"
    tags:
      - name: plugin
  plugin_phase_change:
    action: receive
    channel:
      $ref: "#/channels/pluginSocket"
    messages:
      - $ref: "#/channels/pluginSocket/messages/plugin_phase_change"
    tags:
      - name: plugin
  plugin_score:
    action: receive
    channel:
      $ref: "#/channels/pluginSocket"
    messages:
      - $ref: "#/channels/pluginSocket/messages/plugin_score"
    tags:
      - name: plugin
  plugin_backup:
    action: receive
    channel:
      $ref: "#/channels/pluginSocket"
    messages:
      - $ref: "#/channels/pluginSocket/messages/plugin_backup"
    tags:
      - name: plugin
  plugin_command_result:
    action: receive
    channel:
      $ref: "#/channels/pluginSocket"
    messages:
      - $ref: "#/channels/pluginSocket/messages/plugin_command_result"
    tags:
      - name: plugin
  plugin_quitting:
    action: receive
    channel:
      $ref: "#/channels/pluginSocket"
    messages:
      - $ref: "#/channels/pluginSocket/messages/plugin_quitting"
    tags:
      - name: plugin
  plugin_exec:
    action: send
    channel:
      $ref: "#/channels/pluginSocket"
    messages:
      - $ref: "#/channels/pluginSocket/messages/plugin_exec"
    tags:
      - name: plugin
  plugin_restore_round:
    action: send
    channel:
      $ref: "#/channels/pluginSocket"
    messages:
      - $ref: "#/channels/pluginSocket/messages/plugin_restore_round"
    tags:
      - name: plugin
  plugin_start_match:
    action: send
    channel:
      $ref: "#/channels/pluginSocket"
    messages:
      - $ref: "#/channels/pluginSocket/messages/plugin_start_match"
    tags:
      - name: plugin
  plugin_get_server_status:
    action: send
    channel:
      $ref: "#/channels/pluginSocket"
    messages:
      - $ref: "#/channels/pluginSocket/messages/plugin_get_server_status"
    tags:
      - name: plugin
  plugin_ack:
    action: send
    channel:
      $ref: "#/channels/pluginSocket"
    messages:
      - $ref: "#/channels/pluginSocket/messages/plugin_ack"
    tags:
      - name: plugin
components:
  messages:
    hello:
      name: hello
      title: hello
      summary: The agent's first message on every connection, and sent only then.
      description: |-
        The agent's first message on every connection, and sent only then. An applied
        [`set_config`](#/components/schemas/ControllerSetConfig) answers with the settings instead.

        Answer an accepted [`hello`](#/components/schemas/AgentHello) with [`welcome`](#/components/schemas/ControllerWelcome). Until then the
        agent sends only replies, or a [`shutdown`](#/components/schemas/AgentShutdown) when it stops. Runtime changes
        use status messages. Reconnecting never changes the node's configuration.

        Rejected unless `slot_status` names each slot of `config.slots` once and names no other slot.
      tags:
        - name: required
        - name: socket
      payload:
        $ref: "#/components/schemas/AgentHello"
      examples:
        - payload:
            type: hello
            agent:
              version: 0.1.0
              image: ghcr.io/ankorcs/ankor-agent:1.2.0
              image_id: sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae
            host:
              cpu_count: 8
              os_name: Debian GNU/Linux 13 (trixie)
              kernel: 6.12.43+deb13-amd64
            public_ip: 203.0.113.7
            country: FR
            config:
              slots:
                - id: s1
                  port: 27015
                  tv_port: 27020
                  gslt_hint: 7E8F9A0B1C
              pinned_ip: null
              controller:
                host: cs.example.com
                tls: true
                socket_path: /ws/agent
                events_path: /api/node/events
              auto_update: true
              memory_mb: null
              demo_upload: null
            slot_status:
              - slot_id: s1
                state: idle
                allocation_id: null
                match_id: null
                plugin_online: false
                gslt_state: ok
                reserved: false
                server_steam_id: null
            cs2:
              build_id: "20114612"
              latest_build_id: "20114612"
              image: ghcr.io/ankorcs/ankor-cs2:1.2.0
              image_id: sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
              update: null
              image_update: null
            unavailable: null
            demos:
              - slot_id: s2
                file_name: 20260928-183000_0199a0b4-5c00-7000-8000-00000000abc0_de_nuke.dem
                size_bytes: 164626432
                modified_at: 1759084200000
                recording: false
                uploading: false
                upload_error: null
            server_exits:
              - slot_id: s1
                allocation_id: 9c04e1d2-3a7b-4f6e-8d21-5e0b9c7a4f13
                reason: clean_exit
                exit_code: 0
                detail: null
            event_backlog:
              servers:
                - allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
                  pending: 212
              server_count: 1
              event_count: 212
              reason: HTTP 503
            agent_update:
              image: ghcr.io/ankorcs/ankor-agent:1.4.0
              step: null
              outcome: completed
              detail: null
    node_status:
      name: node_status
      title: node_status
      summary: Replaces `cs2`, `unavailable` and `agent_update` of the last [`hello`](#/components/schemas/AgentHello).
      description: |-
        Replaces `cs2`, `unavailable` and `agent_update` of the last [`hello`](#/components/schemas/AgentHello).

        Sent when they change:

        - When a maintenance operation starts or ends, so also when a CS2 install or update finishes,
          whoever started it, or [`update_cs2_image`](#/components/schemas/ControllerUpdateCs2Image) has switched the image.
        - When `agent_update` or `cs2.image_update` moves from `pulling` to `switching`.
        - When the newest public build changes.
        - When an agent swap that is still proving its replacement ends.

        Each frame is the node's state when it was read, not a step in a history, so a change that is
        over before its report reads the state may go unreported.
      tags:
        - name: required
        - name: socket
      payload:
        $ref: "#/components/schemas/AgentNodeStatus"
      examples:
        - payload:
            type: node_status
            cs2:
              build_id: "20114612"
              latest_build_id: "20114670"
              image: ghcr.io/ankorcs/ankor-cs2:1.2.0
              image_id: sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
              update: null
              image_update:
                image: ghcr.io/ankorcs/ankor-cs2:1.2.0
                step: null
                outcome: completed
                detail: null
            unavailable: null
            agent_update: null
        - payload:
            type: node_status
            cs2:
              build_id: "20114612"
              latest_build_id: "20114670"
              image: ghcr.io/ankorcs/ankor-cs2:1.2.0
              image_id: sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
              update:
                id: 7c2e9a41-3b5d-4f68-9e0a-1d4c8b7f2a63
                mode: update
                automatic: true
                outcome: null
                detail: null
              image_update: null
            unavailable:
              code: maintenance
              operation: cs2_update
              detail: CS2 update
            agent_update: null
        - payload:
            type: node_status
            cs2:
              build_id: "20114670"
              latest_build_id: "20114670"
              image: ghcr.io/ankorcs/ankor-cs2:1.2.0
              image_id: sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
              update:
                id: 7c2e9a41-3b5d-4f68-9e0a-1d4c8b7f2a63
                mode: update
                automatic: true
                outcome: completed
                detail: null
              image_update: null
            unavailable:
              code: event_storage
              operation: null
              detail: "event spool schema mismatch: /srv/ankor/events/streams.sqlite is at version 3, newer than this agent's 2. Update the agent, or delete /srv/ankor/events on the host and restart it to start a new spool, which deletes every event not yet delivered to the controller"
            agent_update: null
        - payload:
            type: node_status
            cs2:
              build_id: "20114612"
              latest_build_id: "20114670"
              image: ghcr.io/ankorcs/ankor-cs2:1.2.0
              image_id: sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
              update: null
              image_update:
                image: ghcr.io/ankorcs/ankor-cs2:1.3.0
                step: pulling
                outcome: null
                detail: null
            unavailable:
              code: maintenance
              operation: cs2_image_update
              detail: CS2 image update
            agent_update: null
        - payload:
            type: node_status
            cs2:
              build_id: "20114612"
              latest_build_id: "20114670"
              image: ghcr.io/ankorcs/ankor-cs2:1.2.0
              image_id: sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
              update: null
              image_update: null
            unavailable:
              code: maintenance
              operation: agent_update
              detail: agent update
            agent_update:
              image: ghcr.io/ankorcs/ankor-agent:1.5.0
              step: pulling
              outcome: null
              detail: null
    shutdown:
      name: shutdown
      title: shutdown
      summary: The agent is stopping cleanly and starts no new work.
      description: |-
        The agent is stopping cleanly and starts no new work. It answers a request that arrives after
        this frame with `shutting_down` at once. Replies to the requests it is running may still follow
        for up to 5 seconds, and then it closes with code `1000`.

        Send no new requests after it. A request without a reply when the socket closes may or may not
        have run; the node's reports after it reconnects say which. Its servers keep running, and the
        agent takes them over when it returns. It is sent whether or not the controller has welcomed the
        [`hello`](#/components/schemas/AgentHello).
      tags:
        - name: optional
        - name: socket
      payload:
        $ref: "#/components/schemas/AgentShutdown"
      examples:
        - payload:
            type: shutdown
            signal: SIGTERM
    slot_status:
      name: slot_status
      title: slot_status
      summary: A slot's status changed since the last [`hello`](#/components/schemas/AgentHello) or [`slot_status`](#/components/schemas/AgentSlotStatus) for it.
      description: |-
        A slot's status changed since the last [`hello`](#/components/schemas/AgentHello) or
        [`slot_status`](#/components/schemas/AgentSlotStatus) for it. Rejected when it breaks a rule of
        [`SlotStatus`](#/components/schemas/SlotStatus).
      tags:
        - name: required
        - name: socket
      payload:
        $ref: "#/components/schemas/AgentSlotStatus"
      examples:
        - payload:
            type: slot_status
            slot_id: s1
            state: running
            allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
            match_id: 0199a0b4-5c00-7000-8000-00000000abc1
            plugin_online: true
            gslt_state: ok
            reserved: false
            server_steam_id: "85568392936840254"
        - payload:
            type: slot_status
            slot_id: s2
            state: idle
            allocation_id: null
            match_id: null
            plugin_online: false
            gslt_state: ok
            reserved: false
            server_steam_id: null
    server_exited:
      name: server_exited
      title: server_exited
      summary: A server has ended and its container is gone.
      description: |-
        A server has ended and its container is gone. Sent ahead of the `idle`
        [`slot_status`](#/components/schemas/AgentSlotStatus) of its slot, also for a launch that failed before or while
        its container started.

        A server that ended while no connection was welcomed goes in `server_exits` of the next
        [`hello`](#/components/schemas/AgentHello) instead. If it ended after that [`hello`](#/components/schemas/AgentHello) went out,
        it is sent after the [`welcome`](#/components/schemas/ControllerWelcome), ahead of the slot statuses.

        The agent keeps a [`server_exited`](#/components/schemas/AgentServerExited) until the socket takes its write,
        across its restarts, for up to 7 days, so one may also come again after
        a restart. One the socket took is not sent again, even if the connection drops before the
        controller reads it. The agent does not keep them while it cannot open that store, which it reports as
        `unavailable` with `event_storage`. While the store does not answer, the agent keeps one only
        once the store answers again. A server that ends while the agent is not running is missing
        from its next [`hello`](#/components/schemas/AgentHello).
      tags:
        - name: optional
        - name: socket
      payload:
        $ref: "#/components/schemas/AgentServerExited"
      examples:
        - payload:
            type: server_exited
            slot_id: s2
            allocation_id: 9c04e1d2-3a7b-4f6e-8d21-5e0b9c7a4f13
            reason: clean_exit
            exit_code: 0
            detail: null
        - payload:
            type: server_exited
            slot_id: s1
            allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
            reason: crashed
            exit_code: 139
            detail: null
        - payload:
            type: server_exited
            slot_id: s2
            allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
            reason: config_rejected
            exit_code: 0
            detail: "roster.team1.players: 6 players, more than max_players allows"
    log_batch:
      name: log_batch
      title: log_batch
      summary: One source's lines, sent only while a [`watch_logs`](#/components/schemas/ControllerWatchLogs) holds the lease of that source.
      description: |-
        One source's lines, sent only while a [`watch_logs`](#/components/schemas/ControllerWatchLogs) holds the lease of that
        source. A replay of the source starts with `reset` and may span several frames. Between replays,
        new lines follow in batches. A replay larger than the remaining log budget of the agent leaves
        out the oldest lines.
      tags:
        - name: optional
        - name: socket
      payload:
        $ref: "#/components/schemas/AgentLogBatch"
      examples:
        - payload:
            type: log_batch
            source:
              kind: server
              allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
            replay_id: null
            reset: true
            lines:
              - at: 1700000014000
                level: null
                line: "Host activate: Loading (de_mirage)"
        - payload:
            type: log_batch
            source:
              kind: agent
            replay_id: 0d6f1c52-3a7e-4b9d-8c21-5e4f7a9b2c13
            reset: false
            lines:
              - at: 1700000013000
                level: warn
                line: "[agent:ws] reconnecting in 4000ms"
    log_index:
      name: log_index
      title: log_index
      summary: "Every source the agent keeps, whole every time: its log, each CS2 install or update running or among the last 1 ended, and each server with output, running or among the last 10 ended."
      description: |-
        Every source the agent keeps, whole every time: its log, each CS2 install or update running or
        among the last 1 ended, and each server with output, running or among the
        last 10 ended.

        Sent while any log lease runs, with each lease that starts and each replay asked for. Also sent
        when an update starts or ends, a server's first line arrives, a server ends, or a source's output
        is dropped, and every 30 seconds while lines arrive. Counts and `last_line_at`
        are as of that frame. A source missing from it is no longer kept.
      tags:
        - name: optional
        - name: socket
      payload:
        $ref: "#/components/schemas/AgentLogIndex"
      examples:
        - payload:
            type: log_index
            sources:
              - source:
                  kind: agent
                line_count: 312
                last_line_at: 1700000014000
                ended: false
              - source:
                  kind: cs2_update
                  update_id: 7c2e9a41-3b5d-4f68-9e0a-1d4c8b7f2a63
                line_count: 0
                last_line_at: null
                ended: false
              - source:
                  kind: server
                  allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
                line_count: 1840
                last_line_at: 1700000013000
                ended: false
              - source:
                  kind: server
                  allocation_id: 9c04e1d2-3a7b-4f5e-8d6c-1b2a3c4d5e6f
                line_count: 2000
                last_line_at: 1699999400000
                ended: true
    cs2_update_progress:
      name: cs2_update_progress
      title: cs2_update_progress
      summary: Progress of a CS2 install or update while it runs.
      description: |-
        Progress of a CS2 install or update while it runs. `cs2.update` in
        [`node_status`](#/components/schemas/AgentNodeStatus) says how it ended.
      tags:
        - name: optional
        - name: socket
      payload:
        $ref: "#/components/schemas/AgentCs2UpdateProgress"
      examples:
        - payload:
            type: cs2_update_progress
            update_id: 7c2e9a41-3b5d-4f68-9e0a-1d4c8b7f2a63
            percent: 42.5
            message: downloading
        - payload:
            type: cs2_update_progress
            update_id: 7c2e9a41-3b5d-4f68-9e0a-1d4c8b7f2a63
            percent: 100
            message: install/update complete
    demo:
      name: demo
      title: demo
      summary: "One demo as it is now, sent when it appears or changes: its file, its upload starting or stopping, or its `upload_error`."
      description: |-
        One demo as it is now, sent when it appears or changes: its file, its upload starting or
        stopping, or its `upload_error`. It replaces the demo with the same `slot_id` and `file_name`, or
        adds it. [`hello`](#/components/schemas/AgentHello) lists the newest 50. A demo that
        appears after the [`hello`](#/components/schemas/AgentHello) is added, however many the node keeps.
      tags:
        - name: optional
        - name: socket
      payload:
        $ref: "#/components/schemas/AgentDemo"
      examples:
        - payload:
            type: demo
            slot_id: s1
            file_name: 20261008-201500_0199a0b4-5c00-7a6e-8d4f-2f1d0c9b8a7e_de_mirage.dem
            size_bytes: 52428800
            modified_at: 1759954500000
            recording: true
            uploading: false
            upload_error: null
        - payload:
            type: demo
            slot_id: s1
            file_name: 20260928-201500_0199a0b4-5c00-7000-8000-00000000abc1_de_mirage.dem
            size_bytes: 181403648
            modified_at: 1759090500000
            recording: false
            uploading: true
            upload_error: null
        - payload:
            type: demo
            slot_id: s2
            file_name: 20260928-183000_0199a0b4-5c00-7000-8000-00000000abc0_de_nuke.dem
            size_bytes: 164626432
            modified_at: 1759084200000
            recording: false
            uploading: false
            upload_error: HTTP 503; retrying in 30 s
    demo_removed:
      name: demo_removed
      title: demo_removed
      summary: A demo that the controller knows of is no longer on the node.
      tags:
        - name: optional
        - name: socket
      payload:
        $ref: "#/components/schemas/AgentDemoRemoved"
      examples:
        - payload:
            type: demo_removed
            slot_id: s1
            file_name: 20260928-201500_0199a0b4-5c00-7000-8000-00000000abc1_de_mirage.dem
            reason: uploaded
    demo_progress:
      name: demo_progress
      title: demo_progress
      summary: How far the uploading demo is, at most once a second while it uploads.
      tags:
        - name: optional
        - name: socket
      payload:
        $ref: "#/components/schemas/AgentDemoProgress"
      examples:
        - payload:
            type: demo_progress
            slot_id: s1
            file_name: 20260928-201500_0199a0b4-5c00-7000-8000-00000000abc1_de_mirage.dem
            sent_bytes: 45350912
            bytes_per_second: 12500000
    resource_batch:
      name: resource_batch
      title: resource_batch
      summary: The agent's resource ticks, oldest first.
      description: |-
        The agent's resource ticks, oldest first. A [`watch_resources`](#/components/schemas/ControllerWatchResources) lease
        that starts receives every tick the agent holds, then each new tick while it runs. The agent
        sends nothing while nobody watches, and nothing is stored.
      tags:
        - name: optional
        - name: socket
      payload:
        $ref: "#/components/schemas/AgentResourceBatch"
      examples:
        - payload:
            type: resource_batch
            ticks:
              - at: 1700000014000
                disk:
                  cap: 107374182400
                  used: 64424509440
                memory:
                  cap: 17179869184
                  used: 6442450944
                cpu:
                  max: 71
                  average: 38
            reset: true
    event_backlog:
      name: event_backlog
      title: event_backlog
      summary: The node's [`EventBacklog`](#/components/schemas/EventBacklog) now, replacing the backlog of the last [`hello`](#/components/schemas/AgentHello) or [`event_backlog`](#/components/schemas/AgentEventBacklog).
      description: |-
        The node's [`EventBacklog`](#/components/schemas/EventBacklog) now, replacing the backlog of the last [`hello`](#/components/schemas/AgentHello)
        or [`event_backlog`](#/components/schemas/AgentEventBacklog). Sent when it changes: after each failed upload, as
        its counts grow, and once, with nothing stuck, when uploads work again. Rejected when it breaks a
        rule of [`EventBacklog`](#/components/schemas/EventBacklog).
      tags:
        - name: optional
        - name: socket
      payload:
        $ref: "#/components/schemas/AgentEventBacklog"
      examples:
        - payload:
            type: event_backlog
            servers:
              - allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
                pending: 212
            server_count: 1
            event_count: 212
            reason: HTTP 503
    phase_change:
      name: phase_change
      title: phase_change
      summary: A match moved to another phase, as its plugin reports it, whether or not the match sends statistics.
      description: |-
        A match moved to another phase, as its plugin reports it, whether or not the match sends
        statistics. An [`event_batch`](#/components/schemas/EventBatch) carries the events behind it, such as
        [`going_live`](#/components/schemas/GoingLive) and [`map_end`](#/components/schemas/MapEnd).

        The agent sends it once. If the plugin is not connected to the agent, or the controller has not
        welcomed the agent's [`hello`](#/components/schemas/AgentHello), it is lost, and
        [`get_server_status`](#/components/schemas/ControllerGetServerStatus) reads the phase again.
      tags:
        - name: optional
        - name: socket
      payload:
        $ref: "#/components/schemas/AgentPhaseChange"
      examples:
        - payload:
            type: phase_change
            allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
            phase: live
    score:
      name: score
      title: score
      summary: "A match's score changed, as its plugin reads it off the scoreboard, whether or not the match sends statistics: after a round, when going live resets it, or after a restore."
      description: |-
        A match's score changed, as its plugin reads it off the scoreboard, whether or not the match
        sends statistics: after a round, when going live resets it, or after a restore. An
        [`event_batch`](#/components/schemas/EventBatch) carries the rounds behind it as [`round_end`](#/components/schemas/RoundEnd)
        and [`match_restored`](#/components/schemas/MatchRestored).

        Like [`phase_change`](#/components/schemas/AgentPhaseChange), it is sent once, and
        [`get_server_status`](#/components/schemas/ControllerGetServerStatus) reads the score again.
      tags:
        - name: optional
        - name: socket
      payload:
        $ref: "#/components/schemas/AgentScore"
      examples:
        - payload:
            type: score
            allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
            team1_score: 7
            team2_score: 5
    backup:
      name: backup
      title: backup
      summary: The [`round_backup`](#/components/schemas/RoundBackup) of a match that sends no statistics, which a match with statistics carries in its [`event_batch`](#/components/schemas/EventBatch) instead.
      description: |-
        The [`round_backup`](#/components/schemas/RoundBackup) of a match that sends no statistics, which a match with
        statistics carries in its [`event_batch`](#/components/schemas/EventBatch) instead. Keep the newest of each match
        to restore it on a new server.

        The message wraps the complete event, unchanged, so code that reads a `round_backup` from the
        event stream reads `backup` the same way. The wrapper adds only the server that sent it.

        Like [`phase_change`](#/components/schemas/AgentPhaseChange), it is sent once, so a backup is lost if the plugin
        or the agent is not connected when the round starts. The next round's backup replaces it.
      tags:
        - name: optional
        - name: socket
      payload:
        $ref: "#/components/schemas/AgentBackup"
      examples:
        - payload:
            type: backup
            allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
            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"
                {
                	"round"		"13"
                	…
                }
              roster:
                team1:
                  name: Blue
                  players:
                    - "76561198000000001"
                    - "76561198000000002"
                team2:
                  name: Orange
                  players:
                    - "76561198000000011"
                    - "76561198000000012"
                spectators: []
    reply:
      name: reply
      title: reply
      summary: The answer to a request, in any order.
      description: |-
        The answer to a request, in any order. A success carries the request's data, and a failure its
        reason.
      tags:
        - name: required
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/reply"
      payload:
        $ref: "#/components/schemas/AgentReply"
      examples:
        - payload:
            type: reply
            reply_to: r2
            request: stop_server
            ok: true
            data:
              allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
        - payload:
            type: reply
            reply_to: r9
            request: stop_server
            ok: false
            error:
              code: unknown_allocation
              detail: unknown_allocation
    start_server:
      name: start_server
      title: start_server
      summary: Launch a server on an idle slot the agent picks.
      description: |-
        Launch a server on an idle slot the agent picks. The server logs in with the slot's token. The
        agent reports the slot `starting` before any container work, so two launches never share one.

        The reply comes once the container has started, before the server is up, and
        [`slot_status`](#/components/schemas/AgentSlotStatus) reports it `running` later. The agent pulls no image here.
        A launch on a `cs2_image` whose plugin files the agent has not extracted yet waits while it does.
        Refused with:

        - A [`StartRefusal`](#/components/schemas/StartRefusal) when the node cannot take the launch.
        - `allocation_in_use` when the node is already launching the allocation or its server holds a
          slot.
        - `allocation_reused` when the node launched the allocation before.
        - `failed` when Docker fails the launch, once its container is gone, after a
          [`server_exited`](#/components/schemas/AgentServerExited) with `launch_failed`.
      tags:
        - name: required
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/request"
      payload:
        $ref: "#/components/schemas/ControllerStartServer"
      examples:
        - payload:
            type: start_server
            id: r1
            allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
            launch:
              map_name: de_mirage
              workshop_id: null
              hostname: Ankor | cs.example.com
              steam_web_api_key: null
              game_password: hunter2
              chat_tag:
                text: Ankor
                color: yellow
              game_mode: competitive
              max_players: 2
              gotv:
                password: hunter2
                delay_seconds: 90
              idle_shutdown_seconds: null
              exec: []
              commands:
                - mp_halftime_duration 15
                - mp_team_timeout_max 2
                - mp_overtime_startmoney 12500
              match:
                match_id: 0199a0b4-5c00-7000-8000-00000000abc1
                send_statistics: true
                roster:
                  team1:
                    name: team_alpha
                    players:
                      - "76561198000000001"
                  team2:
                    name: team_bravo
                    players:
                      - "76561198000000002"
                  spectators: []
                sides: knife
                knife_decision_seconds: 60
                ready_mode: manual
                spectators_ready_up: false
                allow_force_ready: false
                ready_timeout_seconds: null
                record_demo: true
                rules:
                  max_rounds: 24
                  overtime: true
                  overtime_max_rounds: 6
                  friendly_fire: true
                restore: null
        - payload:
            type: start_server
            id: r11
            allocation_id: 3f8a2c61-9d4e-4b7a-a1c5-7e2d0b9f6a48
            launch:
              map_name: de_inferno
              workshop_id: null
              hostname: Ankor | cs.example.com
              steam_web_api_key: null
              game_password: hunter2
              chat_tag:
                text: Ankor
                color: yellow
              game_mode: wingman
              max_players: 4
              gotv:
                password: hunter2
                delay_seconds: 90
              idle_shutdown_seconds: null
              exec: []
              commands: []
              match:
                match_id: 0199a0b4-5c00-7000-8000-00000000abc2
                send_statistics: false
                roster:
                  team1:
                    name: team_alpha
                    players:
                      - "76561198000000001"
                      - "76561198000000003"
                  team2:
                    name: team_bravo
                    players:
                      - "76561198000000002"
                      - "76561198000000004"
                  spectators: []
                sides: team1_ct
                knife_decision_seconds: 60
                ready_mode: auto
                spectators_ready_up: false
                allow_force_ready: false
                ready_timeout_seconds: null
                record_demo: true
                rules:
                  max_rounds: 16
                  overtime: false
                  overtime_max_rounds: 6
                  friendly_fire: true
                restore: null
        - payload:
            type: start_server
            id: r8
            allocation_id: c1d2e3f4-5a6b-4c7d-8e9f-0a1b2c3d4e5f
            launch:
              map_name: de_mirage
              workshop_id: null
              hostname: Ankor | cs.example.com
              steam_web_api_key: null
              game_password: hunter2
              chat_tag:
                text: Ankor
                color: yellow
              game_mode: competitive
              max_players: 2
              gotv:
                password: hunter2
                delay_seconds: 90
              idle_shutdown_seconds: null
              exec: []
              commands: []
              match:
                match_id: 0199a0b4-5c00-7000-8000-00000000abc1
                send_statistics: true
                roster:
                  team1:
                    name: team_alpha
                    players:
                      - "76561198000000001"
                  team2:
                    name: team_bravo
                    players:
                      - "76561198000000002"
                  spectators: []
                sides: knife
                knife_decision_seconds: 60
                ready_mode: manual
                spectators_ready_up: false
                allow_force_ready: false
                ready_timeout_seconds: null
                record_demo: true
                rules:
                  max_rounds: 24
                  overtime: true
                  overtime_max_rounds: 6
                  friendly_fire: true
                restore:
                  team1_side: t
                  rules:
                    max_rounds: 24
                    overtime: true
                    overtime_max_rounds: 6
                    friendly_fire: true
                  file: |
                    "SaveFile"
                    {
                    	"timestamp"		"Tue Oct 20 20:14:00 2026"
                    	"round"		"13"
                    }
        - payload:
            type: start_server
            id: r9
            allocation_id: 9c04e1d2-3a7b-4f6e-8d21-5e0b9c7a4f13
            launch:
              workshop_id: "3070987642"
              map_name: null
              hostname: Retakes | cs.example.com
              steam_web_api_key: 0123456789ABCDEF0123456789ABCDEF
              game_password: hunter2
              chat_tag:
                text: Ankor
                color: yellow
              game_mode: competitive
              max_players: 10
              gotv:
                password: null
                delay_seconds: 0
              idle_shutdown_seconds: 180
              exec:
                - node/retakes
              commands:
                - mp_roundtime 2
              match: null
        - payload:
            type: start_server
            id: r3
            allocation_id: e7a3d9c1-6b2f-4e8a-9c5d-1f0b3a7e2d64
            launch:
              hostname: Ankor
              steam_web_api_key: null
              game_password: null
              chat_tag:
                text: Ankor
                color: yellow
              game_mode: competitive
              max_players: 10
              workshop_id: null
              map_name: de_dust2
              gotv:
                password: null
                delay_seconds: 0
              idle_shutdown_seconds: null
              exec: []
              commands: []
              match: null
    start_server_reply:
      name: start_server_reply
      title: start_server_reply
      summary: "The reply to `start_server`: its data on success, or why it failed."
      tags:
        - name: required
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/reply"
      payload:
        $ref: "#/components/schemas/StartServerResult"
      examples:
        - payload:
            type: reply
            reply_to: r1
            request: start_server
            ok: true
            data:
              allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
              slot_id: s1
        - payload:
            type: reply
            reply_to: r1
            request: start_server
            ok: false
            error:
              code: no_free_slot
              detail: no_free_slot
        - payload:
            type: reply
            reply_to: r1
            request: start_server
            ok: false
            error:
              code: allocation_in_use
              detail: allocation 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e already holds a slot
    stop_server:
      name: stop_server
      title: stop_server
      summary: Stop an allocation's server.
      description: |-
        Stop an allocation's server. Once asked, the agent stops it: a stop that Docker fails is answered
        with `failed`, and the agent tries it again until the server is gone. The slot stays held until
        then.

        The reply comes once its container has stopped: up to 10 seconds before the kill, then up
        to 35 seconds seconds for the kill and the reply. A stop during a launch
        of the allocation first waits for the launch's current step to end. That step can take minutes on
        the first launch after an image switch, which extracts the overlay, so the reply can come after
        the session's wait. If the launch is stopped between its steps, it starts no server and its
        `start_server` fails. If it is stopped while Docker starts its container, its `start_server`
        succeeds and the server is then stopped. Refused with `unknown_allocation` for an allocation the
        node does not run.
      tags:
        - name: optional
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/request"
      payload:
        $ref: "#/components/schemas/ControllerStopServer"
      examples:
        - payload:
            type: stop_server
            id: r2
            allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
    stop_server_reply:
      name: stop_server_reply
      title: stop_server_reply
      summary: "The reply to `stop_server`: its data on success, or why it failed."
      tags:
        - name: optional
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/reply"
      payload:
        $ref: "#/components/schemas/StopServerResult"
      examples:
        - payload:
            type: reply
            reply_to: r2
            request: stop_server
            ok: true
            data:
              allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
        - payload:
            type: reply
            reply_to: r2
            request: stop_server
            ok: false
            error:
              code: unknown_allocation
              detail: unknown_allocation
    get_server_status:
      name: get_server_status
      title: get_server_status
      summary: Read a server's state as its plugin reports it now.
      description: |-
        Read a server's state as its plugin reports it now.

        The reply comes within 10 seconds. Refused with:

        - `unknown_allocation` when the node does not run the server.
        - `plugin_offline` while the slot's `plugin_online` is `false` or when the plugin leaves before
          answering.
        - `plugin_status_timeout` after 10 seconds.
      tags:
        - name: optional
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/request"
      payload:
        $ref: "#/components/schemas/ControllerGetServerStatus"
      examples:
        - payload:
            type: get_server_status
            id: r5
            allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
    get_server_status_reply:
      name: get_server_status_reply
      title: get_server_status_reply
      summary: "The reply to `get_server_status`: its data on success, or why it failed."
      tags:
        - name: optional
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/reply"
      payload:
        $ref: "#/components/schemas/GetServerStatusResult"
      examples:
        - payload:
            type: reply
            reply_to: r5
            request: get_server_status
            ok: true
            data:
              allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
              map_name: de_mirage
              match:
                match_id: 0199a0b4-5c00-7000-8000-00000000abc1
                phase: live
                team1_side: t
                team2_side: ct
                team1_score: 7
                team2_score: 6
              players:
                - steam_id: "76561198000000001"
                  name: alpha
                  side: t
              config_error: null
              ready: true
              server_steam_id: "85568392936840254"
        - payload:
            type: reply
            reply_to: r5
            request: get_server_status
            ok: false
            error:
              code: plugin_offline
              detail: plugin_offline
    update_cs2:
      name: update_cs2
      title: update_cs2
      summary: Install or update CS2 on the node's shared volume.
      description: |-
        Install or update CS2 on the node's shared volume. The first install happens only through this
        request.

        The reply comes immediately with the update's id, as [`UpdateCs2Reply`](#/components/schemas/UpdateCs2Reply), and the update runs
        after it, which can take tens of minutes. Refused with `slots_held` while any slot is held and
        `maintenance_running` while a maintenance operation runs.

        `cs2.update` in [`node_status`](#/components/schemas/AgentNodeStatus) reports the update from `outcome: null` to
        how it ended, a failed pull of the cs2 image included. Meanwhile
        [`cs2_update_progress`](#/components/schemas/AgentCs2UpdateProgress) and [`log_batch`](#/components/schemas/AgentLogBatch) report
        on it, each naming it by that id.

        An installed node also updates itself to `cs2.latest_build_id` once every slot is idle, unless
        its `auto_update` is `false`.
      tags:
        - name: required
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/request"
      payload:
        $ref: "#/components/schemas/ControllerUpdateCs2"
      examples:
        - payload:
            type: update_cs2
            id: r3
            mode: update
    update_cs2_reply:
      name: update_cs2_reply
      title: update_cs2_reply
      summary: "The reply to `update_cs2`: its data on success, or why it failed."
      tags:
        - name: required
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/reply"
      payload:
        $ref: "#/components/schemas/UpdateCs2Result"
      examples:
        - payload:
            type: reply
            reply_to: r3
            request: update_cs2
            ok: true
            data:
              update_id: 7c2e9a41-3b5d-4f68-9e0a-1d4c8b7f2a63
        - payload:
            type: reply
            reply_to: r3
            request: update_cs2
            ok: false
            error:
              code: slots_held
              detail: slots_held
    check_cs2_build:
      name: check_cs2_build
      title: check_cs2_build
      summary: Read the newest public CS2 build from Steam now instead of at the next scheduled read, which comes every 30 minutes.
      description: |-
        Read the newest public CS2 build from Steam now instead of at the next scheduled read, which
        comes every 30 minutes.

        It receives no reply. The agent handles at most one per 60 seconds and
        drops any sooner one. One sent during a scheduled read shares it. Neither busy slots nor
        maintenance stop it. A changed build arrives as [`node_status`](#/components/schemas/AgentNodeStatus), and an
        installed node updates to it once every slot is idle, as it always does.
      tags:
        - name: optional
        - name: socket
      payload:
        $ref: "#/components/schemas/ControllerCheckCs2Build"
      examples:
        - payload:
            type: check_cs2_build
    delete_demo:
      name: delete_demo
      title: delete_demo
      summary: Delete one demo by slot and file name.
      description: |-
        Delete one demo by slot and file name.

        The reply comes once the file is removed. Refused with:

        - `unknown_slot` when the node has no directory for the slot.
        - `unknown_demo` when the slot has no demo of that name.
        - `demo_recording` while its server records the demo.
        - `demo_uploading` while the demo uploads.
        - `invalid_request` when the name is outside the slot's `demos/` directory.
        - `failed` when the removal fails.
      tags:
        - name: optional
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/request"
      payload:
        $ref: "#/components/schemas/ControllerDeleteDemo"
      examples:
        - payload:
            type: delete_demo
            id: r8
            slot_id: s1
            file_name: 20260928-201500_0199a0b4-5c00-7000-8000-00000000abc1_de_mirage.dem
    delete_demo_reply:
      name: delete_demo_reply
      title: delete_demo_reply
      summary: "The reply to `delete_demo`: its data on success, or why it failed."
      tags:
        - name: optional
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/reply"
      payload:
        $ref: "#/components/schemas/DeleteDemoResult"
      examples:
        - payload:
            type: reply
            reply_to: r8
            request: delete_demo
            ok: true
            data:
              slot_id: s1
              file_name: 20260928-201500_0199a0b4-5c00-7000-8000-00000000abc1_de_mirage.dem
        - payload:
            type: reply
            reply_to: r8
            request: delete_demo
            ok: false
            error:
              code: demo_uploading
              detail: 20261008-201500_0199a0b4-5c00-7a6e-8d4f-2f1d0c9b8a7e_de_mirage.dem is uploading
    reserve_slot:
      name: reserve_slot
      title: reserve_slot
      summary: Keep an idle slot from launches while the controller replaces its token, so that no server starts on a token that the controller is about to revoke at Steam.
      description: |-
        Keep an idle slot from launches while the controller replaces its token, so that no server
        starts on a token that the controller is about to revoke at Steam.

        The agent marks the slot `reserved` before it replies, the same way a launch marks its slot
        `starting`, so no [`start_server`](#/components/schemas/ControllerStartServer) can take the slot once the reply is sent.
        The reply comes immediately. Refused with `slot_held` for a slot holding a server, and with
        `unknown_slot` for a slot the node does not have.

        The reservation ends with the [`set_config`](#/components/schemas/ControllerSetConfig) that gives the slot a new token,
        with the slot's removal, after 60 seconds, or with the agent's restart.
        Asking again restarts the wait.
      tags:
        - name: optional
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/request"
      payload:
        $ref: "#/components/schemas/ControllerReserveSlot"
      examples:
        - payload:
            type: reserve_slot
            id: r9
            slot_id: s1
    reserve_slot_reply:
      name: reserve_slot_reply
      title: reserve_slot_reply
      summary: "The reply to `reserve_slot`: its data on success, or why it failed."
      tags:
        - name: optional
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/reply"
      payload:
        $ref: "#/components/schemas/ReserveSlotResult"
      examples:
        - payload:
            type: reply
            reply_to: r9
            request: reserve_slot
            ok: true
            data:
              slot_id: s1
        - payload:
            type: reply
            reply_to: r9
            request: reserve_slot
            ok: false
            error:
              code: slot_held
              detail: slot s1 holds a server
    set_config:
      name: set_config
      title: set_config
      summary: Apply an edit to `agent.yml`, or refuse it without saving any part.
      description: |-
        Apply an edit to `agent.yml`, or refuse it without saving any part. Nothing is queued.

        The reply comes once the edit is saved and applied, and carries the node's current settings, the
        [`NodeSettings`](#/components/schemas/NodeSettings) that `hello.config` reports. An edit that changes nothing is not saved
        again. A slot missing from the reply's `slots` has been removed. Refused with:

        - `slot_held` when the edit would remove or re-port a held slot, or give its server's token to
          another slot.
        - `slots_held` when the edit changes any key under `controller` (its address, TLS or paths) while any
          slot is held.
        - `slot_token_required` or `demo_header_value_required` when the edit keeps a value that the
          node does not have.
        - `invalid_request` when two slots have one token or anything else fails validation.

        After the reply, the agent sends a [`slot_status`](#/components/schemas/AgentSlotStatus) for each slot that the
        edit added or gave a new token. The slot reads `unchecked` until the agent has logged the new
        token on to Steam, which the agent starts then unless a server on the node is using the token. A
        change to `controller` closes the socket with `1000` once the reply has gone, and the agent sends its
        [`hello`](#/components/schemas/AgentHello) to the new address.

        Required of a controller that hands nodes an `agent.yml` without slots, because it is how they
        receive any.
      tags:
        - name: optional
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/request"
      payload:
        $ref: "#/components/schemas/ControllerSetConfig"
      examples:
        - payload:
            type: set_config
            id: r4
            config:
              slots:
                - id: s1
                  port: 27015
                  tv_port: 27020
                - id: s2
                  port: 27016
                  tv_port: 27021
                  gslt: Q4T8N2WX7C1RJ5KM9BH3ZD6LP0YF4SG2
              pinned_ip: null
              controller:
                host: cs.example.com
                tls: true
                socket_path: /ws/agent
                events_path: /api/node/events
              auto_update: true
              memory_mb: 6144
              demo_upload:
                url: https://demos.example.com/upload
                header:
                  name: Authorization
                  value: Bearer 9b2e
                limit_mbit: 40
    set_config_reply:
      name: set_config_reply
      title: set_config_reply
      summary: "The reply to `set_config`: its data on success, or why it failed."
      tags:
        - name: optional
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/reply"
      payload:
        $ref: "#/components/schemas/SetConfigResult"
      examples:
        - payload:
            type: reply
            reply_to: r4
            request: set_config
            ok: true
            data:
              slots:
                - id: s1
                  port: 27015
                  tv_port: 27020
                  gslt_hint: 7E8F9A0B1C
                - id: s2
                  port: 27016
                  tv_port: 27021
                  gslt_hint: 6LP0YF4SG2
              pinned_ip: null
              controller:
                host: cs.example.com
                tls: true
                socket_path: /ws/agent
                events_path: /api/node/events
              auto_update: true
              memory_mb: 6144
              demo_upload:
                url: https://demos.example.com/upload
                header:
                  name: Authorization
                  value_sha256: e39ca46d71d4b93e08230ee35a7a64f6e188958014de50dfd8be910631edb679
                limit_mbit: 40
        - payload:
            type: reply
            reply_to: r4
            request: set_config
            ok: false
            error:
              code: slot_held
              detail: "held slots would change: s1"
    update_agent:
      name: update_agent
      title: update_agent
      summary: Replace the agent container with another image.
      description: |-
        Replace the agent container with another image. Running servers keep running.

        The reply comes immediately with what happens next, as [`UpdateAgentReply`](#/components/schemas/UpdateAgentReply), and
        `agent_update` reports the rest: its `step` while it runs, then how it ended. Refused with
        `maintenance_running` while a maintenance operation runs, with `image_missing` for an image the
        node lacks without `pull`, and with `image_not_allowed` for an image outside the official agent
        repository while the node's `official_images_only` is on.

        Switching stops this agent, and the agent that comes up, the new one or the old one put back,
        reports how it went. The update counts as a maintenance operation until it ends, and a switch
        until its replacement has proved it stays up.
      tags:
        - name: optional
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/request"
      payload:
        $ref: "#/components/schemas/ControllerUpdateAgent"
      examples:
        - payload:
            type: update_agent
            id: r5
            image: ghcr.io/ankorcs/ankor-agent:1.2.0
            pull: true
    update_agent_reply:
      name: update_agent_reply
      title: update_agent_reply
      summary: "The reply to `update_agent`: its data on success, or why it failed."
      tags:
        - name: optional
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/reply"
      payload:
        $ref: "#/components/schemas/UpdateAgentResult"
      examples:
        - payload:
            type: reply
            reply_to: r5
            request: update_agent
            ok: true
            data:
              image: ghcr.io/ankorcs/ankor-agent:1.2.0
              step: pulling
        - payload:
            type: reply
            reply_to: r5
            request: update_agent
            ok: true
            data:
              image: ghcr.io/ankorcs/ankor-agent:1.2.0
              step: unchanged
        - payload:
            type: reply
            reply_to: r5
            request: update_agent
            ok: false
            error:
              code: maintenance_running
              detail: maintenance_running
    update_cs2_image:
      name: update_cs2_image
      title: update_cs2_image
      summary: Switch future launches to another cs2 image.
      description: |-
        Switch future launches to another cs2 image. Running servers keep theirs, and so does a launch
        that took its slot before the switch.

        The reply comes immediately with what happens next, as [`UpdateCs2ImageReply`](#/components/schemas/UpdateCs2ImageReply). Refused with
        `maintenance_running` while a maintenance operation runs, with `image_missing` for an image the
        node lacks without `pull`, and with `image_not_allowed` for an image outside the official cs2
        repository while the node's `official_images_only` is on.

        The agent then pulls the image when asked to, extracts its plugin files and removes the images
        it supersedes. `cs2.image_update` in [`node_status`](#/components/schemas/AgentNodeStatus) reports its `step`
        while it runs, then how it ended, in the same [`node_status`](#/components/schemas/AgentNodeStatus) as the new
        `cs2.image`.
      tags:
        - name: optional
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/request"
      payload:
        $ref: "#/components/schemas/ControllerUpdateCs2Image"
      examples:
        - payload:
            type: update_cs2_image
            id: r6
            image: ghcr.io/ankorcs/ankor-cs2:1.2.0
            pull: true
    update_cs2_image_reply:
      name: update_cs2_image_reply
      title: update_cs2_image_reply
      summary: "The reply to `update_cs2_image`: its data on success, or why it failed."
      tags:
        - name: optional
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/reply"
      payload:
        $ref: "#/components/schemas/UpdateCs2ImageResult"
      examples:
        - payload:
            type: reply
            reply_to: r6
            request: update_cs2_image
            ok: true
            data:
              image: ghcr.io/ankorcs/ankor-cs2:1.2.0
              step: pulling
        - payload:
            type: reply
            reply_to: r6
            request: update_cs2_image
            ok: false
            error:
              code: maintenance_running
              detail: maintenance_running
    watch_resources:
      name: watch_resources
      title: watch_resources
      summary: Start or renew the resource lease, which lasts 2 minutes.
      description: |-
        Start or renew the resource lease, which lasts 2 minutes. It receives no reply.

        While the lease runs, the agent streams [`resource_batch`](#/components/schemas/AgentResourceBatch). A lease that
        starts first sends every tick the agent holds.
      tags:
        - name: optional
        - name: socket
      payload:
        $ref: "#/components/schemas/ControllerWatchResources"
      examples:
        - payload:
            type: watch_resources
            replay: false
    watch_logs:
      name: watch_logs
      title: watch_logs
      summary: Start or renew the lease of one log source, which lasts 2 minutes.
      description: |-
        Start or renew the lease of one log source, which lasts 2 minutes. It receives no
        reply.

        While the lease runs, the agent streams the source's [`log_batch`](#/components/schemas/AgentLogBatch) frames.
        While any source's lease runs, the agent also sends [`log_index`](#/components/schemas/AgentLogIndex). A lease
        that starts replays the source, to every watcher unless a `replay_id` names the one that asked.
        Any number of sources can be watched together, each on a lease of its own.
      tags:
        - name: optional
        - name: socket
      payload:
        $ref: "#/components/schemas/ControllerWatchLogs"
      examples:
        - payload:
            type: watch_logs
            source:
              kind: server
              allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
            replay_id: 0d6f1c52-3a7e-4b9d-8c21-5e4f7a9b2c13
        - payload:
            type: watch_logs
            source:
              kind: agent
            replay_id: null
    welcome:
      name: welcome
      title: welcome
      summary: The controller's answer to a [`hello`](#/components/schemas/AgentHello) it accepted, before anything else on the socket.
      description: |-
        The controller's answer to a [`hello`](#/components/schemas/AgentHello) it accepted, before anything else on the
        socket. Without one the agent redials after 15 seconds.

        After each [`welcome`](#/components/schemas/ControllerWelcome) the agent sends what changed since the
        [`hello`](#/components/schemas/AgentHello), its pending [`server_exited`](#/components/schemas/AgentServerExited) first and then
        the changed statuses, demos, backlog and watched streams. It sends each state once, so a slot
        that went from `starting` to `running` arrives as `running`.
      tags:
        - name: required
        - name: socket
      payload:
        $ref: "#/components/schemas/ControllerWelcome"
      examples:
        - payload:
            type: welcome
    hello_refused:
      name: hello_refused
      title: hello_refused
      summary: The controller's answer to a [`hello`](#/components/schemas/AgentHello) it will not accept, followed by a close with code `1008` and reason `hello_refused`.
      description: |-
        The controller's answer to a [`hello`](#/components/schemas/AgentHello) it will not accept, followed by a close
        with code `1008` and reason `hello_refused`. The agent stops dialling until it is restarted,
        because the same [`hello`](#/components/schemas/AgentHello) would be refused again.
      tags:
        - name: optional
        - name: socket
      payload:
        $ref: "#/components/schemas/ControllerHelloRefused"
      examples:
        - payload:
            type: hello_refused
            reason: invalid_hello
            detail: missing host.cpu_count
    console_command:
      name: console_command
      title: console_command
      summary: Run a console command on a server through its plugin.
      description: |-
        Run a console command on a server through its plugin.

        The reply comes once the plugin has handed the command to the engine, which reports nothing
        back, so the reply cannot say that it worked. Refused with:

        - `unknown_allocation` when the node does not run the server.
        - `plugin_offline` while the slot's `plugin_online` is `false`.
        - `plugin_status_timeout` when the plugin does not answer.
        - `command_blocked` when the server runs a match and the line 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`. The plugin then runs none of the line.

        The output of the command joins the server's log, which [`log_batch`](#/components/schemas/AgentLogBatch) streams
        while a [`watch_logs`](#/components/schemas/ControllerWatchLogs) lease covers it and replays when one starts.
      tags:
        - name: optional
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/request"
      payload:
        $ref: "#/components/schemas/ControllerConsoleCommand"
      examples:
        - payload:
            type: console_command
            id: r7
            allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
            command: mp_restartgame 1
    console_command_reply:
      name: console_command_reply
      title: console_command_reply
      summary: "The reply to `console_command`: its data on success, or why it failed."
      tags:
        - name: optional
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/reply"
      payload:
        $ref: "#/components/schemas/ConsoleCommandResult"
      examples:
        - payload:
            type: reply
            reply_to: r7
            request: console_command
            ok: true
            data:
              allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
        - payload:
            type: reply
            reply_to: r7
            request: console_command
            ok: false
            error:
              code: plugin_offline
              detail: plugin_offline
    restore_round:
      name: restore_round
      title: restore_round
      summary: Play a running match on from one of its earlier rounds, on the same server, without a new launch.
      description: |-
        Play a running match on from one of its earlier rounds, on the same server, without a new
        launch. The engine writes a backup as each live round starts, and the node keeps the backups for
        the server's life. This request loads the one for `round`.

        The reply comes once the plugin has asked the engine to load the backup. The
        [`match_restored`](#/components/schemas/MatchRestored) event, which the plugin sends ahead of the restored
        round's [`round_start`](#/components/schemas/RoundStart), confirms that the engine did. Refused with:

        - `unknown_allocation` when the node does not run the server.
        - `no_backup` when the node has no backup of that round, as in a game mode without backups
          ([`GameMode`](#/components/schemas/GameMode)).
        - `plugin_offline` while the slot's `plugin_online` is `false`.
        - `plugin_status_timeout` when the plugin does not answer.
        - `no_match`, `not_live` or `at_halftime` when the plugin refuses the restore.

        The node keeps the backup of every round it has played, including rounds after one it was
        restored to. If `round` is past the current round, the server loads play that the match has
        since left. Whether to allow that is the controller's rule.
      tags:
        - name: optional
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/request"
      payload:
        $ref: "#/components/schemas/ControllerRestoreRound"
      examples:
        - payload:
            type: restore_round
            id: r10
            allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
            round: 14
    restore_round_reply:
      name: restore_round_reply
      title: restore_round_reply
      summary: "The reply to `restore_round`: its data on success, or why it failed."
      tags:
        - name: optional
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/reply"
      payload:
        $ref: "#/components/schemas/RestoreRoundResult"
      examples:
        - payload:
            type: reply
            reply_to: r10
            request: restore_round
            ok: true
            data:
              allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
              round: 14
        - payload:
            type: reply
            reply_to: r10
            request: restore_round
            ok: false
            error:
              code: no_backup
              detail: "no backup of round 14: ankor_round13.txt is not on the node"
    start_match:
      name: start_match
      title: start_match
      summary: Start a match in warmup whatever its `ready_mode`, without waiting for anyone to be ready.
      description: |-
        Start a match in warmup whatever its `ready_mode`, without waiting for anyone to be ready. An
        admin can always start the match this way, and under `external` it is the only way to.

        The reply comes once the plugin has started the countdown to the knife round or live play.
        Refused with:

        - `unknown_allocation` when the node does not run the server.
        - `plugin_offline` while the slot's `plugin_online` is `false`.
        - `plugin_status_timeout` when the plugin does not answer.
        - `no_match`, `not_in_warmup` or `team_empty` when the plugin refuses the start.

        A repeat while the countdown runs is accepted and changes nothing.
      tags:
        - name: optional
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/request"
      payload:
        $ref: "#/components/schemas/ControllerStartMatch"
      examples:
        - payload:
            type: start_match
            id: r11
            allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
    start_match_reply:
      name: start_match_reply
      title: start_match_reply
      summary: "The reply to `start_match`: its data on success, or why it failed."
      tags:
        - name: optional
        - name: socket
      correlationId:
        $ref: "#/components/correlationIds/reply"
      payload:
        $ref: "#/components/schemas/StartMatchResult"
      examples:
        - payload:
            type: reply
            reply_to: r11
            request: start_match
            ok: true
            data:
              allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
        - payload:
            type: reply
            reply_to: r11
            request: start_match
            ok: false
            error:
              code: plugin_offline
              detail: plugin_offline
    event_batch:
      name: event_batch
      title: event_batch
      summary: "The body of an event upload: 1 to 128 frames of the server that `allocation_id` names, with consecutive `seq`."
      description: |-
        The body of an event upload: 1 to 128 frames of the server that
        `allocation_id` names, with consecutive `seq`. Store frames idempotently by `allocation_id` and
        `seq`, because a lost receipt means the same frames arrive again.

        This schema checks only each frame's position, so one bad event cannot fail the batch. Parse each
        frame on its own with [`EventBatchFrame`](#/components/schemas/EventBatchFrame), and quarantine a frame that fails by listing it in
        `rejected` of the [`EventBatchReceipt`](#/components/schemas/EventBatchReceipt).

        A server plays the one match of its launch, so the match of a batch is the match that the controller
        launched `allocation_id` with. The frames come to at most 2 MiB
        together.

        Rejected unless each frame's `seq` is one above the `seq` of the frame before it.
      tags:
        - name: uploads
      bindings:
        http: {}
      payload:
        $ref: "#/components/schemas/EventBatch"
      examples:
        - payload:
            allocation_id: 5b1e2c7a-8d4f-4c3b-9a6e-2f1d0c9b8a7e
            frames:
              - seq: 0
                event:
                  type: map_start
                  timestamp: 1700000014000
                  tick: 0
                  plugin_version: 1.2.0
                  framework_version: 1.1.4
                  server_version: 1.41.0.3
                  map_name: de_mirage
    event_batch_receipt:
      name: event_batch_receipt
      title: event_batch_receipt
      summary: The controller's answer to a batch it holds.
      description: |-
        The controller's answer to a batch it holds. Every frame of the batch is either canonical
        history or quarantined under one of the `rejected` entries, and the agent may delete them all.
        Resending a quarantined frame would fail the same way.
      tags:
        - name: uploads
      bindings:
        http:
          statusCode: 200
      payload:
        $ref: "#/components/schemas/EventBatchReceipt"
      examples:
        - payload:
            rejected: []
    event_batch_rejection:
      name: event_batch_rejection
      title: event_batch_rejection
      summary: The body of a `409` answer to an event upload.
      description: |-
        The body of a `409` answer to an event upload. The controller could not read the identities of
        the batch, so it refuses the whole batch. The agent retries that server with a backoff of its
        own, and the node's other servers upload meanwhile.
      tags:
        - name: uploads
      bindings:
        http:
          statusCode: 409
      payload:
        $ref: "#/components/schemas/EventBatchRejection"
      examples:
        - payload:
            code: event_integrity_error
            detail: seq 12 was already stored with different content
    map_start:
      name: map_start
      title: map_start
      summary: Sent when a map has loaded on the server, as the first event of the map's history.
      description: |-
        Sent when a map has loaded on the server, as the first event of the map's history.

        The plugin sends one on every load. A workshop map loads twice, and an admin's `changelevel`
        loads another map.
      tags:
        - name: match
      payload:
        $ref: "#/components/schemas/MapStart"
      examples:
        - payload:
            type: map_start
            timestamp: 1700000014000
            tick: 0
            plugin_version: 1.2.0
            framework_version: 1.1.4
            server_version: 1.41.0.3
            map_name: de_mirage
    player_connect:
      name: player_connect
      title: player_connect
      summary: Sent when a player connects after the match leaves warmup.
      description: |-
        Sent when a player connects after the match leaves warmup.
        [`get_server_status`](#/components/schemas/ControllerGetServerStatus) lists who is on the server during warmup.
      tags:
        - name: players
      payload:
        $ref: "#/components/schemas/PlayerConnect"
      examples:
        - payload:
            type: player_connect
            timestamp: 1700000014000
            tick: 64
            player:
              steam_id: "76561198000000001"
              name: alpha
              side: spec
              team: 1
            rejected: false
    player_disconnect:
      name: player_disconnect
      title: player_disconnect
      summary: Sent when a player leaves after the match leaves warmup.
      tags:
        - name: players
      payload:
        $ref: "#/components/schemas/PlayerDisconnect"
      examples:
        - payload:
            type: player_disconnect
            timestamp: 1700000900000
            tick: 150000
            player:
              steam_id: "76561198000000002"
              name: bravo
              side: t
              team: 2
            reason: Disconnect
    knife_start:
      name: knife_start
      title: knife_start
      summary: Sent when the knife round begins.
      tags:
        - name: knife
      payload:
        $ref: "#/components/schemas/KnifeStart"
      examples:
        - payload:
            type: knife_start
            timestamp: 1700000030000
            tick: 2048
    knife_end:
      name: knife_end
      title: knife_end
      summary: Sent when the knife round ends.
      description: |-
        Sent when the knife round ends. When both teams survive, the counts in this event decide the
        winner.

        Rejected when a team's health is above 1000 times its players alive.
      tags:
        - name: knife
      payload:
        $ref: "#/components/schemas/KnifeEnd"
      examples:
        - payload:
            type: knife_end
            timestamp: 1700000090000
            tick: 6000
            winner_team: 2
            winner_side: t
            team1_alive: 0
            team2_alive: 2
            team1_health: 0
            team2_health: 145
    side_choice:
      name: side_choice
      title: side_choice
      summary: Sent when the knife round winners choose a side, or run out of time and stay.
      tags:
        - name: knife
      payload:
        $ref: "#/components/schemas/SideChoice"
      examples:
        - payload:
            type: side_choice
            timestamp: 1700000100000
            tick: 6600
            team: 2
            choice: swap
            team1_side: t
    going_live:
      name: going_live
      title: going_live
      summary: Sent when the match goes live, with the round settings that it plays under.
      tags:
        - name: match
      payload:
        $ref: "#/components/schemas/GoingLive"
      examples:
        - payload:
            type: going_live
            timestamp: 1700000014000
            tick: 100000
            team1_side: ct
            max_rounds: 24
            overtime: true
            overtime_max_rounds: 6
            friendly_fire: false
    round_backup:
      name: round_backup
      title: round_backup
      summary: Sent as each live round starts.
      description: |-
        Sent as each live round starts. The newest one of a map, with the launch of that map, holds
        everything that a restore needs on another node.
      tags:
        - name: match
      payload:
        $ref: "#/components/schemas/RoundBackup"
      examples:
        - payload:
            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"
              {
              	"round"		"13"
              	…
              }
            roster:
              team1:
                name: Blue
                players:
                  - "76561198000000001"
                  - "76561198000000002"
              team2:
                name: Orange
                players:
                  - "76561198000000011"
                  - "76561198000000012"
              spectators: []
    match_restored:
      name: match_restored
      title: match_restored
      summary: Sent when the server has loaded a round backup, from the `restore` of its launch, from a [`restore_round`](#/components/schemas/ControllerRestoreRound), or from an `mp_backup_restore_load_file` that someone ran by hand.
      description: |-
        Sent when the server has loaded a round backup, from the `restore` of its launch, from a
        [`restore_round`](#/components/schemas/ControllerRestoreRound), or from an `mp_backup_restore_load_file` that someone
        ran by hand.

        The [`round_start`](#/components/schemas/RoundStart) that follows plays `round` again. From that
        [`round_start`](#/components/schemas/RoundStart) on, the events of the server replace everything that it sent
        for that round and the rounds after it.

        The plugin reads the scores and the side from the engine once the engine has loaded the file. The
        plugin refuses a load during the knife round, the side choice, the halftime break and after the
        match has ended.
      tags:
        - name: match
      payload:
        $ref: "#/components/schemas/MatchRestored"
      examples:
        - payload:
            type: match_restored
            timestamp: 1700001300000
            tick: 190000
            round: 14
            team1_side: t
            team1_score: 7
            team2_score: 6
    round_start:
      name: round_start
      title: round_start
      summary: Sent when a round begins.
      description: |-
        Sent when a round begins.

        The same round can begin again with no [`round_end`](#/components/schemas/RoundEnd) between. The engine restarts
        a round that a team leaves before it is decided, as when every player on it disconnects or loses
        their connection. The later `round_start` replaces the earlier one, and the round is played
        from it.
      tags:
        - name: rounds
      payload:
        $ref: "#/components/schemas/RoundStart"
      examples:
        - payload:
            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
    round_freeze_end:
      name: round_freeze_end
      title: round_freeze_end
      summary: Sent when freeze time ends, as the authoritative list of the players who take part in the round after reconnects during freeze time.
      description: |-
        Sent when freeze time ends, as the authoritative list of the players who take part in the round
        after reconnects during freeze time.
      tags:
        - name: rounds
      payload:
        $ref: "#/components/schemas/RoundFreezeEnd"
      examples:
        - payload:
            type: round_freeze_end
            timestamp: 1700000215000
            tick: 110960
            round: 2
            round_time: 0
            team1_side: ct
            players:
              - player:
                  steam_id: "76561198000000001"
                  name: alpha
                  side: ct
                  team: 1
                alive: true
                equipment_value: 4100
                cash: 0
                start_cash: 2400
                cash_spent: 2400
    round_buy_end:
      name: round_buy_end
      title: round_buy_end
      summary: Sent when buy time closes, `mp_buytime` after freeze time ends, with the equipment value that each roster team carried, as the sum of the engine's figure for each pawn.
      description: |-
        Sent when buy time closes, `mp_buytime` after freeze time ends, with the equipment value that
        each roster team carried, as the sum of the engine's figure for each pawn.

        The plugin reads a player at the last buy-zone exit inside the window, or live at the close while
        the player is still in a buy zone. Snapshots at a death or disconnect keep a buy that was made
        before the window closes. An earlier buy-zone exit counts for a player who never returned.

        Rejected unless `team1_equipment_value` and `team2_equipment_value` are each the sum of
        `equipment_value` over the `players` whose `player.team` is that team.
      tags:
        - name: rounds
      payload:
        $ref: "#/components/schemas/RoundBuyEnd"
      examples:
        - payload:
            type: round_buy_end
            timestamp: 1700000235000
            tick: 112240
            round: 2
            round_time: 20
            players:
              - player:
                  steam_id: "76561198000000001"
                  name: alpha
                  side: ct
                  team: 1
                equipment_value: 4100
                cash: 0
                start_cash: 2400
                cash_spent: 2400
                sampled_tick: 111800
              - player:
                  steam_id: "76561198000000002"
                  name: bravo
                  side: t
                  team: 2
                equipment_value: 3700
                cash: 300
                start_cash: 3400
                cash_spent: 3100
                sampled_tick: 111900
            team1_equipment_value: 4100
            team2_equipment_value: 3700
    round_end:
      name: round_end
      title: round_end
      summary: Sent when a round is decided.
      description: |-
        Sent when a round is decided. The last gameplay reports of the round may still follow, and
        [`round_complete`](#/components/schemas/RoundComplete) closes it.
      tags:
        - name: rounds
      payload:
        $ref: "#/components/schemas/RoundEnd"
      examples:
        - payload:
            type: round_end
            timestamp: 1700000300000
            tick: 116400
            round: 2
            round_time: 85.25
            winner_side: ct
            winner_team: 1
            reason: cts_win
            team1_score: 2
            team2_score: 0
            round_length: 70.25
    round_complete:
      name: round_complete
      title: round_complete
      summary: Sent after every gameplay report of a scored round, its [`round_shots`](#/components/schemas/RoundShots) included, before the next round or the map end.
      description: |-
        Sent after every gameplay report of a scored round, its [`round_shots`](#/components/schemas/RoundShots)
        included, before the next round or the map end.
      tags:
        - name: rounds
      payload:
        $ref: "#/components/schemas/RoundComplete"
      examples:
        - payload:
            type: round_complete
            timestamp: 1700000305000
            tick: 116720
            round: 2
            round_time: 90.25
    player_death:
      name: player_death
      title: player_death
      summary: Sent when a player dies.
      description: |-
        Sent when a player dies. Rejected when the `assister` has the `steam_id` of the `victim` or the
        `attacker`.
      tags:
        - name: combat
      payload:
        $ref: "#/components/schemas/PlayerDeath"
      examples:
        - payload:
            type: player_death
            timestamp: 1700000260000
            tick: 113900
            round: 2
            round_time: 45.5
            victim:
              steam_id: "76561198000000002"
              name: bravo
              side: t
              team: 2
              position:
                x: -1200.5
                y: 350
                z: -160
                pitch: 2.5
                yaw: 180
            attacker:
              steam_id: "76561198000000001"
              name: alpha
              side: ct
              team: 1
              position:
                x: -400
                y: 330.25
                z: -160
                pitch: 1
                yaw: 178.5
            assister: null
            weapon:
              id: 16
              fallback: null
            cause: player
            headshot: true
            penetrated: 0
            thru_smoke: false
            no_scope: false
            attacker_blind: false
            attacker_in_air: false
            flash_assist: false
            friendly_fire: false
            victim_blind_by: null
            victim_blind_remaining: 0
            victim_equipment_value: 3700
            victim_grenades_held:
              - weapon: 43
                quantity: 2
                unit_value: 200
    player_hurt:
      name: player_hurt
      title: player_hurt
      summary: Sent when a player takes damage.
      description: |-
        Sent when a player takes damage. Rejected when `health_before` is a number and `health_after` is
        above it.
      tags:
        - name: combat
      payload:
        $ref: "#/components/schemas/PlayerHurt"
      examples:
        - payload:
            type: player_hurt
            timestamp: 1700000260000
            tick: 113900
            round: 2
            round_time: 45.5
            victim:
              steam_id: "76561198000000002"
              name: bravo
              side: t
              team: 2
              position:
                x: -1200.5
                y: 350
                z: -160
                pitch: 2.5
                yaw: 180
            attacker:
              steam_id: "76561198000000001"
              name: alpha
              side: ct
              team: 1
              position:
                x: -400
                y: 330.25
                z: -160
                pitch: 1
                yaw: 178.5
            weapon:
              id: 16
              fallback: null
            cause: player
            dmg_health: 100
            dmg_armor: 0
            health_before: 100
            health_after: 0
            hitgroup: 1
            friendly_fire: false
            projectile_impact: false
            grenade_id: null
    player_blind:
      name: player_blind
      title: player_blind
      summary: Sent when a flashbang blinds a player.
      tags:
        - name: combat
      payload:
        $ref: "#/components/schemas/PlayerBlind"
      examples:
        - payload:
            type: player_blind
            timestamp: 1700000250000
            tick: 113260
            round: 2
            round_time: 36.1
            victim:
              steam_id: "76561198000000002"
              name: bravo
              side: t
              team: 2
              position:
                x: -1150
                y: 345.5
                z: -160
                pitch: 0
                yaw: -5
            attacker:
              steam_id: "76561198000000001"
              name: alpha
              side: ct
              team: 1
              position:
                x: -380
                y: 300
                z: -160
                pitch: -20
                yaw: 175
            duration: 2.4
            friendly_fire: false
            grenade_id: 3
    grenade_throw:
      name: grenade_throw
      title: grenade_throw
      summary: Sent when a grenade leaves a player's hand.
      description: |-
        Sent when a grenade leaves a player's hand. Its `grenade_id` identifies the grenade in the events
        that follow: its [`grenade_detonate`](#/components/schemas/GrenadeDetonate) and [`grenade_end`](#/components/schemas/GrenadeEnd),
        and the [`player_hurt`](#/components/schemas/PlayerHurt) and [`player_blind`](#/components/schemas/PlayerBlind) that it causes.
      tags:
        - name: combat
      payload:
        $ref: "#/components/schemas/GrenadeThrow"
      examples:
        - payload:
            type: grenade_throw
            timestamp: 1700000248400
            tick: 113200
            round: 2
            round_time: 34.5
            grenade_id: 3
            player:
              steam_id: "76561198000000001"
              name: alpha
              side: ct
              team: 1
              position:
                x: -380
                y: 300
                z: -160
                pitch: -20
                yaw: 175
            grenade: flashbang
    grenade_detonate:
      name: grenade_detonate
      title: grenade_detonate
      summary: Sent when a grenade detonates or a fire grenade lights.
      description: |-
        Sent when a grenade detonates or a fire grenade lights. A grenade that is removed before then
        sends none.
      tags:
        - name: combat
      payload:
        $ref: "#/components/schemas/GrenadeDetonate"
      examples:
        - payload:
            type: grenade_detonate
            timestamp: 1700000250000
            tick: 113260
            round: 2
            round_time: 36.1
            grenade_id: 3
            player:
              steam_id: "76561198000000001"
              name: alpha
              side: ct
              team: 1
            detonate_pos:
              x: -900
              y: 340
              z: -60
    grenade_end:
      name: grenade_end
      title: grenade_end
      summary: Sent when the effect of a grenade is over, because its last entity was removed or the round closed or reset on it.
      description: |-
        Sent when the effect of a grenade is over, because its last entity was removed or the round
        closed or reset on it. Every grenade that sent a [`grenade_throw`](#/components/schemas/GrenadeThrow) sends one
        before the [`round_complete`](#/components/schemas/RoundComplete) of its round. A grenade whose round a restore
        cut off sends none; the [`match_restored`](#/components/schemas/MatchRestored) replaces that round.
      tags:
        - name: combat
      payload:
        $ref: "#/components/schemas/GrenadeEnd"
      examples:
        - payload:
            type: grenade_end
            timestamp: 1700000250000
            tick: 113260
            round: 2
            grenade_id: 3
            player:
              steam_id: "76561198000000001"
              name: alpha
              side: ct
              team: 1
            end_round_time: 36.1
            fire_end: null
            extinguished_by: null
    round_shots:
      name: round_shots
      title: round_shots
      summary: Sent once for each round when it closes, at halftime, the next [`round_start`](#/components/schemas/RoundStart) or the [`map_end`](#/components/schemas/MapEnd), with the shots that each player fired over the round.
      description: |-
        Sent once for each round when it closes, at halftime, the next [`round_start`](#/components/schemas/RoundStart)
        or the [`map_end`](#/components/schemas/MapEnd), with the shots that each player fired over the round.

        A round that starts again with no [`round_end`](#/components/schemas/RoundEnd), as described on
        [`round_start`](#/components/schemas/RoundStart), sends none for the attempt it abandoned. Neither does a round
        that a restore cut off, which the [`match_restored`](#/components/schemas/MatchRestored) replaces.

        `players` lists each player who fired, and is empty when nobody did. The plugin counts shots
        from the engine's `weapon_fire`, up to 4096 for each player, and counts no
        more. Knives and grenades are left out, because they fire no rounds. A throw arrives as
        [`grenade_throw`](#/components/schemas/GrenadeThrow) instead. Hits are the [`player_hurt`](#/components/schemas/PlayerHurt)
        events of the same round, attacker and weapon.

        Rejected when a player's shots add up to more than 4096, or when a player or a
        weapon of a player is listed twice.
      tags:
        - name: rounds
      payload:
        $ref: "#/components/schemas/RoundShots"
      examples:
        - payload:
            type: round_shots
            timestamp: 1700000300000
            tick: 116400
            round: 2
            players:
              - player:
                  steam_id: "76561198000000001"
                  name: alpha
                  side: ct
                  team: 1
                weapons:
                  - weapon:
                      id: 16
                      fallback: null
                    shots: 3
                    ticks:
                      - 113880.25
                      - 113888.5
                      - 113896
    bomb_begin_plant:
      name: bomb_begin_plant
      title: bomb_begin_plant
      summary: Sent when a player begins to plant the bomb.
      tags:
        - name: bomb
      payload:
        $ref: "#/components/schemas/BombBeginPlant"
      examples:
        - payload:
            type: bomb_begin_plant
            timestamp: 1700000270000
            tick: 114540
            round: 3
            round_time: 55.5
            player:
              steam_id: "76561198000000002"
              name: bravo
              side: t
              team: 2
              position:
                x: -2100
                y: 820
                z: -125
                pitch: 35
                yaw: 90
            site: a
    bomb_abort_plant:
      name: bomb_abort_plant
      title: bomb_abort_plant
      summary: Sent when a player abandons a plant.
      tags:
        - name: bomb
      payload:
        $ref: "#/components/schemas/BombAbortPlant"
      examples:
        - payload:
            type: bomb_abort_plant
            timestamp: 1700000271000
            tick: 114604
            round: 3
            round_time: 56.5
            player:
              steam_id: "76561198000000002"
              name: bravo
              side: t
              team: 2
              position:
                x: -2100
                y: 820
                z: -125
                pitch: 10
                yaw: 40
            site: a
    bomb_planted:
      name: bomb_planted
      title: bomb_planted
      summary: Sent when the bomb is planted.
      tags:
        - name: bomb
      payload:
        $ref: "#/components/schemas/BombPlanted"
      examples:
        - payload:
            type: bomb_planted
            timestamp: 1700000276000
            tick: 114924
            round: 3
            round_time: 61.5
            player:
              steam_id: "76561198000000002"
              name: bravo
              side: t
              team: 2
              position:
                x: -2100
                y: 820
                z: -125
                pitch: 35
                yaw: 90
            site: a
    bomb_begin_defuse:
      name: bomb_begin_defuse
      title: bomb_begin_defuse
      summary: Sent when a player begins to defuse the bomb.
      tags:
        - name: bomb
      payload:
        $ref: "#/components/schemas/BombBeginDefuse"
      examples:
        - payload:
            type: bomb_begin_defuse
            timestamp: 1700000290000
            tick: 115820
            round: 3
            round_time: 75.5
            player:
              steam_id: "76561198000000001"
              name: alpha
              side: ct
              team: 1
              position:
                x: -2080
                y: 860
                z: -125
                pitch: 40
                yaw: -90
            has_kit: true
    bomb_abort_defuse:
      name: bomb_abort_defuse
      title: bomb_abort_defuse
      summary: Sent when a player abandons a defuse.
      tags:
        - name: bomb
      payload:
        $ref: "#/components/schemas/BombAbortDefuse"
      examples:
        - payload:
            type: bomb_abort_defuse
            timestamp: 1700000291000
            tick: 115884
            round: 3
            round_time: 76.5
            player:
              steam_id: "76561198000000001"
              name: alpha
              side: ct
              team: 1
              position:
                x: -2080
                y: 860
                z: -125
                pitch: 40
                yaw: -90
    bomb_defused:
      name: bomb_defused
      title: bomb_defused
      summary: Sent when the bomb is defused.
      tags:
        - name: bomb
      payload:
        $ref: "#/components/schemas/BombDefused"
      examples:
        - payload:
            type: bomb_defused
            timestamp: 1700000296000
            tick: 116204
            round: 3
            round_time: 81.5
            player:
              steam_id: "76561198000000001"
              name: alpha
              side: ct
              team: 1
              position:
                x: -2080
                y: 860
                z: -125
                pitch: 40
                yaw: -90
            time_remaining: 20
            has_kit: true
    bomb_exploded:
      name: bomb_exploded
      title: bomb_exploded
      summary: Sent when the bomb explodes.
      tags:
        - name: bomb
      payload:
        $ref: "#/components/schemas/BombExploded"
      examples:
        - payload:
            type: bomb_exploded
            timestamp: 1700000316000
            tick: 117484
            round: 4
            round_time: 101.5
    round_mvp:
      name: round_mvp
      title: round_mvp
      summary: Sent when the engine names the MVP of a round.
      tags:
        - name: rounds
      payload:
        $ref: "#/components/schemas/RoundMvp"
      examples:
        - payload:
            type: round_mvp
            timestamp: 1700000300000
            tick: 116400
            round: 2
            round_time: 85.25
            player:
              steam_id: "76561198000000001"
              name: alpha
              side: ct
              team: 1
            reason: 1
    map_end:
      name: map_end
      title: map_end
      summary: Sent when the map is over.
      description: |-
        Sent when the map is over. After a `played` map, the scoreboard stays up for 60 seconds, then the
        server kicks the players, sends its remaining events and quits. After a `ready_timeout`, the
        server kicks the players at once.

        Rejected unless `winner_team` is the team with the higher score of a `played` map, or null when
        the scores are equal or the map was not played out. Rejected unless `teams` is set for a
        `ready_timeout`, with both scores 0, and null otherwise.
      tags:
        - name: match
      payload:
        $ref: "#/components/schemas/MapEnd"
      examples:
        - payload:
            type: map_end
            timestamp: 1700003614000
            tick: 460000
            reason: played
            winner_team: 1
            team1_score: 13
            team2_score: 9
            teams: null
        - payload:
            type: map_end
            timestamp: 1700000310000
            tick: 19840
            reason: ready_timeout
            winner_team: null
            team1_score: 0
            team2_score: 0
            teams:
              team1:
                connected: 5
                ready: true
              team2:
                connected: 3
                ready: false
              spectators: null
    log:
      name: log
      title: log
      summary: Sent when the plugin logs a line worth keeping with the match.
      tags:
        - name: match
      payload:
        $ref: "#/components/schemas/PluginLog"
      examples:
        - payload:
            type: log
            timestamp: 1700000015000
            tick: 128
            level: warn
            message: force ready of team 1 by 76561198000000001 (alpha)
    plugin_event:
      name: plugin_event
      title: event
      summary: One sequenced event, which the plugin sends and the agent spools without its `type`, then uploads as an [`EventBatchFrame`](#/components/schemas/EventBatchFrame) of an [`event_batch`](#/components/schemas/EventBatch).
      description: |-
        One sequenced event, which the plugin sends and the agent spools without its `type`, then uploads
        as an [`EventBatchFrame`](#/components/schemas/EventBatchFrame) of an [`event_batch`](#/components/schemas/EventBatch). `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`](#/components/schemas/EventAck).
      tags:
        - name: plugin
      payload:
        $ref: "#/components/schemas/PluginEventFrameView"
      examples:
        - payload:
            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
    plugin_server_status:
      name: plugin_server_status
      title: server_status
      summary: A game server's state, which the plugin sends in answer to every [`get_server_status`](#/components/schemas/PluginGetServerStatus).
      description: |-
        A game server's state, which the plugin sends in answer to every
        [`get_server_status`](#/components/schemas/PluginGetServerStatus). The plugin also sends it unasked once when it
        connects and once when it turns `ready`.
      tags:
        - name: plugin
      payload:
        $ref: "#/components/schemas/ServerStatus"
      examples:
        - payload:
            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"
        - payload:
            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"
    plugin_phase_change:
      name: plugin_phase_change
      title: phase_change
      summary: The match moved to another phase, which the plugin sends whether or not the match sends statistics, and the agent forwards as [`phase_change`](#/components/schemas/AgentPhaseChange).
      description: |-
        The match moved to another phase, which the plugin sends whether or not the match sends
        statistics, and the agent forwards as [`phase_change`](#/components/schemas/AgentPhaseChange). Like
        [`server_status`](#/components/schemas/ServerStatus), it is sent once and lost without a connection.
      tags:
        - name: plugin
      payload:
        $ref: "#/components/schemas/PluginPhaseChange"
      examples:
        - payload:
            type: phase_change
            phase: knife
    plugin_score:
      name: plugin_score
      title: score
      summary: The match's score changed, which the plugin sends like [`phase_change`](#/components/schemas/PluginPhaseChange), and the agent forwards as [`score`](#/components/schemas/AgentScore).
      description: |-
        The match's score changed, which the plugin sends like [`phase_change`](#/components/schemas/PluginPhaseChange),
        and the agent forwards as [`score`](#/components/schemas/AgentScore).
      tags:
        - name: plugin
      payload:
        $ref: "#/components/schemas/PluginScore"
      examples:
        - payload:
            type: score
            team1_score: 7
            team2_score: 5
    plugin_backup:
      name: plugin_backup
      title: backup
      summary: The `round_backup` of a match that sends no statistics, which the plugin sends like [`phase_change`](#/components/schemas/PluginPhaseChange), and the agent forwards as [`backup`](#/components/schemas/AgentBackup).
      description: |-
        The `round_backup` of a match that sends no statistics, which the plugin
        sends like [`phase_change`](#/components/schemas/PluginPhaseChange), and the agent forwards as
        [`backup`](#/components/schemas/AgentBackup). `backup` holds the complete event, unchanged, as the
        event stream would carry it.
      tags:
        - name: plugin
      payload:
        $ref: "#/components/schemas/PluginBackup"
      examples:
        - payload:
            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"
                {
                	"round"		"13"
                	…
                }
              roster:
                team1:
                  name: Blue
                  players:
                    - "76561198000000001"
                    - "76561198000000002"
                team2:
                  name: Orange
                  players:
                    - "76561198000000011"
                    - "76561198000000012"
                spectators: []
    plugin_command_result:
      name: plugin_command_result
      title: command_result
      summary: The plugin's answer to a command that carries a `request_id`.
      tags:
        - name: plugin
      payload:
        $ref: "#/components/schemas/CommandResult"
      examples:
        - payload:
            type: command_result
            request_id: 4f0c9d2e-7b1a-4c6e-9f3d-8a2b5e1c7d90
            error: null
        - payload:
            type: command_result
            request_id: 4f0c9d2e-7b1a-4c6e-9f3d-8a2b5e1c7d90
            error: team_empty
    plugin_quitting:
      name: plugin_quitting
      title: quitting
      summary: Tells the agent that the plugin is about to run `quit` after its match, with every event acknowledged and the demo finished.
      description: |-
        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`](#/components/schemas/ServerStatus), it is not resent,
        so one sent without a connection is lost.
      tags:
        - name: plugin
      payload:
        $ref: "#/components/schemas/Quitting"
      examples:
        - payload:
            type: quitting
    plugin_exec:
      name: plugin_exec
      title: exec
      summary: Runs a server console command.
      description: |-
        Runs a server console command. The plugin answers with a [`command_result`](#/components/schemas/CommandResult)
        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.
      tags:
        - name: plugin
      payload:
        $ref: "#/components/schemas/ExecCommand"
      examples:
        - payload:
            type: exec
            command: mp_pause_match
            request_id: c81d4e2a-6f3b-4a97-8e5d-2b9f0a7c3e61
    plugin_restore_round:
      name: plugin_restore_round
      title: restore_round
      summary: Loads the server's backup of a round, as [`restore_round`](#/components/schemas/ControllerRestoreRound) from the controller asks.
      description: |-
        Loads the server's backup of a round, as [`restore_round`](#/components/schemas/ControllerRestoreRound) 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`](#/components/schemas/CommandResult).
      tags:
        - name: plugin
      payload:
        $ref: "#/components/schemas/PluginRestoreRound"
      examples:
        - payload:
            type: restore_round
            round: 14
            request_id: 9a3e6c1f-2d7b-4e85-b0c4-6f1a8d2e3b57
    plugin_start_match:
      name: plugin_start_match
      title: start_match
      summary: Starts a match in warmup whatever its `ready_mode`, as [`start_match`](#/components/schemas/ControllerStartMatch) from the controller asks.
      description: |-
        Starts a match in warmup whatever its `ready_mode`, as [`start_match`](#/components/schemas/ControllerStartMatch) from
        the controller asks. The plugin answers with a [`command_result`](#/components/schemas/CommandResult).
      tags:
        - name: plugin
      payload:
        $ref: "#/components/schemas/PluginStartMatch"
      examples:
        - payload:
            type: start_match
            request_id: 4f0c9d2e-7b1a-4c6e-9f3d-8a2b5e1c7d90
    plugin_get_server_status:
      name: plugin_get_server_status
      title: get_server_status
      summary: Asks the plugin for its server's state, which the plugin answers with a [`server_status`](#/components/schemas/ServerStatus).
      description: |-
        Asks the plugin for its server's state, which the plugin answers with a
        [`server_status`](#/components/schemas/ServerStatus).
      tags:
        - name: plugin
      payload:
        $ref: "#/components/schemas/PluginGetServerStatus"
      examples:
        - payload:
            type: get_server_status
            request_id: s1
    plugin_ack:
      name: plugin_ack
      title: ack
      summary: Acknowledges that every frame with `seq <= to_seq` is stored on the node, so the plugin may discard them.
      description: |-
        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.
      tags:
        - name: plugin
      payload:
        $ref: "#/components/schemas/EventAck"
      examples:
        - payload:
            type: ack
            to_seq: 128
  schemas:
    AgentHello:
      type: object
      properties:
        type:
          type: string
          const: hello
        agent:
          $ref: "#/components/schemas/AgentInfo"
          description: The agent sending this.
        host:
          $ref: "#/components/schemas/HostInfo"
          description: The host the agent runs on.
        public_ip:
          anyOf:
            - $ref: "#/components/schemas/Ipv4"
            - type: "null"
          description: |-
            The node's public IPv4 address as the agent looked it up at start. Null when the lookup failed,
            and players then reach the node only at `config.pinned_ip`. Players connect to
            `config.pinned_ip` whenever it is set. The agent looks again only when it restarts.
        country:
          anyOf:
            - $ref: "#/components/schemas/CountryCode"
            - type: "null"
          description: |-
            Where the node's address is, as a geolocation lookup reports it when the agent starts. Null
            when that lookup failed. The agent tries again only when it restarts.
        config:
          $ref: "#/components/schemas/NodeSettings"
          description: The configuration the agent runs with.
        slot_status:
          maxItems: 64
          type: array
          items:
            $ref: "#/components/schemas/SlotStatus"
          description: One per configured slot.
        cs2:
          $ref: "#/components/schemas/Cs2State"
          description: The game install and image.
        unavailable:
          anyOf:
            - $ref: "#/components/schemas/NodeUnavailable"
            - type: "null"
          description: |-
            Why the node cannot run servers. Null when it can. Changes arrive as
            [`node_status`](#/components/schemas/AgentNodeStatus).
        demos:
          maxItems: 50
          type: array
          items:
            $ref: "#/components/schemas/DemoFile"
          description: |-
            The demos kept on the node, up to 50. [`demo`](#/components/schemas/AgentDemo) reports
            each change afterwards.
        server_exits:
          maxItems: 64
          type: array
          items:
            $ref: "#/components/schemas/ServerExit"
          description: |-
            Servers that ended while no connection was welcomed, oldest first, each with its reason. Their
            slots already show idle here, so a [`server_exited`](#/components/schemas/AgentServerExited) after the
            [`welcome`](#/components/schemas/ControllerWelcome) would come too late. It carries the oldest 64 at
            most, and the rest follow as [`server_exited`](#/components/schemas/AgentServerExited) after the
            [`welcome`](#/components/schemas/ControllerWelcome).
        event_backlog:
          $ref: "#/components/schemas/EventBacklog"
          description: |-
            The node's event backlog when the [`hello`](#/components/schemas/AgentHello) was built.
            [`event_backlog`](#/components/schemas/AgentEventBacklog) replaces it.
        agent_update:
          anyOf:
            - $ref: "#/components/schemas/AgentUpdate"
            - type: "null"
          description: |-
            The swap in progress, or else the one this agent saw end, or else the last that failed. A
            failure is kept, across agent restarts, until a swap holds or `node-setup.sh` starts the agent.
            Null when there is none of these. A swap that held shows in `agent.image`.
      required:
        - type
        - agent
        - host
        - public_ip
        - country
        - config
        - slot_status
        - cs2
        - unavailable
        - demos
        - server_exits
        - event_backlog
        - agent_update
      description: |-
        The agent's first message on every connection, and sent only then. An applied
        [`set_config`](#/components/schemas/ControllerSetConfig) answers with the settings instead.

        Answer an accepted [`hello`](#/components/schemas/AgentHello) with [`welcome`](#/components/schemas/ControllerWelcome). Until then the
        agent sends only replies, or a [`shutdown`](#/components/schemas/AgentShutdown) when it stops. Runtime changes
        use status messages. Reconnecting never changes the node's configuration.

        Rejected unless `slot_status` names each slot of `config.slots` once and names no other slot.
    AgentInfo:
      type: object
      properties:
        version:
          type: string
          minLength: 1
          maxLength: 64
          description: The agent package version.
        image:
          $ref: "#/components/schemas/ImageReference"
          description: The image this agent's container was created from.
        image_id:
          $ref: "#/components/schemas/ImageId"
          description: The id Docker holds for that image. Two builds of one tag differ here.
      required:
        - version
        - image
        - image_id
      description: The running agent.
    ImageReference:
      type: string
      pattern: ^[A-Za-z0-9][A-Za-z0-9._\-/:@]{0,254}$
      description: A Docker image reference, as `cs2_image` and `agent_image` in `agent.yml` hold it.
    ImageId:
      type: string
      pattern: ^sha256:[0-9a-f]{64}$
      description: "The id Docker gives an image: `sha256:` and 64 hex digits."
    HostInfo:
      type: object
      properties:
        cpu_count:
          type: integer
          exclusiveMinimum: 0
          maximum: 9007199254740991
          description: Logical CPUs the agent can use.
        os_name:
          anyOf:
            - type: string
              minLength: 1
              maxLength: 256
            - type: "null"
          description: |-
            The host's os-release `PRETTY_NAME`, up to 256 characters. Null when
            unreadable.
        kernel:
          type: string
          minLength: 1
          maxLength: 256
          description: The kernel release, which the agent's container shares with the host.
      required:
        - cpu_count
        - os_name
        - kernel
      description: The machine the agent runs on.
    Ipv4:
      type: string
      description: |-
        A dotted IPv4 address. Rejected unless it is four numbers from 0 to 255 joined by dots, each
        without leading zeros.
    CountryCode:
      type: string
      pattern: ^[A-Z]{2}$
      description: "An ISO 3166-1 alpha-2 country code: `DK`, `FR`, or `GB` for the United Kingdom."
    NodeSettings:
      type: object
      properties:
        slots:
          $ref: "#/components/schemas/SlotLayout"
          description: The slots servers run in.
        pinned_ip:
          anyOf:
            - $ref: "#/components/schemas/Ipv4"
            - type: "null"
          description: |-
            The address players connect to. Null for the node's `public_ip`. Servers already running keep
            the address their launch announced.
        controller:
          $ref: "#/components/schemas/ControllerAddress"
          description: Where the agent dials the controller. It redials when any part of it changes.
        auto_update:
          type: boolean
          description: |-
            Whether the node keeps itself up to date: CS2 on `cs2.latest_build_id`, and its agent and cs2
            images on the newest release of their major. When false, the node stays on `cs2.build_id` until
            an [`update_cs2`](#/components/schemas/ControllerUpdateCs2), and accepts launches while it is behind
            `cs2.latest_build_id`, so players on a newer client cannot join its servers. Its images then
            change only with [`update_agent`](#/components/schemas/ControllerUpdateAgent) and
            [`update_cs2_image`](#/components/schemas/ControllerUpdateCs2Image).
        demo_upload:
          anyOf:
            - $ref: "#/components/schemas/ReportedDemoUpload"
            - type: "null"
          description: |-
            Where the agent uploads demos. Null for no uploads, which keeps demos on the node. The header's
            value is reported as its SHA-256.
        memory_mb:
          anyOf:
            - $ref: "#/components/schemas/MemoryLimit"
            - type: "null"
          description: |-
            MiB of memory each game container may use. Null for no cap. A change applies to servers launched
            afterwards, and CS2 installs and updates are exempt.

            Rejected when it is not a whole number from 1024 to 65536.
      required:
        - slots
        - pinned_ip
        - controller
        - auto_update
        - demo_upload
        - memory_mb
      description: |-
        The part of `agent.yml` that the controller may edit, as [`hello`](#/components/schemas/AgentHello) reports it
        applied.
    SlotLayout:
      maxItems: 64
      type: array
      items:
        $ref: "#/components/schemas/ReportedSlot"
      description: |-
        Up to 64 slots. Rejected when two slots share an `id`, or when two ports are equal.
        Every slot's `port` and `tv_port` count, so a slot's two ports differ too. It is also rejected
        when a port is 4500, where the agent listens for plugins.
    ReportedSlot:
      type: object
      properties:
        id:
          $ref: "#/components/schemas/SlotId"
          description: The slot's id, 1 to 32 letters, digits, `_` and `-`. It is unique on its node.
        port:
          $ref: "#/components/schemas/Port"
          description: |-
            The game port, UDP. Every `port` and `tv_port` on a node is different, free on the host, and
            not 4500, the port the agent listens on for plugins.
            [Host networking](https://docs.ankor.sh/how-it-works/#host-networking) explains why.
        tv_port:
          $ref: "#/components/schemas/Port"
          description: The GOTV port, UDP, under the same rules as `port`.
        gslt_hint:
          $ref: "#/components/schemas/GsltHint"
          description: The last ten characters of the slot's token.
      required:
        - id
        - port
        - tv_port
        - gslt_hint
      description: A slot as the agent reports it, with the last ten characters of its token in place of the token.
    SlotId:
      type: string
      pattern: ^[A-Za-z0-9_-]{1,32}$
      description: A slot's id, which is also part of the names of its containers.
    Port:
      type: integer
      minimum: 1
      maximum: 65535
      description: A TCP or UDP port.
    GsltHint:
      type: string
      pattern: ^[A-Za-z0-9]{10}$
      description: |-
        The last ten characters of a token, which the agent reports instead of the token. They are enough
        to tell apart every token on one Steam account.
    ControllerAddress:
      type: object
      properties:
        host:
          $ref: "#/components/schemas/ControllerHost"
          description: The controller's host with an optional port.
        tls:
          type: boolean
          description: Whether the agent dials over TLS.
        socket_path:
          $ref: "#/components/schemas/ControllerPath"
          description: |-
            The path of the agent socket. It is `/ws/agent` unless `agent.yml` sets
            `controller.socket_path`.
        events_path:
          $ref: "#/components/schemas/ControllerPath"
          description: |-
            The path event batches are posted to. It is `/api/node/events` unless `agent.yml` sets
            `controller.events_path`.
      required:
        - host
        - tls
        - socket_path
        - events_path
      description: |-
        Where a node dials the controller: a host with an optional port, whether it encrypts, and the
        paths it reaches there.
    ControllerHost:
      type: string
      description: |-
        A host with an optional port and no scheme or path, such as `cs.example.com`,
        `192.168.0.10:3000` or `[fd00::1]`.

        Rejected when it contains whitespace, a control character, `/`, `\`, `?` or `#`. It is also
        rejected unless `http://` followed by the value parses as a URL that has a host, an optional port
        and nothing else, such as a user name.
    ControllerPath:
      type: string
      maxLength: 255
      pattern: ^\/(?:[A-Za-z0-9\-._~!$&'()*+,;=:@/]|%[0-9A-Fa-f]{2})*$
      description: |-
        A path on the controller's host. It starts with `/`, followed by URL path characters and percent
        escapes, without a query or fragment.
    ReportedDemoUpload:
      type: object
      properties:
        url:
          $ref: "#/components/schemas/DemoUploadUrl"
          description: The upload target.
        header:
          anyOf:
            - type: object
              properties:
                name:
                  type: string
                  pattern: ^[A-Za-z0-9-]{1,64}$
                  description: |-
                    The header's name, up to 64 letters, digits and `-`. Rejected when it is, in any case, one of
                    `host`, `content-type`, `content-length`, `transfer-encoding`, `connection`, `keep-alive`, `upgrade`, `expect`, `te`, `trailer`, `ankor-protocol`, `ankor-file-name`, `ankor-match-id` and `ankor-allocation-id`, which the agent sets itself or which change how the request is
                    framed.
                value_sha256:
                  type: string
                  pattern: ^[0-9a-f]{64}$
                  description: |-
                    The lowercase hex SHA-256 of the value's UTF-8 bytes, to compare with the value the controller
                    set.
              required:
                - name
                - value_sha256
            - type: "null"
          description: The header sent with each upload. Null for none.
        limit_mbit:
          anyOf:
            - $ref: "#/components/schemas/DemoUploadLimit"
            - type: "null"
          description: Megabits a second an upload may send while a game server runs on the node. Null for no limit.
      required:
        - url
        - header
        - limit_mbit
      description: |-
        A demo upload as the node reports it, with the header's value replaced by its hash, because the
        value is a credential that the controller set.
    DemoUploadUrl:
      type: string
      maxLength: 255
      format: uri
      description: |-
        The URL finished demos are posted to. A target that needs credentials takes them in the configured
        `header`, such as `Authorization: Basic …`, which the node reports only as a hash.

        Rejected when the URL carries a user name or a password.
    DemoUploadLimit:
      type: integer
      minimum: 1
      maximum: 10000
      description: Whole megabits a second, from 1 to 10 Gbit/s.
    MemoryLimit:
      type: integer
      minimum: 1024
      maximum: 65536
      description: |-
        MiB of memory each game container may use. CS2 installs and updates are exempt.

        Rejected when it is not a whole number from 1024 to 65536.
    SlotStatus:
      type: object
      properties:
        slot_id:
          $ref: "#/components/schemas/SlotId"
          description: The slot this describes.
        state:
          $ref: "#/components/schemas/SlotState"
          description: What the slot is doing.
        allocation_id:
          anyOf:
            - $ref: "#/components/schemas/AllocationId"
            - type: "null"
          description: The server on the slot. Null when the slot is `idle`.
        match_id:
          anyOf:
            - $ref: "#/components/schemas/MatchId"
            - type: "null"
          description: The server's match id. Null for a server without a match or a match launched without one.
        plugin_online:
          type: boolean
          description: |-
            Whether the server's plugin is connected to the agent.
            [`get_server_status`](#/components/schemas/ControllerGetServerStatus), [`console_command`](#/components/schemas/ControllerConsoleCommand),
            [`restore_round`](#/components/schemas/ControllerRestoreRound) and [`start_match`](#/components/schemas/ControllerStartMatch) reach it only
            while it is.
        gslt_state:
          $ref: "#/components/schemas/GsltState"
          description: |-
            Whether Steam accepts the slot's token. [`start_server`](#/components/schemas/ControllerStartServer) never picks a
            slot whose token is `invalid`.
        reserved:
          type: boolean
          description: |-
            Whether the idle slot is kept from launches, while the controller replaces its token with
            [`reserve_slot`](#/components/schemas/ControllerReserveSlot) or while the agent logs its token on to Steam.
        server_steam_id:
          anyOf:
            - $ref: "#/components/schemas/SteamId64"
            - type: "null"
          description: |-
            The `server_steam_id` that the server's plugin reported. The agent sends a
            [`slot_status`](#/components/schemas/AgentSlotStatus) as soon as it learns the ID. Null when the slot is `idle`
            or until the server is `ready`.
      required:
        - slot_id
        - state
        - allocation_id
        - match_id
        - plugin_online
        - gslt_state
        - reserved
        - server_steam_id
      description: |-
        One slot as the agent sees it.

        Rejected when an `idle` slot names an `allocation_id`, a `match_id` or a `server_steam_id`, or
        when a slot in any other state has no `allocation_id`. It is also rejected when a slot that is
        not `idle` is `reserved`.
    SlotState:
      type: string
      enum:
        - idle
        - starting
        - running
        - stopping
      description: What a slot is doing.
      x-enumDescriptions:
        idle: the slot has no server.
        starting: the slot holds a server that its plugin has not yet reported ready for players with a `server_status` with `ready`. A server that a restarted agent took over starts here too.
        running: the plugin has reported the server ready.
        stopping: the server is being stopped.
    AllocationId:
      type: string
      allOf:
        - type: string
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
        - type: string
          pattern: ^[^A-F]*$
      description: |-
        The id the controller picks for each server it launches. It identifies that server and its
        events from then on.

        The id is a lowercase UUID, new for every launch. UUIDv7 is recommended, because its time-ordered
        values keep a database index on it compact as events arrive.
    MatchId:
      type: string
      allOf:
        - type: string
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
        - type: string
          pattern: ^[^A-F]*$
      description: |-
        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.
    GsltState:
      type: string
      enum:
        - ok
        - invalid
        - unchecked
      description: Whether Steam accepts a slot's token.
      x-enumDescriptions:
        ok: a logon with the token succeeded, either the agent's own or that of a server that turned `ready`.
        invalid: Steam refused the token.
        unchecked: the agent has seen neither since the token was set.
    SteamId64:
      type: string
      pattern: ^\d{17}$
      description: A SteamID64 as 17 decimal digits.
    Cs2State:
      type: object
      properties:
        build_id:
          anyOf:
            - $ref: "#/components/schemas/BuildId"
            - type: "null"
          description: The installed game build. Null before the first install.
        latest_build_id:
          anyOf:
            - $ref: "#/components/schemas/BuildId"
            - type: "null"
          description: |-
            The newest public CS2 build the agent read from Steam. Null until its first successful read.
            The agent updates an installed node to it on its own once every slot is idle, unless the
            node's `auto_update` is `false`.
        image:
          $ref: "#/components/schemas/ImageReference"
          description: The cs2 image set in `agent.yml`, which future launches use.
        image_id:
          anyOf:
            - $ref: "#/components/schemas/ImageId"
            - type: "null"
          description: The local id that `image` resolves to on the node. Null until the image is there.
        update:
          anyOf:
            - $ref: "#/components/schemas/Cs2Update"
            - type: "null"
          description: |-
            The install or update that runs now, or else the last one to end since the agent started. Null
            before any. An agent that starts while an update that it started earlier still runs reports
            that update.
        image_update:
          anyOf:
            - $ref: "#/components/schemas/Cs2ImageUpdate"
            - type: "null"
          description: |-
            The switch of `image` that runs now, or else the last one to end since the agent started. Null
            before any.
      required:
        - build_id
        - latest_build_id
        - image
        - image_id
        - update
        - image_update
      description: The node's game install and image.
    BuildId:
      type: string
      pattern: ^\d{1,20}$
      description: A Steam build id, in decimal.
    Cs2Update:
      type: object
      properties:
        id:
          $ref: "#/components/schemas/UpdateId"
          description: Identifies the update and its output, the `cs2_update` [`LogSource`](#/components/schemas/LogSource) with this `update_id`.
        mode:
          $ref: "#/components/schemas/Cs2UpdateMode"
          description: How the update runs.
        automatic:
          type: boolean
          description: |-
            Whether the agent started the update on its own, to reach the newest public build. When false,
            an [`update_cs2`](#/components/schemas/ControllerUpdateCs2) started it.
        outcome:
          anyOf:
            - $ref: "#/components/schemas/Cs2UpdateOutcome"
            - type: "null"
          description: How the update ended. Null while it runs.
        detail:
          anyOf:
            - type: string
              minLength: 1
              maxLength: 512
            - type: "null"
          description: |-
            Why the update failed, up to 512 characters. Null unless it
            failed.
      required:
        - id
        - mode
        - automatic
        - outcome
        - detail
      description: "A CS2 install or update on the node: one running, or the last one to end since the agent started."
    UpdateId:
      type: string
      allOf:
        - type: string
          pattern: ^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$
        - type: string
          pattern: ^[^A-F]*$
      description: Chosen by the agent for each CS2 install or update it runs or finds running. A lowercase UUID.
    Cs2UpdateMode:
      type: string
      enum:
        - update
        - verify
        - reinstall
      description: How thoroughly a CS2 install or update runs.
      x-enumDescriptions:
        update: downloads what changed.
        verify: also re-hashes every file.
        reinstall: wipes the install volume first and downloads all of it.
    Cs2UpdateOutcome:
      type: string
      enum:
        - completed
        - pull_failed
        - overlay_failed
        - failed
        - unobserved
      description: |-
        How a CS2 install or update ended.

        An update container that an earlier agent process started reports progress again until it exits.
        It ends `completed` or `failed` by its exit code, or `unobserved`.
      x-enumDescriptions:
        completed: the game files are installed.
        pull_failed: the cs2 image could not be pulled.
        overlay_failed: its addon overlay could not be extracted.
        failed: any other failure, such as the update container exiting with an error.
        unobserved: an update container that an earlier agent process started exited, and the agent did not see the exit.
    Cs2ImageUpdate:
      type: object
      properties:
        image:
          $ref: "#/components/schemas/ImageReference"
          description: The image the switch is to.
        step:
          anyOf:
            - $ref: "#/components/schemas/ImageUpdateStep"
            - type: "null"
          description: Where the switch is while it runs. Null once it ended.
        outcome:
          anyOf:
            - $ref: "#/components/schemas/Cs2ImageUpdateOutcome"
            - type: "null"
          description: How the switch ended. Null while it runs.
        detail:
          anyOf:
            - type: string
              minLength: 1
              maxLength: 512
            - type: "null"
          description: |-
            Why the switch failed, up to 512 characters. Null unless it
            failed.
      required:
        - image
        - step
        - outcome
        - detail
      description: "A switch of the cs2 image: the one running now, or the last one to end since the agent started."
    ImageUpdateStep:
      type: string
      enum:
        - pulling
        - switching
      description: |-
        Where an [`update_agent`](#/components/schemas/ControllerUpdateAgent) or [`update_cs2_image`](#/components/schemas/ControllerUpdateCs2Image)
        is.
      x-enumDescriptions:
        pulling: the image is being pulled.
        switching: the image is on the node and is put into use. A switch of the agent replaces its container, so the node goes offline and comes back. A switch of the cs2 image extracts its plugin files for future launches.
    Cs2ImageUpdateOutcome:
      type: string
      enum:
        - completed
        - pull_failed
        - overlay_failed
        - failed
      description: How an [`update_cs2_image`](#/components/schemas/ControllerUpdateCs2Image) ended.
      x-enumDescriptions:
        completed: future launches use the image.
        pull_failed: the image could not be pulled.
        overlay_failed: its addon overlay could not be extracted.
        failed: anything else, such as the agent not saving the image setting.
    NodeUnavailable:
      type: object
      properties:
        code:
          $ref: "#/components/schemas/NodeUnavailableCode"
        operation:
          anyOf:
            - $ref: "#/components/schemas/MaintenanceOperation"
            - type: "null"
          description: The maintenance operation that runs when `code` is `maintenance`. Null for any other `code`.
        detail:
          type: string
          minLength: 1
          maxLength: 512
          description: The agent's message, up to 512 characters.
      required:
        - code
        - operation
        - detail
      description: |-
        Why the node cannot run servers now, with `maintenance` first while it lasts. Meanwhile
        [`start_server`](#/components/schemas/ControllerStartServer) is refused with `maintenance_running` for `maintenance` and
        `node_unavailable` otherwise. Everything else works.

        Rejected when `operation` is null while `code` is `maintenance`, or set while `code` is another
        value.
    NodeUnavailableCode:
      type: string
      enum:
        - maintenance
        - event_storage
        - disk_space
      description: Why a node cannot run servers, one of the values of [`NODE_UNAVAILABLE`](#/components/schemas/NodeUnavailableCode).
      x-enumDescriptions:
        maintenance: The [`MaintenanceOperation`](#/components/schemas/MaintenanceOperation) in `operation` runs. It clears when the operation ends.
        event_storage: |-
          The agent cannot use the local store that keeps gameplay events until the controller takes
          them. `detail` says why.
          - If a call to the store waits past its limit, as on a disk that hangs, the code clears once
            the store answers. Deleting the store or restarting the agent loses what waits to be written.
          - If the agent cannot open the store, such as one that a newer agent wrote, a corrupt file or
            one it cannot write, the code lasts until a restart. Update the agent, or delete the store on
            the host and restart the agent. Deleting it loses the events and server exits it keeps.
        disk_space: |-
          The disk under the agent's data directory has less than 1024 MiB free, too
          little for a match's events and demo. The agent asks the disk every 30 seconds and before each
          launch, so the code clears within 30 seconds of space being freed, and a launch that arrives
          sooner is taken if the disk has room by then.
    MaintenanceOperation:
      type: string
      enum:
        - cs2_update
        - agent_update
        - cs2_image_update
        - image_cleanup
      description: The maintenance operation that runs on the node.
      x-enumDescriptions:
        cs2_update: a CS2 install or update, also one an earlier agent started.
        agent_update: an [`update_agent`](#/components/schemas/ControllerUpdateAgent), until its replacement has proved it stays up.
        cs2_image_update: an [`update_cs2_image`](#/components/schemas/ControllerUpdateCs2Image).
        image_cleanup: the agent's removal of replaced images when it starts.
    DemoFile:
      type: object
      properties:
        slot_id:
          $ref: "#/components/schemas/SlotId"
          description: The slot whose directory holds it.
        file_name:
          $ref: "#/components/schemas/DemoFileName"
          description: The file's name.
        size_bytes:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: Bytes in the file.
        modified_at:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: Unix milliseconds when the file was last written.
        recording:
          type: boolean
          description: |-
            Whether its server is still recording it. While it is, the demo grows with each report and is
            neither uploaded nor deleted.
        uploading:
          type: boolean
          description: |-
            Whether it is uploading now. [`demo_progress`](#/components/schemas/AgentDemoProgress) reports how far, and a
            node uploads one demo at a time.
        upload_error:
          anyOf:
            - type: string
              minLength: 1
              maxLength: 512
            - type: "null"
          description: |-
            Why its upload failed or why it waits, up to 512 characters: a
            failure being retried, a refusal as invalid, or every demo held while the target refuses the
            node or has failed for a day. Null once an upload works again or the target changes.
      required:
        - slot_id
        - file_name
        - size_bytes
        - modified_at
        - recording
        - uploading
        - upload_error
      description: A demo kept on the node.
    DemoFileName:
      type: string
      pattern: ^[A-Za-z0-9._-]{1,156}\.dem$
      description: A demo's file name, as it sits in its launch's directory, `demos/<allocation_id>/` in its slot.
    ServerExit:
      type: object
      properties:
        slot_id:
          $ref: "#/components/schemas/SlotId"
          description: The slot it ran in.
        allocation_id:
          $ref: "#/components/schemas/AllocationId"
          description: The server that ended.
        reason:
          $ref: "#/components/schemas/ServerExitReason"
          description: Why it ended.
        exit_code:
          anyOf:
            - type: integer
              minimum: -9007199254740991
              maximum: 9007199254740991
            - type: "null"
          description: The container's exit code. Null when the agent did not see one.
        detail:
          anyOf:
            - type: string
              minLength: 1
              maxLength: 512
            - type: "null"
          description: |-
            What the reason does not say, up to 512 characters: the
            plugin's `config_error` for `config_rejected`, and Docker's message for `launch_failed`. Null
            for every other reason.
      required:
        - slot_id
        - allocation_id
        - reason
        - exit_code
        - detail
      description: A server whose container is gone, and why.
    ServerExitReason:
      type: string
      enum:
        - idle_empty
        - config_rejected
        - start_timeout
        - quit_timeout
        - slot_removed
        - requested
        - clean_exit
        - crashed
        - launch_failed
        - lost
      description: |-
        Why a server ended: one of the [`AgentStopReason`](#/components/schemas/ServerExitReason) values when the agent stopped it on its
        own, or one of these.
      x-enumDescriptions:
        idle_empty: the server stayed empty for its launch's `idle_shutdown_seconds`.
        config_rejected: the plugin refused its match config, as `config_error` says.
        start_timeout: the server was still `starting` 60 seconds after its launch, or after a restarted agent took it over.
        quit_timeout: the server was still running 10 seconds after its plugin sent `quitting`.
        slot_removed: a restarted agent found the server running on a slot that `agent.yml` no longer has.
        requested: a [`stop_server`](#/components/schemas/ControllerStopServer) from the controller stopped it.
        clean_exit: it ended on its own with exit code `0`, as after its match or a console `quit`.
        crashed: it ended on its own with another exit code.
        launch_failed: its container never started.
        lost: it ended and the agent did not see how, such as a container removed by hand.
    EventBacklog:
      type: object
      properties:
        servers:
          maxItems: 100
          type: array
          items:
            $ref: "#/components/schemas/StuckServer"
          description: The servers with the oldest stuck events, oldest first, up to 100.
        server_count:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: Servers with stuck events on the node, those past `servers` too.
        event_count:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: Events that every stuck server holds together.
        reason:
          anyOf:
            - type: string
              minLength: 1
              maxLength: 512
            - type: "null"
          description: |-
            Why the last upload failed, up to 512 characters. Null when
            nothing is stuck, and only then.
      required:
        - servers
        - server_count
        - event_count
        - reason
      description: |-
        The events that wait on the node because uploading them fails, as the agent read them. While
        uploads work, nothing is stuck, so `servers` is empty, the counts are zero and `reason` is
        `null`.

        Rejected unless `servers` holds as many entries as `server_count`, or
        100 when `server_count` is larger. It is also rejected when
        `event_count` is below the sum of `pending` over `servers`. A `reason` is rejected when
        `server_count` is zero, and a null `reason` when it is not.
    StuckServer:
      type: object
      properties:
        allocation_id:
          $ref: "#/components/schemas/AllocationId"
          description: The server whose plugin sent them.
        pending:
          type: integer
          exclusiveMinimum: 0
          maximum: 9007199254740991
          description: Events of the server still queued on the node.
      required:
        - allocation_id
        - pending
      description: A server whose events wait on the node.
    AgentUpdate:
      type: object
      properties:
        image:
          $ref: "#/components/schemas/ImageReference"
          description: The agent image the update was to.
        step:
          anyOf:
            - $ref: "#/components/schemas/ImageUpdateStep"
            - type: "null"
          description: |-
            Where the update is while it runs, with the replacement still on trial in `switching`. Null
            once it ended.
        outcome:
          anyOf:
            - $ref: "#/components/schemas/AgentUpdateOutcome"
            - type: "null"
          description: How the update ended. Null while it runs.
        detail:
          anyOf:
            - type: string
              minLength: 1
              maxLength: 512
            - type: "null"
          description: |-
            Why the update failed, up to 512 characters. Null unless it
            failed.
      required:
        - image
        - step
        - outcome
        - detail
      description: "An agent update: the one in progress, one this agent saw end, or the last one to fail."
    AgentUpdateOutcome:
      type: string
      enum:
        - completed
        - unchanged
        - pull_failed
        - failed
      description: How an [`update_agent`](#/components/schemas/ControllerUpdateAgent) ended.
      x-enumDescriptions:
        completed: the replacement agent stayed up and the old container is removed.
        unchanged: the node already ran that image, so nothing was swapped.
        pull_failed: the image could not be pulled, so the node never tried it.
        failed: the swap did not hold and the old agent was put back, or its helper did not start.
    AgentNodeStatus:
      type: object
      properties:
        type:
          type: string
          const: node_status
        cs2:
          $ref: "#/components/schemas/Cs2State"
          description: The game install and image.
        unavailable:
          anyOf:
            - $ref: "#/components/schemas/NodeUnavailable"
            - type: "null"
          description: Why the node cannot run servers. Null when it can.
        agent_update:
          anyOf:
            - $ref: "#/components/schemas/AgentUpdate"
            - type: "null"
          description: The last agent swap, as [`hello`](#/components/schemas/AgentHello) has it.
      required:
        - type
        - cs2
        - unavailable
        - agent_update
      description: |-
        Replaces `cs2`, `unavailable` and `agent_update` of the last [`hello`](#/components/schemas/AgentHello).

        Sent when they change:

        - When a maintenance operation starts or ends, so also when a CS2 install or update finishes,
          whoever started it, or [`update_cs2_image`](#/components/schemas/ControllerUpdateCs2Image) has switched the image.
        - When `agent_update` or `cs2.image_update` moves from `pulling` to `switching`.
        - When the newest public build changes.
        - When an agent swap that is still proving its replacement ends.

        Each frame is the node's state when it was read, not a step in a history, so a change that is
        over before its report reads the state may go unreported.
    AgentShutdown:
      type: object
      properties:
        type:
          type: string
          const: shutdown
        signal:
          type: string
          enum:
            - SIGINT
            - SIGTERM
            - SIGQUIT
          description: The signal that stopped the agent.
      required:
        - type
        - signal
      description: |-
        The agent is stopping cleanly and starts no new work. It answers a request that arrives after
        this frame with `shutting_down` at once. Replies to the requests it is running may still follow
        for up to 5 seconds, and then it closes with code `1000`.

        Send no new requests after it. A request without a reply when the socket closes may or may not
        have run; the node's reports after it reconnects say which. Its servers keep running, and the
        agent takes them over when it returns. It is sent whether or not the controller has welcomed the
        [`hello`](#/components/schemas/AgentHello).
    AgentSlotStatus:
      type: object
      properties:
        type:
          type: string
          const: slot_status
        slot_id:
          $ref: "#/components/schemas/SlotId"
          description: The slot this describes.
        state:
          $ref: "#/components/schemas/SlotState"
          description: What the slot is doing.
        allocation_id:
          anyOf:
            - $ref: "#/components/schemas/AllocationId"
            - type: "null"
          description: The server on the slot. Null when the slot is `idle`.
        match_id:
          anyOf:
            - $ref: "#/components/schemas/MatchId"
            - type: "null"
          description: The server's match id. Null for a server without a match or a match launched without one.
        plugin_online:
          type: boolean
          description: |-
            Whether the server's plugin is connected to the agent.
            [`get_server_status`](#/components/schemas/ControllerGetServerStatus), [`console_command`](#/components/schemas/ControllerConsoleCommand),
            [`restore_round`](#/components/schemas/ControllerRestoreRound) and [`start_match`](#/components/schemas/ControllerStartMatch) reach it only
            while it is.
        gslt_state:
          $ref: "#/components/schemas/GsltState"
          description: |-
            Whether Steam accepts the slot's token. [`start_server`](#/components/schemas/ControllerStartServer) never picks a
            slot whose token is `invalid`.
        reserved:
          type: boolean
          description: |-
            Whether the idle slot is kept from launches, while the controller replaces its token with
            [`reserve_slot`](#/components/schemas/ControllerReserveSlot) or while the agent logs its token on to Steam.
        server_steam_id:
          anyOf:
            - $ref: "#/components/schemas/SteamId64"
            - type: "null"
          description: |-
            The `server_steam_id` that the server's plugin reported. The agent sends a
            [`slot_status`](#/components/schemas/AgentSlotStatus) as soon as it learns the ID. Null when the slot is `idle`
            or until the server is `ready`.
      required:
        - type
        - slot_id
        - state
        - allocation_id
        - match_id
        - plugin_online
        - gslt_state
        - reserved
        - server_steam_id
      description: |-
        A slot's status changed since the last [`hello`](#/components/schemas/AgentHello) or
        [`slot_status`](#/components/schemas/AgentSlotStatus) for it. Rejected when it breaks a rule of
        [`SlotStatus`](#/components/schemas/SlotStatus).
    AgentServerExited:
      type: object
      properties:
        slot_id:
          $ref: "#/components/schemas/SlotId"
          description: The slot it ran in.
        allocation_id:
          $ref: "#/components/schemas/AllocationId"
          description: The server that ended.
        reason:
          $ref: "#/components/schemas/ServerExitReason"
          description: Why it ended.
        exit_code:
          anyOf:
            - type: integer
              minimum: -9007199254740991
              maximum: 9007199254740991
            - type: "null"
          description: The container's exit code. Null when the agent did not see one.
        detail:
          anyOf:
            - type: string
              minLength: 1
              maxLength: 512
            - type: "null"
          description: |-
            What the reason does not say, up to 512 characters: the
            plugin's `config_error` for `config_rejected`, and Docker's message for `launch_failed`. Null
            for every other reason.
        type:
          type: string
          const: server_exited
      required:
        - slot_id
        - allocation_id
        - reason
        - exit_code
        - detail
        - type
      description: |-
        A server has ended and its container is gone. Sent ahead of the `idle`
        [`slot_status`](#/components/schemas/AgentSlotStatus) of its slot, also for a launch that failed before or while
        its container started.

        A server that ended while no connection was welcomed goes in `server_exits` of the next
        [`hello`](#/components/schemas/AgentHello) instead. If it ended after that [`hello`](#/components/schemas/AgentHello) went out,
        it is sent after the [`welcome`](#/components/schemas/ControllerWelcome), ahead of the slot statuses.

        The agent keeps a [`server_exited`](#/components/schemas/AgentServerExited) until the socket takes its write,
        across its restarts, for up to 7 days, so one may also come again after
        a restart. One the socket took is not sent again, even if the connection drops before the
        controller reads it. The agent does not keep them while it cannot open that store, which it reports as
        `unavailable` with `event_storage`. While the store does not answer, the agent keeps one only
        once the store answers again. A server that ends while the agent is not running is missing
        from its next [`hello`](#/components/schemas/AgentHello).
    AgentLogBatch:
      type: object
      properties:
        type:
          type: string
          const: log_batch
        source:
          $ref: "#/components/schemas/LogSource"
          description: The log these lines belong to.
        replay_id:
          anyOf:
            - $ref: "#/components/schemas/ReplayId"
            - type: "null"
          description: |-
            The [`watch_logs`](#/components/schemas/ControllerWatchLogs) replay these frames answer, for that watcher alone. Null
            for new lines and for a replay sent to every watcher.
        reset:
          type: boolean
          description: Whether a replay starts, so the controller drops every line it holds of this source first.
        lines:
          type: array
          items:
            $ref: "#/components/schemas/LogLine"
          description: The lines, oldest first.
      required:
        - type
        - source
        - replay_id
        - reset
        - lines
      description: |-
        One source's lines, sent only while a [`watch_logs`](#/components/schemas/ControllerWatchLogs) holds the lease of that
        source. A replay of the source starts with `reset` and may span several frames. Between replays,
        new lines follow in batches. A replay larger than the remaining log budget of the agent leaves
        out the oldest lines.
    LogSource:
      oneOf:
        - type: object
          properties:
            kind:
              type: string
              const: agent
          required:
            - kind
        - type: object
          properties:
            kind:
              type: string
              const: cs2_update
            update_id:
              $ref: "#/components/schemas/UpdateId"
              description: The install or update that printed it, the `id` of its `cs2.update`.
          required:
            - kind
            - update_id
        - type: object
          properties:
            kind:
              type: string
              const: server
            allocation_id:
              $ref: "#/components/schemas/AllocationId"
              description: The server that printed it.
          required:
            - kind
            - allocation_id
      description: "One log of a node: the agent's log, one CS2 install or update's output, or one server's output."
    ReplayId:
      type: string
      minLength: 1
      maxLength: 64
      description: Identifies one watcher's replay, so the frames answering it reach only that watcher.
    LogLine:
      type: object
      properties:
        at:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: Unix milliseconds when the agent logged or read the line.
        level:
          anyOf:
            - $ref: "#/components/schemas/AgentLogLevel"
            - type: "null"
          description: How serious the agent rated the line. Null for server and CS2 update output.
        line:
          type: string
          maxLength: 2000
          description: |-
            The line without its newline. The agent's lines carry the prefix of the component that logged
            them. A line keeps its ANSI colour escapes: SGR, `ESC[` with digits and `;`, then `m`. The agent
            removes every other escape, every control character but tab, and the Unicode characters that
            reorder text or break a line. The length limit may cut the last escape short.
      required:
        - at
        - level
        - line
      description: One log line.
    AgentLogLevel:
      type: string
      enum:
        - info
        - warn
        - error
      description: How serious an agent log line is.
    AgentLogIndex:
      type: object
      properties:
        type:
          type: string
          const: log_index
        sources:
          maxItems: 77
          type: array
          items:
            $ref: "#/components/schemas/LogSourceSummary"
          description: |-
            The agent's log first, then CS2 updates in the order they started, then servers in the order
            their output began.
      required:
        - type
        - sources
      description: |-
        Every source the agent keeps, whole every time: its log, each CS2 install or update running or
        among the last 1 ended, and each server with output, running or among the
        last 10 ended.

        Sent while any log lease runs, with each lease that starts and each replay asked for. Also sent
        when an update starts or ends, a server's first line arrives, a server ends, or a source's output
        is dropped, and every 30 seconds while lines arrive. Counts and `last_line_at`
        are as of that frame. A source missing from it is no longer kept.
    LogSourceSummary:
      type: object
      properties:
        source:
          $ref: "#/components/schemas/LogSource"
          description: Which log.
        line_count:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: Lines kept of it.
        last_line_at:
          anyOf:
            - type: integer
              minimum: 0
              maximum: 9007199254740991
            - type: "null"
          description: Unix milliseconds of the newest line kept of it. Null for a source with no lines kept.
        ended:
          type: boolean
          description: |-
            Whether the source has ended, as a server whose slot went idle or a CS2 install or update that
            ended. Always `false` for the agent's log.
      required:
        - source
        - line_count
        - last_line_at
        - ended
      description: One source the agent keeps.
    AgentCs2UpdateProgress:
      type: object
      properties:
        type:
          type: string
          const: cs2_update_progress
        update_id:
          $ref: "#/components/schemas/UpdateId"
          description: |-
            The update it reports on, as `cs2.update.id`. A frame can arrive before the
            [`node_status`](#/components/schemas/AgentNodeStatus) that names the update.
        percent:
          anyOf:
            - type: number
              minimum: 0
              maximum: 100
            - type: "null"
          description: Percent done, as `steamcmd` reports it. Null when `steamcmd` reports none.
        message:
          type: string
          maxLength: 512
          description: What the install is doing, up to 512 characters.
      required:
        - type
        - update_id
        - percent
        - message
      description: |-
        Progress of a CS2 install or update while it runs. `cs2.update` in
        [`node_status`](#/components/schemas/AgentNodeStatus) says how it ended.
    AgentDemo:
      type: object
      properties:
        slot_id:
          $ref: "#/components/schemas/SlotId"
          description: The slot whose directory holds it.
        file_name:
          $ref: "#/components/schemas/DemoFileName"
          description: The file's name.
        size_bytes:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: Bytes in the file.
        modified_at:
          type: integer
          minimum: -9007199254740991
          maximum: 9007199254740991
          description: Unix milliseconds when the file was last written.
        recording:
          type: boolean
          description: |-
            Whether its server is still recording it. While it is, the demo grows with each report and is
            neither uploaded nor deleted.
        uploading:
          type: boolean
          description: |-
            Whether it is uploading now. [`demo_progress`](#/components/schemas/AgentDemoProgress) reports how far, and a
            node uploads one demo at a time.
        upload_error:
          anyOf:
            - type: string
              minLength: 1
              maxLength: 512
            - type: "null"
          description: |-
            Why its upload failed or why it waits, up to 512 characters: a
            failure being retried, a refusal as invalid, or every demo held while the target refuses the
            node or has failed for a day. Null once an upload works again or the target changes.
        type:
          type: string
          const: demo
      required:
        - slot_id
        - file_name
        - size_bytes
        - modified_at
        - recording
        - uploading
        - upload_error
        - type
      description: |-
        One demo as it is now, sent when it appears or changes: its file, its upload starting or
        stopping, or its `upload_error`. It replaces the demo with the same `slot_id` and `file_name`, or
        adds it. [`hello`](#/components/schemas/AgentHello) lists the newest 50. A demo that
        appears after the [`hello`](#/components/schemas/AgentHello) is added, however many the node keeps.
    AgentDemoRemoved:
      type: object
      properties:
        type:
          type: string
          const: demo_removed
        slot_id:
          $ref: "#/components/schemas/SlotId"
          description: The slot whose directory held it.
        file_name:
          $ref: "#/components/schemas/DemoFileName"
          description: The file's name.
        reason:
          $ref: "#/components/schemas/DemoRemovedReason"
          description: Why it is no longer on the node.
      required:
        - type
        - slot_id
        - file_name
        - reason
      description: A demo that the controller knows of is no longer on the node.
    DemoRemovedReason:
      type: string
      enum:
        - uploaded
        - deleted
      description: Why a demo is no longer on the node.
      x-enumDescriptions:
        uploaded: the target took it and the agent removed it.
        deleted: "[`delete_demo`](#/components/schemas/ControllerDeleteDemo) or anything else removed it."
    AgentDemoProgress:
      type: object
      properties:
        type:
          type: string
          const: demo_progress
        slot_id:
          $ref: "#/components/schemas/SlotId"
          description: The slot whose directory holds it.
        file_name:
          $ref: "#/components/schemas/DemoFileName"
          description: The file's name.
        sent_bytes:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: Bytes sent so far.
        bytes_per_second:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: |-
            Bytes a second since the previous [`demo_progress`](#/components/schemas/AgentDemoProgress) of this upload, or
            since it began.
      required:
        - type
        - slot_id
        - file_name
        - sent_bytes
        - bytes_per_second
      description: How far the uploading demo is, at most once a second while it uploads.
    AgentResourceBatch:
      type: object
      properties:
        type:
          type: string
          const: resource_batch
        ticks:
          minItems: 1
          maxItems: 100
          type: array
          items:
            $ref: "#/components/schemas/ResourceTick"
          description: |-
            The ticks, oldest first: every tick the agent holds, up to the last
            100, or the ticks since the previous frame.
        reset:
          type: boolean
          description: |-
            Whether `ticks` is every tick the agent holds and replaces the ticks the controller holds. It
            is `true` when a [`watch_resources`](#/components/schemas/ControllerWatchResources) starts the lease or asks for a
            replay, and on the first tick after each accepted [`hello`](#/components/schemas/AgentHello) while the lease
            runs. When false, `ticks` adds to them.
      required:
        - type
        - ticks
        - reset
      description: |-
        The agent's resource ticks, oldest first. A [`watch_resources`](#/components/schemas/ControllerWatchResources) lease
        that starts receives every tick the agent holds, then each new tick while it runs. The agent
        sends nothing while nobody watches, and nothing is stored.
    ResourceTick:
      type: object
      properties:
        at:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: Unix milliseconds when the tick was taken.
        disk:
          anyOf:
            - $ref: "#/components/schemas/ResourceUsage"
            - type: "null"
          description: |-
            The filesystem holding the node's data directory, as `df` counts it. Null when it could not be
            read.
        memory:
          $ref: "#/components/schemas/ResourceUsage"
          description: The memory the node may use, and how much it uses. Cache the kernel can free counts as free.
        cpu:
          anyOf:
            - type: object
              properties:
                max:
                  type: integer
                  minimum: 0
                  maximum: 100
                  description: Percent busy at the busiest moment of the tick.
                average:
                  type: integer
                  minimum: 0
                  maximum: 100
                  description: Percent busy on average over the tick.
              required:
                - max
                - average
            - type: "null"
          description: How busy the node's CPUs are, in percent of all the CPUs it may use. Null in the first tick.
      required:
        - at
        - disk
        - memory
        - cpu
      description: One sample of the node's disk, memory and CPU, taken every 3 seconds.
    ResourceUsage:
      type: object
      properties:
        cap:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: Bytes of capacity.
        used:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: Bytes in use.
      required:
        - cap
        - used
      description: A capacity and how much of it is in use, in bytes.
    AgentEventBacklog:
      type: object
      properties:
        type:
          type: string
          const: event_backlog
        servers:
          maxItems: 100
          type: array
          items:
            $ref: "#/components/schemas/StuckServer"
          description: The servers with the oldest stuck events, oldest first, up to 100.
        server_count:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: Servers with stuck events on the node, those past `servers` too.
        event_count:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: Events that every stuck server holds together.
        reason:
          anyOf:
            - type: string
              minLength: 1
              maxLength: 512
            - type: "null"
          description: |-
            Why the last upload failed, up to 512 characters. Null when
            nothing is stuck, and only then.
      required:
        - type
        - servers
        - server_count
        - event_count
        - reason
      description: |-
        The node's [`EventBacklog`](#/components/schemas/EventBacklog) now, replacing the backlog of the last [`hello`](#/components/schemas/AgentHello)
        or [`event_backlog`](#/components/schemas/AgentEventBacklog). Sent when it changes: after each failed upload, as
        its counts grow, and once, with nothing stuck, when uploads work again. Rejected when it breaks a
        rule of [`EventBacklog`](#/components/schemas/EventBacklog).
    AgentPhaseChange:
      type: object
      properties:
        type:
          type: string
          const: phase_change
        allocation_id:
          $ref: "#/components/schemas/AllocationId"
          description: The server whose match changed phase.
        phase:
          $ref: "#/components/schemas/MatchPhase"
          description: The phase the match is in now.
      required:
        - type
        - allocation_id
        - phase
      description: |-
        A match moved to another phase, as its plugin reports it, whether or not the match sends
        statistics. An [`event_batch`](#/components/schemas/EventBatch) carries the events behind it, such as
        [`going_live`](#/components/schemas/GoingLive) and [`map_end`](#/components/schemas/MapEnd).

        The agent sends it once. If the plugin is not connected to the agent, or the controller has not
        welcomed the agent's [`hello`](#/components/schemas/AgentHello), it is lost, and
        [`get_server_status`](#/components/schemas/ControllerGetServerStatus) reads the phase again.
    MatchPhase:
      type: string
      enum:
        - loading
        - warmup
        - pending_restore
        - knife
        - side_choice
        - live
        - post_live
      description: Where a match is.
      x-enumDescriptions:
        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.
    AgentScore:
      type: object
      properties:
        type:
          type: string
          const: score
        allocation_id:
          $ref: "#/components/schemas/AllocationId"
          description: The server whose match scored.
        team1_score:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: Team 1's score.
        team2_score:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: Team 2's score.
      required:
        - type
        - allocation_id
        - team1_score
        - team2_score
      description: |-
        A match's score changed, as its plugin reads it off the scoreboard, whether or not the match
        sends statistics: after a round, when going live resets it, or after a restore. An
        [`event_batch`](#/components/schemas/EventBatch) carries the rounds behind it as [`round_end`](#/components/schemas/RoundEnd)
        and [`match_restored`](#/components/schemas/MatchRestored).

        Like [`phase_change`](#/components/schemas/AgentPhaseChange), it is sent once, and
        [`get_server_status`](#/components/schemas/ControllerGetServerStatus) reads the score again.
    AgentBackup:
      type: object
      properties:
        type:
          type: string
          const: backup
        allocation_id:
          $ref: "#/components/schemas/AllocationId"
          description: The server whose match the backup is of.
        backup:
          $ref: "#/components/schemas/RoundBackup"
          description: The [`round_backup`](#/components/schemas/RoundBackup) event, as the event stream would carry it.
      required:
        - type
        - allocation_id
        - backup
      description: |-
        The [`round_backup`](#/components/schemas/RoundBackup) of a match that sends no statistics, which a match with
        statistics carries in its [`event_batch`](#/components/schemas/EventBatch) instead. Keep the newest of each match
        to restore it on a new server.

        The message wraps the complete event, unchanged, so code that reads a `round_backup` from the
        event stream reads `backup` the same way. The wrapper adds only the server that sent it.

        Like [`phase_change`](#/components/schemas/AgentPhaseChange), it is sent once, so a backup is lost if the plugin
        or the agent is not connected when the round starts. The next round's backup replaces it.
    RoundBackup:
      type: object
      properties:
        type:
          type: string
          const: round_backup
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        team1_side:
          $ref: "#/components/schemas/PlayingSide"
          description: |-
            Team 1's side in that round. On a launch, the plugin seats players by it through warmup, before
            the load.
        rules:
          $ref: "#/components/schemas/MatchRules"
          description: |-
            The rules that the map was played under. A restore plays on under these rules instead of the
            rules of its launch.
        file:
          type: string
          minLength: 1
          maxLength: 524288
          pattern: ^[\t\n\r\P{Cc}]*$
          description: |-
            The backup file as the engine wrote it, up to 524288 characters. The
            file holds the money and equipment of every player, keyed by account, and only
            `mp_backup_restore_load_file` reads it.

            The file is text with tabs and line breaks, so a file at the length cap stays within the frame
            limits once JSON escapes it. Rejected when it holds a lone UTF-16 surrogate.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The one-based round that the backup starts. The file numbers rounds from zero, one lower.
        team1_score:
          type: integer
          minimum: 0
          maximum: 1000
          description: Team 1's score before that round.
        team2_score:
          type: integer
          minimum: 0
          maximum: 1000
          description: Team 2's score before that round.
        roster:
          $ref: "#/components/schemas/Roster"
          description: The teams and spectators that the map is played with.
      required:
        - type
        - timestamp
        - tick
        - team1_side
        - rules
        - file
        - round
        - team1_score
        - team2_score
        - roster
      description: |-
        Sent as each live round starts. The newest one of a map, with the launch of that map, holds
        everything that a restore needs on another node.
    PlayingSide:
      type: string
      enum:
        - t
        - ct
      description: A playing side, for a field that never holds `spec`.
      x-enumDescriptions:
        t: terrorists.
        ct: counter-terrorists.
    MatchRules:
      type: object
      properties:
        max_rounds:
          type: integer
          exclusiveMinimum: 0
          maximum: 240
          description: |-
            Rounds in regulation, as `mp_maxrounds`, up to 240. MR12 is 24 and MR15 is
            30.
        overtime:
          type: boolean
          description: Whether a tie after regulation goes to overtime, as `mp_overtime_enable`.
        overtime_max_rounds:
          type: integer
          exclusiveMinimum: 0
          maximum: 60
          description: |-
            Rounds in one overtime, both halves together, as `mp_overtime_maxrounds`. The bound is 60, ten
            times the stock 6.
        friendly_fire:
          type: boolean
          description: Whether bullets and melee hurt teammates. Grenades always do.
      required:
        - max_rounds
        - overtime
        - overtime_max_rounds
        - friendly_fire
      description: |-
        The rules of a match, as a launch asks for them and as [`going_live`](#/components/schemas/GoingLive) and
        [`round_backup`](#/components/schemas/RoundBackup) report the server playing them.

        The plugin reads the round settings off the server's cvars. `friendly_fire` comes from the
        launch, and Ankor applies it through the damage reductions instead of `mp_friendlyfire`.
    RoundNumber:
      type: integer
      exclusiveMinimum: 0
      maximum: 1000
      description: A one-based round number. A map's history holds far fewer than the 1000 it allows.
    Roster:
      type: object
      properties:
        team1:
          $ref: "#/components/schemas/RosterTeam"
          description: Team 1, which events number `1`.
        team2:
          $ref: "#/components/schemas/RosterTeam"
          description: Team 2, which events number `2`.
        spectators:
          maxItems: 2
          type: array
          items:
            $ref: "#/components/schemas/SteamId64"
          description: |-
            Players allowed in as spectators, who watch with the game's free camera, up to
            2. When `ready_mode` is `manual`, they ready up too.
      required:
        - team1
        - team2
        - spectators
      description: |-
        The teams and spectators of a match. Each player holds one place on the server, whichever team or
        list the player is in.

        Rejected when a SteamID appears twice across both teams and `spectators`, or when they hold more
        than 64 together.
    RosterTeam:
      type: object
      properties:
        name:
          type: string
          maxLength: 30
          pattern: \S
          description: |-
            The team name that players see, up to 30 characters. Rejected when it
            is blank, or when it holds a NUL or a lone UTF-16 surrogate.
        players:
          minItems: 1
          type: array
          items:
            $ref: "#/components/schemas/SteamId64"
          description: |-
            The players of the team, captain first. The captain may use `!forceready` and picks the side
            after the knife round. A team has at least one player, so it always has a captain.
      required:
        - name
        - players
      description: One team of a [`Roster`](#/components/schemas/Roster).
    AgentReply:
      oneOf:
        - $ref: "#/components/schemas/AgentReplySuccess"
        - $ref: "#/components/schemas/AgentReplyFailure"
      description: |-
        The answer to a request, in any order. A success carries the request's data, and a failure its
        reason.
    AgentReplySuccess:
      oneOf:
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: start_server
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/StartServerReply"
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: stop_server
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/StopServerReply"
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: get_server_status
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/GetServerStatusReply"
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: update_cs2
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/UpdateCs2Reply"
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: delete_demo
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/DeleteDemoReply"
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: reserve_slot
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/ReserveSlotReply"
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: set_config
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/NodeSettings"
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: update_agent
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/UpdateAgentReply"
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: update_cs2_image
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/UpdateCs2ImageReply"
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: console_command
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/ConsoleCommandReply"
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: restore_round
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/RestoreRoundReply"
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: start_match
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/StartMatchReply"
          required:
            - type
            - reply_to
            - request
            - ok
            - data
      description: |-
        A request that succeeded. `request` names the request, so `data` can be read without the request
        that `reply_to` answers. The settings in a [`set_config`](#/components/schemas/ControllerSetConfig) reply are the
        node's, whichever edit asked.
    RequestId:
      type: string
      minLength: 1
      maxLength: 64
      description: |-
        Chosen by the controller. The reply repeats it as `reply_to`. While a request is running, the
        agent ignores another with the same id on that connection, because a reply could not tell the two
        apart.
    StartServerReply:
      type: object
      properties:
        allocation_id:
          $ref: "#/components/schemas/AllocationId"
          description: The allocation the request launched.
        slot_id:
          $ref: "#/components/schemas/SlotId"
          description: The slot the server runs in.
      required:
        - allocation_id
        - slot_id
      description: The server that started, and the slot the agent picked for it.
    StopServerReply:
      type: object
      properties:
        allocation_id:
          $ref: "#/components/schemas/AllocationId"
          description: The allocation the request stopped.
      required:
        - allocation_id
      description: The server that stopped.
    GetServerStatusReply:
      type: object
      properties:
        map_name:
          type: string
          maxLength: 64
          description: The map the server is on, up to 64 characters.
        match:
          anyOf:
            - type: object
              properties:
                match_id:
                  anyOf:
                    - $ref: "#/components/schemas/MatchId"
                    - type: "null"
                  description: |-
                    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.
                phase:
                  $ref: "#/components/schemas/MatchPhase"
                  description: Where the match is.
                team1_side:
                  $ref: "#/components/schemas/PlayingSide"
                  description: Team 1's current side.
                team2_side:
                  $ref: "#/components/schemas/PlayingSide"
                  description: |-
                    Team 2's current side, always the other side, sent so a reader shows each team without
                    deriving it.
                team1_score:
                  type: integer
                  minimum: 0
                  maximum: 9007199254740991
                  description: Team 1's score.
                team2_score:
                  type: integer
                  minimum: 0
                  maximum: 9007199254740991
                  description: Team 2's score.
              required:
                - match_id
                - phase
                - team1_side
                - team2_side
                - team1_score
                - team2_score
            - type: "null"
          description: |-
            The loaded match and its team state. Null on a server without a match or after a refused
            config.
        players:
          maxItems: 64
          type: array
          items:
            type: object
            properties:
              steam_id:
                $ref: "#/components/schemas/SteamId64"
                description: The player's SteamID64.
              name:
                $ref: "#/components/schemas/PlayerName"
                description: The player's name.
              side:
                $ref: "#/components/schemas/Side"
                description: The player's side.
            required:
              - steam_id
              - name
              - side
          description: Everyone connected, up to 64.
        config_error:
          anyOf:
            - type: string
              maxLength: 512
            - type: "null"
          description: |-
            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.
        ready:
          type: boolean
          description: |-
            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_id:
          anyOf:
            - $ref: "#/components/schemas/SteamId64"
            - type: "null"
          description: |-
            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`.
        allocation_id:
          $ref: "#/components/schemas/AllocationId"
          description: The allocation the request asked about.
      required:
        - map_name
        - match
        - players
        - config_error
        - ready
        - server_steam_id
        - allocation_id
      description: The server asked about, and its state as its plugin reports it now.
    PlayerName:
      type: string
      maxLength: 64
      pattern: ^[^\p{Cc}]*$
      description: |-
        A player's name, cut to up to 64 characters, without control
        characters. Rejected when it holds a lone UTF-16 surrogate.
    Side:
      type: string
      enum:
        - t
        - ct
        - spec
      description: The side a player is on.
      x-enumDescriptions:
        t: terrorists, a playing side.
        ct: counter-terrorists, a playing side.
        spec: spectators and players on no team.
    UpdateCs2Reply:
      type: object
      properties:
        update_id:
          $ref: "#/components/schemas/UpdateId"
          description: |-
            The update's id, as `cs2.update.id`, [`cs2_update_progress`](#/components/schemas/AgentCs2UpdateProgress) and
            its log source carry it.
      required:
        - update_id
      description: The update an accepted [`update_cs2`](#/components/schemas/ControllerUpdateCs2) started.
    DeleteDemoReply:
      type: object
      properties:
        slot_id:
          $ref: "#/components/schemas/SlotId"
          description: The slot whose directory held it.
        file_name:
          $ref: "#/components/schemas/DemoFileName"
          description: The file removed.
      required:
        - slot_id
        - file_name
      description: The demo that was removed.
    ReserveSlotReply:
      type: object
      properties:
        slot_id:
          $ref: "#/components/schemas/SlotId"
          description: The slot reserved.
      required:
        - slot_id
      description: The slot that is reserved.
    UpdateAgentReply:
      type: object
      properties:
        image:
          $ref: "#/components/schemas/ImageReference"
          description: The agent image the update is to.
        step:
          type: string
          enum:
            - pulling
            - switching
            - unchanged
          description: The next step.
          x-enumDescriptions:
            pulling: the image is being pulled.
            switching: the image is on the node and is put into use. A switch of the agent replaces its container, so the node goes offline and comes back. A switch of the cs2 image extracts its plugin files for future launches.
      required:
        - image
        - step
      description: |-
        What follows an accepted [`update_agent`](#/components/schemas/ControllerUpdateAgent): the update's first
        [`ImageUpdateStep`](#/components/schemas/ImageUpdateStep), or `unchanged` when the node runs that image already, as the registry or
        the local image says, and nothing more happens.
    UpdateCs2ImageReply:
      type: object
      properties:
        image:
          $ref: "#/components/schemas/ImageReference"
          description: The cs2 image the switch is to.
        step:
          $ref: "#/components/schemas/ImageUpdateStep"
          description: The next step.
      required:
        - image
        - step
      description: |-
        What follows an accepted [`update_cs2_image`](#/components/schemas/ControllerUpdateCs2Image): its first
        [`ImageUpdateStep`](#/components/schemas/ImageUpdateStep).
    ConsoleCommandReply:
      type: object
      properties:
        allocation_id:
          $ref: "#/components/schemas/AllocationId"
          description: The allocation the command went to.
      required:
        - allocation_id
      description: The server the command was handed to.
    RestoreRoundReply:
      type: object
      properties:
        allocation_id:
          $ref: "#/components/schemas/AllocationId"
          description: The allocation the restore went to.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The round it loads.
      required:
        - allocation_id
        - round
      description: The server and round the restore was handed to.
    StartMatchReply:
      type: object
      properties:
        allocation_id:
          $ref: "#/components/schemas/AllocationId"
          description: The allocation the start went to.
      required:
        - allocation_id
      description: The server the start was handed to.
    AgentReplyFailure:
      type: object
      properties:
        type:
          type: string
          const: reply
        reply_to:
          $ref: "#/components/schemas/RequestId"
          description: The `id` of the request this answers.
        request:
          anyOf:
            - $ref: "#/components/schemas/RequestType"
            - type: "null"
          description: The `type` of the request this answers. Null when the agent did not know it.
        ok:
          type: boolean
          const: false
          description: The request failed.
        error:
          $ref: "#/components/schemas/AgentError"
          description: Why it failed.
      required:
        - type
        - reply_to
        - request
        - ok
        - error
      description: A request that failed.
    RequestType:
      type: string
      enum:
        - start_server
        - stop_server
        - get_server_status
        - update_cs2
        - delete_demo
        - reserve_slot
        - set_config
        - update_agent
        - update_cs2_image
        - console_command
        - restore_round
        - start_match
      description: |-
        The `type` of each request the controller sends, which its [`reply`](#/components/schemas/AgentReply) repeats as
        `request`.
    AgentError:
      type: object
      properties:
        code:
          $ref: "#/components/schemas/AgentErrorCode"
          description: What went wrong. Branch on it.
        detail:
          type: string
          maxLength: 512
          description: |-
            The rest of the reason, such as Docker's message, up to 512
            characters.
      required:
        - code
        - detail
      description: Why a request failed. Branch on `code`. `detail` is for people.
    AgentErrorCode:
      type: string
      enum:
        - no_free_slot
        - cs2_outdated
        - cs2_not_installed
        - node_unavailable
        - slots_held
        - slot_held
        - unknown_slot
        - slot_token_required
        - demo_header_value_required
        - unknown_allocation
        - maintenance_running
        - image_missing
        - image_not_allowed
        - demo_uploading
        - unknown_demo
        - demo_recording
        - plugin_offline
        - plugin_status_timeout
        - no_match
        - not_in_warmup
        - team_empty
        - not_live
        - at_halftime
        - no_backup
        - command_blocked
        - invalid_request
        - allocation_in_use
        - allocation_reused
        - unsupported_request
        - shutting_down
        - failed
      description: The `code` of a failed reply, one of the values of [`AGENT_ERRORS`](#/components/schemas/AgentErrorCode).
      x-enumDescriptions:
        no_free_slot: No slot is idle, unreserved and on a token Steam has not refused.
        cs2_outdated: |-
          A newer public CS2 build is out and the node has not updated to it yet, so its `cs2.build_id`
          is behind `cs2.latest_build_id`. The node takes launches again once it has updated. A node
          whose `auto_update` is `false` never refuses with this.
        cs2_not_installed: |-
          CS2 is not installed on the node, so its `cs2.build_id` is `null`. The node takes launches once
          an [`update_cs2`](#/components/schemas/ControllerUpdateCs2) has installed it.
        node_unavailable: The node reports `unavailable` for a reason other than `maintenance`.
        slots_held: |-
          An operation that needs every slot idle found one held: [`update_cs2`](#/components/schemas/ControllerUpdateCs2), or a
          [`set_config`](#/components/schemas/ControllerSetConfig) that changes any key under `controller`.
        slot_held: |-
          A slot holds a server that is `starting`, `running` or `stopping`, and
          [`set_config`](#/components/schemas/ControllerSetConfig) would remove or re-port the slot or give its server's token
          to another slot, or [`reserve_slot`](#/components/schemas/ControllerReserveSlot) asked for it. `detail` names the
          slots.
        unknown_slot: The node has no slot with that id.
        slot_token_required: |-
          [`set_config`](#/components/schemas/ControllerSetConfig) kept the token of a slot the node does not have yet. A new
          slot needs a new token.
        demo_header_value_required: |-
          [`set_config`](#/components/schemas/ControllerSetConfig) kept the value of a demo upload header the node does not
          send. A new header needs its value.
        unknown_allocation: No server with that allocation id.
        maintenance_running: |-
          A maintenance operation runs on the node. That operation is unaffected, and `unavailable` shows
          it as `maintenance`.
        image_missing: |-
          The image is not on the node and the request asked for no pull. Asking again with `pull`
          fetches it.
        image_not_allowed: |-
          The image is not an official one, and the node's `official_images_only` is on. The node's
          operator decides which images it runs.
        demo_uploading: The demo is uploading. It can be deleted once the upload ends.
        unknown_demo: The slot has no demo of that name, such as one an upload or a delete has already removed.
        demo_recording: The demo is still being recorded. It can be deleted once its server ends the recording.
        plugin_offline: The server's plugin is not connected to the agent, or left before it answered.
        plugin_status_timeout: |-
          The plugin did not answer [`get_server_status`](#/components/schemas/ControllerGetServerStatus),
          [`console_command`](#/components/schemas/ControllerConsoleCommand), [`start_match`](#/components/schemas/ControllerStartMatch) or
          [`restore_round`](#/components/schemas/ControllerRestoreRound) within 10 seconds.
        no_match: The server runs without a match.
        not_in_warmup: The match is past its warmup.
        team_empty: A team has no player connected.
        not_live: The match is not live, such as in warmup, during the knife round or after it ended.
        at_halftime: |-
          The match is in its halftime break, while the sides are about to swap. Ask again once the next
          round has started.
        no_backup: |-
          The node has no backup of the round that [`restore_round`](#/components/schemas/ControllerRestoreRound) named for that
          server.
        command_blocked: |-
          [`console_command`](#/components/schemas/ControllerConsoleCommand) named 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 the server runs a match. A new launch restarts a match,
          and [`restore_round`](#/components/schemas/ControllerRestoreRound) restores one.
        invalid_request: The request did not match its schema.
        allocation_in_use: |-
          [`start_server`](#/components/schemas/ControllerStartServer) named an allocation this node is already launching or
          running. Its server exists or is on its way, as after a [`start_server`](#/components/schemas/ControllerStartServer)
          sent again when its reply was slow.
        allocation_reused: |-
          [`start_server`](#/components/schemas/ControllerStartServer) named an allocation that this node launched before, among
          the last 100. The node files a server's events and demos under its
          allocation, so every launch needs a new one.
        unsupported_request: The agent does not know the request's `type`, or the plugin the command's.
        shutting_down: |-
          The agent is stopping and started no work for the request, as after its
          [`shutdown`](#/components/schemas/AgentShutdown). Send the request again once the node reconnects.
        failed: Any failure that no other code names, such as a Docker failure.
    ControllerStartServer:
      type: object
      properties:
        type:
          type: string
          const: start_server
        id:
          $ref: "#/components/schemas/RequestId"
          description: |-
            Chosen by the controller and unique among its open requests. The reply repeats it as
            `reply_to`. The agent ignores a request whose id is still running on the connection.
        allocation_id:
          $ref: "#/components/schemas/AllocationId"
          description: |-
            Identifies the server from now on. Every launch needs a new one, a relaunch too, because the
            node files the server's events and demos under it. A launch that a [`StartRefusal`](#/components/schemas/StartRefusal) turned
            away used nothing, so the same id may be sent again. Any other failure uses it up.
        launch:
          $ref: "#/components/schemas/ServerLaunchConfig"
          description: What to launch.
      required:
        - type
        - id
        - allocation_id
        - launch
      description: |-
        Launch a server on an idle slot the agent picks. The server logs in with the slot's token. The
        agent reports the slot `starting` before any container work, so two launches never share one.

        The reply comes once the container has started, before the server is up, and
        [`slot_status`](#/components/schemas/AgentSlotStatus) reports it `running` later. The agent pulls no image here.
        A launch on a `cs2_image` whose plugin files the agent has not extracted yet waits while it does.
        Refused with:

        - A [`StartRefusal`](#/components/schemas/StartRefusal) when the node cannot take the launch.
        - `allocation_in_use` when the node is already launching the allocation or its server holds a
          slot.
        - `allocation_reused` when the node launched the allocation before.
        - `failed` when Docker fails the launch, once its container is gone, after a
          [`server_exited`](#/components/schemas/AgentServerExited) with `launch_failed`.
    ServerLaunchConfig:
      type: object
      properties:
        hostname:
          default: null
          type:
            - string
            - "null"
          description: |-
            The server's name, which players see in game, such as on the scoreboard. It is up to 128
            characters. Null or empty leaves the game's own name, and null is the value when absent.
            Rejected when it holds an ASCII control character, such as a line break.
        steam_web_api_key:
          default: null
          anyOf:
            - anyOf:
                - $ref: "#/components/schemas/SteamWebApiKey"
                - type: string
                  const: ""
            - type: "null"
          description: |-
            The Steam Web API key that workshop downloads need. Null or empty skips them, and null is the
            value when absent. **Whoever runs the node can read the key.**
        game_password:
          default: null
          anyOf:
            - $ref: "#/components/schemas/GamePassword"
            - type: "null"
          description: The password players type to join. Null or empty for none, and null when absent.
        chat_tag:
          default: {}
          allOf:
            - $ref: "#/components/schemas/ChatTag"
          description: The tag in front of the plugin's chat lines. When absent, each of its fields takes its default.
        game_mode:
          default: competitive
          allOf:
            - $ref: "#/components/schemas/GameMode"
          description: The game mode, which sets the game's rules. When absent, `competitive`.
        max_players:
          type: integer
          minimum: 1
          maximum: 64
          description: |-
            The player capacity of the server, as `-maxplayers`. A match's roster, players and spectators
            together, must fit.
        workshop_id:
          default: null
          anyOf:
            - $ref: "#/components/schemas/WorkshopId"
            - type: "null"
          description: A workshop map to play. Null for a stock map, and when absent. A match plays a stock map.
        map_name:
          anyOf:
            - $ref: "#/components/schemas/StockMapName"
            - type: "null"
          description: |-
            The stock map to play, required without `workshop_id`. With a `workshop_id`, the server boots
            this map before it switches to the workshop map. Null for `de_dust2` in that case.
        gotv:
          default: null
          anyOf:
            - type: object
              properties:
                password:
                  default: null
                  anyOf:
                    - $ref: "#/components/schemas/GamePassword"
                    - type: "null"
                  description: |-
                    The GOTV password, as `tv_password`, apart from `game_password`. Null or empty for none, and
                    null when absent.
                delay_seconds:
                  type: integer
                  minimum: 0
                  maximum: 9007199254740991
                  description: Seconds GOTV runs behind the game.
              required:
                - delay_seconds
            - type: "null"
          description: |-
            GOTV that spectators can watch. Null for none, and null when absent. A match that
            records a demo still runs GOTV then, because demos are recorded through it. That GOTV runs with
            `tv_delay 0` and a random `tv_password` that the agent sends nowhere, so no spectator can join.

            Whether GOTV runs is set on the command line, because GOTV starts with the map. The other
            settings apply on every map load and when a match goes live, never while a demo records.
        idle_shutdown_seconds:
          anyOf:
            - type: integer
              exclusiveMinimum: 0
              maximum: 9007199254740991
            - type: "null"
          description: |-
            Seconds the server may stay empty once its slot is `running` before the agent stops it,
            reporting `idle_empty` as its [`ServerExitReason`](#/components/schemas/ServerExitReason). Null for a server that runs until
            stopped.
            Hibernation must stay off. See [When the cfgs run](https://docs.ankor.sh/protocol/#when-the-cfgs-run).
        exec:
          maxItems: 16
          type: array
          items:
            $ref: "#/components/schemas/CfgName"
          description: |-
            Cfgs the server runs in order, after the game mode's cfg and the match's `rules`. A server
            without a match runs them 1 second after every map load, and a match server runs them at warmup
            and when the match goes live. Warmup and the knife round run Ankor's cfgs over them.
        commands:
          maxItems: 128
          type: array
          items:
            type: string
          description: |-
            Console lines that the server runs after `exec`, in order and at the same moments, so they
            override `exec` and `rules`. A match server leaves out 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`. Rejected when a line holds an ASCII control character, such as
            a line break, which would end the console line.
        match:
          default: null
          anyOf:
            - $ref: "#/components/schemas/MatchConfig"
            - type: "null"
          description: |-
            The match Ankor's plugin plays. Null runs a plain CS2 server, and is the value when absent. The
            plugin then runs no match, and the map, `exec`, `commands` and the node's plugins set the game.
      required:
        - max_players
        - map_name
        - idle_shutdown_seconds
        - exec
        - commands
      description: |-
        What a game server is launched with: any CS2 server, and the match Ankor's plugin plays on it, if
        any.

        Rejected when both `workshop_id` and `map_name` are null. With a `match`, it is also rejected:

        - when `workshop_id` is set, because a match plays a stock map.
        - when the roster's players and spectators together outnumber `max_players`.
        - when `game_mode` is not `casual`, `competitive` or `wingman`.
        - when `ready_mode` is `external` and `ready_timeout_seconds` is set.
    SteamWebApiKey:
      type: string
      pattern: ^[A-Za-z0-9]{1,64}$
      description: A Steam Web API key, which the server needs to download workshop maps.
    GamePassword:
      type: string
      maxLength: 128
      pattern: ^[!#-:<-~]*$
      description: |-
        The password players type. It is printable ASCII without spaces, `"` or `;`, because the server
        would read those as separators.
    ChatTag:
      type: object
      properties:
        text:
          default: Ankor
          type: string
          minLength: 1
          maxLength: 24
          pattern: ^[^[\]\p{Cc}]+$
          description: |-
            The tag's text, without brackets or control characters. Brackets would end the tag early in
            SwiftlyS2's colour markup. When absent, `Ankor`.
        color:
          default: blue
          allOf:
            - $ref: "#/components/schemas/ChatColor"
          description: The tag's colour. When absent, `blue`.
      description: The bracketed tag in front of every plugin chat line.
    ChatColor:
      type: string
      enum:
        - white
        - darkred
        - lightpurple
        - green
        - olive
        - lime
        - red
        - gray
        - lightyellow
        - yellow
        - silver
        - bluegrey
        - lightblue
        - blue
        - darkblue
        - purple
        - magenta
        - lightred
        - gold
        - orange
      description: |-
        A chat colour name from
        [SwiftlyS2's chat styling](https://swiftlys2.net/docs/guides/chat-and-html-styling).
    GameMode:
      type: string
      enum:
        - casual
        - competitive
        - wingman
        - rush
        - arms_race
        - demolition
        - deathmatch
        - custom
      description: |-
        The game mode a server runs, which sets the game's rules. The engine runs the mode's cfg, such
        as `gamemode_competitive` or `gamemode_competitive2v2` for `wingman`, on every map load, and the
        plugin runs it again when a match goes live.

        `custom` is Valve's mode for community content. Its cfg sets little, so the launch's `exec` and
        `commands` set the game.

        A match plays only in `casual`, `competitive` or `wingman`. A server without a match runs in any
        mode.
    WorkshopId:
      type: string
      pattern: ^\d{1,20}$
      description: A Steam Workshop map's id.
    StockMapName:
      type: string
      pattern: ^(?!\d+$)[A-Za-z0-9_-]{1,64}$
      description: The name of a stock map, such as `de_mirage`. A name of all digits would be a workshop id.
    CfgName:
      type: string
      maxLength: 128
      pattern: ^[A-Za-z0-9_-]+(\/[A-Za-z0-9_-]+)*$
      description: |-
        A cfg the server runs, relative to `game/csgo/cfg/` and without `.cfg`, such as `node/<name>`
        from the node's cfgs.
    MatchConfig:
      type: object
      properties:
        match_id:
          default: null
          anyOf:
            - $ref: "#/components/schemas/MatchId"
            - type: "null"
          description: |-
            The match's id, which names its demos and which [`slot_status`](#/components/schemas/AgentSlotStatus) and
            [`get_server_status`](#/components/schemas/ControllerGetServerStatus) report. Null for none, and when absent.
        send_statistics:
          default: false
          type: boolean
          description: |-
            Whether the plugin sends the match's gameplay events, its [`round_backup`](#/components/schemas/RoundBackup)
            among them, which the agent uploads in an [`event_batch`](#/components/schemas/EventBatch). When absent,
            `false`.

            Every match reports its [`phase_change`](#/components/schemas/AgentPhaseChange) and [`score`](#/components/schemas/AgentScore).
            A match without statistics sends each [`round_backup`](#/components/schemas/RoundBackup) in
            [`backup`](#/components/schemas/AgentBackup) instead, and a match with statistics only in its events.
        roster:
          $ref: "#/components/schemas/Roster"
          description: Who may join, and on which team.
        sides:
          $ref: "#/components/schemas/SidesMode"
          description: How starting sides are decided.
        knife_decision_seconds:
          default: 60
          type: integer
          minimum: 5
          maximum: 300
          description: |-
            Seconds the knife winners' captain has to type `!stay`, `!swap`, `!ct` or `!t`, from 5 to 300.
            The server stays in warmup meanwhile, with its clock counting these seconds down. An answer
            starts the match immediately. Without one, the team stays on its side when the clock runs out.
            When absent, 60.
        ready_mode:
          default: auto
          allOf:
            - $ref: "#/components/schemas/ReadyMode"
          description: How warmup ends. When absent, `auto`.
        spectators_ready_up:
          default: false
          type: boolean
          description: |-
            Whether, under `manual`, the match also waits for every rostered spectator to connect and type
            `!ready`, and any of them can force-ready the spectators. When absent, `false`, so the match
            waits for the teams alone.
        allow_force_ready:
          default: false
          type: boolean
          description: |-
            Whether, under `manual`, captains and rostered spectators may type `!forceready`. When absent,
            `false`, so the match waits for every rostered player's `!ready`.
        ready_timeout_seconds:
          default: null
          anyOf:
            - type: integer
              exclusiveMinimum: 0
              maximum: 1800
            - type: "null"
          description: |-
            Seconds from warmup until both teams must be ready, up to 1800. Null for no time limit, and
            when absent.

            When the time passes, the server kicks every player and the map ends with
            [`map_end`](#/components/schemas/MapEnd), which says who was connected and ready. The controller decides what
            that means for the match. Must be null with `external`, where only the controller starts the
            match.
        record_demo:
          type: boolean
          description: Whether GOTV records the live match as a demo.
        rules:
          default: {}
          allOf:
            - $ref: "#/components/schemas/LaunchRules"
          description: |-
            The rules the match is played under, applied over those of the game mode. When absent, every
            rule takes its default.
        restore:
          default: null
          anyOf:
            - $ref: "#/components/schemas/RestoreFile"
            - type: "null"
          description: |-
            A live round to play on from, taken from the [`round_backup`](#/components/schemas/RoundBackup) that reported
            it. Null for a match that plays the map from its start, and when absent.

            With a restore, the server skips the knife round and the side choice whatever `sides` says, and
            after warmup it loads the backup instead of going live. Team 1 takes the restore's
            `team1_side`, the round and the scores come from the file, and the restore's `rules` replace
            the match's `rules`. The agent writes `file` to the slot for `mp_backup_restore_load_file`.
      required:
        - roster
        - sides
        - record_demo
      description: |-
        The match a server is launched with, which covers one map. Each server plays one match. A restore
        launches the same `match_id` again under a new `allocation_id`, from a
        [`round_backup`](#/components/schemas/RoundBackup) that the first server sent.
    SidesMode:
      type: string
      enum:
        - knife
        - team1_ct
        - team1_t
      description: How starting sides are decided.
      x-enumDescriptions:
        knife: a knife round decides them.
        team1_ct: team 1 starts as CT.
        team1_t: team 1 starts as T.
    ReadyMode:
      type: string
      enum:
        - auto
        - manual
        - external
      description: |-
        How warmup ends.

        In every mode, [`start_match`](#/components/schemas/ControllerStartMatch) from the controller starts the match.
      x-enumDescriptions:
        auto: counts down once every rostered player is connected. Players have no ready commands.
        manual: also waits for each rostered player and each rostered spectator to type `!ready`, which `!unready` withdraws. With `allow_force_ready`, a captain's `!forceready` readies the captain's team with whichever of its players are connected, and any rostered spectator's `!forceready` readies the spectators.
        external: only [`start_match`](#/components/schemas/ControllerStartMatch) from the controller starts the match, and players have no commands for it.
    LaunchRules:
      type: object
      properties:
        max_rounds:
          default: 24
          type: integer
          exclusiveMinimum: 0
          maximum: 240
          description: Rounds in regulation, as `mp_maxrounds`. When absent, 24.
        overtime:
          default: true
          type: boolean
          description: Whether a tie after regulation goes to overtime, as `mp_overtime_enable`. When absent, `true`.
        overtime_max_rounds:
          default: 6
          type: integer
          exclusiveMinimum: 0
          maximum: 60
          description: Rounds in one overtime, both halves together, as `mp_overtime_maxrounds`. When absent, 6.
        friendly_fire:
          default: true
          type: boolean
          description: Whether bullets and melee hurt teammates. Grenades always do. When absent, `true`.
      description: |-
        [`MatchRules`](#/components/schemas/MatchRules) as a launch asks for them, where each rule left out takes the value of a
        typical competitive match. The reports of [`going_live`](#/components/schemas/GoingLive) and
        [`round_backup`](#/components/schemas/RoundBackup) always carry every rule.
    RestoreFile:
      type: object
      properties:
        team1_side:
          $ref: "#/components/schemas/PlayingSide"
          description: |-
            Team 1's side in that round. On a launch, the plugin seats players by it through warmup, before
            the load.
        rules:
          $ref: "#/components/schemas/MatchRules"
          description: |-
            The rules that the map was played under. A restore plays on under these rules instead of the
            rules of its launch.
        file:
          type: string
          minLength: 1
          maxLength: 524288
          pattern: ^[\t\n\r\P{Cc}]*$
          description: |-
            The backup file as the engine wrote it, up to 524288 characters. The
            file holds the money and equipment of every player, keyed by account, and only
            `mp_backup_restore_load_file` reads it.

            The file is text with tabs and line breaks, so a file at the length cap stays within the frame
            limits once JSON escapes it. Rejected when it holds a lone UTF-16 surrogate.
      required:
        - team1_side
        - rules
        - file
      description: |-
        What a server needs to play on from the backup of a live round. That is the engine's backup file,
        which holds the round, the scores and the money and equipment of every player, and the two things
        that the file does not hold.

        A launch carries one as `match.restore`.
    StartServerResult:
      oneOf:
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: start_server
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/StartServerReply"
              description: The server that started, and the slot the agent picked for it.
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - $ref: "#/components/schemas/AgentReplyFailure"
      description: "The reply to `start_server`: its data on success, or why it failed."
    ControllerStopServer:
      type: object
      properties:
        type:
          type: string
          const: stop_server
        allocation_id:
          $ref: "#/components/schemas/AllocationId"
          description: The server to stop.
        id:
          $ref: "#/components/schemas/RequestId"
          description: |-
            Chosen by the controller and unique among its open requests. The reply repeats it as
            `reply_to`. The agent ignores a request whose id is still running on the connection.
      required:
        - type
        - allocation_id
        - id
      description: |-
        Stop an allocation's server. Once asked, the agent stops it: a stop that Docker fails is answered
        with `failed`, and the agent tries it again until the server is gone. The slot stays held until
        then.

        The reply comes once its container has stopped: up to 10 seconds before the kill, then up
        to 35 seconds seconds for the kill and the reply. A stop during a launch
        of the allocation first waits for the launch's current step to end. That step can take minutes on
        the first launch after an image switch, which extracts the overlay, so the reply can come after
        the session's wait. If the launch is stopped between its steps, it starts no server and its
        `start_server` fails. If it is stopped while Docker starts its container, its `start_server`
        succeeds and the server is then stopped. Refused with `unknown_allocation` for an allocation the
        node does not run.
    StopServerResult:
      oneOf:
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: stop_server
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/StopServerReply"
              description: The server that stopped.
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - $ref: "#/components/schemas/AgentReplyFailure"
      description: "The reply to `stop_server`: its data on success, or why it failed."
    ControllerGetServerStatus:
      type: object
      properties:
        type:
          type: string
          const: get_server_status
        id:
          $ref: "#/components/schemas/RequestId"
          description: |-
            Chosen by the controller and unique among its open requests. The reply repeats it as
            `reply_to`. The agent ignores a request whose id is still running on the connection.
        allocation_id:
          $ref: "#/components/schemas/AllocationId"
          description: The server to ask.
      required:
        - type
        - id
        - allocation_id
      description: |-
        Read a server's state as its plugin reports it now.

        The reply comes within 10 seconds. Refused with:

        - `unknown_allocation` when the node does not run the server.
        - `plugin_offline` while the slot's `plugin_online` is `false` or when the plugin leaves before
          answering.
        - `plugin_status_timeout` after 10 seconds.
    GetServerStatusResult:
      oneOf:
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: get_server_status
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/GetServerStatusReply"
              description: The server asked about, and its state as its plugin reports it now.
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - $ref: "#/components/schemas/AgentReplyFailure"
      description: "The reply to `get_server_status`: its data on success, or why it failed."
    ControllerUpdateCs2:
      type: object
      properties:
        type:
          type: string
          const: update_cs2
        id:
          $ref: "#/components/schemas/RequestId"
          description: |-
            Chosen by the controller and unique among its open requests. The reply repeats it as
            `reply_to`. The agent ignores a request whose id is still running on the connection.
        mode:
          $ref: "#/components/schemas/Cs2UpdateMode"
          description: How thoroughly to update.
      required:
        - type
        - id
        - mode
      description: |-
        Install or update CS2 on the node's shared volume. The first install happens only through this
        request.

        The reply comes immediately with the update's id, as [`UpdateCs2Reply`](#/components/schemas/UpdateCs2Reply), and the update runs
        after it, which can take tens of minutes. Refused with `slots_held` while any slot is held and
        `maintenance_running` while a maintenance operation runs.

        `cs2.update` in [`node_status`](#/components/schemas/AgentNodeStatus) reports the update from `outcome: null` to
        how it ended, a failed pull of the cs2 image included. Meanwhile
        [`cs2_update_progress`](#/components/schemas/AgentCs2UpdateProgress) and [`log_batch`](#/components/schemas/AgentLogBatch) report
        on it, each naming it by that id.

        An installed node also updates itself to `cs2.latest_build_id` once every slot is idle, unless
        its `auto_update` is `false`.
    UpdateCs2Result:
      oneOf:
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: update_cs2
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/UpdateCs2Reply"
              description: The update an accepted [`update_cs2`](#/components/schemas/ControllerUpdateCs2) started.
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - $ref: "#/components/schemas/AgentReplyFailure"
      description: "The reply to `update_cs2`: its data on success, or why it failed."
    ControllerCheckCs2Build:
      type: object
      properties:
        type:
          type: string
          const: check_cs2_build
      required:
        - type
      description: |-
        Read the newest public CS2 build from Steam now instead of at the next scheduled read, which
        comes every 30 minutes.

        It receives no reply. The agent handles at most one per 60 seconds and
        drops any sooner one. One sent during a scheduled read shares it. Neither busy slots nor
        maintenance stop it. A changed build arrives as [`node_status`](#/components/schemas/AgentNodeStatus), and an
        installed node updates to it once every slot is idle, as it always does.
    ControllerDeleteDemo:
      type: object
      properties:
        type:
          type: string
          const: delete_demo
        id:
          $ref: "#/components/schemas/RequestId"
          description: |-
            Chosen by the controller and unique among its open requests. The reply repeats it as
            `reply_to`. The agent ignores a request whose id is still running on the connection.
        slot_id:
          $ref: "#/components/schemas/SlotId"
          description: The slot whose directory holds it.
        file_name:
          $ref: "#/components/schemas/DemoFileName"
          description: The file to delete.
      required:
        - type
        - id
        - slot_id
        - file_name
      description: |-
        Delete one demo by slot and file name.

        The reply comes once the file is removed. Refused with:

        - `unknown_slot` when the node has no directory for the slot.
        - `unknown_demo` when the slot has no demo of that name.
        - `demo_recording` while its server records the demo.
        - `demo_uploading` while the demo uploads.
        - `invalid_request` when the name is outside the slot's `demos/` directory.
        - `failed` when the removal fails.
    DeleteDemoResult:
      oneOf:
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: delete_demo
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/DeleteDemoReply"
              description: The demo that was removed.
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - $ref: "#/components/schemas/AgentReplyFailure"
      description: "The reply to `delete_demo`: its data on success, or why it failed."
    ControllerReserveSlot:
      type: object
      properties:
        type:
          type: string
          const: reserve_slot
        id:
          $ref: "#/components/schemas/RequestId"
          description: |-
            Chosen by the controller and unique among its open requests. The reply repeats it as
            `reply_to`. The agent ignores a request whose id is still running on the connection.
        slot_id:
          $ref: "#/components/schemas/SlotId"
          description: The slot to reserve.
      required:
        - type
        - id
        - slot_id
      description: |-
        Keep an idle slot from launches while the controller replaces its token, so that no server
        starts on a token that the controller is about to revoke at Steam.

        The agent marks the slot `reserved` before it replies, the same way a launch marks its slot
        `starting`, so no [`start_server`](#/components/schemas/ControllerStartServer) can take the slot once the reply is sent.
        The reply comes immediately. Refused with `slot_held` for a slot holding a server, and with
        `unknown_slot` for a slot the node does not have.

        The reservation ends with the [`set_config`](#/components/schemas/ControllerSetConfig) that gives the slot a new token,
        with the slot's removal, after 60 seconds, or with the agent's restart.
        Asking again restarts the wait.
    ReserveSlotResult:
      oneOf:
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: reserve_slot
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/ReserveSlotReply"
              description: The slot that is reserved.
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - $ref: "#/components/schemas/AgentReplyFailure"
      description: "The reply to `reserve_slot`: its data on success, or why it failed."
    ControllerSetConfig:
      type: object
      properties:
        type:
          type: string
          const: set_config
        id:
          $ref: "#/components/schemas/RequestId"
          description: |-
            Chosen by the controller and unique among its open requests. The reply repeats it as
            `reply_to`. The agent ignores a request whose id is still running on the connection.
        config:
          $ref: "#/components/schemas/NodeSettingsEdit"
          description: The settings to change.
      required:
        - type
        - id
        - config
      description: |-
        Apply an edit to `agent.yml`, or refuse it without saving any part. Nothing is queued.

        The reply comes once the edit is saved and applied, and carries the node's current settings, the
        [`NodeSettings`](#/components/schemas/NodeSettings) that `hello.config` reports. An edit that changes nothing is not saved
        again. A slot missing from the reply's `slots` has been removed. Refused with:

        - `slot_held` when the edit would remove or re-port a held slot, or give its server's token to
          another slot.
        - `slots_held` when the edit changes any key under `controller` (its address, TLS or paths) while any
          slot is held.
        - `slot_token_required` or `demo_header_value_required` when the edit keeps a value that the
          node does not have.
        - `invalid_request` when two slots have one token or anything else fails validation.

        After the reply, the agent sends a [`slot_status`](#/components/schemas/AgentSlotStatus) for each slot that the
        edit added or gave a new token. The slot reads `unchecked` until the agent has logged the new
        token on to Steam, which the agent starts then unless a server on the node is using the token. A
        change to `controller` closes the socket with `1000` once the reply has gone, and the agent sends its
        [`hello`](#/components/schemas/AgentHello) to the new address.

        Required of a controller that hands nodes an `agent.yml` without slots, because it is how they
        receive any.
    NodeSettingsEdit:
      type: object
      properties:
        slots:
          maxItems: 64
          type: array
          items:
            $ref: "#/components/schemas/SlotEdit"
          description: |-
            The slots servers run in. [`SlotEdit`](#/components/schemas/SlotEdit) describes their tokens.

            Rejected when two slots share an `id` or a `gslt`, or when two ports are equal. Every slot's
            `port` and `tv_port` count, so a slot's two ports differ too. It is also rejected when a port
            is 4500, where the agent listens for plugins. The agent fills in the kept
            tokens and refuses a layout in which two slots then share one.
        pinned_ip:
          anyOf:
            - $ref: "#/components/schemas/Ipv4"
            - type: "null"
          description: |-
            The address players connect to. Null for the node's `public_ip`. Servers already running keep
            the address their launch announced.
        controller:
          allOf:
            - $ref: "#/components/schemas/ControllerAddress"
          description: Where the agent dials the controller. It redials when any part of it changes.
        auto_update:
          type: boolean
          description: |-
            Whether the node keeps itself up to date: CS2 on `cs2.latest_build_id`, and its agent and cs2
            images on the newest release of their major. When false, the node stays on `cs2.build_id` until
            an [`update_cs2`](#/components/schemas/ControllerUpdateCs2), and accepts launches while it is behind
            `cs2.latest_build_id`, so players on a newer client cannot join its servers. Its images then
            change only with [`update_agent`](#/components/schemas/ControllerUpdateAgent) and
            [`update_cs2_image`](#/components/schemas/ControllerUpdateCs2Image).
        demo_upload:
          anyOf:
            - $ref: "#/components/schemas/DemoUploadEdit"
            - type: "null"
          description: |-
            Where the agent uploads demos. Null for no uploads, which keeps demos on the node.
            [`DemoUploadEdit`](#/components/schemas/DemoUploadEdit) describes its header.
        memory_mb:
          anyOf:
            - $ref: "#/components/schemas/MemoryLimit"
            - type: "null"
          description: |-
            MiB of memory each game container may use. Null for no cap. A change applies to servers launched
            afterwards, and CS2 installs and updates are exempt.

            Rejected when it is not a whole number from 1024 to 65536.
      description: |-
        [`NodeSettings`](#/components/schemas/NodeSettings) as [`set_config`](#/components/schemas/ControllerSetConfig) asks for them. A missing field keeps
        the node's value. Null is a value, as [`NodeSettings`](#/components/schemas/NodeSettings) describes. `slots`, when present, is
        the whole layout.
    SlotEdit:
      type: object
      properties:
        id:
          $ref: "#/components/schemas/SlotId"
          description: The slot's id, 1 to 32 letters, digits, `_` and `-`. It is unique on its node.
        port:
          $ref: "#/components/schemas/Port"
          description: |-
            The game port, UDP. Every `port` and `tv_port` on a node is different, free on the host, and
            not 4500, the port the agent listens on for plugins.
            [Host networking](https://docs.ankor.sh/how-it-works/#host-networking) explains why.
        tv_port:
          $ref: "#/components/schemas/Port"
          description: The GOTV port, UDP, under the same rules as `port`.
        gslt:
          allOf:
            - $ref: "#/components/schemas/Gslt"
          description: The slot's new token. Without one, the slot keeps the token it has.
      required:
        - id
        - port
        - tv_port
      description: |-
        A slot as [`set_config`](#/components/schemas/ControllerSetConfig) asks for it.

        `gslt` is a new token, which may replace the token of a slot that runs a server. That server
        keeps the token it started with, and no other slot may use that token until the server ends
        (`slot_held`). Without `gslt` the slot keeps its current token, so a slot the node does not have
        yet needs one (`slot_token_required`), and a slot from a [`hello`](#/components/schemas/AgentHello) can be sent
        back unchanged.
    Gslt:
      type: string
      pattern: ^[A-Za-z0-9]{10,64}$
      description: A Game Server Login Token, which the servers of a slot log in to Steam with.
    DemoUploadEdit:
      type: object
      properties:
        url:
          $ref: "#/components/schemas/DemoUploadUrl"
          description: The upload target.
        header:
          anyOf:
            - type: object
              properties:
                name:
                  type: string
                  pattern: ^[A-Za-z0-9-]{1,64}$
                  description: |-
                    The header's name, up to 64 letters, digits and `-`. Rejected when it is, in any case, one of
                    `host`, `content-type`, `content-length`, `transfer-encoding`, `connection`, `keep-alive`, `upgrade`, `expect`, `te`, `trailer`, `ankor-protocol`, `ankor-file-name`, `ankor-match-id` and `ankor-allocation-id`, which the agent sets itself or which change how the request is
                    framed.
                value:
                  type: string
                  maxLength: 1024
                  pattern: ^[^\r\n\u0000]*$
                  description: The header's value, sent verbatim, without line breaks.
              required:
                - name
            - type: "null"
          description: |-
            A header to send with each upload. Null for none. Without `value`, the header keeps the value
            of the node's current header of that name, so the `demo_upload` of a [`hello`](#/components/schemas/AgentHello)
            can be sent back unchanged.
        limit_mbit:
          anyOf:
            - $ref: "#/components/schemas/DemoUploadLimit"
            - type: "null"
          description: Megabits a second an upload may send while a game server runs on the node. Null for no limit.
      required:
        - url
        - header
        - limit_mbit
      description: |-
        A demo upload as [`set_config`](#/components/schemas/ControllerSetConfig) sets it. A header without its value keeps the
        value the node has.
    SetConfigResult:
      oneOf:
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: set_config
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/NodeSettings"
              description: |-
                The part of `agent.yml` that the controller may edit, as [`hello`](#/components/schemas/AgentHello) reports it
                applied.
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - $ref: "#/components/schemas/AgentReplyFailure"
      description: "The reply to `set_config`: its data on success, or why it failed."
    ControllerUpdateAgent:
      type: object
      properties:
        type:
          type: string
          const: update_agent
        id:
          $ref: "#/components/schemas/RequestId"
          description: |-
            Chosen by the controller and unique among its open requests. The reply repeats it as
            `reply_to`. The agent ignores a request whose id is still running on the connection.
        image:
          $ref: "#/components/schemas/ImageReference"
          description: The agent image to run.
        pull:
          type: boolean
          description: Whether to pull the image. When false, the agent uses an image already on the node.
      required:
        - type
        - id
        - image
        - pull
      description: |-
        Replace the agent container with another image. Running servers keep running.

        The reply comes immediately with what happens next, as [`UpdateAgentReply`](#/components/schemas/UpdateAgentReply), and
        `agent_update` reports the rest: its `step` while it runs, then how it ended. Refused with
        `maintenance_running` while a maintenance operation runs, with `image_missing` for an image the
        node lacks without `pull`, and with `image_not_allowed` for an image outside the official agent
        repository while the node's `official_images_only` is on.

        Switching stops this agent, and the agent that comes up, the new one or the old one put back,
        reports how it went. The update counts as a maintenance operation until it ends, and a switch
        until its replacement has proved it stays up.
    UpdateAgentResult:
      oneOf:
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: update_agent
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/UpdateAgentReply"
              description: |-
                What follows an accepted [`update_agent`](#/components/schemas/ControllerUpdateAgent): the update's first
                [`ImageUpdateStep`](#/components/schemas/ImageUpdateStep), or `unchanged` when the node runs that image already, as the registry or
                the local image says, and nothing more happens.
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - $ref: "#/components/schemas/AgentReplyFailure"
      description: "The reply to `update_agent`: its data on success, or why it failed."
    ControllerUpdateCs2Image:
      type: object
      properties:
        type:
          type: string
          const: update_cs2_image
        id:
          $ref: "#/components/schemas/RequestId"
          description: |-
            Chosen by the controller and unique among its open requests. The reply repeats it as
            `reply_to`. The agent ignores a request whose id is still running on the connection.
        image:
          $ref: "#/components/schemas/ImageReference"
          description: The cs2 image future launches use.
        pull:
          type: boolean
          description: Whether to pull the image. When false, the agent uses an image already on the node.
      required:
        - type
        - id
        - image
        - pull
      description: |-
        Switch future launches to another cs2 image. Running servers keep theirs, and so does a launch
        that took its slot before the switch.

        The reply comes immediately with what happens next, as [`UpdateCs2ImageReply`](#/components/schemas/UpdateCs2ImageReply). Refused with
        `maintenance_running` while a maintenance operation runs, with `image_missing` for an image the
        node lacks without `pull`, and with `image_not_allowed` for an image outside the official cs2
        repository while the node's `official_images_only` is on.

        The agent then pulls the image when asked to, extracts its plugin files and removes the images
        it supersedes. `cs2.image_update` in [`node_status`](#/components/schemas/AgentNodeStatus) reports its `step`
        while it runs, then how it ended, in the same [`node_status`](#/components/schemas/AgentNodeStatus) as the new
        `cs2.image`.
    UpdateCs2ImageResult:
      oneOf:
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: update_cs2_image
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/UpdateCs2ImageReply"
              description: |-
                What follows an accepted [`update_cs2_image`](#/components/schemas/ControllerUpdateCs2Image): its first
                [`ImageUpdateStep`](#/components/schemas/ImageUpdateStep).
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - $ref: "#/components/schemas/AgentReplyFailure"
      description: "The reply to `update_cs2_image`: its data on success, or why it failed."
    ControllerWatchResources:
      type: object
      properties:
        type:
          type: string
          const: watch_resources
        replay:
          type: boolean
          description: Whether to send every tick the agent holds now, also while the lease runs.
      required:
        - type
        - replay
      description: |-
        Start or renew the resource lease, which lasts 2 minutes. It receives no reply.

        While the lease runs, the agent streams [`resource_batch`](#/components/schemas/AgentResourceBatch). A lease that
        starts first sends every tick the agent holds.
    ControllerWatchLogs:
      type: object
      properties:
        type:
          type: string
          const: watch_logs
        source:
          $ref: "#/components/schemas/LogSource"
          description: The log to stream. A source the agent does not keep streams nothing until it prints.
        replay_id:
          anyOf:
            - $ref: "#/components/schemas/ReplayId"
            - type: "null"
          description: |-
            The id that the replay frames carry, for the watcher that asked for the replay. Null for no
            replay, as in a renewal. A lease that starts still replays the source, to every watcher.
      required:
        - type
        - source
        - replay_id
      description: |-
        Start or renew the lease of one log source, which lasts 2 minutes. It receives no
        reply.

        While the lease runs, the agent streams the source's [`log_batch`](#/components/schemas/AgentLogBatch) frames.
        While any source's lease runs, the agent also sends [`log_index`](#/components/schemas/AgentLogIndex). A lease
        that starts replays the source, to every watcher unless a `replay_id` names the one that asked.
        Any number of sources can be watched together, each on a lease of its own.
    ControllerWelcome:
      type: object
      properties:
        type:
          type: string
          const: welcome
      required:
        - type
      description: |-
        The controller's answer to a [`hello`](#/components/schemas/AgentHello) it accepted, before anything else on the
        socket. Without one the agent redials after 15 seconds.

        After each [`welcome`](#/components/schemas/ControllerWelcome) the agent sends what changed since the
        [`hello`](#/components/schemas/AgentHello), its pending [`server_exited`](#/components/schemas/AgentServerExited) first and then
        the changed statuses, demos, backlog and watched streams. It sends each state once, so a slot
        that went from `starting` to `running` arrives as `running`.
    ControllerHelloRefused:
      type: object
      properties:
        type:
          type: string
          const: hello_refused
        reason:
          $ref: "#/components/schemas/HelloRefusedReason"
        detail:
          type: string
          maxLength: 512
          description: A reason for the operator reading the agent's log, such as the field that failed.
      required:
        - type
        - reason
        - detail
      description: |-
        The controller's answer to a [`hello`](#/components/schemas/AgentHello) it will not accept, followed by a close
        with code `1008` and reason `hello_refused`. The agent stops dialling until it is restarted,
        because the same [`hello`](#/components/schemas/AgentHello) would be refused again.
    HelloRefusedReason:
      type: string
      enum:
        - invalid_hello
      description: |-
        Why the controller refused a [`hello`](#/components/schemas/AgentHello): `invalid_hello`, the
        [`hello`](#/components/schemas/AgentHello) fails its schema. A controller refuses nothing else at hello: an agent
        on a version it does not support connects, so it can still be updated, and receives no launches.
    ControllerConsoleCommand:
      type: object
      properties:
        type:
          type: string
          const: console_command
        id:
          $ref: "#/components/schemas/RequestId"
          description: |-
            Chosen by the controller and unique among its open requests. The reply repeats it as
            `reply_to`. The agent ignores a request whose id is still running on the connection.
        allocation_id:
          $ref: "#/components/schemas/AllocationId"
          description: The server to run it on.
        command:
          type: string
          maxLength: 500
          allOf:
            - type: string
              pattern: \S
            - type: string
              pattern: ^(?:[^\p{Cc}]|[\t\n])*$
          description: |-
            The console command line, up to 500 characters, handed to the
            engine as it is, so a newline or `;` runs more than one command. It must not be blank, and tabs
            and newlines are its only control characters.
      required:
        - type
        - id
        - allocation_id
        - command
      description: |-
        Run a console command on a server through its plugin.

        The reply comes once the plugin has handed the command to the engine, which reports nothing
        back, so the reply cannot say that it worked. Refused with:

        - `unknown_allocation` when the node does not run the server.
        - `plugin_offline` while the slot's `plugin_online` is `false`.
        - `plugin_status_timeout` when the plugin does not answer.
        - `command_blocked` when the server runs a match and the line 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`. The plugin then runs none of the line.

        The output of the command joins the server's log, which [`log_batch`](#/components/schemas/AgentLogBatch) streams
        while a [`watch_logs`](#/components/schemas/ControllerWatchLogs) lease covers it and replays when one starts.
    ConsoleCommandResult:
      oneOf:
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: console_command
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/ConsoleCommandReply"
              description: The server the command was handed to.
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - $ref: "#/components/schemas/AgentReplyFailure"
      description: "The reply to `console_command`: its data on success, or why it failed."
    ControllerRestoreRound:
      type: object
      properties:
        type:
          type: string
          const: restore_round
        id:
          $ref: "#/components/schemas/RequestId"
          description: |-
            Chosen by the controller and unique among its open requests. The reply repeats it as
            `reply_to`. The agent ignores a request whose id is still running on the connection.
        allocation_id:
          $ref: "#/components/schemas/AllocationId"
          description: The server whose match to restore.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The one-based round to play on from, whose backup the engine wrote as the round started.
      required:
        - type
        - id
        - allocation_id
        - round
      description: |-
        Play a running match on from one of its earlier rounds, on the same server, without a new
        launch. The engine writes a backup as each live round starts, and the node keeps the backups for
        the server's life. This request loads the one for `round`.

        The reply comes once the plugin has asked the engine to load the backup. The
        [`match_restored`](#/components/schemas/MatchRestored) event, which the plugin sends ahead of the restored
        round's [`round_start`](#/components/schemas/RoundStart), confirms that the engine did. Refused with:

        - `unknown_allocation` when the node does not run the server.
        - `no_backup` when the node has no backup of that round, as in a game mode without backups
          ([`GameMode`](#/components/schemas/GameMode)).
        - `plugin_offline` while the slot's `plugin_online` is `false`.
        - `plugin_status_timeout` when the plugin does not answer.
        - `no_match`, `not_live` or `at_halftime` when the plugin refuses the restore.

        The node keeps the backup of every round it has played, including rounds after one it was
        restored to. If `round` is past the current round, the server loads play that the match has
        since left. Whether to allow that is the controller's rule.
    RestoreRoundResult:
      oneOf:
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: restore_round
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/RestoreRoundReply"
              description: The server and round the restore was handed to.
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - $ref: "#/components/schemas/AgentReplyFailure"
      description: "The reply to `restore_round`: its data on success, or why it failed."
    ControllerStartMatch:
      type: object
      properties:
        type:
          type: string
          const: start_match
        id:
          $ref: "#/components/schemas/RequestId"
          description: |-
            Chosen by the controller and unique among its open requests. The reply repeats it as
            `reply_to`. The agent ignores a request whose id is still running on the connection.
        allocation_id:
          $ref: "#/components/schemas/AllocationId"
          description: The server whose match to start.
      required:
        - type
        - id
        - allocation_id
      description: |-
        Start a match in warmup whatever its `ready_mode`, without waiting for anyone to be ready. An
        admin can always start the match this way, and under `external` it is the only way to.

        The reply comes once the plugin has started the countdown to the knife round or live play.
        Refused with:

        - `unknown_allocation` when the node does not run the server.
        - `plugin_offline` while the slot's `plugin_online` is `false`.
        - `plugin_status_timeout` when the plugin does not answer.
        - `no_match`, `not_in_warmup` or `team_empty` when the plugin refuses the start.

        A repeat while the countdown runs is accepted and changes nothing.
    StartMatchResult:
      oneOf:
        - type: object
          properties:
            type:
              type: string
              const: reply
            reply_to:
              $ref: "#/components/schemas/RequestId"
              description: The `id` of the request this answers.
            request:
              type: string
              const: start_match
            ok:
              type: boolean
              const: true
              description: The request succeeded.
            data:
              $ref: "#/components/schemas/StartMatchReply"
              description: The server the start was handed to.
          required:
            - type
            - reply_to
            - request
            - ok
            - data
        - $ref: "#/components/schemas/AgentReplyFailure"
      description: "The reply to `start_match`: its data on success, or why it failed."
    EventBatch:
      type: object
      properties:
        allocation_id:
          $ref: "#/components/schemas/AllocationId"
          description: The server that sent every event of the batch.
        frames:
          minItems: 1
          maxItems: 128
          type: array
          items:
            $ref: "#/components/schemas/EventBatchFrame"
            description: |-
              One frame of an event batch: an event and its position among the events of the server that the
              batch names. A node runs each allocation once and refuses to launch one it has run before, so the
              batch's `allocation_id` and the frame's `seq` identify the event. Parse each frame of a batch on
              its own with this schema.
          description: The frames, in `seq` order.
      required:
        - allocation_id
        - frames
      description: |-
        The body of an event upload: 1 to 128 frames of the server that
        `allocation_id` names, with consecutive `seq`. Store frames idempotently by `allocation_id` and
        `seq`, because a lost receipt means the same frames arrive again.

        This schema checks only each frame's position, so one bad event cannot fail the batch. Parse each
        frame on its own with [`EventBatchFrame`](#/components/schemas/EventBatchFrame), and quarantine a frame that fails by listing it in
        `rejected` of the [`EventBatchReceipt`](#/components/schemas/EventBatchReceipt).

        A server plays the one match of its launch, so the match of a batch is the match that the controller
        launched `allocation_id` with. The frames come to at most 2 MiB
        together.

        Rejected unless each frame's `seq` is one above the `seq` of the frame before it.
    EventBatchFrame:
      type: object
      properties:
        seq:
          $ref: "#/components/schemas/EventSeq"
          description: The event's position among the events of its server.
        event:
          $ref: "#/components/schemas/PluginToControllerMessage"
          description: The gameplay event, one of [`PluginToControllerMessage`](#/components/schemas/PluginToControllerMessage).
      required:
        - seq
        - event
      description: |-
        One frame of an event batch: an event and its position among the events of the server that the
        batch names. A node runs each allocation once and refuses to launch one it has run before, so the
        batch's `allocation_id` and the frame's `seq` identify the event. Parse each frame of a batch on
        its own with this schema.
    EventSeq:
      type: integer
      minimum: 0
      maximum: 9007199254740991
      description: An event's position among its server's events, from 0.
    PluginToControllerMessage:
      oneOf:
        - $ref: "#/components/schemas/MapStart"
        - $ref: "#/components/schemas/PlayerConnect"
        - $ref: "#/components/schemas/PlayerDisconnect"
        - $ref: "#/components/schemas/KnifeStart"
        - $ref: "#/components/schemas/KnifeEnd"
        - $ref: "#/components/schemas/SideChoice"
        - $ref: "#/components/schemas/GoingLive"
        - $ref: "#/components/schemas/RoundBackup"
        - $ref: "#/components/schemas/MatchRestored"
        - $ref: "#/components/schemas/RoundStart"
        - $ref: "#/components/schemas/RoundFreezeEnd"
        - $ref: "#/components/schemas/RoundBuyEnd"
        - $ref: "#/components/schemas/RoundEnd"
        - $ref: "#/components/schemas/RoundComplete"
        - $ref: "#/components/schemas/PlayerDeath"
        - $ref: "#/components/schemas/PlayerHurt"
        - $ref: "#/components/schemas/PlayerBlind"
        - $ref: "#/components/schemas/GrenadeThrow"
        - $ref: "#/components/schemas/GrenadeDetonate"
        - $ref: "#/components/schemas/GrenadeEnd"
        - $ref: "#/components/schemas/RoundShots"
        - $ref: "#/components/schemas/BombBeginPlant"
        - $ref: "#/components/schemas/BombAbortPlant"
        - $ref: "#/components/schemas/BombPlanted"
        - $ref: "#/components/schemas/BombBeginDefuse"
        - $ref: "#/components/schemas/BombAbortDefuse"
        - $ref: "#/components/schemas/BombDefused"
        - $ref: "#/components/schemas/BombExploded"
        - $ref: "#/components/schemas/RoundMvp"
        - $ref: "#/components/schemas/MapEnd"
        - $ref: "#/components/schemas/PluginLog"
      description: Any gameplay event.
    MapStart:
      type: object
      properties:
        type:
          type: string
          const: map_start
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        plugin_version:
          type: string
          maxLength: 128
          description: The version of the Ankor plugin, up to 128 characters. Rejected when it holds a NUL or a lone UTF-16 surrogate.
        framework_version:
          type: string
          maxLength: 128
          description: The version of SwiftlyS2, up to 128 characters. Rejected when it holds a NUL or a lone UTF-16 surrogate.
        server_version:
          type: string
          maxLength: 128
          description: The version of the CS2 server, up to 128 characters. Rejected when it holds a NUL or a lone UTF-16 surrogate.
        map_name:
          type: string
          pattern: ^[A-Za-z0-9_/-]{1,128}$
          description: The map that loaded, as the engine names it.
      required:
        - type
        - timestamp
        - tick
        - plugin_version
        - framework_version
        - server_version
        - map_name
      description: |-
        Sent when a map has loaded on the server, as the first event of the map's history.

        The plugin sends one on every load. A workshop map loads twice, and an admin's `changelevel`
        loads another map.
    PlayerConnect:
      type: object
      properties:
        type:
          type: string
          const: player_connect
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        player:
          $ref: "#/components/schemas/PlayerRef"
          description: The player who connected.
        rejected:
          type: boolean
          description: Whether the plugin kicked the player because the SteamID is not on any roster.
      required:
        - type
        - timestamp
        - tick
        - player
        - rejected
      description: |-
        Sent when a player connects after the match leaves warmup.
        [`get_server_status`](#/components/schemas/ControllerGetServerStatus) lists who is on the server during warmup.
    PlayerRef:
      type: object
      properties:
        steam_id:
          $ref: "#/components/schemas/SteamId64"
          description: The player's SteamID64.
        name:
          $ref: "#/components/schemas/PlayerName"
          description: The player's name when the event happened.
        side:
          $ref: "#/components/schemas/Side"
          description: The side that the player was on.
        team:
          anyOf:
            - $ref: "#/components/schemas/TeamNumber"
            - type: "null"
          description: |-
            The roster team of the player, which the plugin looks up in its roster. Null for spectators and
            unknown players.
      required:
        - steam_id
        - name
        - side
        - team
      description: A player as the event saw them.
    TeamNumber:
      anyOf:
        - type: number
          const: 1
        - type: number
          const: 2
      description: A team of the [`Roster`](#/components/schemas/Roster), team 1 or team 2, whichever side it plays.
    PlayerDisconnect:
      type: object
      properties:
        type:
          type: string
          const: player_disconnect
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        player:
          $ref: "#/components/schemas/PlayerRef"
          description: The player who left.
        reason:
          type: string
          maxLength: 64
          description: The engine's disconnect reason, up to 64 characters. Rejected when it holds a NUL or a lone UTF-16 surrogate.
      required:
        - type
        - timestamp
        - tick
        - player
        - reason
      description: Sent when a player leaves after the match leaves warmup.
    KnifeStart:
      type: object
      properties:
        type:
          type: string
          const: knife_start
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
      required:
        - type
        - timestamp
        - tick
      description: Sent when the knife round begins.
    KnifeEnd:
      type: object
      properties:
        type:
          type: string
          const: knife_end
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        winner_team:
          $ref: "#/components/schemas/TeamNumber"
          description: The team that won the knife round.
        winner_side:
          $ref: "#/components/schemas/PlayingSide"
          description: The side that the winning team won on.
        team1_alive:
          type: integer
          minimum: 0
          maximum: 64
          description: Players of team 1 alive at the end.
        team2_alive:
          type: integer
          minimum: 0
          maximum: 64
          description: Players of team 2 alive at the end.
        team1_health:
          type: integer
          minimum: 0
          maximum: 64000
          description: |-
            Health points left to team 1, summed over its players alive, each at most
            1000.
        team2_health:
          type: integer
          minimum: 0
          maximum: 64000
          description: Health points left to team 2, summed like `team1_health`.
      required:
        - type
        - timestamp
        - tick
        - winner_team
        - winner_side
        - team1_alive
        - team2_alive
        - team1_health
        - team2_health
      description: |-
        Sent when the knife round ends. When both teams survive, the counts in this event decide the
        winner.

        Rejected when a team's health is above 1000 times its players alive.
    SideChoice:
      type: object
      properties:
        type:
          type: string
          const: side_choice
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        team:
          $ref: "#/components/schemas/TeamNumber"
          description: The team that chose.
        choice:
          type: string
          enum:
            - stay
            - swap
          description: What the team chose.
          x-enumDescriptions:
            stay: keep the sides of the knife round.
            swap: exchange the sides.
        team1_side:
          $ref: "#/components/schemas/PlayingSide"
          description: The side of team 1 for the first half, after the choice.
      required:
        - type
        - timestamp
        - tick
        - team
        - choice
        - team1_side
      description: Sent when the knife round winners choose a side, or run out of time and stay.
    GoingLive:
      type: object
      properties:
        type:
          type: string
          const: going_live
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        team1_side:
          $ref: "#/components/schemas/PlayingSide"
          description: Team 1's side for the first half.
        max_rounds:
          type: integer
          exclusiveMinimum: 0
          maximum: 240
          description: |-
            Rounds in regulation, as `mp_maxrounds`, up to 240. MR12 is 24 and MR15 is
            30.
        overtime:
          type: boolean
          description: Whether a tie after regulation goes to overtime, as `mp_overtime_enable`.
        overtime_max_rounds:
          type: integer
          exclusiveMinimum: 0
          maximum: 60
          description: |-
            Rounds in one overtime, both halves together, as `mp_overtime_maxrounds`. The bound is 60, ten
            times the stock 6.
        friendly_fire:
          type: boolean
          description: Whether bullets and melee hurt teammates. Grenades always do.
      required:
        - type
        - timestamp
        - tick
        - team1_side
        - max_rounds
        - overtime
        - overtime_max_rounds
        - friendly_fire
      description: Sent when the match goes live, with the round settings that it plays under.
    MatchRestored:
      type: object
      properties:
        type:
          type: string
          const: match_restored
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The one-based round that the match plays on from.
        team1_side:
          $ref: "#/components/schemas/PlayingSide"
          description: Team 1's side in that round.
        team1_score:
          type: integer
          minimum: 0
          maximum: 1000
          description: Team 1's score before that round.
        team2_score:
          type: integer
          minimum: 0
          maximum: 1000
          description: Team 2's score before that round.
      required:
        - type
        - timestamp
        - tick
        - round
        - team1_side
        - team1_score
        - team2_score
      description: |-
        Sent when the server has loaded a round backup, from the `restore` of its launch, from a
        [`restore_round`](#/components/schemas/ControllerRestoreRound), or from an `mp_backup_restore_load_file` that someone
        ran by hand.

        The [`round_start`](#/components/schemas/RoundStart) that follows plays `round` again. From that
        [`round_start`](#/components/schemas/RoundStart) on, the events of the server replace everything that it sent
        for that round and the rounds after it.

        The plugin reads the scores and the side from the engine once the engine has loaded the file. The
        plugin refuses a load during the knife round, the side choice, the halftime break and after the
        match has ended.
    RoundStart:
      type: object
      properties:
        type:
          type: string
          const: round_start
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The round that the event happened in, even when the event arrives later.
        round_time:
          type: number
          minimum: 0
          maximum: 4000
          description: Seconds since the round started.
        team1_side:
          $ref: "#/components/schemas/PlayingSide"
          description: Team 1's side this round.
        players:
          maxItems: 64
          type: array
          items:
            $ref: "#/components/schemas/RoundPlayer"
          description: |-
            The players present when the round started, each once. An empty array is authoritative, so the
            round had no players. Rejected when two entries share a `player.steam_id`.
        carryover:
          anyOf:
            - type: object
              properties:
                from_round:
                  $ref: "#/components/schemas/RoundNumber"
                  description: The round after which the plugin took the snapshot.
                sampled_tick:
                  type: number
                  minimum: 0
                  maximum: 9007199254740991
                  description: The server tick when the plugin took the snapshot.
                players:
                  maxItems: 64
                  type: array
                  items:
                    type: object
                    properties:
                      player:
                        $ref: "#/components/schemas/PlayerRef"
                        description: The player sampled.
                      alive:
                        type: boolean
                        description: Whether the player was alive when sampled.
                      equipment_value:
                        type: integer
                        minimum: 0
                        maximum: 65535
                        description: Dollars of equipment that the player carried, the engine's figure.
                      cash:
                        type: integer
                        minimum: 0
                        maximum: 65535
                        description: Dollars in hand when sampled.
                      start_cash:
                        type: integer
                        minimum: 0
                        maximum: 65535
                        description: Dollars in hand at the start of the round.
                      cash_spent:
                        type: integer
                        minimum: 0
                        maximum: 131070
                        description: Dollars spent this round by the time of the sample.
                      inventory:
                        anyOf:
                          - $ref: "#/components/schemas/PlayerInventory"
                          - type: "null"
                        description: What the player carried. Null when the plugin could not read the pawn.
                    required:
                      - player
                      - alive
                      - equipment_value
                      - cash
                      - start_cash
                      - cash_spent
                      - inventory
                  description: |-
                    The players as that round left them, with what each carried. Rejected when two entries share a
                    `player.steam_id`.
              required:
                - from_round
                - sampled_tick
                - players
            - type: "null"
          description: |-
            The players of the previous round as that round left them, after exit kills and pickups. Null
            across an equipment reset.
      required:
        - type
        - timestamp
        - tick
        - round
        - round_time
        - team1_side
        - players
        - carryover
      description: |-
        Sent when a round begins.

        The same round can begin again with no [`round_end`](#/components/schemas/RoundEnd) between. The engine restarts
        a round that a team leaves before it is decided, as when every player on it disconnects or loses
        their connection. The later `round_start` replaces the earlier one, and the round is played
        from it.
    RoundPlayer:
      type: object
      properties:
        player:
          $ref: "#/components/schemas/PlayerRef"
          description: The player sampled.
        alive:
          type: boolean
          description: Whether the player was alive when sampled.
        equipment_value:
          type: integer
          minimum: 0
          maximum: 65535
          description: Dollars of equipment that the player carried, the engine's figure.
        cash:
          type: integer
          minimum: 0
          maximum: 65535
          description: Dollars in hand when sampled.
        start_cash:
          type: integer
          minimum: 0
          maximum: 65535
          description: Dollars in hand at the start of the round.
        cash_spent:
          type: integer
          minimum: 0
          maximum: 131070
          description: Dollars spent this round by the time of the sample.
      required:
        - player
        - alive
        - equipment_value
        - cash
        - start_cash
        - cash_spent
      description: The state and money of a player when the plugin sampled the round. Money is in dollars.
    PlayerInventory:
      type: object
      properties:
        items:
          maxItems: 64
          type: array
          items:
            $ref: "#/components/schemas/InventoryItem"
          description: Weapons, grenades and other items held, up to 64.
        armor:
          type: integer
          minimum: 0
          maximum: 1000
          description: Armor points, up to 1000, ten times the stock 100.
        helmet:
          type: boolean
          description: Whether the armor includes a helmet.
        defuse_kit:
          type: boolean
          description: Whether the player carries a defuse kit.
      required:
        - items
        - armor
        - helmet
        - defuse_kit
      description: What a player carries. A null inventory means that the plugin could not read the pawn.
    InventoryItem:
      type: object
      properties:
        weapon:
          type: integer
          exclusiveMinimum: 0
          maximum: 65535
          description: The engine's item definition index.
        quantity:
          type: integer
          exclusiveMinimum: 0
          maximum: 20
          description: Items of this kind that the player holds, up to ten times the stock limit of 2 flashbangs.
        unit_value:
          type: integer
          minimum: 0
          maximum: 52000
          description: Dollars that one item costs, up to ten times the dearest stock item, the M249 at 5,200.
      required:
        - weapon
        - quantity
        - unit_value
      description: |-
        One kind of item that a player holds: the engine's item definition index as `weapon`, a quantity,
        and the unit price in this game build.
    RoundFreezeEnd:
      type: object
      properties:
        type:
          type: string
          const: round_freeze_end
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The round that the event happened in, even when the event arrives later.
        round_time:
          type: number
          minimum: 0
          maximum: 4000
          description: Seconds since the round started.
        team1_side:
          $ref: "#/components/schemas/PlayingSide"
          description: Team 1's side this round.
        players:
          maxItems: 64
          type: array
          items:
            $ref: "#/components/schemas/RoundPlayer"
          description: Everyone who takes part in the round, each once. Rejected when two entries share a `player.steam_id`.
      required:
        - type
        - timestamp
        - tick
        - round
        - round_time
        - team1_side
        - players
      description: |-
        Sent when freeze time ends, as the authoritative list of the players who take part in the round
        after reconnects during freeze time.
    RoundBuyEnd:
      type: object
      properties:
        type:
          type: string
          const: round_buy_end
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The round that the event happened in, even when the event arrives later.
        round_time:
          type: number
          minimum: 0
          maximum: 4000
          description: Seconds since the round started.
        players:
          maxItems: 64
          type: array
          items:
            type: object
            properties:
              player:
                $ref: "#/components/schemas/PlayerRef"
                description: The player read.
              equipment_value:
                type: integer
                minimum: 0
                maximum: 65535
                description: Dollars of equipment that the player carried.
              cash:
                type: integer
                minimum: 0
                maximum: 65535
                description: Dollars in hand.
              start_cash:
                type: integer
                minimum: 0
                maximum: 65535
                description: Dollars in hand at the start of the round.
              cash_spent:
                type: integer
                minimum: 0
                maximum: 131070
                description: Dollars spent this round.
              sampled_tick:
                type: number
                minimum: 0
                maximum: 9007199254740991
                description: The server tick when the plugin read the player.
            required:
              - player
              - equipment_value
              - cash
              - start_cash
              - cash_spent
              - sampled_tick
          description: |-
            The samples behind the team totals, each player once. Rejected when two entries share a
            `player.steam_id`.
        team1_equipment_value:
          type: integer
          minimum: 0
          maximum: 4194240
          description: |-
            Dollars of equipment that team 1 carried, the sum of `equipment_value` over its `players`, so
            the event states the economy of the round on its own.
        team2_equipment_value:
          type: integer
          minimum: 0
          maximum: 4194240
          description: Dollars of equipment that team 2 carried, summed like `team1_equipment_value`.
      required:
        - type
        - timestamp
        - tick
        - round
        - round_time
        - players
        - team1_equipment_value
        - team2_equipment_value
      description: |-
        Sent when buy time closes, `mp_buytime` after freeze time ends, with the equipment value that
        each roster team carried, as the sum of the engine's figure for each pawn.

        The plugin reads a player at the last buy-zone exit inside the window, or live at the close while
        the player is still in a buy zone. Snapshots at a death or disconnect keep a buy that was made
        before the window closes. An earlier buy-zone exit counts for a player who never returned.

        Rejected unless `team1_equipment_value` and `team2_equipment_value` are each the sum of
        `equipment_value` over the `players` whose `player.team` is that team.
    RoundEnd:
      type: object
      properties:
        type:
          type: string
          const: round_end
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The round that the event happened in, even when the event arrives later.
        round_time:
          type: number
          minimum: 0
          maximum: 4000
          description: Seconds since the round started.
        winner_side:
          $ref: "#/components/schemas/PlayingSide"
          description: The side that won the round.
        winner_team:
          $ref: "#/components/schemas/TeamNumber"
          description: The roster team that won the round.
        reason:
          $ref: "#/components/schemas/RoundEndReason"
          description: Why the round ended.
        team1_score:
          type: integer
          minimum: 0
          maximum: 1000
          description: Team 1's score after this round.
        team2_score:
          type: integer
          minimum: 0
          maximum: 1000
          description: Team 2's score after this round.
        round_length:
          type: number
          minimum: 0
          maximum: 4000
          description: Seconds that the round lasted, from the end of freeze time to the end of the round.
      required:
        - type
        - timestamp
        - tick
        - round
        - round_time
        - winner_side
        - winner_team
        - reason
        - team1_score
        - team2_score
        - round_length
      description: |-
        Sent when a round is decided. The last gameplay reports of the round may still follow, and
        [`round_complete`](#/components/schemas/RoundComplete) closes it.
    RoundEndReason:
      type: string
      enum:
        - unknown
        - target_bombed
        - terrorists_escaped
        - cts_prevent_escape
        - escaping_terrorists_neutralized
        - bomb_defused
        - cts_win
        - terrorists_win
        - round_draw
        - all_hostages_rescued
        - target_saved
        - hostages_not_rescued
        - terrorists_not_escaped
        - game_commencing
        - terrorists_surrender
        - cts_surrender
        - terrorists_planted
        - cts_reached_hostage
        - survival_win
        - survival_draw
      description: |-
        Why a round ended, as the engine names it. When the engine gives a reason that this list does not
        name, the plugin sends `unknown`.
      x-enumDescriptions:
        target_bombed: The bomb exploded.
        bomb_defused: The bomb was defused.
        cts_win: Every T was killed.
        terrorists_win: Every CT was killed.
        target_saved: Time ran out without the bomb planted.
        terrorists_surrender: The Ts voted to surrender. The map ends after this round.
        cts_surrender: The CTs voted to surrender. The map ends after this round.
    RoundComplete:
      type: object
      properties:
        type:
          type: string
          const: round_complete
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The round that the event happened in, even when the event arrives later.
        round_time:
          type: number
          minimum: 0
          maximum: 4000
          description: Seconds since the round started.
      required:
        - type
        - timestamp
        - tick
        - round
        - round_time
      description: |-
        Sent after every gameplay report of a scored round, its [`round_shots`](#/components/schemas/RoundShots)
        included, before the next round or the map end.
    PlayerDeath:
      type: object
      properties:
        type:
          type: string
          const: player_death
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The round that the event happened in, even when the event arrives later.
        round_time:
          type: number
          minimum: 0
          maximum: 4000
          description: Seconds since the round started.
        victim:
          $ref: "#/components/schemas/PlacedPlayer"
          description: The player who died.
        attacker:
          anyOf:
            - $ref: "#/components/schemas/PlayerAt"
            - type: "null"
          description: |-
            The player credited with the kill. Null when no player dealt it.

            For a grenade kill, the position is where the thrower was at the death. The
            [`grenade_throw`](#/components/schemas/GrenadeThrow) gives where they threw from.
        assister:
          anyOf:
            - $ref: "#/components/schemas/PlayerRef"
            - type: "null"
          description: The player credited with the assist. Null when there is none.
        weapon:
          $ref: "#/components/schemas/Weapon"
          description: The killing weapon.
        cause:
          $ref: "#/components/schemas/DamageCause"
          description: What caused the death.
        headshot:
          type: boolean
          description: Whether the killing hit was to the head.
        penetrated:
          type: integer
          minimum: 0
          maximum: 10
          description: Surfaces that the bullet went through. The bound is a margin, not a measured engine limit.
        thru_smoke:
          type: boolean
          description: Whether the killing shot passed through smoke.
        no_scope:
          type: boolean
          description: Whether the attacker fired a scoped weapon unscoped.
        attacker_blind:
          type: boolean
          description: Whether the attacker was flashed.
        attacker_in_air:
          type: boolean
          description: Whether the attacker was airborne.
        flash_assist:
          type: boolean
          description: Whether the assist came from a flashbang.
        friendly_fire:
          type: boolean
          description: Whether the attacker and the victim are on the same roster team, a team kill.
        victim_blind_by:
          anyOf:
            - $ref: "#/components/schemas/PlayerRef"
            - type: "null"
          description: |-
            The player who last blinded the victim, through a [`player_blind`](#/components/schemas/PlayerBlind) still
            running at the death. Null when the victim could see.

            `victim_blind_remaining` gives the seconds of that blind left. When the attacker is the
            blinder, the kill is a self-flash kill. The consumer decides how much blind must remain for
            that to count.
        victim_blind_remaining:
          type: number
          minimum: 0
          maximum: 10
          description: Seconds left of the blind from `victim_blind_by`. Zero when the victim could see.
        victim_equipment_value:
          type: integer
          minimum: 0
          maximum: 65535
          description: |-
            Dollars of equipment that the victim carried when the killing blow landed, the engine's
            figure.
        victim_grenades_held:
          anyOf:
            - maxItems: 64
              type: array
              items:
                $ref: "#/components/schemas/InventoryItem"
            - type: "null"
          description: |-
            Grenades still in the victim's inventory when the killing blow landed. The C4 and the knife are
            not grenades. Two flashbangs show as a `quantity` of 2, and `unit_value` is the price per
            grenade.

            An empty array means that the victim held none. Null when the plugin could not read the
            inventory from before the damage.
      required:
        - type
        - timestamp
        - tick
        - round
        - round_time
        - victim
        - attacker
        - assister
        - weapon
        - cause
        - headshot
        - penetrated
        - thru_smoke
        - no_scope
        - attacker_blind
        - attacker_in_air
        - flash_assist
        - friendly_fire
        - victim_blind_by
        - victim_blind_remaining
        - victim_equipment_value
        - victim_grenades_held
      description: |-
        Sent when a player dies. Rejected when the `assister` has the `steam_id` of the `victim` or the
        `attacker`.
    PlacedPlayer:
      type: object
      properties:
        steam_id:
          $ref: "#/components/schemas/SteamId64"
          description: The player's SteamID64.
        name:
          $ref: "#/components/schemas/PlayerName"
          description: The player's name when the event happened.
        side:
          $ref: "#/components/schemas/Side"
          description: The side that the player was on.
        team:
          anyOf:
            - $ref: "#/components/schemas/TeamNumber"
            - type: "null"
          description: |-
            The roster team of the player, which the plugin looks up in its roster. Null for spectators and
            unknown players.
        position:
          $ref: "#/components/schemas/PlayerPosition"
          description: Where the player stood and looked.
      required:
        - steam_id
        - name
        - side
        - team
        - position
      description: |-
        A [`PlayerRef`](#/components/schemas/PlayerRef) with the position of the player, for a player who acted in the world when the
        event happened, so the position is always known.
    PlayerPosition:
      type: object
      properties:
        x:
          type: number
          minimum: -32768
          maximum: 32768
          description: Engine units along the map's x axis.
        y:
          type: number
          minimum: -32768
          maximum: 32768
          description: Engine units along the map's y axis.
        z:
          type: number
          minimum: -32768
          maximum: 32768
          description: Engine units along the map's z axis, the height.
        pitch:
          type: number
          minimum: -90
          maximum: 90
          description: Degrees of eye pitch. A positive pitch looks down.
        yaw:
          type: number
          minimum: -360
          maximum: 360
          description: Degrees of eye yaw, counter-clockwise from the +x axis.
      required:
        - x
        - y
        - z
        - pitch
        - yaw
      description: Where a player stood, as a [`WorldPos`](#/components/schemas/WorldPos), and where the player looked.
    PlayerAt:
      type: object
      properties:
        steam_id:
          $ref: "#/components/schemas/SteamId64"
          description: The player's SteamID64.
        name:
          $ref: "#/components/schemas/PlayerName"
          description: The player's name when the event happened.
        side:
          $ref: "#/components/schemas/Side"
          description: The side that the player was on.
        team:
          anyOf:
            - $ref: "#/components/schemas/TeamNumber"
            - type: "null"
          description: |-
            The roster team of the player, which the plugin looks up in its roster. Null for spectators and
            unknown players.
        position:
          anyOf:
            - $ref: "#/components/schemas/PlayerPosition"
            - type: "null"
          description: Where the player stood and looked. Null when the player had no body in the world.
      required:
        - steam_id
        - name
        - side
        - team
        - position
      description: A [`PlayerRef`](#/components/schemas/PlayerRef) with the position of the player when the event happened.
    Weapon:
      type: object
      properties:
        id:
          anyOf:
            - type: integer
              exclusiveMinimum: 0
              maximum: 65535
            - type: "null"
          description: The item definition index, a 16-bit unsigned integer. Null when the item has none.
        fallback:
          anyOf:
            - type: string
              pattern: ^[A-Za-z0-9_]{1,64}$
            - type: "null"
          description: |-
            The engine class name of an item without an index. Null when `id` is set, or when the engine
            named no weapon either.
      required:
        - id
        - fallback
      description: |-
        A weapon, by the engine's item definition index or, for an item without one, the engine's class
        name. Rejected when `id` is a number and `fallback` is not null.
    DamageCause:
      type: string
      enum:
        - player
        - bomb
        - fall
        - self
        - disconnect
        - other
      description: What caused damage or a death.
      x-enumDescriptions:
        player: another player dealt it, with a weapon or a grenade.
        bomb: the planted bomb exploded.
        fall: the victim fell.
        self: the victim's own grenade or fire.
        disconnect: the victim left the server, which kills a living player.
        other: anything else, such as a map's damage trigger or the plugin moving a player to the spectators.
    PlayerHurt:
      type: object
      properties:
        type:
          type: string
          const: player_hurt
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The round that the event happened in, even when the event arrives later.
        round_time:
          type: number
          minimum: 0
          maximum: 4000
          description: Seconds since the round started.
        victim:
          $ref: "#/components/schemas/PlacedPlayer"
          description: The player hit.
        attacker:
          anyOf:
            - $ref: "#/components/schemas/PlayerAt"
            - type: "null"
          description: |-
            The player who dealt the damage. Null for world damage.

            For grenade damage, the position is where the thrower was at the hit. The
            [`grenade_throw`](#/components/schemas/GrenadeThrow) gives where they threw from.
        weapon:
          $ref: "#/components/schemas/Weapon"
          description: What dealt the damage.
        cause:
          $ref: "#/components/schemas/DamageCause"
          description: What caused the damage.
        dmg_health:
          type: integer
          minimum: 0
          maximum: 1000
          description: |-
            Health damage as the engine reports it, overkill included. `health_before` and `health_after`
            give the health that the victim actually lost.
        health_before:
          anyOf:
            - type: integer
              minimum: 0
              maximum: 1000
            - type: "null"
          description: |-
            The victim's health before the hit, as the engine reports it. Null when the plugin could not
            read it.
        health_after:
          type: integer
          minimum: 0
          maximum: 1000
          description: The victim's health after the hit, at most `health_before`.
        dmg_armor:
          type: integer
          minimum: 0
          maximum: 1000
          description: Armor damage.
        hitgroup:
          type: integer
          minimum: 0
          maximum: 15
          description: |-
            The engine's hit group, `HitGroup_t` in the Source 2 SDK. The plugin sends the engine's invalid
            group as `0`, and `9` is unused.

            - `0`: generic.
            - `1`: head.
            - `2`: chest.
            - `3`: stomach.
            - `4`: left arm.
            - `5`: right arm.
            - `6`: left leg.
            - `7`: right leg.
            - `8`: neck.
            - `10`: gear, the backpack hitbox.
        friendly_fire:
          type: boolean
          description: Whether the attacker and the victim are on the same roster team.
        projectile_impact:
          type: boolean
          description: |-
            Whether the grenade itself struck the player before it detonated. Such a hit counts as damage
            and is stored as a hit, but not as utility damage, which matches the in-game scoreboard.
        grenade_id:
          anyOf:
            - $ref: "#/components/schemas/GrenadeId"
            - type: "null"
          description: |-
            The grenade that dealt the damage, as its [`grenade_throw`](#/components/schemas/GrenadeThrow) identifies it.
            Null for anything else.
      required:
        - type
        - timestamp
        - tick
        - round
        - round_time
        - victim
        - attacker
        - weapon
        - cause
        - dmg_health
        - health_before
        - health_after
        - dmg_armor
        - hitgroup
        - friendly_fire
        - projectile_impact
        - grenade_id
      description: |-
        Sent when a player takes damage. Rejected when `health_before` is a number and `health_after` is
        above it.
    GrenadeId:
      type: integer
      minimum: 0
      maximum: 1000000
      description: |-
        The zero-based id of a grenade within its server, counted in throw order. A server throws far
        fewer than a million.
    PlayerBlind:
      type: object
      properties:
        type:
          type: string
          const: player_blind
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The round that the event happened in, even when the event arrives later.
        round_time:
          type: number
          minimum: 0
          maximum: 4000
          description: Seconds since the round started.
        victim:
          $ref: "#/components/schemas/PlacedPlayer"
          description: The player blinded.
        attacker:
          $ref: "#/components/schemas/PlayerAt"
          description: |-
            The player who threw the flashbang, placed where they were when it went off. The
            [`grenade_throw`](#/components/schemas/GrenadeThrow) gives where they threw from.
        duration:
          type: number
          minimum: 0
          maximum: 10
          description: Seconds that the blind lasts.
        friendly_fire:
          type: boolean
          description: Whether the thrower and the victim are on the same roster team.
        grenade_id:
          anyOf:
            - $ref: "#/components/schemas/GrenadeId"
            - type: "null"
          description: |-
            The flashbang, as its [`grenade_throw`](#/components/schemas/GrenadeThrow) identifies it. Null when the plugin
            did not see it thrown.
      required:
        - type
        - timestamp
        - tick
        - round
        - round_time
        - victim
        - attacker
        - duration
        - friendly_fire
        - grenade_id
      description: Sent when a flashbang blinds a player.
    GrenadeThrow:
      type: object
      properties:
        type:
          type: string
          const: grenade_throw
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The round that the event happened in, even when the event arrives later.
        round_time:
          type: number
          minimum: 0
          maximum: 4000
          description: Seconds since the round started.
        grenade_id:
          $ref: "#/components/schemas/GrenadeId"
          description: The grenade's id, unique within the server.
        player:
          $ref: "#/components/schemas/PlacedPlayer"
          description: The player who threw the grenade, placed where they stood when the projectile spawned.
        grenade:
          $ref: "#/components/schemas/GrenadeKind"
          description: The kind of grenade thrown.
      required:
        - type
        - timestamp
        - tick
        - round
        - round_time
        - grenade_id
        - player
        - grenade
      description: |-
        Sent when a grenade leaves a player's hand. Its `grenade_id` identifies the grenade in the events
        that follow: its [`grenade_detonate`](#/components/schemas/GrenadeDetonate) and [`grenade_end`](#/components/schemas/GrenadeEnd),
        and the [`player_hurt`](#/components/schemas/PlayerHurt) and [`player_blind`](#/components/schemas/PlayerBlind) that it causes.
    GrenadeKind:
      type: string
      enum:
        - flashbang
        - hegrenade
        - smokegrenade
        - molotov
        - incgrenade
        - decoy
      description: The engine class name of a grenade.
    GrenadeDetonate:
      type: object
      properties:
        type:
          type: string
          const: grenade_detonate
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The round that the event happened in, even when the event arrives later.
        round_time:
          type: number
          minimum: 0
          maximum: 4000
          description: Seconds since the round started.
        grenade_id:
          $ref: "#/components/schemas/GrenadeId"
          description: The grenade, as its [`grenade_throw`](#/components/schemas/GrenadeThrow) identifies it.
        player:
          $ref: "#/components/schemas/PlayerRef"
          description: The player who threw the grenade, as its [`grenade_throw`](#/components/schemas/GrenadeThrow) saw them.
        detonate_pos:
          anyOf:
            - $ref: "#/components/schemas/WorldPos"
            - type: "null"
          description: Where the engine reported the detonation. Null for a fire that lit without reporting one.
      required:
        - type
        - timestamp
        - tick
        - round
        - round_time
        - grenade_id
        - player
        - detonate_pos
      description: |-
        Sent when a grenade detonates or a fire grenade lights. A grenade that is removed before then
        sends none.
    WorldPos:
      type: object
      properties:
        x:
          type: number
          minimum: -32768
          maximum: 32768
          description: Engine units along the map's x axis.
        y:
          type: number
          minimum: -32768
          maximum: 32768
          description: Engine units along the map's y axis.
        z:
          type: number
          minimum: -32768
          maximum: 32768
          description: Engine units along the map's z axis, the height.
      required:
        - x
        - y
        - z
      description: A point in the world, in engine units.
    GrenadeEnd:
      type: object
      properties:
        type:
          type: string
          const: grenade_end
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The round that the grenade was thrown in.
        grenade_id:
          $ref: "#/components/schemas/GrenadeId"
          description: The grenade, as its [`grenade_throw`](#/components/schemas/GrenadeThrow) identifies it.
        player:
          $ref: "#/components/schemas/PlayerRef"
          description: The player who threw the grenade, as its [`grenade_throw`](#/components/schemas/GrenadeThrow) saw them.
        end_round_time:
          type: number
          minimum: 0
          maximum: 4000
          description: |-
            Seconds since the round started, at the moment the effect ended. A fire ends when it goes out.
            When the round closed on the grenade, the value is the round length plus the restart delay,
            which is when the engine clears the entities.
        fire_end:
          anyOf:
            - $ref: "#/components/schemas/FireEnd"
            - type: "null"
          description: |-
            How the fire ended. Null for a grenade that was not a fire, and for a fire projectile removed
            before it lit.
        extinguished_by:
          anyOf:
            - $ref: "#/components/schemas/GrenadeId"
            - type: "null"
          description: |-
            The smoke whose cloud put this fire out, credited to the nearest live smoke. Null when no smoke
            put the fire out.
      required:
        - type
        - timestamp
        - tick
        - round
        - grenade_id
        - player
        - end_round_time
        - fire_end
        - extinguished_by
      description: |-
        Sent when the effect of a grenade is over, because its last entity was removed or the round
        closed or reset on it. Every grenade that sent a [`grenade_throw`](#/components/schemas/GrenadeThrow) sends one
        before the [`round_complete`](#/components/schemas/RoundComplete) of its round. A grenade whose round a restore
        cut off sends none; the [`match_restored`](#/components/schemas/MatchRestored) replaces that round.
    FireEnd:
      type: string
      enum:
        - expired
        - extinguished
        - smothered
      description: How a fire ended.
      x-enumDescriptions:
        expired: the fire burned out, or the round closed on it.
        extinguished: a smoke put the fire out.
        smothered: the fire grenade landed inside a smoke.
    RoundShots:
      type: object
      properties:
        type:
          type: string
          const: round_shots
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The round that the shots were fired in.
        players:
          maxItems: 64
          type: array
          items:
            $ref: "#/components/schemas/PlayerShots"
          description: Each player who fired, once each.
      required:
        - type
        - timestamp
        - tick
        - round
        - players
      description: |-
        Sent once for each round when it closes, at halftime, the next [`round_start`](#/components/schemas/RoundStart)
        or the [`map_end`](#/components/schemas/MapEnd), with the shots that each player fired over the round.

        A round that starts again with no [`round_end`](#/components/schemas/RoundEnd), as described on
        [`round_start`](#/components/schemas/RoundStart), sends none for the attempt it abandoned. Neither does a round
        that a restore cut off, which the [`match_restored`](#/components/schemas/MatchRestored) replaces.

        `players` lists each player who fired, and is empty when nobody did. The plugin counts shots
        from the engine's `weapon_fire`, up to 4096 for each player, and counts no
        more. Knives and grenades are left out, because they fire no rounds. A throw arrives as
        [`grenade_throw`](#/components/schemas/GrenadeThrow) instead. Hits are the [`player_hurt`](#/components/schemas/PlayerHurt)
        events of the same round, attacker and weapon.

        Rejected when a player's shots add up to more than 4096, or when a player or a
        weapon of a player is listed twice.
    PlayerShots:
      type: object
      properties:
        player:
          $ref: "#/components/schemas/PlayerRef"
          description: The player.
        weapons:
          minItems: 1
          maxItems: 4096
          type: array
          items:
            $ref: "#/components/schemas/WeaponShots"
          description: Each weapon that the player fired, once each.
      required:
        - player
        - weapons
      description: The shots that one player fired over a round.
    WeaponShots:
      type: object
      properties:
        weapon:
          $ref: "#/components/schemas/Weapon"
          description: The weapon fired.
        shots:
          type: integer
          exclusiveMinimum: 0
          maximum: 4096
          description: Shots fired, which equals the number of `ticks`.
        ticks:
          minItems: 1
          maxItems: 4096
          type: array
          items:
            type: number
            minimum: 0
            maximum: 9007199254740991
            description: |-
              The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

              The plugin sends events as they happen, so `seq` follows the same order.
              [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
              closes, and the `ticks` of each weapon place each shot.
          description: The server tick of each shot.
      required:
        - weapon
        - shots
        - ticks
      description: |-
        The shots that a player fired with one weapon over a round. Rejected unless `shots` equals the
        number of `ticks`.
    BombBeginPlant:
      type: object
      properties:
        type:
          type: string
          const: bomb_begin_plant
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The round that the event happened in, even when the event arrives later.
        round_time:
          type: number
          minimum: 0
          maximum: 4000
          description: Seconds since the round started.
        player:
          $ref: "#/components/schemas/PlacedPlayer"
          description: The player planting.
        site:
          anyOf:
            - type: string
              enum:
                - a
                - b
            - type: "null"
          description: The bombsite. Null when the engine's target index did not resolve to a `func_bomb_target`.
      required:
        - type
        - timestamp
        - tick
        - round
        - round_time
        - player
        - site
      description: Sent when a player begins to plant the bomb.
    BombAbortPlant:
      type: object
      properties:
        type:
          type: string
          const: bomb_abort_plant
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The round that the event happened in, even when the event arrives later.
        round_time:
          type: number
          minimum: 0
          maximum: 4000
          description: Seconds since the round started.
        player:
          $ref: "#/components/schemas/PlacedPlayer"
          description: The player who stopped planting.
        site:
          anyOf:
            - type: string
              enum:
                - a
                - b
            - type: "null"
          description: The bombsite. Null when the engine's target index did not resolve to a `func_bomb_target`.
      required:
        - type
        - timestamp
        - tick
        - round
        - round_time
        - player
        - site
      description: Sent when a player abandons a plant.
    BombPlanted:
      type: object
      properties:
        type:
          type: string
          const: bomb_planted
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The round that the event happened in, even when the event arrives later.
        round_time:
          type: number
          minimum: 0
          maximum: 4000
          description: Seconds since the round started.
        player:
          $ref: "#/components/schemas/PlacedPlayer"
          description: The player who planted.
        site:
          anyOf:
            - type: string
              enum:
                - a
                - b
            - type: "null"
          description: The bombsite. Null when the engine's target index did not resolve to a `func_bomb_target`.
      required:
        - type
        - timestamp
        - tick
        - round
        - round_time
        - player
        - site
      description: Sent when the bomb is planted.
    BombBeginDefuse:
      type: object
      properties:
        type:
          type: string
          const: bomb_begin_defuse
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The round that the event happened in, even when the event arrives later.
        round_time:
          type: number
          minimum: 0
          maximum: 4000
          description: Seconds since the round started.
        player:
          $ref: "#/components/schemas/PlacedPlayer"
          description: The player defusing.
        has_kit:
          type: boolean
          description: Whether the defuser has a kit.
      required:
        - type
        - timestamp
        - tick
        - round
        - round_time
        - player
        - has_kit
      description: Sent when a player begins to defuse the bomb.
    BombAbortDefuse:
      type: object
      properties:
        type:
          type: string
          const: bomb_abort_defuse
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The round that the event happened in, even when the event arrives later.
        round_time:
          type: number
          minimum: 0
          maximum: 4000
          description: Seconds since the round started.
        player:
          $ref: "#/components/schemas/PlacedPlayer"
          description: The player who stopped defusing.
      required:
        - type
        - timestamp
        - tick
        - round
        - round_time
        - player
      description: Sent when a player abandons a defuse.
    BombDefused:
      type: object
      properties:
        type:
          type: string
          const: bomb_defused
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The round that the event happened in, even when the event arrives later.
        round_time:
          type: number
          minimum: 0
          maximum: 4000
          description: Seconds since the round started.
        player:
          $ref: "#/components/schemas/PlacedPlayer"
          description: The player who defused.
        time_remaining:
          type: number
          minimum: 0
          maximum: 90
          description: Seconds left on the bomb, up to 90, the most that `mp_c4timer` allows.
        has_kit:
          type: boolean
          description: Whether the defuser has a kit.
      required:
        - type
        - timestamp
        - tick
        - round
        - round_time
        - player
        - time_remaining
        - has_kit
      description: Sent when the bomb is defused.
    BombExploded:
      type: object
      properties:
        type:
          type: string
          const: bomb_exploded
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The round that the event happened in, even when the event arrives later.
        round_time:
          type: number
          minimum: 0
          maximum: 4000
          description: Seconds since the round started.
      required:
        - type
        - timestamp
        - tick
        - round
        - round_time
      description: Sent when the bomb explodes.
    RoundMvp:
      type: object
      properties:
        type:
          type: string
          const: round_mvp
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The round that the event happened in, even when the event arrives later.
        round_time:
          type: number
          minimum: 0
          maximum: 4000
          description: Seconds since the round started.
        player:
          $ref: "#/components/schemas/PlayerRef"
          description: The MVP of the round.
        reason:
          type: integer
          minimum: 0
          maximum: 31
          description: The engine's MVP reason code, a small enum.
      required:
        - type
        - timestamp
        - tick
        - round
        - round_time
        - player
        - reason
      description: Sent when the engine names the MVP of a round.
    MapEnd:
      type: object
      properties:
        type:
          type: string
          const: map_end
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        reason:
          $ref: "#/components/schemas/MapEndReason"
          description: Why the map ended.
        winner_team:
          anyOf:
            - $ref: "#/components/schemas/TeamNumber"
            - type: "null"
          description: The team with the higher score of a `played` map. Null for a draw, and for any other end.
        team1_score:
          type: integer
          minimum: 0
          maximum: 1000
          description: Team 1's score when the map ended.
        team2_score:
          type: integer
          minimum: 0
          maximum: 1000
          description: Team 2's score when the map ended.
        teams:
          anyOf:
            - type: object
              properties:
                team1:
                  $ref: "#/components/schemas/ReadyTeam"
                team2:
                  $ref: "#/components/schemas/ReadyTeam"
                spectators:
                  anyOf:
                    - $ref: "#/components/schemas/ReadyTeam"
                    - type: "null"
                  description: The rostered spectators, as one group. Null unless the match has them ready up.
              required:
                - team1
                - team2
                - spectators
            - type: "null"
          description: Who was on the server and ready when `ready_timeout_seconds` passed. Null for any other end.
      required:
        - type
        - timestamp
        - tick
        - reason
        - winner_team
        - team1_score
        - team2_score
        - teams
      description: |-
        Sent when the map is over. After a `played` map, the scoreboard stays up for 60 seconds, then the
        server kicks the players, sends its remaining events and quits. After a `ready_timeout`, the
        server kicks the players at once.

        Rejected unless `winner_team` is the team with the higher score of a `played` map, or null when
        the scores are equal or the map was not played out. Rejected unless `teams` is set for a
        `ready_timeout`, with both scores 0, and null otherwise.
    MapEndReason:
      type: string
      enum:
        - played
        - ready_timeout
        - abandoned
      description: |-
        Why a map ended:

        Only a `played` map has a winner. The controller decides what any other end means for the match.
      x-enumDescriptions:
        played: the map was played out.
        ready_timeout: "`ready_timeout_seconds` passed in warmup, before the match started."
        abandoned: every rostered player left after the match began, in the knife round, the side choice or live play.
    ReadyTeam:
      type: object
      properties:
        connected:
          type: integer
          minimum: 0
          maximum: 64
          description: Rostered players of the team on the server.
        ready:
          type: boolean
          description: Whether the team counted as ready, its captain's force-ready included.
      required:
        - connected
        - ready
      description: A team's players when the ready deadline passed.
    PluginLog:
      type: object
      properties:
        type:
          type: string
          const: log
        timestamp:
          type: integer
          minimum: 1577836800000
          maximum: 4102444800000
          description: Unix milliseconds on the server's wall clock.
        tick:
          type: number
          minimum: 0
          maximum: 9007199254740991
          description: |-
            The server tick when the event happened, as the engine's map tick plus its sub-tick fraction.

            The plugin sends events as they happen, so `seq` follows the same order.
            [`round_shots`](#/components/schemas/RoundShots) is the exception, because the plugin sends it when the round
            closes, and the `ticks` of each weapon place each shot.
        level:
          type: string
          enum:
            - debug
            - info
            - warn
            - error
          description: The severity of the line.
        message:
          type: string
          maxLength: 8192
          description: The log line, up to 8192 characters. Rejected when it holds a NUL or a lone UTF-16 surrogate.
      required:
        - type
        - timestamp
        - tick
        - level
        - message
      description: Sent when the plugin logs a line worth keeping with the match.
    EventBatchReceipt:
      type: object
      properties:
        rejected:
          type: array
          items:
            type: object
            properties:
              seq:
                $ref: "#/components/schemas/EventSeq"
                description: The quarantined frame.
              reason:
                type: string
                minLength: 1
                description: Why it was quarantined.
            required:
              - seq
              - reason
          description: Frames quarantined instead of stored.
      required:
        - rejected
      description: |-
        The controller's answer to a batch it holds. Every frame of the batch is either canonical
        history or quarantined under one of the `rejected` entries, and the agent may delete them all.
        Resending a quarantined frame would fail the same way.
    EventBatchRejection:
      type: object
      properties:
        code:
          type: string
          const: event_integrity_error
          description: Always `event_integrity_error`.
        detail:
          type: string
          description: What made the batch untrustworthy.
      required:
        - code
        - detail
      description: |-
        The body of a `409` answer to an event upload. The controller could not read the identities of
        the batch, so it refuses the whole batch. The agent retries that server with a backoff of its
        own, and the node's other servers upload meanwhile.
    PluginEventFrameView:
      type: object
      properties:
        seq:
          $ref: "#/components/schemas/EventSeq"
          description: The event's position among the events of its server.
        event:
          $ref: "#/components/schemas/PluginToControllerMessage"
          description: The gameplay event, one of [`PluginToControllerMessage`](#/components/schemas/PluginToControllerMessage).
        type:
          type: string
          const: event
      required:
        - seq
        - event
        - type
      description: |-
        One sequenced event, which the plugin sends and the agent spools without its `type`, then uploads
        as an [`EventBatchFrame`](#/components/schemas/EventBatchFrame) of an [`event_batch`](#/components/schemas/EventBatch). `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`](#/components/schemas/EventAck).
    ServerStatus:
      type: object
      properties:
        map_name:
          type: string
          maxLength: 64
          description: The map the server is on, up to 64 characters.
        match:
          anyOf:
            - type: object
              properties:
                match_id:
                  anyOf:
                    - $ref: "#/components/schemas/MatchId"
                    - type: "null"
                  description: |-
                    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.
                phase:
                  $ref: "#/components/schemas/MatchPhase"
                  description: Where the match is.
                team1_side:
                  $ref: "#/components/schemas/PlayingSide"
                  description: Team 1's current side.
                team2_side:
                  $ref: "#/components/schemas/PlayingSide"
                  description: |-
                    Team 2's current side, always the other side, sent so a reader shows each team without
                    deriving it.
                team1_score:
                  type: integer
                  minimum: 0
                  maximum: 9007199254740991
                  description: Team 1's score.
                team2_score:
                  type: integer
                  minimum: 0
                  maximum: 9007199254740991
                  description: Team 2's score.
              required:
                - match_id
                - phase
                - team1_side
                - team2_side
                - team1_score
                - team2_score
            - type: "null"
          description: |-
            The loaded match and its team state. Null on a server without a match or after a refused
            config.
        players:
          maxItems: 64
          type: array
          items:
            type: object
            properties:
              steam_id:
                $ref: "#/components/schemas/SteamId64"
                description: The player's SteamID64.
              name:
                $ref: "#/components/schemas/PlayerName"
                description: The player's name.
              side:
                $ref: "#/components/schemas/Side"
                description: The player's side.
            required:
              - steam_id
              - name
              - side
          description: Everyone connected, up to 64.
        config_error:
          anyOf:
            - type: string
              maxLength: 512
            - type: "null"
          description: |-
            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.
        ready:
          type: boolean
          description: |-
            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_id:
          anyOf:
            - $ref: "#/components/schemas/SteamId64"
            - type: "null"
          description: |-
            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`.
        type:
          type: string
          const: server_status
        request_id:
          anyOf:
            - type: string
              maxLength: 64
            - type: "null"
          description: |-
            The `request_id` of the [`get_server_status`](#/components/schemas/PluginGetServerStatus) this answers. Null
            when the request had none or the status was sent unasked.
      required:
        - map_name
        - match
        - players
        - config_error
        - ready
        - server_steam_id
        - type
        - request_id
      description: |-
        A game server's state, which the plugin sends in answer to every
        [`get_server_status`](#/components/schemas/PluginGetServerStatus). The plugin also sends it unasked once when it
        connects and once when it turns `ready`.
    PluginPhaseChange:
      type: object
      properties:
        type:
          type: string
          const: phase_change
        phase:
          $ref: "#/components/schemas/MatchPhase"
          description: The phase the match is in now.
      required:
        - type
        - phase
      description: |-
        The match moved to another phase, which the plugin sends whether or not the match sends
        statistics, and the agent forwards as [`phase_change`](#/components/schemas/AgentPhaseChange). Like
        [`server_status`](#/components/schemas/ServerStatus), it is sent once and lost without a connection.
    PluginScore:
      type: object
      properties:
        type:
          type: string
          const: score
        team1_score:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: Team 1's score.
        team2_score:
          type: integer
          minimum: 0
          maximum: 9007199254740991
          description: Team 2's score.
      required:
        - type
        - team1_score
        - team2_score
      description: |-
        The match's score changed, which the plugin sends like [`phase_change`](#/components/schemas/PluginPhaseChange),
        and the agent forwards as [`score`](#/components/schemas/AgentScore).
    PluginBackup:
      type: object
      properties:
        type:
          type: string
          const: backup
        backup:
          $ref: "#/components/schemas/RoundBackup"
          description: The [`round_backup`](#/components/schemas/RoundBackup) event, as the event stream would carry it.
      required:
        - type
        - backup
      description: |-
        The `round_backup` of a match that sends no statistics, which the plugin
        sends like [`phase_change`](#/components/schemas/PluginPhaseChange), and the agent forwards as
        [`backup`](#/components/schemas/AgentBackup). `backup` holds the complete event, unchanged, as the
        event stream would carry it.
    CommandResult:
      type: object
      properties:
        type:
          type: string
          const: command_result
        request_id:
          type: string
          maxLength: 64
          description: The `request_id` of the command this answers.
        error:
          anyOf:
            - $ref: "#/components/schemas/CommandRefusal"
            - type: "null"
          description: Why the plugin refused the command. Null when the plugin carried it out.
      required:
        - type
        - request_id
        - error
      description: The plugin's answer to a command that carries a `request_id`.
    CommandRefusal:
      type: string
      enum:
        - no_match
        - not_in_warmup
        - team_empty
        - no_backup
        - not_live
        - at_halftime
        - command_blocked
        - unsupported_request
      description: Why a plugin did not carry out a command, as an [`AgentErrorCode`](#/components/schemas/AgentErrorCode).
    Quitting:
      type: object
      properties:
        type:
          type: string
          const: quitting
      required:
        - type
      description: |-
        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`](#/components/schemas/ServerStatus), it is not resent,
        so one sent without a connection is lost.
    ExecCommand:
      type: object
      properties:
        type:
          type: string
          const: exec
        command:
          type: string
          description: The console command line.
        request_id:
          type: string
          maxLength: 64
          description: An id that the answering [`command_result`](#/components/schemas/CommandResult) carries back.
      required:
        - type
        - command
        - request_id
      description: |-
        Runs a server console command. The plugin answers with a [`command_result`](#/components/schemas/CommandResult)
        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.
    PluginRestoreRound:
      type: object
      properties:
        type:
          type: string
          const: restore_round
        round:
          $ref: "#/components/schemas/RoundNumber"
          description: The round to play on from, one-based.
        request_id:
          type: string
          maxLength: 64
          description: An id that the answering [`command_result`](#/components/schemas/CommandResult) carries back.
      required:
        - type
        - round
        - request_id
      description: |-
        Loads the server's backup of a round, as [`restore_round`](#/components/schemas/ControllerRestoreRound) 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`](#/components/schemas/CommandResult).
    PluginStartMatch:
      type: object
      properties:
        type:
          type: string
          const: start_match
        request_id:
          type: string
          maxLength: 64
          description: An id that the answering [`command_result`](#/components/schemas/CommandResult) carries back.
      required:
        - type
        - request_id
      description: |-
        Starts a match in warmup whatever its `ready_mode`, as [`start_match`](#/components/schemas/ControllerStartMatch) from
        the controller asks. The plugin answers with a [`command_result`](#/components/schemas/CommandResult).
    PluginGetServerStatus:
      type: object
      properties:
        type:
          type: string
          const: get_server_status
        request_id:
          anyOf:
            - type: string
              maxLength: 64
            - type: "null"
          description: |-
            An id that the answering [`server_status`](#/components/schemas/ServerStatus) carries back, to match it to this
            request. Null for none.
      required:
        - type
        - request_id
      description: |-
        Asks the plugin for its server's state, which the plugin answers with a
        [`server_status`](#/components/schemas/ServerStatus).
    EventAck:
      type: object
      properties:
        type:
          type: string
          const: ack
        to_seq:
          $ref: "#/components/schemas/EventSeq"
      required:
        - type
        - to_seq
      description: |-
        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.
    StartRefusal:
      type: string
      enum:
        - no_free_slot
        - maintenance_running
        - cs2_not_installed
        - cs2_outdated
        - node_unavailable
        - shutting_down
      description: |-
        The [`start_server`](#/components/schemas/ControllerStartServer) refusals the agent sends before reserving a slot.
        Nothing started, so another node may take the launch.
  correlationIds:
    request:
      description: The request's id, which its reply repeats.
      location: $message.payload#/id
    reply:
      description: The id of the request this answers.
      location: $message.payload#/reply_to
  securitySchemes:
    nodeToken:
      type: http
      scheme: bearer
      description: "`token` from the node's agent.yml."
