{
  "openapi": "3.0.3",
  "info": {
    "title": "Multiplayer API",
    "description": "Cross-platform multiplayer backend for game developers: player accounts, game rooms, matchmaking lobbies, leaderboards, friends/invites, text moderation and realtime updates. Served by api.michitai.com.\n\nAuthentication is header-only: `X-Api-Token` (project key, required on every endpoint), `X-Api-Private-Token` (admin operations), `X-Game-Player-Token` (player-scoped calls; obtained once from `game_players.php/register` as `private_key` and stored by the client). Tokens must never appear in URLs.\n\nAll request and response bodies are JSON. Errors are returned as `{\"success\": false, \"error\": \"<message>\"}` with HTTP 4xx/5xx. Machine-readable discovery: `/llms.txt`, `/llms-full.txt`, MCP endpoint at `POST /mcp` (streamable HTTP transport).\n\nUnity note: pass `?format=unity` and send JSON payloads as strings (`player_data_json`, `rules_json`, `request_data_json`, `response_data_json`) instead of nested objects.",
    "version": "1.0.0",
    "contact": {
      "name": "Nichita Levandovici (Michitai)",
      "email": "support@michitai.com",
      "url": "https://api.michitai.com/"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://api.michitai.com/LICENSE"
    }
  },
  "servers": [
    { "url": "https://api.michitai.com", "description": "Production" }
  ],
  "tags": [
    { "name": "Players", "description": "Player accounts: register, login, sessions, heartbeat, bans" },
    { "name": "GameData", "description": "Structured JSON persistence per game and per player" },
    { "name": "Rooms", "description": "Game rooms: lifecycle, player actions, updates" },
    { "name": "Matchmaking", "description": "Lobbies, join requests, direct join, lobby-to-room start" },
    { "name": "Leaderboard", "description": "Ranked queries over player_data fields" },
    { "name": "Friends", "description": "Friends lists, #tag lookup, Steam import, lobby/room invites" },
    { "name": "Realtime", "description": "Realtime transport tokens for wss/udp room events" },
    { "name": "Moderation", "description": "Server-side text screening" },
    { "name": "Server", "description": "Server utilities" }
  ],
  "components": {
    "securitySchemes": {
      "ApiToken": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Token",
        "description": "Project API key from the developer cabinet. Required on every endpoint."
      },
      "PrivateToken": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Private-Token",
        "description": "Project admin key from the cabinet. Required for admin operations (player list, ban/unban, game data update). Never ship it in a game client."
      },
      "PlayerToken": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Game-Player-Token",
        "description": "Per-player token returned once as `private_key` by game_players.php/register. The client stores it; rotate via game_players.php/rotate."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["success", "error"],
        "properties": {
          "success": { "type": "boolean", "enum": [false] },
          "error": { "type": "string", "description": "Human-readable, actionable error message" },
          "retry_after": { "type": "integer", "description": "Seconds until retry is allowed (HTTP 429 only)" }
        }
      },
      "JsonObject": {
        "type": "object",
        "description": "Arbitrary game-defined JSON payload",
        "additionalProperties": true
      },
      "PlayerTag": {
        "type": "string",
        "description": "Permanent public player tag, display form '#XXXXXXXX' (8 chars from digits 2-9 and consonants). Lookup accepts it with or without '#'.",
        "example": "#XK4B9P2R"
      },
      "FriendInfo": {
        "type": "object",
        "properties": {
          "player_id": { "type": "integer" },
          "player_name": { "type": "string" },
          "player_tag": { "$ref": "#/components/schemas/PlayerTag" },
          "is_online": { "type": "boolean" },
          "steam_id": { "type": ["string", "null"] },
          "relationship": { "type": ["string", "null"], "enum": ["friend", "incoming", "outgoing", null] }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid token(s)",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "BadRequest": {
        "description": "Validation or state error",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "NotFound": {
        "description": "Resource or endpoint not found",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "RateLimited": {
        "description": "Rate limit exceeded; honor Retry-After",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "parameters": {
      "MatchmakingId": {
        "name": "matchmakingId",
        "in": "path",
        "required": true,
        "schema": { "type": "string" },
        "description": "32-hex-character lobby id returned by matchmaking.php/create or /list"
      },
      "RequestId": {
        "name": "requestId",
        "in": "path",
        "required": true,
        "schema": { "type": "string" },
        "description": "Join-request id returned by matchmaking.php/{id}/request"
      },
      "RoomId": {
        "name": "roomId",
        "in": "path",
        "required": true,
        "schema": { "type": "string", "pattern": "^[a-f0-9-]{32,36}$" },
        "description": "32-hex-character room id returned by game_room.php/create or /list"
      },
      "ActionId": {
        "name": "actionId",
        "in": "path",
        "required": true,
        "schema": { "type": "string" },
        "description": "Action id returned by game_room.php/actions"
      }
    }
  },
  "paths": {
    "/api/game_players.php/register": {
      "post": {
        "tags": ["Players"],
        "summary": "Register a player (guest or Steam)",
        "description": "Creates a player account and returns `private_key` — the X-Game-Player-Token for all later player calls. Shown once; store it. For Steam, send `steam_ticket` (hex-encoded ISteamUser::GetAuthTicketForWebApi ticket, single-use) and `steam_identity` matching what the client passed; the same call creates the account on first use and logs in on later launches.",
        "operationId": "registerPlayer",
        "security": [{ "ApiToken": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "player_name": { "type": "string", "description": "2-50 chars, validated against the game's name rules; required for guest registration" },
                  "player_data": { "$ref": "#/components/schemas/JsonObject" },
                  "steam_ticket": { "type": "string", "description": "Hex-encoded Steam auth ticket; alternative to guest registration" },
                  "steam_identity": { "type": "string", "description": "Identity string passed to GetAuthTicketForWebApi (omit if none)" }
                }
              },
              "examples": {
                "guest": { "value": { "player_name": "TestPlayer", "player_data": { "level": 1, "score": 0 } } },
                "steam": { "value": { "steam_ticket": "14000000A1B2C3...", "steam_identity": "michitai", "player_name": "SteamPlayer" } }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Player registered; `private_key` is the player token, `player_tag` the public friend tag",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "player_id": { "type": "string" },
                    "private_key": { "type": "string", "description": "X-Game-Player-Token — shown once" },
                    "player_name": { "type": "string" },
                    "player_tag": { "$ref": "#/components/schemas/PlayerTag" },
                    "game_id": { "type": "integer" },
                    "auth": { "type": "string", "enum": ["guest", "steam"] },
                    "account": { "type": "string", "enum": ["created", "existing"] },
                    "steam_id": { "type": "string" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_players.php/login": {
      "put": {
        "tags": ["Players"],
        "summary": "Authenticate a player",
        "operationId": "loginPlayer",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "responses": {
          "200": {
            "description": "Player record",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "player": {
                      "type": "object",
                      "properties": {
                        "id": { "type": "integer" },
                        "game_id": { "type": "integer" },
                        "player_name": { "type": "string" },
                        "player_tag": { "$ref": "#/components/schemas/PlayerTag" },
                        "player_data": { "$ref": "#/components/schemas/JsonObject" },
                        "is_online": { "type": "boolean" },
                        "last_login": { "type": ["string", "null"] },
                        "last_heartbeat": { "type": ["string", "null"] },
                        "last_logout": { "type": ["string", "null"] },
                        "created_at": { "type": "string" },
                        "updated_at": { "type": "string" }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "description": "Player is banned", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/game_players.php/heartbeat": {
      "post": {
        "tags": ["Players"],
        "summary": "Update player heartbeat",
        "description": "Keeps the player marked online; send periodically while the game runs (a cron job marks stale players offline).",
        "operationId": "playerHeartbeat",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "responses": {
          "200": { "description": "Heartbeat recorded", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "message": { "type": "string" }, "last_heartbeat": { "type": "string" } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_players.php/logout": {
      "post": {
        "tags": ["Players"],
        "summary": "Logout player",
        "operationId": "logoutPlayer",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "responses": {
          "200": { "description": "Logged out", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "message": { "type": "string" }, "last_logout": { "type": "string" } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_players.php/rename": {
      "put": {
        "tags": ["Players"],
        "summary": "Rename player",
        "description": "New name must satisfy the game's per-project name rules (allowed scripts, case policy, length 2-50).",
        "operationId": "renamePlayer",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["new_name"], "properties": { "new_name": { "type": "string", "minLength": 2, "maxLength": 50 } } } } }
        },
        "responses": {
          "200": { "description": "Renamed", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "message": { "type": "string" }, "new_name": { "type": "string" }, "player_id": { "type": "integer" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_players.php/rotate": {
      "post": {
        "tags": ["Players"],
        "summary": "Rotate player token",
        "description": "Issues a new player token and invalidates the current one. Store the returned `private_key` — shown once.",
        "operationId": "rotatePlayerToken",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "responses": {
          "200": { "description": "New token issued", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "private_key": { "type": "string" }, "player_id": { "type": "integer" } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_players.php/list": {
      "get": {
        "tags": ["Players"],
        "summary": "List all players (admin)",
        "operationId": "listPlayers",
        "security": [{ "ApiToken": [], "PrivateToken": [] }],
        "responses": {
          "200": {
            "description": "All players in the project",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "count": { "type": "integer" },
                    "players": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "integer" },
                          "player_name": { "type": "string" },
                          "is_online": { "type": "boolean" },
                          "last_login": { "type": ["string", "null"] },
                          "last_logout": { "type": ["string", "null"] },
                          "last_heartbeat": { "type": ["string", "null"] },
                          "created_at": { "type": "string" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_players.php/ban": {
      "post": {
        "tags": ["Players"],
        "summary": "Ban a player (admin)",
        "operationId": "banPlayer",
        "security": [{ "ApiToken": [], "PrivateToken": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["player_id", "ban_duration"],
                "properties": {
                  "player_id": { "type": "integer" },
                  "ban_duration": { "type": "string", "enum": ["hour", "day", "week", "month", "quarter", "year", "forever"] },
                  "ban_reason": { "type": "string" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Player banned; `banned_until` is null for permanent bans", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "ban_id": { "type": "string" }, "player_id": { "type": "integer" }, "ban_duration": { "type": "string" }, "banned_until": { "type": ["string", "null"] } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_players.php/unban": {
      "post": {
        "tags": ["Players"],
        "summary": "Unban a player (admin)",
        "operationId": "unbanPlayer",
        "security": [{ "ApiToken": [], "PrivateToken": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["player_id"], "properties": { "player_id": { "type": "integer" } } } } } },
        "responses": {
          "200": { "description": "Player unbanned", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "player_id": { "type": "integer" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_data.php/game/get": {
      "get": {
        "tags": ["GameData"],
        "summary": "Get game-level data",
        "operationId": "getGameData",
        "security": [{ "ApiToken": [] }],
        "responses": {
          "200": { "description": "Stored game JSON", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "type": { "type": "string", "enum": ["game"] }, "game_id": { "type": "integer" }, "data": { "$ref": "#/components/schemas/JsonObject" } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_data.php/game/update": {
      "put": {
        "tags": ["GameData"],
        "summary": "Update game-level data (admin)",
        "description": "The request body is stored verbatim as the game's JSON document.",
        "operationId": "updateGameData",
        "security": [{ "ApiToken": [], "PrivateToken": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JsonObject" } } } },
        "responses": {
          "200": { "description": "Updated", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "updated_at": { "type": "string" } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_data.php/player/get": {
      "get": {
        "tags": ["GameData"],
        "summary": "Get the calling player's data",
        "operationId": "getPlayerData",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "responses": {
          "200": { "description": "Stored player JSON", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "type": { "type": "string", "enum": ["player"] }, "player_id": { "type": "integer" }, "player_name": { "type": "string" }, "data": { "$ref": "#/components/schemas/JsonObject" } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "description": "Player is banned", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/game_data.php/player/update": {
      "put": {
        "tags": ["GameData"],
        "summary": "Update the calling player's data",
        "description": "The request body is stored verbatim as the player's JSON document.",
        "operationId": "updatePlayerData",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/JsonObject" } } } },
        "responses": {
          "200": { "description": "Updated", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "updated_at": { "type": "string" } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_room.php/create": {
      "post": {
        "tags": ["Rooms"],
        "summary": "Create a game room (caller becomes host)",
        "description": "Returns `room_id` and a `join_token` others can use to bypass the password. Fails if the player is already in a room or a matchmaking lobby.",
        "operationId": "createRoom",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "room_name": { "type": "string", "maxLength": 120, "description": "Validated against the game's room-title rules" },
                  "password": { "type": "string", "description": "Optional room password; send \"\" later via /password to remove" },
                  "max_players": { "type": "integer", "minimum": 2, "maximum": 16, "default": 6 },
                  "host_switch": { "type": "boolean", "description": "Transfer host when the host leaves; false deletes the room on host leave" },
                  "realtime": { "type": "boolean", "description": "Enable realtime transport; members can then mint tokens via realtime.php/token" },
                  "rules": { "$ref": "#/components/schemas/JsonObject" },
                  "player_data": { "$ref": "#/components/schemas/JsonObject", "description": "Per-player data visible to room members" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Room created", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "room_id": { "type": "string" }, "room_name": { "type": "string" }, "join_token": { "type": "string" }, "is_host": { "type": "boolean" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_room.php/list": {
      "post": {
        "tags": ["Rooms"],
        "summary": "List joinable rooms",
        "operationId": "listRooms",
        "security": [{ "ApiToken": [] }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "properties": { "search": { "type": "string" }, "limit": { "type": "integer", "default": 20 } } } } }
        },
        "responses": {
          "200": {
            "description": "Rooms with free slots",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "rooms": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "room_id": { "type": "string" },
                          "room_name": { "type": "string" },
                          "max_players": { "type": "integer" },
                          "current_players": { "type": "integer" },
                          "has_password": { "type": "boolean" },
                          "host_switch": { "type": "boolean" },
                          "can_leave": { "type": "boolean" },
                          "realtime": { "type": "boolean" },
                          "rules": { "$ref": "#/components/schemas/JsonObject" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_room.php/{roomId}/join": {
      "post": {
        "tags": ["Rooms"],
        "summary": "Join a room",
        "description": "Send `password` for passworded rooms, or `join_token` (from an invite or /current) to bypass the password.",
        "operationId": "joinRoom",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "parameters": [{ "$ref": "#/components/parameters/RoomId" }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "password": { "type": "string" },
                  "join_token": { "type": "string", "description": "Invite token; bypasses password" },
                  "player_data": { "$ref": "#/components/schemas/JsonObject" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Joined", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "room_id": { "type": "string" }, "is_host": { "type": "boolean" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_room.php/players": {
      "get": {
        "tags": ["Rooms"],
        "summary": "List players in current room",
        "operationId": "listRoomPlayers",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "responses": {
          "200": { "description": "Room roster", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "players": { "type": "array", "items": { "$ref": "#/components/schemas/JsonObject" } }, "last_updated": { "type": "string" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_room.php/leave": {
      "post": {
        "tags": ["Rooms"],
        "summary": "Leave current room",
        "description": "If the host leaves: with `host_switch` the host role transfers, otherwise the room is deleted.",
        "operationId": "leaveRoom",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "responses": {
          "200": { "description": "Left", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_room.php/heartbeat": {
      "post": {
        "tags": ["Rooms"],
        "summary": "Room heartbeat",
        "operationId": "roomHeartbeat",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "responses": {
          "200": { "description": "Heartbeat recorded", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_room.php/current": {
      "get": {
        "tags": ["Rooms"],
        "summary": "Current room status",
        "description": "Room details for the calling player; members also see `join_token` for self-serve invites.",
        "operationId": "currentRoom",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "responses": {
          "200": { "description": "Room status", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "room": { "$ref": "#/components/schemas/JsonObject" } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_room.php/stop": {
      "post": {
        "tags": ["Rooms"],
        "summary": "Stop room (host only)",
        "operationId": "stopRoom",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "responses": {
          "200": { "description": "Stopped", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "message": { "type": "string" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_room.php/kick": {
      "post": {
        "tags": ["Rooms"],
        "summary": "Kick a player (host only)",
        "operationId": "kickRoomPlayer",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["player_id"], "properties": { "player_id": { "type": "integer" } } } } } },
        "responses": {
          "200": { "description": "Kicked", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "kicked_player_id": { "type": "integer" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_room.php/password": {
      "post": {
        "tags": ["Rooms"],
        "summary": "Set or remove room password (host only)",
        "description": "Send an empty string to remove the password.",
        "operationId": "setRoomPassword",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["password"], "properties": { "password": { "type": "string" } } } } } },
        "responses": {
          "200": { "description": "Updated", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "message": { "type": "string" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_room.php/actions": {
      "post": {
        "tags": ["Rooms"],
        "summary": "Send a request-response action to room players",
        "description": "Non-host players may only target \"host\". Targets complete the action via /actions/{actionId}/complete; the sender retrieves responses via /actions/poll.",
        "operationId": "sendRoomAction",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["target_players", "action_type"],
                "properties": {
                  "target_players": { "type": "string", "enum": ["all", "host", "others", "specific"] },
                  "target_players_ids": { "type": "array", "items": { "type": "integer" }, "description": "Required when target_players is \"specific\"" },
                  "action_type": { "type": "string", "description": "Game-defined verb, e.g. \"move\", \"attack\"" },
                  "request_data": { "$ref": "#/components/schemas/JsonObject" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Action queued per target", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "actions_sent": { "type": "integer" }, "action_ids": { "type": "array", "items": { "type": "string" } }, "target_players_ids": { "type": "array", "items": { "type": "integer" } } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_room.php/actions/poll": {
      "get": {
        "tags": ["Rooms"],
        "summary": "Poll responses to actions the caller sent",
        "operationId": "pollActionResponses",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "responses": {
          "200": { "description": "Completed/failed action responses", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "actions": { "type": "array", "items": { "$ref": "#/components/schemas/JsonObject" } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_room.php/actions/pending": {
      "get": {
        "tags": ["Rooms"],
        "summary": "Actions awaiting the caller's response",
        "operationId": "pendingActions",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "responses": {
          "200": { "description": "Pending actions addressed to this player", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "actions": { "type": "array", "items": { "$ref": "#/components/schemas/JsonObject" } } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_room.php/actions/{actionId}/complete": {
      "post": {
        "tags": ["Rooms"],
        "summary": "Complete a pending action",
        "operationId": "completeAction",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "parameters": [{ "$ref": "#/components/parameters/ActionId" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["status"],
                "properties": {
                  "status": { "type": "string", "enum": ["completed", "failed"] },
                  "response_data": { "$ref": "#/components/schemas/JsonObject" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Recorded", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_room.php/updates": {
      "post": {
        "tags": ["Rooms"],
        "summary": "Broadcast a fire-and-forget update",
        "description": "Non-host players may only target \"host\". Receivers read updates via /updates/poll.",
        "operationId": "sendRoomUpdate",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["target_players", "type"],
                "properties": {
                  "target_players": { "type": "string", "enum": ["all", "host", "others", "specific"] },
                  "target_players_ids": { "type": "array", "items": { "type": "integer" }, "description": "Required when target_players is \"specific\"" },
                  "type": { "type": "string", "description": "Game-defined event type, e.g. \"play_animation\"" },
                  "data": { "$ref": "#/components/schemas/JsonObject" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Queued", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/game_room.php/updates/poll": {
      "post": {
        "tags": ["Rooms"],
        "summary": "Poll incoming updates",
        "description": "Pass `last_update` from the previous response for incremental reads.",
        "operationId": "pollRoomUpdates",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "from_players": { "type": "string", "enum": ["all", "host", "others", "specific"], "default": "host" },
                  "from_players_ids": { "type": "array", "items": { "type": "integer" }, "description": "Required when from_players is \"specific\"" },
                  "last_update": { "type": "string", "description": "Cursor from the previous poll" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Updates since cursor", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "updates": { "type": "array", "items": { "$ref": "#/components/schemas/JsonObject" } }, "last_update": { "type": "string" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/matchmaking.php/list": {
      "post": {
        "tags": ["Matchmaking"],
        "summary": "List open matchmaking lobbies",
        "operationId": "listLobbies",
        "security": [{ "ApiToken": [] }],
        "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "search": { "type": "string" }, "limit": { "type": "integer", "default": 20 } } } } } },
        "responses": {
          "200": {
            "description": "Open lobbies",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "lobbies": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "matchmaking_id": { "type": "string" },
                          "matchmaking_name": { "type": "string" },
                          "max_players": { "type": "integer" },
                          "current_players": { "type": "integer" },
                          "has_password": { "type": "boolean" },
                          "realtime_room": { "type": "boolean" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/matchmaking.php/create": {
      "post": {
        "tags": ["Matchmaking"],
        "summary": "Create a matchmaking lobby (caller becomes host)",
        "description": "Returns `matchmaking_id` and `join_token` (shareable invite that bypasses password and host approval). `strict_full` requires a full lobby to /start; `join_by_requests` forces joins through /request + host approval.",
        "operationId": "createLobby",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["max_players"],
                "properties": {
                  "matchmaking_name": { "type": "string", "maxLength": 120, "description": "Validated against the game's room-title rules" },
                  "max_players": { "type": "integer", "minimum": 2, "maximum": 16 },
                  "strict_full": { "type": "boolean" },
                  "join_by_requests": { "type": "boolean" },
                  "host_switch": { "type": "boolean" },
                  "can_leave_room": { "type": "boolean", "description": "Carried into the game room created by /start" },
                  "realtime_room": { "type": "boolean", "description": "Started room supports realtime transport" },
                  "password": { "type": "string" },
                  "rules": { "$ref": "#/components/schemas/JsonObject" },
                  "player_data": { "$ref": "#/components/schemas/JsonObject" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Lobby created", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "matchmaking_id": { "type": "string" }, "matchmaking_name": { "type": "string" }, "max_players": { "type": "integer" }, "join_token": { "type": "string" }, "is_host": { "type": "boolean" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/matchmaking.php/{matchmakingId}/request": {
      "post": {
        "tags": ["Matchmaking"],
        "summary": "Request to join a lobby",
        "description": "For `join_by_requests` lobbies. Returns a `request_id`; poll it via /{requestId}/status until approved/rejected.",
        "operationId": "requestJoinLobby",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "parameters": [{ "$ref": "#/components/parameters/MatchmakingId" }],
        "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "player_data": { "$ref": "#/components/schemas/JsonObject" } } } } } },
        "responses": {
          "200": { "description": "Request filed", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "request_id": { "type": "string" }, "status": { "type": "string", "enum": ["pending"] } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/matchmaking.php/{requestId}/response": {
      "post": {
        "tags": ["Matchmaking"],
        "summary": "Approve or reject a join request (host only)",
        "operationId": "respondJoinRequest",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "parameters": [{ "$ref": "#/components/parameters/RequestId" }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["action"], "properties": { "action": { "type": "string", "enum": ["approve", "reject"] } } } } } },
        "responses": {
          "200": { "description": "Request resolved", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "status": { "type": "string", "enum": ["approved", "rejected"] } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/matchmaking.php/{requestId}/status": {
      "get": {
        "tags": ["Matchmaking"],
        "summary": "Check own join-request status",
        "operationId": "joinRequestStatus",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "parameters": [{ "$ref": "#/components/parameters/RequestId" }],
        "responses": {
          "200": { "description": "Request state", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "request": { "$ref": "#/components/schemas/JsonObject" } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/matchmaking.php/current": {
      "get": {
        "tags": ["Matchmaking"],
        "summary": "Current lobby status for the caller",
        "description": "Lobby details; members also see `join_token` for self-serve invites.",
        "operationId": "currentLobby",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "responses": {
          "200": { "description": "Lobby state", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "matchmaking": { "$ref": "#/components/schemas/JsonObject" } } } } } },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/matchmaking.php/{matchmakingId}/join": {
      "post": {
        "tags": ["Matchmaking"],
        "summary": "Join a lobby directly",
        "description": "Fails on `join_by_requests` lobbies — use /request instead. `password` or `join_token` may be required.",
        "operationId": "joinLobby",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "parameters": [{ "$ref": "#/components/parameters/MatchmakingId" }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "password": { "type": "string" },
                  "join_token": { "type": "string" },
                  "player_data": { "$ref": "#/components/schemas/JsonObject" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Joined", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "matchmaking_id": { "type": "string" }, "is_host": { "type": "boolean" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/matchmaking.php/leave": {
      "post": {
        "tags": ["Matchmaking"],
        "summary": "Leave current lobby",
        "operationId": "leaveLobby",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "responses": {
          "200": { "description": "Left", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/matchmaking.php/players": {
      "get": {
        "tags": ["Matchmaking"],
        "summary": "List players in current lobby",
        "operationId": "listLobbyPlayers",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "responses": {
          "200": { "description": "Lobby roster", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "players": { "type": "array", "items": { "$ref": "#/components/schemas/JsonObject" } } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/matchmaking.php/heartbeat": {
      "post": {
        "tags": ["Matchmaking"],
        "summary": "Lobby heartbeat",
        "operationId": "lobbyHeartbeat",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "responses": {
          "200": { "description": "Heartbeat recorded", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/matchmaking.php/remove": {
      "post": {
        "tags": ["Matchmaking"],
        "summary": "Delete lobby (host only)",
        "operationId": "removeLobby",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "responses": {
          "200": { "description": "Removed", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/matchmaking.php/start": {
      "post": {
        "tags": ["Matchmaking"],
        "summary": "Start game from lobby (host only)",
        "description": "Converts the lobby into a game room, moving all members. With `strict_full` the lobby must be full.",
        "operationId": "startLobby",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "responses": {
          "200": { "description": "Game room created from lobby", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "room_id": { "type": "string" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/matchmaking.php/stop": {
      "post": {
        "tags": ["Matchmaking"],
        "summary": "Stop lobby (host only, before start)",
        "operationId": "stopLobby",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "responses": {
          "200": { "description": "Stopped", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "message": { "type": "string" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/matchmaking.php/kick": {
      "post": {
        "tags": ["Matchmaking"],
        "summary": "Kick a player from lobby (host only, before start)",
        "operationId": "kickLobbyPlayer",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["player_id"], "properties": { "player_id": { "type": "integer" } } } } } },
        "responses": {
          "200": { "description": "Kicked", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "kicked_player_id": { "type": "integer" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/matchmaking.php/password": {
      "post": {
        "tags": ["Matchmaking"],
        "summary": "Set or remove lobby password (host only, before start)",
        "description": "Send an empty string to remove the password.",
        "operationId": "setLobbyPassword",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["password"], "properties": { "password": { "type": "string" } } } } } },
        "responses": {
          "200": { "description": "Updated", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "message": { "type": "string" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/leaderboard.php": {
      "post": {
        "tags": ["Leaderboard"],
        "summary": "Ranked leaderboard over player_data fields",
        "description": "Sorts players by numeric fields inside their stored `player_data` JSON. Order of `sort_by` defines tie-breaking.",
        "operationId": "getLeaderboard",
        "security": [{ "ApiToken": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["sort_by"],
                "properties": {
                  "sort_by": { "type": "array", "items": { "type": "string" }, "minItems": 1, "description": "player_data field names, e.g. [\"level\", \"score\"]" },
                  "limit": { "type": "integer", "minimum": 1, "maximum": 100, "default": 10 }
                }
              },
              "example": { "sort_by": ["level"], "limit": 10 }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ranked entries",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "leaderboard": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "rank": { "type": "integer" },
                          "player_id": { "type": "integer" },
                          "player_name": { "type": "string" },
                          "player_data": { "$ref": "#/components/schemas/JsonObject" }
                        }
                      }
                    },
                    "total": { "type": "integer" },
                    "sort_by": { "type": "array", "items": { "type": "string" } },
                    "limit": { "type": "integer" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/friends.php/list": {
      "get": {
        "tags": ["Friends"],
        "summary": "Friends list, pending requests, own tag",
        "operationId": "listFriends",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "responses": {
          "200": {
            "description": "Friends plus incoming/outgoing requests",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "player_tag": { "$ref": "#/components/schemas/PlayerTag" },
                    "friends": { "type": "array", "items": { "$ref": "#/components/schemas/FriendInfo" } },
                    "incoming": { "type": "array", "items": { "$ref": "#/components/schemas/FriendInfo" } },
                    "outgoing": { "type": "array", "items": { "$ref": "#/components/schemas/FriendInfo" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/friends.php/lookup": {
      "post": {
        "tags": ["Friends"],
        "summary": "Look up a player by #tag",
        "operationId": "lookupPlayer",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["player_tag"], "properties": { "player_tag": { "$ref": "#/components/schemas/PlayerTag" } } } } } },
        "responses": {
          "200": { "description": "Player plus relationship to caller", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "player": { "$ref": "#/components/schemas/FriendInfo" }, "relationship": { "type": ["string", "null"] } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/friends.php/request": {
      "post": {
        "tags": ["Friends"],
        "summary": "Send friend request",
        "description": "Target by `player_tag` or `player_id`. Auto-accepts when the target already requested the caller — then status is \"accepted\".",
        "operationId": "sendFriendRequest",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "player_tag": { "$ref": "#/components/schemas/PlayerTag" },
                  "player_id": { "type": "integer" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Request state", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "status": { "type": "string", "enum": ["pending", "accepted"] }, "player_id": { "type": "integer" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/friends.php/accept": {
      "post": {
        "tags": ["Friends"],
        "summary": "Accept an incoming friend request",
        "operationId": "acceptFriend",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["player_id"], "properties": { "player_id": { "type": "integer" } } } } } },
        "responses": {
          "200": { "description": "Accepted", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/friends.php/decline": {
      "post": {
        "tags": ["Friends"],
        "summary": "Decline an incoming friend request",
        "operationId": "declineFriend",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["player_id"], "properties": { "player_id": { "type": "integer" } } } } } },
        "responses": {
          "200": { "description": "Declined", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/friends.php/remove": {
      "post": {
        "tags": ["Friends"],
        "summary": "Remove a friend",
        "operationId": "removeFriend",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["player_id"], "properties": { "player_id": { "type": "integer" } } } } } },
        "responses": {
          "200": { "description": "Removed", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/friends.php/steam": {
      "post": {
        "tags": ["Friends"],
        "summary": "Find game accounts of Steam friends",
        "description": "Pass up to 100 SteamID64s; returns the game accounts that linked those Steam accounts — each a FriendInfo suitable for /request.",
        "operationId": "steamFriendLookup",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["steam_ids"],
                "properties": { "steam_ids": { "type": "array", "items": { "type": "string" }, "maxItems": 100 } }
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Matching players", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "players": { "type": "array", "items": { "$ref": "#/components/schemas/FriendInfo" } } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/friends.php/invite": {
      "post": {
        "tags": ["Friends"],
        "summary": "Invite a friend to the caller's lobby/room",
        "description": "The invite authorizes the invitee to join without password or host approval.",
        "operationId": "inviteFriend",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["player_id"], "properties": { "player_id": { "type": "integer" } } } } } },
        "responses": {
          "200": { "description": "Invite created", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "invite_id": { "type": "string" }, "status": { "type": "string", "enum": ["pending"] } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/friends.php/invites": {
      "get": {
        "tags": ["Friends"],
        "summary": "List pending lobby/room invites",
        "operationId": "listInvites",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "responses": {
          "200": {
            "description": "Pending invites",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "invites": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "invite_id": { "type": "string" },
                          "kind": { "type": "string", "enum": ["matchmaking", "room"] },
                          "name": { "type": "string" },
                          "from_player": { "$ref": "#/components/schemas/FriendInfo" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/friends.php/invites/accept": {
      "post": {
        "tags": ["Friends"],
        "summary": "Accept an invite — joins the lobby/room directly",
        "operationId": "acceptInvite",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["invite_id"], "properties": { "invite_id": { "type": "string" } } } } } },
        "responses": {
          "200": { "description": "Joined; matchmaking_id or room_id identifies where", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "status": { "type": "string" }, "invite_id": { "type": "string" }, "matchmaking_id": { "type": "string" }, "room_id": { "type": "string" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/friends.php/invites/decline": {
      "post": {
        "tags": ["Friends"],
        "summary": "Decline an invite",
        "operationId": "declineInvite",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["invite_id"], "properties": { "invite_id": { "type": "string" } } } } } },
        "responses": {
          "200": { "description": "Declined", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } } } } } },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/realtime.php/token": {
      "post": {
        "tags": ["Realtime"],
        "summary": "Mint a realtime connection token",
        "description": "Requires the player to be in a realtime-enabled room (created with realtime/realtime_room). Returns the token plus `realtime_server` (host, wss port, udp_port) the client connects to.",
        "operationId": "realtimeToken",
        "security": [{ "ApiToken": [], "PlayerToken": [] }],
        "responses": {
          "200": {
            "description": "Realtime token and server info",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "token": { "type": "string", "description": "Sent as X-Realtime-Token to the realtime service" },
                    "player_info": {
                      "type": "object",
                      "properties": {
                        "player_id": { "type": "integer" },
                        "player_name": { "type": "string" },
                        "room_id": { "type": "string" },
                        "is_host": { "type": "boolean" }
                      }
                    },
                    "realtime_server": {
                      "type": "object",
                      "properties": {
                        "host": { "type": "string" },
                        "port": { "type": "integer" },
                        "protocol": { "type": "string" },
                        "udp_port": { "type": "integer" }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/censor.php": {
      "post": {
        "tags": ["Moderation"],
        "summary": "Screen text through the server-side profanity filter",
        "description": "Authoritative moderation check — SDKs also ship a local hash-scan filter for instant feedback, but this server call is authoritative.",
        "operationId": "censorText",
        "security": [{ "ApiToken": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["text"],
                "properties": { "text": { "type": "string", "maxLength": 1000 } }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Verdict",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "clean": { "type": "boolean" },
                    "filtered": { "type": "string", "description": "Masked text, safe to display" }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/api/time.php": {
      "get": {
        "tags": ["Server"],
        "summary": "Server time (UTC)",
        "description": "Use for clock-independent logic. `utc` query param applies an integer hour offset; `format=unity` returns a Unity-friendly shape.",
        "operationId": "serverTime",
        "security": [{ "ApiToken": [] }],
        "parameters": [
          { "name": "utc", "in": "query", "required": false, "schema": { "type": "string", "pattern": "^[+-]?\\d+$" }, "description": "Hour offset, e.g. +1 or -2" },
          { "name": "format", "in": "query", "required": false, "schema": { "type": "string", "enum": ["json", "unity"] } }
        ],
        "responses": {
          "200": {
            "description": "Current server time",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "utc": { "type": "string", "format": "date-time" },
                    "timestamp": { "type": "integer" },
                    "readable": { "type": "string" },
                    "offset": {
                      "type": "object",
                      "properties": {
                        "offset_hours": { "type": "integer" },
                        "offset_string": { "type": "string" },
                        "original_utc": { "type": "string", "format": "date-time" },
                        "original_timestamp": { "type": "integer" }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    }
  }
}
