# Specter Multiplayer # API Documentation ## Base URL Production: `https://api.specterapp.xyz` ## Authentication All APIs (except login) require: - `Api-Key` header with your project API key - `Authorization` header with Bearer token obtained from login --- ## Authentication APIs ### 1. Login with Custom ID ```http POST https://api.specterapp.xyz/v2/client/auth/login-custom ``` **Headers:** ```json { "Content-Type": "application/json", "Api-Key": "{API_KEY}" } ``` **Request Body:** ```json { "customId": "string", "createAccount": true } ``` **Response:** ```json { "status": "success", "data": { "user": { "id": "string", "username": "string", "displayName": "string", "firstName": "string", "lastName": "string", "thumbUrl": "string", "hash": "string" }, "accessToken": "string" } } ``` --- ## Matchmaking APIs ### 2. Find Match ```http POST https://api.specterapp.xyz/v2/client/matchmaking/find-match ``` **Headers:** ```json { "Content-Type": "application/json", "Api-Key": "{API_KEY}", "Authorization": "Bearer {accessToken}" } ``` **Request Body:** ```json { "matchId": "string", // Game mode/match type ID "region": "string", // e.g., "ap-south-1" "partyId": "string" // Optional - if queuing with party } ``` **Response:** ```json { "status": "success", "message": "string" } ``` ### 3. Cancel Match ```http POST https://api.specterapp.xyz/v2/client/matchmaking/cancel-match ``` **Headers:** ```json { "Content-Type": "application/json", "Api-Key": "{API_KEY}", "Authorization": "Bearer {accessToken}" } ``` **Request Body:** ```json {} ``` **Response:** ```json { "status": "success", "message": "string" } ``` ### 4. Accept Match ```http POST https://api.specterapp.xyz/v2/client/matchmaking/accept-match ``` **Headers:** ```json { "Content-Type": "application/json", "Api-Key": "{API_KEY}", "Authorization": "Bearer {accessToken}" } ``` **Request Body:** ```json { "pendingMatchId": "string" // The unique pending match instance ID } ``` **Response:** ```json { "status": "success", "message": "string" } ``` ### 5. Decline Match ```http POST https://api.specterapp.xyz/v2/client/matchmaking/decline-match ``` **Headers:** ```json { "Content-Type": "application/json", "Api-Key": "{API_KEY}", "Authorization": "Bearer {accessToken}" } ``` **Request Body:** ```json { "pendingMatchId": "string" // The unique pending match instance ID } ``` **Response:** ```json { "status": "success", "message": "string" } ``` --- ### 6. Server Ready Notification ```http POST https://api.specterapp.xyz/v2/client/matchmaking/server-ready ``` **Headers:** ```json { "Content-Type": "application/json", "Api-Key": "{API_KEY}", "Authorization": "Bearer {accessToken}" } ``` **Request Body:** ```json { "matchSessionId": "string", // The session ID from match creation "serverInfo": { "host": "string", // Server IP/hostname "port": 7777, // Server port "transportType": "string" // Optional - transport protocol }, "roomId": "string" // Optional - room/instance ID } ``` **Response:** ```json { "status": "success", "code": 200, "errors": [], "message": "Server is ready", "data": [] } ``` **Note:** This endpoint should be called by your game server or orchestration provider when the server instance is ready to accept player connections. ## Match Session APIs ### 7. Start Match Session ```http POST https://api.specterapp.xyz/v2/client/matches/start-session ``` **Headers:** ```json { "Content-Type": "application/json", "Api-Key": "{API_KEY}", "Authorization": "Bearer {accessToken}" } ``` **Request Body:** ```json { "matchId": "string", // Dashboard match ID "matchSessionId": "string", // Unique session ID "userInfo": [ { "id": "string" // User ID (no teams at start) } ] } ``` **Response:** ```json { "status": "success", "data": { "matchSessionId": "string" } } ``` ### 8. End Match Session ```http POST https://api.specterapp.xyz/v2/client/matches/end-session ``` **Headers:** ```json { "Content-Type": "application/json", "Api-Key": "{API_KEY}", "Authorization": "Bearer {accessToken}" } ``` **Request Body:** ```json { "matchSessionId": "string", "userInfo": [ { "id": "string", "outcome": 100, // Final score "team": 1 // Optional - team assignment at end } ] } ``` **Response:** ```json { "status": "success", "message": "string" } ``` --- ## Party APIs ### 9. Create Party ```http POST https://api.specterapp.xyz/v2/client/party/create ``` **Headers:** ```json { "Content-Type": "application/json", "Api-Key": "{API_KEY}", "Authorization": "Bearer {accessToken}" } ``` **Request Body:** ```json { "type": "cooperative", // or "private" "maxSize": 5 // 2-10 players } ``` **Response:** ```json { "status": "success", "data": { "partyId": "string", "inviteCode": "string" // 6-digit code } } ``` ### 10. Join Party ```http POST https://api.specterapp.xyz/v2/client/party/join ``` **Headers:** ```json { "Content-Type": "application/json", "Api-Key": "{API_KEY}", "Authorization": "Bearer {accessToken}" } ``` **Request Body:** ```json { "inviteCode": "string" // 6-digit invite code } ``` **Response:** ```json { "status": "success", "message": "string" } ``` ### 11. Leave Party ```http POST https://api.specterapp.xyz/v2/client/party/leave ``` **Headers:** ```json { "Content-Type": "application/json", "Api-Key": "{API_KEY}", "Authorization": "Bearer {accessToken}" } ``` **Request Body:** ```json { "partyId": "string" } ``` **Response:** ```json { "status": "success", "message": "string" } ``` ### 12. Invite to Party ```http POST https://api.specterapp.xyz/v2/client/party/invite ``` **Headers:** ```json { "Content-Type": "application/json", "Api-Key": "{API_KEY}", "Authorization": "Bearer {accessToken}" } ``` **Request Body:** ```json { "partyId": "string", "inviteeId": "string" // User ID to invite } ``` **Response:** ```json { "status": "success", "data": { "inviteId": "string" } } ``` ### 13. Accept Party Invite ```http POST https://api.specterapp.xyz/v2/client/party/accept-invite ``` **Headers:** ```json { "Content-Type": "application/json", "Api-Key": "{API_KEY}", "Authorization": "Bearer {accessToken}" } ``` **Request Body:** ```json { "inviteId": "string" } ``` **Response:** ```json { "status": "success", "message": "string" } ``` ### 14. Decline Party Invite ```http POST https://api.specterapp.xyz/v2/client/party/decline-invite ``` **Headers:** ```json { "Content-Type": "application/json", "Api-Key": "{API_KEY}", "Authorization": "Bearer {accessToken}" } ``` **Request Body:** ```json { "inviteId": "string" } ``` **Response:** ```json { "status": "success", "message": "string" } ``` ### 15. Transfer Party Leadership ```http POST https://api.specterapp.xyz/v2/client/party/transfer-leader ``` **Headers:** ```json { "Content-Type": "application/json", "Api-Key": "{API_KEY}", "Authorization": "Bearer {accessToken}" } ``` **Request Body:** ```json { "partyId": "string", "newLeaderId": "string" } ``` **Response:** ```json { "status": "success", "message": "string" } ``` ### 16. Kick from Party ```http POST https://api.specterapp.xyz/v2/client/party/kick ``` **Headers:** ```json { "Content-Type": "application/json", "Api-Key": "{API_KEY}", "Authorization": "Bearer {accessToken}" } ``` **Request Body:** ```json { "partyId": "string", "userId": "string" // User to kick } ``` **Response:** ```json { "status": "success", "message": "string" } ``` --- ## Utility APIs ### 17. Get Players (Search) ```http POST https://api.specterapp.xyz/v2/client/app/get-players ``` **Headers:** ```json { "Content-Type": "application/json", "Api-Key": "{API_KEY}", "Authorization": "Bearer {accessToken}" } ``` **Request Body:** ```json { "filters": [ { "type": "string", // Enum: "username" | "firstName" | "lastName" | "email" | "customId" | "displayName" "value": "string" // Search term } ], "limit": 10 } ``` **Filter Types:** The `type` field in filters must be one of the following values: - `username` - Search by username - `firstName` - Search by first name - `lastName` - Search by last name - `email` - Search by email address - `customId` - Search by custom ID - `displayName` - Search by display name **Response:** ```json { "status": "success", "data": [ { "id": "string", "username": "string", "displayName": "string" // ... other user fields } ] } ``` --- ## Important Notes ### Match IDs - **matchId**: The dashboard/template match ID (configured in your dashboard) - **pendingMatchId**: The unique instance ID for accept/decline operations - **matchSessionId**: The actual game session ID ### Party Types - **cooperative**: For queuing together in matchmaking - **private**: For custom/private matches ### Error Responses All APIs may return error responses in the following format: ```json { "status": "error", "message": "Error description", "code": "ERROR_CODE" } ``` # WebSocket Events Documentation ## Connection Setup ### Authentication All WebSocket connections require authentication via handshake: ```javascript const socket = io('wss://multiplayer.specterapp.xyz', { auth: { token: 'YOUR_JWT_TOKEN', // JWT token from login apiKey: 'YOUR_API_KEY' // Project API key } }); ``` ### Connection Events #### `connect` Fired when successfully connected to the server. ```javascript socket.on('connect', () => { console.log('Connected to server'); }); ``` #### `disconnect` Fired when disconnected from the server. ```javascript socket.on('disconnect', (reason) => { console.log('Disconnected:', reason); }); ``` #### `connect_error` Fired when connection fails (usually authentication issues). ```javascript socket.on('connect_error', (error) => { console.log('Connection error:', error.message); }); ``` --- ## Matchmaking Events ### Client → Server Events **Note:** Matchmaking is primarily handled through HTTP APIs: - Find Match: `POST /v2/client/matchmaking/find-match` - Cancel Match: `POST /v2/client/matchmaking/cancel-match` - Accept Match: `POST /v2/client/matchmaking/accept-match` - Decline Match: `POST /v2/client/matchmaking/decline-match` ### Server → Client Events #### `queue:joined` Confirmation that player joined queue (automatically sent after find-match API call). ```javascript socket.on('queue:joined', (data) => { // data = { queueId: string, timestamp: number } }); ``` #### `queue:status` Periodic queue status updates (every 3 seconds while in queue). ```javascript socket.on('queue:status', (data) => { // data = { // position: number, // Your position in queue // estimatedTime: number, // Estimated wait time in seconds // playersInQueue: number // Total players in this queue // } }); ``` #### `match:found` Match has been found, waiting for acceptance. ```javascript socket.on('match:found', (data) => { // data = { // pendingMatchId: string, // Use this for accept/decline API calls // matchId: string, // Dashboard match ID // matchName: string, // players: [{ // userId: string, // username: string, // mmr: number, // team?: number // }], // acceptTimeout: number, // Seconds to accept (usually 15) // region: string // } }); ``` #### `match:reconnect` Reconnect to pending match after disconnect. ```javascript socket.on('match:reconnect', (data) => { // data = { // pendingMatchId: string, // matchId: string, // matchName: string, // players: Array, // acceptTimeout: number // Remaining seconds // } }); ``` #### `match:countdown` Countdown timer for match acceptance. ```javascript socket.on('match:countdown', (data) => { // data = { // pendingMatchId: string, // matchId: string, // timeLeft: number // Seconds remaining // } }); ``` #### `match:playerStatus` Update on which players have accepted. ```javascript socket.on('match:playerStatus', (data) => { // data = { // pendingMatchId: string, // matchId: string, // acceptedCount: number, // totalRequired: number, // acceptedPlayers: [{ userId, username }], // waitingPlayers: [{ userId, username }] // } }); ``` #### `match:accepted` Your match acceptance confirmed. ```javascript socket.on('match:accepted', (data) => { // data = { // pendingMatchId: string, // matchId: string, // userId: string // } }); ``` #### `match:confirmed` All players accepted, waiting for server. ```javascript socket.on('match:confirmed', (data) => { // data = { // pendingMatchId: string, // matchId: string, // sessionId: string, // status: 'waiting_for_server', // message: string // } }); ``` #### `match:server-ready` Game server is ready, connect now. ```javascript socket.on('match:server-ready', (data) => { // data = { // sessionId: string, // serverInfo: { // ip: string, // port: number, // connectionToken?: string // }, // matchName: string, // region: string, // players: [{ userId, username, team }] // } }); ``` #### `match:cancelled` Match was cancelled. ```javascript socket.on('match:cancelled', (data) => { // data = { // pendingMatchId: string, // matchId: string, // reason: 'timeout' | 'player_declined', // decliningPlayer?: string // userId if someone declined // } }); ``` #### `matchmaking:queue-left` Confirmation of leaving queue. ```javascript socket.on('matchmaking:queue-left', (data) => { // data = { success: boolean } }); ``` #### `matchmaking:error` Matchmaking error occurred. ```javascript socket.on('matchmaking:error', (data) => { // data = { message: string } }); ``` #### `queues:live-update` Global queue status broadcast (every 5 seconds). ```javascript socket.on('queues:live-update', (data) => { // data = { // 'queue:match_id:region': { // matchId: string, // matchName: string, // region: string, // players: [{ // userId: string, // username: string, // mmr: number, // waitTime: number, // Seconds in queue // priority: boolean, // priorityLevel: number, // isParty: boolean, // partyId: string | null // }], // count: number // } // } }); ``` --- ## Match Events ### Client → Server Events #### `match:leave` Leave the current match voluntarily. ```javascript socket.emit('match:leave', { matchSessionId: 'session_123', reason: 'voluntary' // optional }); ``` #### `match:update-score` Update player's score during match. ```javascript socket.emit('match:update-score', { matchSessionId: 'session_123', score: 150, metadata: { // optional kills: 10, deaths: 3, assists: 5 } }); ``` #### `match:get-leaderboard` Request current leaderboard. ```javascript socket.emit('match:get-leaderboard', { matchSessionId: 'session_123' }); ``` #### `match:get-status` Get match status and player connection states. ```javascript socket.emit('match:get-status', { matchSessionId: 'session_123' }); ``` ### Server → Client Events #### `match:started` Match has started. ```javascript socket.on('match:started', (data) => { // data = { // matchSessionId: string, // matchName: string, // players: [{ userId, username }], // isTeamBased: boolean, // numberOfTeams: number, // timestamp: number // } }); ``` #### `match:rejoined` Successfully rejoined match after disconnect. ```javascript socket.on('match:rejoined', (data) => { // data = { // matchSessionId: string, // status: 'active' | 'ended', // players: Array, // isTeamBased: boolean, // numberOfTeams: number, // startedAt: number // } }); ``` #### `match:leaderboard-update` Leaderboard has been updated (broadcast to all players). ```javascript socket.on('match:leaderboard-update', (data) => { // data = { // matchSessionId: string, // individual: [{ // userId: string, // username: string, // score: number, // rank: number, // metadata?: any // }], // updatedBy?: string, // Who triggered the update // timestamp?: number // } }); ``` #### `match:leaderboard` Leaderboard response (to requester only). ```javascript socket.on('match:leaderboard', (data) => { // data = { // matchSessionId: string, // individual: Array, // Same structure as above // timestamp: number // } }); ``` #### `match:status` Match status response. ```javascript socket.on('match:status', (data) => { // data = { // matchSessionId: string, // status: 'active' | 'ended', // players: [{ // userId: string, // username: string, // connected: boolean, // Currently connected? // hasLeft: boolean // Permanently left? // }], // isTeamBased: boolean, // numberOfTeams: number, // startedAt: number, // endedAt?: number // } }); ``` #### `match:left` Confirmation of leaving match. ```javascript socket.on('match:left', (data) => { // data = { // matchSessionId: string, // success: boolean // } }); ``` #### `match:player-disconnected` Another player disconnected (may reconnect). ```javascript socket.on('match:player-disconnected', (data) => { // data = { // userId: string, // username?: string, // timestamp: number // } }); ``` #### `match:player-reconnected` Another player reconnected. ```javascript socket.on('match:player-reconnected', (data) => { // data = { // userId: string, // timestamp: number // } }); ``` #### `match:player-left` Another player left voluntarily (won't return). ```javascript socket.on('match:player-left', (data) => { // data = { // userId: string, // username?: string, // timestamp: number // } }); ``` #### `match:player-abandoned` Player abandoned (timeout after disconnect - 60 seconds). ```javascript socket.on('match:player-abandoned', (data) => { // data = { // userId: string, // username?: string, // timestamp: number // } }); ``` #### `match:ended` Match has ended. ```javascript socket.on('match:ended', (data) => { // data = { // matchSessionId: string, // results: [{ // userId: string, // username: string, // outcome: number, // Final score // team?: number // Team assignment (if team-based) // }], // finalLeaderboard: Array, // finalTeamScores?: [{ // Only if team-based // team: number, // score: number, // rank: number // }], // timestamp: number // } }); ``` #### `match:error` Match-related error. ```javascript socket.on('match:error', (data) => { // data = { // message: string, // code: string // } }); ``` --- ## Party Events ### Client → Server Events #### `party:get-state` Get current party state and pending invites. ```javascript socket.emit('party:get-state'); ``` #### `party:ready-status` Update ready status (private parties only). ```javascript socket.emit('party:ready-status', { ready: true }); ``` #### `party:start-private-match` Start private match (leader only, private parties only). ```javascript socket.emit('party:start-private-match', { partyId: 'party_123', matchDetailId: 'match_config_id', projectId: 'project_id', region: 'us-west-1' }); ``` #### `party:chat` Send party chat message. ```javascript socket.emit('party:chat', { message: 'Hello party!' // Max 200 characters }); ``` **Note:** For party management, use the HTTP APIs: - Create Party: `POST /v2/client/party/create` - Join Party: `POST /v2/client/party/join` - Leave Party: `POST /v2/client/party/leave` - Invite to Party: `POST /v2/client/party/invite` - Accept Invite: `POST /v2/client/party/accept-invite` - Decline Invite: `POST /v2/client/party/decline-invite` - Kick from Party: `POST /v2/client/party/kick` - Transfer Leadership: `POST /v2/client/party/transfer-leader` ### Server → Client Events #### `party:current-state` Current party state (sent on connection if in party). ```javascript socket.on('party:current-state', (data) => { // data = { party: Party } }); ``` #### `party:pending-invites` Pending party invites (sent on connection). ```javascript socket.on('party:pending-invites', (data) => { // data = { invites: PartyInvite[] } }); ``` #### `party:state-update` Party state update response. ```javascript socket.on('party:state-update', (data) => { // data = { // currentParty: Party | null, // pendingInvites: PartyInvite[] // } }); ``` #### `party:created` Party was created (sent to creator). ```javascript socket.on('party:created', (data) => { // data = { party: Party } }); ``` #### `party:joined` Successfully joined party. ```javascript socket.on('party:joined', (data) => { // data = { party: Party } }); ``` #### `party:inviteReceived` Received party invite. ```javascript socket.on('party:inviteReceived', (data) => { // data = { // invite: PartyInvite, // partyId: string, // from: string, // userId who invited // inviteCode: string // 6-digit code // } }); ``` #### `party:memberJoined` New member joined party (broadcast to all). ```javascript socket.on('party:memberJoined', (data) => { // data = { // party: Party, // newMember: PartyMember, // userId: string, // partySize: number // } }); ``` #### `party:memberLeft` Member left party. ```javascript socket.on('party:memberLeft', (data) => { // data = { // party: Party, // userId: string, // partySize: number // } }); ``` #### `party:left` You left the party. ```javascript socket.on('party:left', (data) => { // data = { partyId: string } }); ``` #### `party:memberKicked` Member was kicked. ```javascript socket.on('party:memberKicked', (data) => { // data = { // party: Party, // userId: string, // kickedBy: string // } }); ``` #### `party:kicked` You were kicked. ```javascript socket.on('party:kicked', (data) => { // data = { // partyId: string, // kickedBy: string // } }); ``` #### `party:leaderChanged` Party leader changed. ```javascript socket.on('party:leaderChanged', (data) => { // data = { // party: Party, // oldLeader: string, // newLeader: string // } }); ``` #### `party:settingsChanged` Party settings changed. ```javascript socket.on('party:settingsChanged', (data) => { // data = { // settings: any, // party: Party // } }); ``` #### `party:stateChanged` Party state changed. ```javascript socket.on('party:stateChanged', (data) => { // data = { // oldState: 'idle' | 'queuing' | 'in_match', // newState: 'idle' | 'queuing' | 'in_match', // party: Party // } }); ``` #### `party:queueStatusChange` Party queue status changed. ```javascript socket.on('party:queueStatusChange', (data) => { // data = { // inQueue: boolean, // queueInfo?: { // matchDetailId: string, // region: string, // queuedAt: number // } // } }); ``` #### `party:memberReadyStatus` Member ready status changed (private parties). ```javascript socket.on('party:memberReadyStatus', (data) => { // data = { // userId: string, // ready: boolean // } }); ``` #### `party:matchStarting` Private match is starting. ```javascript socket.on('party:matchStarting', (data) => { // data = { // matchSessionId: string, // matchDetailId: string, // region: string // } }); ``` #### `party:chat-message` Party chat message received. ```javascript socket.on('party:chat-message', (data) => { // data = { // userId: string, // message: string, // timestamp: number, // partyId: string // } }); ``` #### `party:disbanded` Party was disbanded. ```javascript socket.on('party:disbanded', (data) => { // data = { partyId: string } }); ``` #### `party:error` Party-related error. ```javascript socket.on('party:error', (data) => { // data = { // message: string, // code: string // } }); ``` --- ## Data Types ### Party ```typescript interface Party { partyId: string; leader: string; members: PartyMember[]; type: 'cooperative' | 'private'; maxSize: number; state: 'idle' | 'queuing' | 'in_match'; createdAt: number; updatedAt: number; inviteCode?: string; currentQueue?: { matchDetailId: string; region: string; queuedAt: number; }; } ``` ### PartyMember ```typescript interface PartyMember { userId: string; username?: string; displayName?: string; firstName?: string; lastName?: string; mmr: number; joinedAt: number; isReady?: boolean; } ``` ### PartyInvite ```typescript interface PartyInvite { inviteId: string; partyId: string; invitedBy: string; invitedUser: string; createdAt: number; expiresAt: number; status: 'pending' | 'accepted' | 'declined' | 'expired'; } ``` --- ## Error Handling All services may emit error events with consistent structure: ```javascript socket.on('error', (data) => { // data = { // message: string, // code?: string, // timestamp: string // } }); ``` Common error codes: - `AUTH_ERROR`: Authentication failed - `STATE_ERROR`: Invalid state transition - `PERMISSION_ERROR`: Insufficient permissions - `NOT_FOUND`: Resource not found - `ALREADY_EXISTS`: Resource already exists - `TIMEOUT`: Operation timed out - `QUEUE_ERROR`: Queue operation failed - `MATCH_ERROR`: Match operation failed - `PARTY_ERROR`: Party operation failed - `LEAVE_MATCH_ERROR`: Error leaving match - `SCORE_UPDATE_ERROR`: Error updating score - `LEADERBOARD_ERROR`: Error getting leaderboard - `STATUS_ERROR`: Error getting status - `READY_ERROR`: Error updating ready status - `MATCH_START_ERROR`: Error starting match - `CHAT_ERROR`: Chat message error --- ## Best Practices 1. **Always handle disconnections**: Implement reconnection logic with exponential backoff. 2. **Subscribe to events before emitting**: Ensure event listeners are set up before triggering actions. 3. **Store critical state locally**: Keep match IDs, party IDs, and session IDs for reconnection. 4. **Handle all error events**: Implement error handlers for service-specific errors. 5. **Use rooms efficiently**: The server uses room-based broadcasting, so events are targeted. 6. **Respect rate limits**: Don't spam events, especially score updates or chat messages. 7. **Clean up listeners**: Remove event listeners when components unmount to prevent memory leaks. 8. **Use APIs for state changes**: Use HTTP APIs for operations that change state (create, join, leave, etc.), WebSocket for real-time updates. 9. **Handle reconnection**: On reconnect, check for active matches and rejoin rooms automatically. ## Connection Example ```javascript // Complete connection setup const socket = io('wss://multiplayer.specterapp.xyz', { auth: { token: localStorage.getItem('accessToken'), apiKey: 'YOUR_API_KEY' }, reconnection: true, reconnectionDelay: 1000, reconnectionDelayMax: 5000, reconnectionAttempts: 5 }); // Global error handler socket.on('error', (error) => { console.error('Socket error:', error); }); // Connection lifecycle socket.on('connect', () => { console.log('Connected'); // Check for rejoin states socket.emit('party:get-state'); }); socket.on('disconnect', (reason) => { console.log('Disconnected:', reason); if (reason === 'io server disconnect') { // Server disconnected us, try manual reconnect socket.connect(); } }); // Service-specific error handlers socket.on('matchmaking:error', (error) => { console.error('Matchmaking error:', error); }); socket.on('match:error', (error) => { console.error('Match error:', error); }); socket.on('party:error', (error) => { console.error('Party error:', error); }); ``` ## Flow Summary ### Matchmaking Flow 1. Call API: `POST /v2/client/matchmaking/find-match` 2. Listen: `queue:joined` for confirmation 3. Listen: `queue:status` for position updates 4. Listen: `match:found` when match is ready 5. Call API: `POST /v2/client/matchmaking/accept-match` or `decline-match` 6. Listen: `match:confirmed` when all accept 7. Listen: `match:server-ready` for connection details ### Match Flow 1. Listen: `match:started` when match begins 2. Emit: `match:update-score` to update scores 3. Listen: `match:leaderboard-update` for score changes 4. Emit: `match:leave` to leave early 5. Listen: `match:ended` when match completes ### Party Flow 1. Call API: `POST /v2/client/party/create` or `join` 2. Listen: `party:created` or `party:joined` 3. Emit: `party:chat` for chat messages 4. Listen: `party:memberJoined` for new members 5. For private matches: - Emit: `party:ready-status` to signal ready - Leader emits: `party:start-private-match` # Specter Multiplayer - Technical Integration Reference ## Overview This document provides the exact API calls and WebSocket events needed to implement each multiplayer flow. Follow these sequences to integrate Specter into your game. --- ## Initial Setup Requirements ### Required Headers for All API Calls: ``` Api-Key: {YOUR_API_KEY} Authorization: Bearer {ACCESS_TOKEN} // After login Content-Type: application/json ``` ### WebSocket Connection: ``` URL: wss://multiplayer.specterapp.xyz Auth: { token: {ACCESS_TOKEN}, apiKey: {API_KEY} } ``` --- ## Authentication Sequence ### Step 1: Login **API Call:** ``` POST /v2/client/auth/login-custom Body: { "customId": "player_unique_id", "createAccount": true } ``` **Response Contains:** - `accessToken` - Use for all future requests - `user.id` - Player's Specter ID - `user.username` - Display name ### Step 2: Establish WebSocket **Connect with auth:** - Use `accessToken` from login - Pass in handshake auth object **Listen for:** - `connect` - Connection successful - `disconnect` - Lost connection - `connect_error` - Authentication failed --- ## Solo Matchmaking Flow ### 1. Start Matchmaking **API Call:** ``` POST /v2/client/matchmaking/find-match Body: { "matchId": "your_match_config_id", "region": "us-west-1" } ``` **Immediate Events:** - `queue:joined` - Confirms entry to queue **Continuous Events (every 3 seconds):** - `queue:status` - Contains: - `position` - Place in queue - `estimatedTime` - Seconds to wait - `playersInQueue` - Total waiting ### 2. Match Found Phase **Event Received:** - `match:found` - Contains: - `pendingMatchId` - **IMPORTANT: Save this for accept/decline** - `matchId` - Dashboard match ID - `players` - Array of matched players - `acceptTimeout` - Seconds to respond (usually 15) **Countdown Events:** - `match:countdown` - Every second with `timeLeft` ### 3. Accept or Decline **To Accept - API Call:** ``` POST /v2/client/matchmaking/accept-match Body: { "pendingMatchId": "match_xxx" // From match:found event } ``` **To Decline - API Call:** ``` POST /v2/client/matchmaking/decline-match Body: { "pendingMatchId": "match_xxx" } ``` **Events After Your Response:** - `match:accepted` - Your acceptance confirmed - `match:playerStatus` - Shows who accepted/waiting ### 4. Match Confirmation **When All Accept - Event:** - `match:confirmed` - Contains: - `sessionId` - Game session ID - `status` - "waiting_for_server" ### 5. Server Ready **Event Received:** - `match:server-ready` - Contains: - `sessionId` - Match session ID - `serverInfo.ip` - Game server IP - `serverInfo.port` - Game server port - `serverInfo.connectionToken` - Optional auth token - `players` - Final player list with teams (if applicable) **Now connect to game server with provided details** ### 6. Cancel Matchmaking **API Call (anytime before match found):** ``` POST /v2/client/matchmaking/cancel-match Body: {} ``` **Event Received:** - `matchmaking:queue-left` - Confirms cancellation --- ## Party System Flow ### Creating a Party **API Call:** ``` POST /v2/client/party/create Body: { "type": "cooperative", // or "private" "maxSize": 5 } ``` **Response Contains:** - `partyId` - Party identifier - `inviteCode` - 6-digit code to share **Events Received:** - `party:created` - Party details - `party:current-state` - Full party state ### Joining a Party **Option A - With Invite Code:** ``` POST /v2/client/party/join Body: { "inviteCode": "ABC123" } ``` **Option B - Accept Direct Invite:** First, receive invite event: - `party:inviteReceived` - Contains `inviteId` Then accept: ``` POST /v2/client/party/accept-invite Body: { "inviteId": "invite_xxx" } ``` **Events After Joining:** - `party:joined` - You joined successfully - `party:memberJoined` - Broadcast to all members ### Party Management **Invite Someone:** ``` POST /v2/client/party/invite Body: { "partyId": "party_xxx", "inviteeId": "user_id_to_invite" } ``` **Leave Party:** ``` POST /v2/client/party/leave Body: { "partyId": "party_xxx" } ``` Events: `party:left` (you), `party:memberLeft` (others) **Kick Member (Leader Only):** ``` POST /v2/client/party/kick Body: { "partyId": "party_xxx", "userId": "user_to_kick" } ``` Events: `party:kicked` (them), `party:memberKicked` (others) **Transfer Leadership:** ``` POST /v2/client/party/transfer-leader Body: { "partyId": "party_xxx", "newLeaderId": "user_xxx" } ``` Event: `party:leaderChanged` ### Party States & Events **State Change Events:** - `party:stateChanged` - When party state changes (idle/queuing/in_match) - `party:queueStatusChange` - When entering/leaving queue - `party:disbanded` - Party was disbanded **Real-time Features:** **Party Chat:** ``` Socket Emit: party:chat Data: { "message": "Hello team!" } ``` Receive: `party:chat-message` **Ready Status (Private Parties):** ``` Socket Emit: party:ready-status Data: { "ready": true } ``` Receive: `party:memberReadyStatus` ### Party Matchmaking **Leader Starts Queue:** ``` POST /v2/client/matchmaking/find-match Body: { "matchId": "match_config_id", "region": "us-west-1", "partyId": "party_xxx" // Include party ID } ``` **All Members Receive:** - Same matchmaking events as solo - Each member must accept individually - If any decline, whole party returns to queue ### Private Match (Party Only) **Leader Starts Private Match:** ``` Socket Emit: party:start-private-match Data: { "partyId": "party_xxx", "matchDetailId": "match_config_id", "projectId": "your_project_id", "region": "us-west-1" } ``` **Events:** - `party:matchStarting` - Match is being created - Then normal match flow events --- ## Match Session Management ### Starting a Match Session **After all players connect to game server, one player (master client/host) starts the session:** **API Call (Master Client):** ``` POST /v2/client/matches/start-session Body: { "matchId": "match_config_id", "matchSessionId": "unique_session_id", "userInfo": [ { "id": "user1_id" }, { "id": "user2_id" } // Just IDs, no teams yet ] } ``` **All Players Receive:** - `match:started` - Contains: - `matchSessionId` - Active session ID - `players` - Player list - `isTeamBased` - Boolean - `numberOfTeams` - Team count ### During Match **Player Rejoining After Disconnect:** - Automatic on reconnect - Receive: `match:rejoined` with current state **Update Score (Any Player for themselves):** ``` Socket Emit: match:update-score Data: { "matchSessionId": "session_xxx", "score": 150, "metadata": { // Optional "kills": 10, "deaths": 3 } } ``` **Everyone Receives:** - `match:leaderboard-update` - New rankings **Get Current Leaderboard:** ``` Socket Emit: match:get-leaderboard Data: { "matchSessionId": "session_xxx" } ``` Receive: `match:leaderboard` **Get Match Status:** ``` Socket Emit: match:get-status Data: { "matchSessionId": "session_xxx" } ``` Receive: `match:status` with player connection states **Leave Match Early:** ``` Socket Emit: match:leave Data: { "matchSessionId": "session_xxx", "reason": "voluntary" } ``` Receive: `match:left` ### Player Connection Events **Other Players:** - `match:player-disconnected` - Temporary disconnect - `match:player-reconnected` - They're back - `match:player-abandoned` - Gone for 60+ seconds - `match:player-left` - Voluntarily quit ### Ending a Match **API Call (Master Client/Host):** ``` POST /v2/client/matches/end-session Body: { "matchSessionId": "session_xxx", "userInfo": [ { "id": "user1_id", "outcome": 100, // Final score "team": 1 // Optional team assignment }, { "id": "user2_id", "outcome": 85, "team": 2 } ] } ``` **All Players Receive:** - `match:ended` - Contains: - `results` - Final scores and teams - `finalLeaderboard` - Individual rankings - `finalTeamScores` - Team totals (if applicable) --- ## Server Ready Notification (For Orchestration) **If using Hathora/other orchestration, when game server is ready:** ``` POST /v2/client/matchmaking/server-ready Body: { "matchSessionId": "session_xxx", "serverInfo": { "host": "123.45.67.89", "port": 7777, "transportType": "tcp" // Optional }, "roomId": "room_xxx" // Optional } ``` This triggers `match:server-ready` event to all players. **Note:** This is called by your game server instance, not by a player client. --- ## Complete Flow Examples with API/Events ### Example: Solo Quick Match 1. **Login:** - Call: `POST /auth/login-custom` - Save: `accessToken` 2. **Connect WebSocket:** - Connect with token - Wait for: `connect` event 3. **Queue:** - Call: `POST /matchmaking/find-match` - Receive: `queue:joined` - Monitor: `queue:status` updates 4. **Match Found:** - Receive: `match:found` - Save: `pendingMatchId` - Watch: `match:countdown` 5. **Accept:** - Call: `POST /matchmaking/accept-match` with `pendingMatchId` - Receive: `match:accepted` - Monitor: `match:playerStatus` 6. **Confirmed:** - Receive: `match:confirmed` with `sessionId` - Wait for: `match:server-ready` (if using orchestration) - Or proceed directly to connect (if P2P or self-hosted) 7. **Connect to Game:** - Use `serverInfo` from `match:server-ready` event - Or establish P2P connections with other players - All players connect to game 8. **Start Match Session:** - **Master Client calls:** `POST /matches/start-session` - Include `matchSessionId` and all player IDs - **All players receive:** `match:started` 9. **Play & Update Scores:** - Each player emits: `match:update-score` for themselves - Everyone monitors: `match:leaderboard-update` 10. **End Match:** - **Master Client calls:** `POST /matches/end-session` - Include final scores and optional team assignments - **All players receive:** `match:ended` ### Example: Party Queue Together 1. **Both Login** (separately) 2. **Create Party:** - Player A: `POST /party/create` - Player A receives: `party:created` - Get: `inviteCode` 3. **Join Party:** - Player B: `POST /party/join` with `inviteCode` - Both receive: `party:memberJoined` 4. **Queue Together:** - Leader: `POST /matchmaking/find-match` with `partyId` - Both receive: all queue events 5. **Both Accept:** - Each calls: `POST /matchmaking/accept-match` - Monitor: `match:playerStatus` 6. **Connect to Game:** - Both connect using server info - May be placed on same team 7. **Start Match:** - **Master Client calls:** `POST /matches/start-session` - **Both receive:** `match:started` 8. **Play Together:** - Update scores individually - End match as normal ### Example: Private Party Match 1. **Create Private Party:** - `POST /party/create` with `type: "private"` 2. **Friends Join:** - Share `inviteCode` - Each: `POST /party/join` 3. **Ready Up:** - Each emits: `party:ready-status` - Monitor: `party:memberReadyStatus` 4. **Start:** - Leader emits: `party:start-private-match` - Receive: `match:confirmed` directly (no matchmaking) 5. **Connect & Start:** - All connect to game - **Master Client calls:** `POST /matches/start-session` - **All receive:** `match:started` 6. **Play:** - Normal match flow from here --- ## Master Client Responsibilities The master client (host) is responsible for: 1. **Starting the match:** Call `POST /matches/start-session` once all players connected 2. **Ending the match:** Call `POST /matches/end-session` with final results 3. **Managing game state:** If P2P, handle authoritative game logic ### Determining Master Client: - First player to connect - Player with lowest user ID - Designated host in private matches - Party leader in party matches --- ## Error Events to Handle ### Matchmaking Errors: - `matchmaking:error` - General matchmaking failure - `match:cancelled` - Match was cancelled (timeout/decline) ### Match Errors: - `match:error` - Match operation failed - Codes: `LEAVE_MATCH_ERROR`, `SCORE_UPDATE_ERROR`, etc. ### Party Errors: - `party:error` - Party operation failed - Codes: `STATE_ERROR`, `PERMISSION_ERROR`, etc. ### Connection Errors: - `connect_error` - Authentication failed - `disconnect` - Lost connection (attempt reconnect) --- ## Global Broadcasts to Monitor ### Queue Updates (Every 5 seconds): - `queues:live-update` - All active queues and players Useful for: - Showing global queue stats - Spectator features - Admin monitoring --- ## State Tracking Requirements ### Must Store: 1. **accessToken** - For all API calls 2. **currentUser.id** - Your player ID 3. **pendingMatchId** - For accept/decline 4. **matchSessionId** - During active match 5. **partyId** - If in party 6. **serverInfo** - To connect to game 7. **isMasterClient** - To know if responsible for start/end ### Should Monitor: 1. **Queue position** - From `queue:status` 2. **Party members** - From party events 3. **Match players** - From match events 4. **Connection state** - Socket connected/disconnected --- ## Testing Checklist ### Authentication: - [ ] Login successful → Store token - [ ] WebSocket connects → Receive `connect` - [ ] Invalid token → Handle `connect_error` ### Matchmaking: - [ ] Find match → Receive `queue:joined` - [ ] Queue updates → Get `queue:status` - [ ] Match found → Save `pendingMatchId` - [ ] Accept match → See `match:confirmed` - [ ] Decline match → Return to queue - [ ] Timeout → Handle `match:cancelled` ### Parties: - [ ] Create party → Get `inviteCode` - [ ] Join party → Receive `party:joined` - [ ] Leave party → Receive `party:left` - [ ] Party queues → All get match events - [ ] Private match → Skip matchmaking ### Matches: - [ ] Master client starts → Call start-session API - [ ] Match starts → All receive `match:started` - [ ] Score updates → See leaderboard changes - [ ] Disconnect → Can rejoin - [ ] Master client ends → Call end-session API - [ ] Match ends → All get final results --- ## Quick Reference ### Key API Endpoints: - Login: `POST /auth/login-custom` - Find Match: `POST /matchmaking/find-match` - Accept: `POST /matchmaking/accept-match` - Decline: `POST /matchmaking/decline-match` - Cancel: `POST /matchmaking/cancel-match` - Create Party: `POST /party/create` - Join Party: `POST /party/join` - **Start Session: `POST /matches/start-session` (Master Client)** - **End Session: `POST /matches/end-session` (Master Client)** ### Critical WebSocket Events: - `queue:status` - Queue position - `match:found` - Match ready to accept - `match:confirmed` - All accepted - `match:server-ready` - Can connect (if using orchestration) - `match:started` - Game begun (after start-session call) - `match:ended` - Game over (after end-session call) - `party:memberJoined` - Party updated - `match:leaderboard-update` - Scores changed ### Important Notes: - **Always use `pendingMatchId` from `match:found` for accept/decline** - **Master client must call start/end session APIs** - **Each player updates their own score via WebSocket** - **Teams are optional and only specified at match end** # Specter Multiplayer Integration Guide ## Overview This guide explains how to integrate Specter's multiplayer system into your game. We'll cover all the flows step-by-step so you understand exactly what happens when players matchmake, form parties, and play matches. --- ## Understanding the Three Main Systems ### 1. **Matchmaking System** Handles finding opponents for players. Players join queues, get matched based on skill/region, and then enter matches. ### 2. **Party System** Lets friends group up before matchmaking. Players can invite friends, queue together, or play private matches. ### 3. **Match System** Manages the actual game session - tracking scores, handling disconnections, and syncing game state. --- ## Player Authentication Flow **Every player must log in first before anything else works.** ### Steps: 1. **Player launches game** → Your game calls login API with their unique ID 2. **Server returns access token** → Save this, you need it for everything 3. **Connect to WebSocket** → Use the token to establish real-time connection 4. **Ready for multiplayer** → Player can now matchmake, create parties, etc. ### What the player sees: - Loading screen → "Connecting to servers..." → Main menu --- ## Solo Matchmaking Flow **When a player wants to find a match alone (no party)** ### Player's Journey: #### Finding a Match: 1. **Player clicks "Find Match"** - Your game calls find-match API - Player enters matchmaking queue 2. **Player waits in queue** - Every 3 seconds, you get queue position updates - Show: "Position 5 of 12" or "Estimated wait: 30 seconds" - Player can cancel anytime 3. **Match found!** - Server found enough players (based on your match config) - 15-second countdown starts - Show all players found with their usernames/MMR 4. **Accept/Decline Phase** - Player must click Accept within 15 seconds - Show who has accepted (green checkmarks) - If anyone declines or timeout → back to queue with priority 5. **All players accepted** - Match is confirmed - Waiting for game server (if using orchestration) - Show: "Setting up match..." 6. **Server ready** - Receive server IP and port - Connect to game server - Start playing! ### Handling Edge Cases: - **Player disconnects while in queue** → Automatically removed from queue - **Player doesn't respond to match** → Given cooldown penalty (configurable) - **Someone declines match** → Others return to queue with priority (skip ahead) --- ## Party System Flow **When players want to play with friends** ### Creating and Managing a Party: #### Party Creation: 1. **Player clicks "Create Party"** - Choose type: Cooperative (for matchmaking) or Private (custom games) - Set max size (2-10 players) - Receive 6-digit invite code 2. **Inviting Friends** - **Option A**: Share 6-digit code → Friends enter code to join - **Option B**: Direct invite → Search player by username, send invite - Invites expire after 5 minutes 3. **Friends Join Party** - They accept invite or enter code - Everyone sees party members list update in real-time - Party chat becomes available 4. **Party Management** - Leader can kick members - Leader can transfer leadership - Members can leave anytime (unless in match) - If leader leaves, oldest member becomes leader ### Party Matchmaking Flow: 1. **Leader clicks "Find Match"** - Entire party enters queue as one unit - All members see queue status - Only leader can cancel 2. **Match Found** - All party members get match notification simultaneously - Each member must accept individually - If any party member declines → whole party returns to queue 3. **In Match** - Party members can be on same team (if team-based) - Party stays together after match ends - Return to party lobby together ### Private Match Flow (Party Only): 1. **Create Private Party** - Set party type as "Private" - Invite exact players you want 2. **Everyone Readies Up** - Each player marks ready - Leader sees ready status 3. **Leader Starts Match** - No matchmaking needed - Direct match creation - Custom rules possible --- ## Match Session Flow **Once players are in an actual game** ### Match Lifecycle: #### Match Start: 1. **Match Session Created** - All players receive match details - Know if team-based or free-for-all - Get starting positions/teams 2. **Players Connect** - Each player's connection tracked - Show connection status to others - 60-second grace period for reconnection #### During Match: **Score Updates:** - Players update scores in real-time - Everyone sees leaderboard updates - Individual scores always tracked - Team scores calculated if applicable **Player States:** - **Connected** - Actively playing - **Disconnected** - Temporary, can reconnect - **Abandoned** - Gone for 60+ seconds - **Left** - Voluntarily quit, won't return **Handling Disconnections:** - Player loses connection → Mark as disconnected - 60 seconds to reconnect → Auto-rejoin match - After 60 seconds → Mark as abandoned - Match continues without them #### Match End: 1. **End Conditions Met** - Time runs out OR - Score target reached OR - Manual end by server 2. **Final Results** - Submit final scores with optional team assignments - Calculate rankings - Update player stats 3. **Post-Match** - Return players to lobby/party - Ready for next match --- ## Team-Based vs Free-For-All Matches ### Free-For-All (No Teams): - Every player for themselves - Simple scoring - just individual scores - Examples: Battle Royale, Deathmatch, Racing **Configuration:** - Set numberOfTeams = 0 - Define min/max players - No team balancing needed ### Team-Based Matches: - Players divided into teams - Team assignment happens during matchmaking - Final results include team rankings **Configuration:** - Set numberOfTeams (2 for 1v1, 3 for 3-way, etc.) - Set teamSize (players per team) - Choose balancing algorithm **Team Formation Methods:** 1. **Random** - Randomly distribute players 2. **Skill-Based** - Balance teams by MMR 3. **Party-Based** - Keep party members together **Important:** Teams are assigned by matchmaking but only revealed at match end. During the match, you only track individual scores. --- ## Complete Flow Examples ### Example 1: Solo Player Quick Match 1. Login → Main Menu 2. Click "Quick Match" → Enter queue 3. Wait (see position updates) → Match found! 4. Accept → Wait for others 5. All accepted → Connect to game server 6. Play match → Update scores 7. Match ends → See results 8. Return to main menu ### Example 2: Friends Playing Together 1. Both players login 2. Player A creates party (Cooperative) 3. Player A shares code "ABC123" 4. Player B joins with code 5. Both see each other in party 6. Player A (leader) clicks "Find Match" 7. Both enter queue together 8. Match found → Both must accept 9. Play match (might be on same team) 10. Match ends → Return to party lobby 11. Ready for next match together ### Example 3: Private Custom Match 1. Create Private party (5v5 custom) 2. Invite 9 specific friends 3. Everyone joins party 4. Set custom rules (if supported) 5. Everyone marks ready 6. Leader starts match 7. No matchmaking - direct to game 8. Play with custom settings 9. End → Back to party --- ## Understanding Match States ### For Matchmaking: - **Finding Match** → In queue, waiting - **Match Found** → Need to accept/decline - **Match Accepted** → Waiting for all players - **Match Confirmed** → Creating session - **Server Ready** → Can connect and play ### For Parties: - **Idle** → In lobby, can do anything - **Queuing** → In matchmaking together - **In Match** → Currently playing ### For Players in Match: - **Active** → Currently playing - **Disconnected** → Lost connection, can return - **Abandoned** → Gone too long - **Left** → Quit voluntarily --- ## Important Configuration Points ### When Setting Up Matches in Dashboard: **Queue Settings:** - `maximumQueueTimeSeconds`: How long before removing from queue - `acceptMatchTimeoutSeconds`: Time to accept match (usually 15) - `declinePenaltySeconds`: Cooldown for declining - `noResponsePenaltySeconds`: Cooldown for not responding **Match Settings:** - `minPlayers`: Minimum to start match - `maxPlayers`: Maximum in one match - `matchDurationMinutes`: How long matches last - `reconnectGracePeriod`: Time to reconnect (60 seconds) **Skill Settings:** - `skillBasedEnabled`: Use MMR matching? - `initialMmrRange`: Starting search range (e.g., ±100) - `mmrExpansionInterval`: Expand range every X seconds - `maximumMmrRange`: Maximum range to search **Party Settings:** - `partiesAllowed`: Can parties queue? - `maxPartySize`: Largest party (2-10) - `preferPartyVsParty`: Try matching parties against parties? --- ## Common Integration Patterns ### Pattern 1: Quick Match Only - No party system needed - Simple find match → play → repeat - Good for: Mobile games, casual games ### Pattern 2: Social Multiplayer - Full party system - Play with friends - Both matchmaking and private matches - Good for: Team games, social games ### Pattern 3: Competitive Ranked - Solo or duo queue only - Strict skill matching - No private matches - Good for: Competitive games, esports ### Pattern 4: Battle Royale Style - Large player counts - No teams during match - Quick requeue after death - Good for: BR games, survival games --- ## Error Handling ### What Players Might See: **"Already in Queue"** - Player tried to queue twice - Solution: Cancel first, then requeue **"Party Leader Only"** - Non-leader tried to queue party - Solution: Ask leader to start **"Cannot Leave During Match"** - Tried leaving party mid-match - Solution: Wait for match to end **"Match No Longer Available"** - Took too long to accept - Solution: Requeue **"Party is Full"** - Tried joining full party - Solution: Create new party --- ## Best Practices ### For Players: 1. **Show clear status** - Always show what's happening 2. **Allow cancellation** - Let players back out when possible 3. **Handle errors gracefully** - Don't crash, show friendly messages 4. **Reconnect automatically** - Don't make players restart 5. **Update frequently** - Keep UI responsive with status updates ### For Parties: 1. **Leader indicators** - Make it clear who's leader 2. **Ready status** - Show who's ready in private matches 3. **Party persistence** - Keep party together between matches 4. **Chat support** - Let party members communicate 5. **Invite expiry** - Clean up old invites ### For Matches: 1. **Grace periods** - Give time for reconnections 2. **Progressive updates** - Update scores frequently, not just at end 3. **Handle leavers** - Game continues even if players leave 4. **Clear endings** - Make match end conditions obvious 5. **Quick restarts** - Get players back into action fast --- ## Debugging Tips ### Use WebSocket events to debug: - Listen to ALL events initially - Log state changes - Track player status - Monitor queue positions - Check party states ### Common Issues: - **"Not authenticated"** → Token expired, re-login - **"No match found"** → Not enough players, check min players - **"Connection timeout"** → WebSocket died, reconnect - **"Party not found"** → Party expired (1 hour timeout) - **"Already in match"** → Previous match not cleaned up --- ## Summary The Specter Multiplayer system handles the complex parts of multiplayer for you: - **Matchmaking** finds appropriate opponents - **Parties** let friends play together - **Matches** track game sessions and scores Your game just needs to: 1. Authenticate players 2. Call the right APIs at the right time 3. Listen for WebSocket events 4. Connect to game servers when ready 5. Update scores during matches The flows are designed to handle edge cases like disconnections, party management, and skill-based matching automatically. Focus on your gameplay - let Specter handle the multiplayer complexity. # Specter + Hathora Integration Guide ## Overview This guide explains how Hathora dedicated game servers automatically provision when players are matched through Specter's matchmaking system. --- ## Setup Requirements ### 1. Specter Dashboard Configuration - Navigate to Orchestration settings in your Specter project - Select **Hathora** as provider - Enter your **Hathora Dev Token** - Save configuration That's all the setup needed in Specter! ### 2. Hathora Dashboard Setup - Create your application in Hathora - Deploy your game server build - Copy your **App ID** and **Deployment ID** - Generate a **Dev Token** for Specter --- ## Region Mapping Configuration When players select a region for matchmaking, Specter automatically maps to the appropriate Hathora server location using the `provider-regions.config.ts` file: ### Region Mapping Table **US Regions:** - `us-east-1` → `Washington_DC` - `us-east-2` → `Washington_DC` - `us-west-1` → `Los_Angeles` - `us-west-2` → `Seattle` **Canada:** - `ca-central-1` → `Chicago` **Europe:** - `eu-north-1` → `London` - `eu-west-1` → `London` - `eu-west-2` → `London` - `eu-west-3` → `Frankfurt` - `eu-central-1` → `Frankfurt` - `eu-south-1` → `Frankfurt` **Middle East & Africa:** - `me-south-1` → `Dubai` - `af-south-1` → `Johannesburg` **Asia Pacific:** - `ap-south-1` → `Mumbai` - `ap-southeast-1` → `Singapore` - `ap-southeast-2` → `Sydney` - `ap-northeast-1` → `Tokyo` - `ap-northeast-2` → `Tokyo` - `ap-northeast-3` → `Tokyo` - `ap-east-1` → `Singapore` **South America:** - `sa-east-1` → `Sao_Paulo` **Note:** The region codes (like `us-west-1`) are just identifiers. What matters is where the actual game server gets provisioned in Hathora. --- ## Integration Flow ### Phase 1: Matchmaking Completes 1. Players queue for a match (selecting region like `us-west-1`) 2. Matchmaking finds compatible players queuing in same region 3. All players accept the match 4. Players receive `match:confirmed` event with `sessionId` ### Phase 2: Automatic Server Provisioning **This happens automatically behind the scenes:** 1. **Specter detects** Hathora is enabled for this match 2. **Specter maps** the region: `us-west-1` → `Los_Angeles` 3. **Specter calls** Hathora API: ``` POST https://api.hathora.dev/rooms/v2/{appId}/create { "roomId": "{matchSessionId}", "region": "Los_Angeles", "roomConfig": { "sessionId": "xxx", "matchDetailId": "xxx", "playerCount": 10, "players": [...] } } ``` 4. **Hathora provisions** a game server in Los Angeles 5. **Game server starts** with the room configuration ### Phase 3: Server Ready 1. **Your game server code** (running on Hathora) calls back to Specter: ``` POST /v2/client/matchmaking/server-ready { "matchSessionId": "session_xxx", "serverInfo": { "host": "123.45.67.89", "port": 7777 } } ``` 2. **Specter broadcasts** to all players: - Event: `match:server-ready` - Data includes server IP and port ### Phase 4: Players Connect 1. Each player receives `match:server-ready` event 2. Players extract connection info: - IP: `serverInfo.host` - Port: `serverInfo.port` 3. Players connect directly to Hathora game server 4. Game begins! --- ## What Your Game Server Needs ### On Server Start: ```csharp // Example in Unity/C# void Start() { // 1. Get room config from Hathora environment string roomConfig = Environment.GetEnvironmentVariable("ROOM_CONFIG"); var config = JsonUtility.FromJson<RoomConfig>(roomConfig); // 2. Notify Specter that server is ready StartCoroutine(NotifyServerReady(config.sessionId)); } IEnumerator NotifyServerReady(string sessionId) { var serverInfo = new { matchSessionId = sessionId, serverInfo = new { host = GetPublicIP(), // Hathora provides this port = 7777 // Your game port } }; // Call Specter API yield return PostToSpecter("/v2/client/matchmaking/server-ready", serverInfo); } ``` ### Room Config Structure: The room config Hathora receives contains: - `sessionId` - The match session ID - `matchDetailId` - The match configuration ID - `matchName` - Name of the game mode - `playerCount` - Number of players - `players` - Array with userId, username, team --- ## Player Connection Flow ### What Players Do: ```javascript // JavaScript example socket.on('match:server-ready', (data) => { const { serverInfo, sessionId } = data; // Connect to Hathora game server connectToGameServer({ ip: serverInfo.host, port: serverInfo.port, sessionId: sessionId }); }); function connectToGameServer({ ip, port, sessionId }) { // Your game's connection logic // Could be WebSocket, UDP, TCP, etc. const gameConnection = new GameClient(); gameConnection.connect(ip, port); gameConnection.join(sessionId); } ``` --- ## How This Compares to Other Approaches ### With Orchestration (Hathora): - Matchmaking finds players - Server automatically provisions in correct region - Players get server IP/port - Connect and play ### Without Orchestration (P2P or Self-Hosted): - Matchmaking finds players - One player hosts or you have pre-running servers - Players connect directly to host or existing server - Play begins **The region selection is the same either way** - it's just about grouping players who want to play in the same geographic area. The difference is whether a server gets automatically provisioned (Hathora) or not. --- ## Testing the Integration ### 1. Test Region Mapping: - Queue in region `us-west-1` - Verify server provisions in Hathora's `Los_Angeles` - Check server location in Hathora dashboard ### 2. Test Connection Flow: - Complete matchmaking - Wait for `match:server-ready` event - Verify IP/port are correct - Connect and play ### 3. Test Multiple Regions: - Queue players in different regions - Verify correct Hathora servers provision - Check latency matches expected region --- ## Troubleshooting ### Server Never Becomes Ready: - Check your game server is calling `/matchmaking/server-ready` - Verify the `matchSessionId` matches - Check Hathora logs for server startup errors ### Wrong Region Selected: - Verify region mapping in `provider-regions.config.ts` - Check player's selected region in matchmaking - Confirm Hathora supports that region ### Connection Failed: - Verify server IP and port from `match:server-ready` event - Check firewall/security groups in Hathora - Ensure game server is listening on correct port --- ## Important Notes 1. **Automatic Provisioning:** Servers are created automatically when matches are confirmed - no manual intervention needed 2. **Region Identifiers:** The region codes (us-west-1, etc.) are just labels for matchmaking. What matters is where Hathora actually provisions the server. 3. **Server Lifecycle:** Hathora automatically terminates servers after inactivity (configurable in Hathora dashboard) 4. **Room ID = Session ID:** The Hathora room ID is the same as the Specter match session ID for easy correlation 5. **Server Must Callback:** Your game server MUST call the server-ready endpoint, or players will wait forever --- ## Benefits of This Integration - **Zero Manual Server Management:** Servers spawn automatically per match - **Regional Game Servers:** Players get servers close to them for low latency - **Cost Efficient:** Only pay for active game time - **No DevOps Required:** Hathora handles all infrastructure - **Industry Standard:** This is how major games handle matchmaking + dedicated servers The integration is designed to be invisible to players - they just click "Find Match" and end up connected to a game server in their region!