Skip to main content

Overview

AutoRefEliminationStage handles the automated refereeing logic for Elimination/Versus matches. It implements a complex state machine to manage picks, bans, timeouts, and scoring for Best-of-N tournaments. Namespace: ss.Internal.Management.Server.AutoRef

Class Definition

public partial class AutoRefEliminationStage : IAutoRef

State Machine

State Enum

public enum MatchState
{
    Idle,
    BanPhaseStart,
    WaitingForBanRed,
    WaitingForBanBlue,
    PickPhaseStart,
    SecondBanPhaseStart,
    WaitingForPickRed,
    WaitingForPickBlue,
    WaitingForStart,
    Playing,
    MatchFinished,
    OnTimeout,
    MatchOnHold
}

State Descriptions

StateDescription
IdleSystem waiting for referee input or configuration
BanPhaseStartDetermines which team bans first
WaitingForBanRedRed team’s turn to ban a map
WaitingForBanBlueBlue team’s turn to ban a map
PickPhaseStartDetermines which team picks first
SecondBanPhaseStartMid-match ban phase (for double ban rounds)
WaitingForPickRedRed team’s turn to pick a map
WaitingForPickBlueBlue team’s turn to pick a map
WaitingForStartMap loaded, waiting for players to ready up
PlayingMap in progress, waiting for results
MatchFinishedOne team has reached the win condition
OnTimeoutTactical timeout active
MatchOnHoldPanic mode - automation paused

State Transition Diagram

Elimination State Machine

Key Methods

StartAsync()

Initializes the match and connects to Bancho.
public async Task StartAsync()
Behavior:
  1. Loads MatchRoom from database by matchId
  2. Validates referee credentials
  3. Loads red/blue team information
  4. Loads round configuration (BestOf, BanRounds, MapPool)
  5. Connects to Bancho IRC
  6. Creates tournament lobby

StopAsync()

Saves match state and closes the lobby.
public async Task StopAsync()
Persisted Data:
  • MpLinkId: osu! match ID
  • PickedMaps: List of picked maps with team colors
  • BannedMaps: List of banned maps with team colors
  • EndTime: UTC timestamp

HandleIrcMessage()

Core event loop that drives the state machine.
internal async Task HandleIrcMessage(IIrcMessage msg)
Responsibilities:
  1. Parse IRC messages from BanchoBot and players
  2. Extract scores using regex: ^(.*) finished playing \(Score: (\d+),
  3. Detect match completion
  4. Handle !panic emergency protocol
  5. Process admin commands (prefix: >)
  6. Drive state transitions via TryStateChange()

ExecuteAdminCommand()

Processes referee commands.
private async Task ExecuteAdminCommand(string sender, string[] args)
Available Commands:
CommandSyntaxDescription
>invite>inviteInvites both teams to the lobby
>finish>finishCloses the lobby
>maps>mapsShows picks, bans, and available maps
>setmap>setmap [slot]Manually sets a map (requires Idle state)
>timeout>timeoutTriggers a referee timeout
>start>startEngages automation
>stop>stopStops automation
>firstpick>firstpick [red/blue]Sets which team picks first
>firstban>firstban [red/blue]Sets which team bans first

ProcessFinalScores()

Aggregates player scores and determines the point winner.
private async Task ProcessFinalScores()
Logic:
  1. Sums scores for each team from currentMapScores dictionary
  2. Increments matchScore[0] (red) or matchScore[1] (blue)
  3. Announces winner in lobby
  4. Displays current match score (e.g., “TeamA 3 - 2 TeamB | Best of 7”)
  5. Clears score dictionary for next map

IsMapAvailable()

Validates if a map can be picked or banned.
private bool IsMapAvailable(string content)
Validation Rules:
  1. Must exist in the round’s MapPool
  2. Must not be in BannedMaps
  3. Must not be in PickedMaps
  4. Cannot be the Tiebreaker (TB1)

TryStateChange()

The state machine brain - evaluates current state and transitions.
private async Task TryStateChange(string sender, string content)
Key Behaviors:
  • Ban Phase: Alternates between teams, enforces BanRounds count
  • Pick Phase: Alternates picks, validates map availability
  • Stolen Picks: If timer expires, opponent gets to pick
  • Double Ban Rounds: Triggers second ban phase after 4 maps (if configured)
  • Tiebreaker: Automatically picked when both teams reach match point
  • Win Condition: Checks if team reached (BestOf - 1) / 2 + 1 points

Score Processing

The system uses regex to extract scores from BanchoBot messages:
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;
}

Pick/Ban Validation

Players type map slots in chat (e.g., NM1, HD2):
  1. State Check: Must be in correct WaitingForPickRed/Blue or WaitingForBanRed/Blue
  2. Sender Validation: Message must come from the active team’s player
  3. Map Validation: Checked via IsMapAvailable()
  4. Add to List: Appended to pickedMaps or bannedMaps with team color
  5. State Transition: Moves to next phase or opponent’s turn

Timeout System

Players can request timeouts by typing !timeout:
if (content == "!timeout")
{
    if (sender == teamRed && !redTimeoutRequest)
    {
        previousState = currentState;
        currentState = MatchState.OnTimeout;
        redTimeoutRequest = true;
    }
    // Similar logic for blue team
}
Limits:
  • One timeout per team per match
  • Only available during pick/ban/waiting states
  • 120-second countdown
  • Returns to previous state when countdown finishes

Panic Protocol

Emergency override system: Trigger: Anyone types !panic
if (content.Contains("!panic"))
{
    currentState = MatchState.MatchOnHold;
    await SendMessageBothWays("!mp aborttimer");
    await SendMessageBothWays($"<@&{REFEREE_ROLE_ID}> PANIC");
}
Recovery: Referee types >panic_over
if (content.Contains(">panic_over") && senderNick == referee)
{
    currentState = MatchState.WaitingForStart;
    await SendMessageBothWays("!mp timer 10");
}

Double Ban Rounds

For rounds configured with BanRounds == 2 and BanMode == SpanishShowdown:
if (currentMatch.Round.BanRounds == 2 && pickedMaps.Count == 4)
{
    currentState = MatchState.SecondBanPhaseStart;
    await SendMessageBothWays(Strings.SecondBanRound);
}
Flow:
  1. Initial ban phase (2 bans per team)
  2. Pick phase (4 maps played)
  3. Second ban phase (2 more bans per team)
  4. Continue picking remaining maps

Tiebreaker Logic

Automatically triggered when both teams reach match point:
if (pickedMaps.Count == currentMatch.Round.BestOf - 1)
{
    await PreparePick("TB1");
    pickedMaps.Add(new RoundChoice { Slot = "TB1", TeamColor = None });
}

Internal Fields

internal Models.MatchRoom? currentMatch;
private readonly string matchId;
private readonly string refDisplayName;
internal IBanchoClient? client;
internal string? lobbyChannelName;
private int[] matchScore = [0, 0];
private bool joined = false;
private bool redTimeoutRequest;
private bool blueTimeoutRequest;
private int mpLinkId;
private Models.TeamColor firstPick;
private Models.TeamColor firstBan;
internal List<Models.RoundChoice> bannedMaps = [];
internal List<Models.RoundChoice> pickedMaps = [];
private Dictionary<string, int> currentMapScores = new();
private Models.TeamColor lastPick;
internal MatchState currentState;
private MatchState previousState;

Build docs developers (and LLMs) love