Skip to main content

Introduction

The AutoRef system is the brain of the tournament automation. It uses finite state machines to orchestrate match flow, respond to player actions, and execute Bancho IRC commands without manual intervention. Each match spawns an independent AutoRef worker that:
  • Connects to Bancho IRC with referee credentials
  • Parses BanchoBot messages to detect events
  • Transitions through predefined states based on rules
  • Executes commands (!mp map, !mp start, etc.) automatically
  • Relays all activity to Discord threads
AutoRef implements the IAutoRef interface (AutoRef/IAutoRef.cs:6), which defines the contract for StartAsync(), StopAsync(), and SendMessageFromDiscord().

Two Automation Modes

The system provides specialized automation for different tournament phases:

Qualifier Automation

Class: AutoRefQualifiersStage (AutoRef/AutoRefQualifiersStage.cs:46)Manages linear mappool progression with automatic loading and timing:

Characteristics

  • No picks or bans: Maps play in fixed order
  • Multi-player lobbies: All registered players compete simultaneously
  • Auto-progression: Advances to next map after cooldown
  • Simple flow: Idle → WaitingForStart → Playing → (repeat)

Workflow

  1. Referee types >start to engage automation
  2. AutoRef loads first map from Round.MapPool
  3. Sets mods based on slot (e.g., “NM1” → NoMod, “HD2” → Hidden)
  4. Starts 120-second ready timer
  5. Begins match when players ready or timer expires
  6. After map finishes, waits 10 seconds (cooldown)
  7. Loads next map and repeats until pool exhausted

State Machine

Key Implementation Details

// AutoRefQualifiersStage.cs:399
private async Task PrepareNextQualifierMap()
{
    if (currentMapIndex >= currentMatch!.Round.MapPool.Count)
    {
        await SendMessageBothWays(Strings.QualifiersOver);
        currentState = MatchState.MatchFinished;
        return;
    }

    var beatmap = currentMatch.Round.MapPool[currentMapIndex];

    await SendMessageBothWays($"!mp map {beatmap.BeatmapID}");
    await SendMessageBothWays($"!mp mods {beatmap.Slot[..2]} NF");
    await SendMessageBothWays("!mp timer 120");

    currentState = MatchState.WaitingForStart;
}
The system uses an async void pattern to create non-blocking cooldowns:
// AutoRefQualifiersStage.cs:356
if (banchoMsg.Contains("The match has finished"))
{
    currentMapIndex++;
    currentState = MatchState.Idle;

    // Fire-and-forget: 10s cooldown before next map
    _ = Task.Run(async () =>
    {
        await Task.Delay(10000);
        await PrepareNextQualifierMap();
    });
}
This intentional fire-and-forget pattern creates breathing room between maps without blocking the event loop.
The AutoRef can bulk-invite all registered players:
// AutoRefQualifiersStage.cs:256
case "invite":
    foreach (var osuId in usersInRoom)
    {
        await SendMessageBothWays($"!mp invite #{osuId}");
        await Task.Delay(500); // Rate limiting
    }
    break;
Players are loaded from the database on startup:
usersInRoom = await db.Players
    .Where(p => p.QualifierRoomId == matchId)
    .Select(p => p.User.OsuID)
    .ToListAsync();

When Automation Triggers

Starting Automation

Automation begins when the referee issues the >start command:
1

Prerequisites

  • Match environment created via /ref start [match_id]
  • AutoRef connected to Bancho IRC
  • Lobby initialized with team settings
  • (Elimination only): >firstpick and >firstban configured
2

Engagement

Referee types >start in Discord thread or IRC lobby
3

Initialization

  • Qualifiers: Sets currentMapIndex = 0, loads first map
  • Elimination: Transitions to BanPhaseStart, requests first ban
4

Event Loop

AutoRef begins listening to BanchoBot messages and driving state transitions

Stopping Automation

Automation can be paused or stopped:
Command: >stop
  • Pauses the state machine at current state
  • Saves previousState for resume
  • Can be resumed with >start (continues from saved state)
// Common to both AutoRef types
case "stop":
    if (currentState == MatchState.Idle)
    {
        await SendMessageBothWays(Strings.AutoAlreadyStopped);
        break;
    }
    await SendMessageBothWays(Strings.StoppingAuto);
    previousState = currentState;
    currentState = MatchState.Idle;
    stoppedPreviously = true;
    break;

Safety Mechanisms

Panic Protocol

The !panic system provides emergency intervention:

Trigger

  • Who: Anyone in the lobby (players, referee, spectators)
  • Command: !panic (typed in IRC or Discord)
  • Effect: Immediately transitions to MatchOnHold state

Behavior

// Common to both AutoRef types (example from QualifiersStage.cs:208)
else if (content.Contains("!panic"))
{
    currentState = MatchState.MatchOnHold;
    await SendMessageBothWays("!mp aborttimer");

    await SendMessageBothWays(
        string.Format(Strings.Panic, 
            Environment.GetEnvironmentVariable("DISCORD_REFEREE_ROLE_ID"), 
            senderNick)
    );
}
This:
  • Aborts any active countdown timers
  • Pings the referee role in Discord
  • Prevents all state transitions
  • Ignores all automation logic

