Documentation Index
Fetch the complete documentation index at: https://mintlify.com/ValoSpectra/Spectra-Server/llms.txt
Use this file to discover all available pages before exploring further.
The MatchController handles the complete lifecycle of matches in Spectra Server, from creation through data synchronization to automatic cleanup.
Match Lifecycle
Matches in Spectra Server go through several stages:
1. Match Creation
Matches are created during observer authentication:
async createMatch(data: IAuthenticationData) {
const existingMatch = this.matches[data.groupCode];
if (existingMatch != null) {
if (data.groupSecret !== existingMatch.groupSecret) {
// Reject: different secret
return "";
}
// Allow reconnection
return "reconnected";
}
const newMatch = new Match(data);
this.matches[data.groupCode] = newMatch;
this.eventNumbers[data.groupCode] = 0;
this.codeToTeamInfo[data.groupCode] = {
leftTeam: data.leftTeam,
rightTeam: data.rightTeam
};
this.teamInfoExpiry[data.groupCode] = Date.now() + 1000 * 60 * 60; // 1 hour
this.startOutgoingSendLoop();
return newMatch.groupSecret;
}
Source: src/controller/MatchController.ts:42-72
The MatchController is a singleton, ensuring only one instance manages all matches across the server.
2. Data Reception
The controller receives match data from authenticated clients:
async receiveMatchData(data: IAuthedData | IAuthedAuxData) {
data.timestamp = Date.now();
// Observer data (has groupCode)
if ("groupCode" in data) {
const trackedMatch = this.matches[data.groupCode];
if (trackedMatch == null) {
return; // Invalid group code
}
await trackedMatch.receiveMatchSpecificData(data);
}
// Auxiliary data (has matchId)
else if ("matchId" in data) {
for (const match of Object.values(this.matches)) {
if (match.matchId == data.matchId) {
await match.receiveMatchSpecificData(data);
}
}
}
}
Source: src/controller/MatchController.ts:96-117
3. State Synchronization
The server broadcasts match updates to overlay clients at 10Hz (100ms intervals):
private startOutgoingSendLoop() {
if (this.sendInterval != null) return; // Already running
this.sendInterval = setInterval(async () => {
for (const groupCode in this.matches) {
// Only send if there are new events
if (this.matches[groupCode].eventNumber > this.eventNumbers[groupCode]) {
this.outgoingWebsocketServer.sendMatchData(
groupCode,
this.matches[groupCode]
);
this.eventNumbers[groupCode] = this.matches[groupCode].eventNumber;
this.eventTimes[groupCode] = Date.now();
}
}
}, 100); // 100ms = 10Hz
}
Source: src/controller/MatchController.ts:154-185
4. Automatic Cleanup
Matches are automatically cleaned up after 30 minutes of inactivity:
// Check if the last event was more than 30 minutes ago
if (Date.now() - this.eventTimes[groupCode] > 1000 * 60 * 30) {
Log.info(
`Match with group code ${groupCode} has been inactive for more than 30 minutes, removing.`
);
try {
if (this.matches[groupCode].isRegistered) {
await DatabaseConnector.completeMatch(this.matches[groupCode]);
}
} catch (e) {
Log.error(`Failed to complete match in backend with group code ${groupCode}, ${e}`);
}
this.removeMatch(groupCode);
}
Source: src/controller/MatchController.ts:167-182
The 30-minute timeout starts from the last received event, not from match creation. Keep sending heartbeat events to prevent premature cleanup.
5. Manual Match Removal
Matches can also be removed manually:
removeMatch(groupCode: string) {
if (this.matches[groupCode] != null) {
delete this.matches[groupCode];
delete this.eventNumbers[groupCode];
WebsocketIncoming.disconnectGroupCode(groupCode);
// Stop send loop if no matches remain
if (Object.keys(this.matches).length == 0 && this.sendInterval != null) {
clearInterval(this.sendInterval);
this.sendInterval = null;
}
}
}
Source: src/controller/MatchController.ts:78-90
Group Codes and Match Identification
Group Codes
Group codes are the primary identifier for matches:
- Format: 6-character alphanumeric string (recommended)
- Uniqueness: Must be unique across active matches
- Case: Usually uppercase for readability
- Persistence: Group code remains for 1 hour after match completion (for team info)
Match IDs
Each match also has a unique match ID (UUID) used for:
- Auxiliary client authentication
- Backend API integration
- Cross-referencing match data
Find a match by its ID:
findMatch(matchId: string) {
return Object.values(this.matches)
.find((match) => match.matchId == matchId)
?.groupCode ?? null;
}
Source: src/controller/MatchController.ts:74-76
Team information is stored separately from match objects with its own expiry:
private codeToTeamInfo: Record<string, { leftTeam: AuthTeam; rightTeam: AuthTeam }> = {};
private teamInfoExpiry: Record<string, number> = {};
Team Data Structure
interface AuthTeam {
name: string; // Full team name
tricode: string; // 3-4 letter abbreviation
url: string; // Team logo URL
attackStart: boolean; // Which side starts attacking
}
Team Info Cleanup
Team information expires 1 hour after match creation:
const cleanupInterval = setInterval(() => {
const now = Date.now();
for (const groupCode in this.teamInfoExpiry) {
if (now > this.teamInfoExpiry[groupCode]) {
delete this.codeToTeamInfo[groupCode];
delete this.teamInfoExpiry[groupCode];
}
}
}, 1000 * 60 * 5); // Check every 5 minutes
Source: src/controller/MatchController.ts:22-34
public getTeamInfoForCode(groupCode: string) {
const teamInfo = this.codeToTeamInfo[groupCode];
if (teamInfo) {
return teamInfo;
} else {
return undefined;
}
}
Source: src/controller/MatchController.ts:188-195
Event Number Tracking
Each match has an event number that increments with every state change:
private matches: Record<string, Match> = {};
private eventNumbers: Record<string, number> = {};
private eventTimes: Record<string, number> = {};
The event number is used to:
- Track when new data is available
- Determine if updates should be broadcast
- Monitor match activity for cleanup
if (this.matches[groupCode].eventNumber > this.eventNumbers[groupCode]) {
// New event available, broadcast it
this.outgoingWebsocketServer.sendMatchData(groupCode, this.matches[groupCode]);
this.eventNumbers[groupCode] = this.matches[groupCode].eventNumber;
this.eventTimes[groupCode] = Date.now();
}
Source: src/controller/MatchController.ts:162-165
Auxiliary Client Management
The controller manages auxiliary client connections (player cameras, etc.):
setAuxDisconnected(groupCode: string, playerId: string) {
if (this.matches[groupCode] != null) {
this.matches[groupCode].setAuxDisconnected(playerId);
}
}
Source: src/controller/MatchController.ts:128-132
When an auxiliary client disconnects, the match is notified to update player camera states.
Match Data for Overlay Logon
When a new overlay client connects, it receives the current match state:
sendMatchDataForLogon(groupCode: string) {
if (this.matches[groupCode] != null) {
const {
replayLog,
eventNumber,
timeoutEndTimeout,
timeoutRemainingLoop,
playercamUrl,
...formattedData
} = this.matches[groupCode] as any;
this.outgoingWebsocketServer.sendMatchData(groupCode, formattedData);
}
}
Source: src/controller/MatchController.ts:134-152
Internal fields (replayLog, timeouts, etc.) are excluded from the data sent to overlay clients.
Monitoring Matches
Get Active Match Count
getMatchCount() {
return Object.keys(this.matches).length;
}
Source: src/controller/MatchController.ts:92-94
Best Practices
Use meaningful group codes
Choose group codes that are easy to communicate and unlikely to collide:// Good
groupCode: "FNATIC"
groupCode: "MATCH1"
// Avoid
groupCode: "A"
groupCode: "123"
Store group secrets securely
Save the group secret returned during authentication to enable reconnection:const secret = acknowledgment.reason; // On successful auth
// Store secret for reconnection
Send regular heartbeats
To prevent 30-minute timeout, send periodic events even during pauses:// Send heartbeat every 5 minutes during long pauses
setInterval(() => {
sendMatchData({ type: "heartbeat", timestamp: Date.now() });
}, 5 * 60 * 1000);
Handle reconnection gracefully
If connection drops, authenticate again with the same group code and secret:if (response.reason === "reconnected") {
// Successfully reconnected to existing match
}