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
Referee types >start to engage automation
AutoRef loads first map from Round.MapPool
Sets mods based on slot (e.g., “NM1” → NoMod, “HD2” → Hidden)
Starts 120-second ready timer
Begins match when players ready or timer expires
After map finishes, waits 10 seconds (cooldown)
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 ();
Elimination Automation Class : AutoRefEliminationStage (AutoRef/AutoRefEliminationStage.cs:83)Handles complex pick/ban phases with timeout management and win detection: Characteristics
Pick/ban phases : Snake draft or fixed order based on Round.BanRounds
Team vs Team : Two players/teams compete
Score tracking : Aggregates individual scores to team totals
Timeout system : Each team gets one 120-second timeout
Win condition detection : Auto-detects match point and tiebreaker
Workflow
Referee sets >firstpick red/blue and >firstban red/blue
Types >start to begin automation
Ban Phase : Teams alternate banning maps (configurable rounds)
Pick Phase : Teams alternate picking maps
After each map, AutoRef parses scores and updates match score
Continues until win condition met or tiebreaker triggered
For “Double Ban” rounds: Additional ban phase after 4 picks
State Machine Key Implementation Details
Individual scores are aggregated to determine point winners: // AutoRefEliminationStage.cs:336
private async Task ProcessFinalScores ()
{
long redTotal = 0 ;
long blueTotal = 0 ;
foreach ( var player in currentMapScores )
{
if ( player . Key . Equals ( currentMatch ! . TeamRed . DisplayName ,
StringComparison . OrdinalIgnoreCase ))
redTotal += player . Value ;
else if ( player . Key . Equals ( currentMatch . TeamBlue . DisplayName ,
StringComparison . OrdinalIgnoreCase ))
blueTotal += player . Value ;
}
if ( redTotal > blueTotal )
{
matchScore [ 0 ] ++ ;
await SendMessageBothWays (
string . Format ( Strings . RedWins , redTotal , blueTotal )
);
}
else
{
matchScore [ 1 ] ++ ;
await SendMessageBothWays (
string . Format ( Strings . BlueWins , blueTotal , redTotal )
);
}
}
Scores are extracted via regex: // AutoRefEliminationStage.cs:273
var match = Regex . Match ( content , @"^(.*) finished playing \(Score: (\d+)," );
if ( match . Success )
{
string nick = match . Groups [ 1 ]. Value ;
int score = int . Parse ( match . Groups [ 2 ]. Value );
currentMapScores [ nick ] = score ;
}
Maps must pass validation before being accepted: // AutoRefEliminationStage.cs:518
private bool IsMapAvailable ( string content )
{
// Validation Rules:
// 1. Must exist in MapPool
// 2. Must not be in BannedMaps
// 3. Must not be in PickedMaps
// 4. Cannot be Tiebreaker (TB1)
bool canAdd =
currentMatch ! . Round . MapPool . Find ( b => b . Slot == content . ToUpper ()) != null &&
bannedMaps . Find ( b => b . Slot == content . ToUpper ()) == null &&
pickedMaps . Find ( b => b . Slot == content . ToUpper ()) == null &&
content . ToUpper () != "TB1" ;
return canAdd ;
}
The system automatically detects when a team reaches match point: // AutoRefEliminationStage.cs:804
if ( pickedMaps . Count == currentMatch . Round . BestOf - 1 )
{
// Trigger tiebreaker
await PreparePick ( "TB1" );
pickedMaps . Add ( new Models . RoundChoice
{
Slot = "TB1" ,
TeamColor = Models . TeamColor . None
});
return ;
}
bool redwin = matchScore [ 0 ] == ( currentMatch . Round . BestOf - 1 ) / 2 + 1 ;
bool bluewin = matchScore [ 1 ] == ( currentMatch . Round . BestOf - 1 ) / 2 + 1 ;
if ( redwin )
{
await SendMessageBothWays (
string . Format ( Strings . MatchWin , currentMatch ! . TeamRed . DisplayName )
);
currentState = MatchState . MatchFinished ;
return ;
}
Some rounds have a mid-match ban phase: // AutoRefEliminationStage.cs:795
if ( currentMatch ! . Round . BanRounds == 2 && pickedMaps . Count == 4 )
{
// Logic for "Double Ban" rounds (Ban → Pick 4 → Ban → Pick rest)
currentState = MatchState . SecondBanPhaseStart ;
await SendMessageBothWays ( Strings . SecondBanRound );
await TryStateChange ( "a" , "a" ); // Force evaluation
}
If a team’s pick timer runs out, the opponent gets to pick: // AutoRefEliminationStage.cs:610
if ( content . Contains ( "Countdown finished" ) && sender == "BanchoBot" )
{
await SendMessageBothWays (
$"The timer has ran out, the opponent will be picking now. " +
$" { currentMatch ! . TeamRed . DisplayName } , please state your pick in chat."
);
await SendMessageBothWays ( "!mp timer 60" );
currentState = MatchState . WaitingForPickRed ;
isStolenPick = true ; // Track that pick order was stolen
}
When Automation Triggers
Starting Automation
Automation begins when the referee issues the >start command:
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
Engagement
Referee types >start in Discord thread or IRC lobby
Initialization
Qualifiers : Sets currentMapIndex = 0, loads first map
Elimination : Transitions to BanPhaseStart, requests first ban
Event Loop
AutoRef begins listening to BanchoBot messages and driving state transitions
Stopping Automation
Automation can be paused or stopped:
Temporary Stop
Permanent Shutdown
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 ;
Command : /ref end [match_id] (Discord)
Calls IAutoRef.StopAsync()
Saves final state to database
Closes Bancho lobby with !mp close
Disconnects IRC client
Archives Discord thread
// AutoRefEliminationStage.cs:171
public async Task StopAsync ()
{
await using var db = new ModelsContext ();
await db . MatchRooms
. Where ( m => m . Id == matchId )
. ExecuteUpdateAsync ( s => s
. SetProperty ( m => m . MpLinkId , mpLinkId )
. SetProperty ( m => m . PickedMaps , pickedMaps )
. SetProperty ( m => m . BannedMaps , bannedMaps )
. SetProperty ( m => m . EndTime , DateTime . UtcNow )
);
await SendMessageBothWays ( "!mp close" );
await client ! . DisconnectAsync ();
}
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:
Begin or resume automation (requires firstpick/firstban for elimination)
Pause automation at current state (can be resumed)
Close the lobby immediately (does not save state)
Qualifiers : Invites all registered players
Elimination : Invites both teams
Manually load a specific map (only works when Idle)
Set which team picks first (required before >start)
Set which team bans first (required before >start)
Display current banned/picked maps and remaining pool
Force a timeout (bypasses team usage limits)
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
await db . MatchRooms
. Where ( m => m . Id == matchId )
. ExecuteUpdateAsync ( s => s
. SetProperty ( m => m . MpLinkId , mpLinkId )
. SetProperty ( m => m . PickedMaps , pickedMaps )
. SetProperty ( m => m . BannedMaps , bannedMaps )
. SetProperty ( m => m . EndTime , DateTime . UtcNow )
);
MpLinkId: Bancho match history link
PickedMaps: Complete pick history with winners
BannedMaps: All banned maps
EndTime: Match completion timestamp
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