Recovery

  • Who: Referee only (validated by username)
  • Command: >panic_over
  • Effect: Returns to WaitingForStart with 10-second timer
if (content.Contains(">panic_over") && 
    senderNick == currentMatch!.Referee.DisplayName.Replace(' ', '_'))
{
    await SendMessageBothWays(Strings.BackToAuto);
    currentState = MatchState.WaitingForStart;
    await SendMessageBothWays("!mp timer 10");
}
Critical Design Decision: Panic protocol is decoupled from the main state machine to ensure it works regardless of current state.

Timeout System (Elimination Only)

Each team gets one 120-second timeout per match:

Usage

  • Command: !timeout (typed by team player)
  • Validation: Checks if team has already used their timeout
  • Effect: Transitions to OnTimeout state, saves previousState
// AutoRefEliminationStage.cs:576
if (sender == currentMatch!.TeamRed.DisplayName.Replace(' ', '_') && !redTimeoutRequest)
{
    await SendMessageBothWays(Strings.RedTimeout);
    previousState = currentState;
    currentState = MatchState.OnTimeout;
    redTimeoutRequest = true; // Mark as used
}

Auto-Resume

When the 120-second countdown finishes:
if (currentState == MatchState.OnTimeout && 
    sender == "BanchoBot" && 
    content == "Countdown finished")
{
    await SendMessageBothWays("!mp timer 120"); // Extra buffer
    await SendMessageBothWays(Strings.TimeoutStart);
    currentState = previousState; // Return to saved state
}
Timeouts can only be called during waiting states (WaitingForPick, WaitingForStart), not during active gameplay.

State Validation

Both AutoRef types validate commands against current state:
// Example: Cannot manually set map while automation is running
case "setmap":
    if (currentState != MatchState.Idle)
    {
        await SendMessageBothWays(Strings.SetMapFail);
        break;
    }
    await PreparePick(args[1]);
    currentState = MatchState.Idle;
    break;

State Machine Drivers

Both AutoRef types use a central event loop to process IRC messages:

Message Handler

// AutoRefQualifiersStage.cs:168
internal async Task HandleIrcMessage(IIrcMessage msg)
{
    string senderNick = /* parse IRC prefix */;
    string content = msg.Parameters[1];

    // 1. System Events (lobby creation)
    // 2. Emergency Protocols (!panic)
    // 3. Drive State Machine
    if(senderNick == "BanchoBot") _ = TryStateChange(content);
    // 4. Admin Commands (>start, >stop, etc.)
}

State Transition Logic

The TryStateChange method evaluates current state + incoming message:
// AutoRefQualifiersStage.cs:330
private async Task TryStateChange(string banchoMsg)
{
    switch (currentState)
    {
        case MatchState.Idle:
            return; // Automation paused
            
        case MatchState.WaitingForStart:
            if (banchoMsg.Contains("All players are ready") || 
                banchoMsg.Contains("Countdown finished"))
            {
                await SendMessageBothWays("!mp start 10");
                currentState = MatchState.Playing;
            }
            break;
            
        case MatchState.Playing:
            if (banchoMsg.Contains("The match has finished"))
            {
                currentMapIndex++;
                currentState = MatchState.Idle;
                // Trigger next map after cooldown
            }
            break;
    }
}
The elimination AutoRef uses a more complex switch with sender validation to ensure only the correct team can pick/ban.

Admin Commands

Referees can issue commands prefixed with > in either Discord or IRC:
>start
trigger
Begin or resume automation (requires firstpick/firstban for elimination)
>stop
trigger
Pause automation at current state (can be resumed)
>finish
trigger
Close the lobby immediately (does not save state)
>invite
trigger
  • Qualifiers: Invites all registered players
  • Elimination: Invites both teams
>setmap [slot]
manual override
Manually load a specific map (only works when Idle)
>firstpick [red|blue]
elimination only
Set which team picks first (required before >start)
>firstban [red|blue]
elimination only
Set which team bans first (required before >start)
>maps
elimination only
Display current banned/picked maps and remaining pool
>timeout
referee override
Force a timeout (bypasses team usage limits)
>panic_over
recovery
Exit panic mode and return to automation (referee only)

Command Validation

All admin commands are validated to ensure only the referee can execute them:
// AutoRefEliminationStage.cs:388
private async Task ExecuteAdminCommand(string sender, string[] args)
{
    if (sender != currentMatch!.Referee.DisplayName.Replace(' ', '_')) 
        return; // Silently ignore non-referee commands
    
    switch (args[0].ToLower())
    {
        // ... command handling
    }
}

Persistence and Recovery

Match state is saved to the database on shutdown:

What Gets Saved

await db.QualifierRooms
    .Where(m => m.Id == matchId)
    .ExecuteUpdateAsync(s => s.SetProperty(m => m.MpLinkId, mpLinkId));
  • MpLinkId: Bancho match history link
No Auto-Resume: If the server crashes, AutoRef instances do NOT automatically reconnect. Matches must be manually restarted.

Architecture

Understand how AutoRef fits into the overall system

Database Schema

See how match state is persisted

Build docs developers (and LLMs) love