Skip to main content
The ScoreResults entity represents a single player’s performance on a beatmap during a tournament match. Scores are parsed from match results and stored for leaderboards, statistics, and match validation.

Class Definition

Namespace: ss.Internal.Management.Server.AutoRef Table: Not explicitly specified (uses default EF Core convention) Source: /home/daytona/workspace/source/ss.Integrated.Management.Server/Database/Models.cs:357
public class ScoreResults
{
    // Properties...
}

Properties

Id
int
required
The unique identifier for this score entry.Column: id (Primary Key)
RoundId
int
required
Foreign key to the Round this score belongs to.Column: round_idRelationship: Many-to-one with Round
UserId
int
required
Foreign key to the User (player) who achieved this score.Column: user_idRelationship: Many-to-one with User
Slot
string
required
The map slot identifier from the round’s map pool (e.g., “NM1”, “HD2”, “DT3”).Column: slot
Score
int
required
The raw score achieved by the player on this beatmap.In ScoreV1 format (standard osu! scoring).Column: score
Accuracy
float
required
The accuracy percentage achieved (0-100).Example: 98.5 represents 98.5% accuracyColumn: accuracy
MaxCombo
int
required
The maximum combo achieved during this play.Column: max_combo
Grade
string
required
The rank grade achieved (SS, S, A, B, C, D, F).Column: grade
Team
User
Navigation property to the User who achieved this score.
[ForeignKey("UserId")]
public User Team { get; set; }
The property name “Team” is misleading - it actually references an individual User/Player, not a team.
Round
Round
Navigation property to the Round this score belongs to.
[ForeignKey("RoundId")]
public Round Round { get; set; }

Score Parsing

Data Source

Scores are typically parsed from:
  1. Bancho Match History API - https://osu.ppy.sh/community/matches/{MpLinkId}
  2. IRC Events - Real-time score updates from BanchoBot

Parsing Workflow

  1. Match completes a beatmap
  2. System fetches match data from osu! API
  3. For each player’s score:
    • Extract score, accuracy, combo, grade
    • Map beatmap ID to round slot (from Round.MapPool)
    • Create ScoreResults entry
    • Link to User and Round

Example Parser (Pseudo-code)

public async Task ParseMatchScores(int mpLinkId, int roundId)
{
    // Fetch match data from osu! API
    var matchData = await osuApi.GetMatch(mpLinkId);
    var round = await context.Rounds
        .Include(r => r.MapPool)
        .FirstOrDefaultAsync(r => r.Id == roundId);
    
    foreach (var game in matchData.Games)
    {
        // Find the slot for this beatmap
        var slot = round.MapPool
            .FirstOrDefault(m => m.BeatmapID == game.BeatmapId)?.Slot;
        
        if (slot == null) continue;
        
        foreach (var score in game.Scores)
        {
            // Find user by osu! ID
            var user = await context.Users
                .FirstOrDefaultAsync(u => u.OsuID == score.UserId);
            
            if (user == null) continue;
            
            var scoreResult = new ScoreResults
            {
                RoundId = roundId,
                UserId = user.Id,
                Slot = slot,
                Score = score.Score,
                Accuracy = CalculateAccuracy(score),
                MaxCombo = score.MaxCombo,
                Grade = DetermineGrade(score)
            };
            
            context.ScoreResults.Add(scoreResult);
        }
    }
    
    await context.SaveChangesAsync();
}

Usage Examples

Recording a Single Score

var score = new ScoreResults
{
    RoundId = 5,  // Grand Finals
    UserId = 42,
    Slot = "NM1",
    Score = 1250000,
    Accuracy = 98.5f,
    MaxCombo = 1234,
    Grade = "S"
};

context.ScoreResults.Add(score);
await context.SaveChangesAsync();

Querying Player Scores for a Round

var userId = 42;
var roundId = 1;  // Qualifiers

var scores = await context.ScoreResults
    .Include(s => s.Round)
    .Where(s => s.UserId == userId && s.RoundId == roundId)
    .OrderByDescending(s => s.Score)
    .ToListAsync();

foreach (var score in scores)
{
    Console.WriteLine($"{score.Slot}: {score.Score:N0} ({score.Accuracy:F2}%) - {score.Grade}");
}

Generating Qualifier Leaderboard

public async Task<List<QualifierResult>> GetQualifierLeaderboard(int roundId)
{
    var scores = await context.ScoreResults
        .Include(s => s.Team)
            .ThenInclude(u => u.OsuData)
        .Where(s => s.RoundId == roundId)
        .ToListAsync();
    
    var leaderboard = scores
        .GroupBy(s => s.UserId)
        .Select(g => new QualifierResult
        {
            Username = g.First().Team.DisplayName,
            TotalScore = g.Sum(s => s.Score),
            AverageAccuracy = g.Average(s => s.Accuracy),
            MapScores = g.ToDictionary(s => s.Slot, s => s.Score)
        })
        .OrderByDescending(r => r.TotalScore)
        .ToList();
    
    return leaderboard;
}

public class QualifierResult
{
    public string Username { get; set; }
    public int TotalScore { get; set; }
    public double AverageAccuracy { get; set; }
    public Dictionary<string, int> MapScores { get; set; }
}

Finding Best Score on a Map

public async Task<ScoreResults?> GetBestScore(int roundId, string slot)
{
    return await context.ScoreResults
        .Include(s => s.Team)
            .ThenInclude(u => u.OsuData)
        .Where(s => s.RoundId == roundId && s.Slot == slot)
        .OrderByDescending(s => s.Score)
        .FirstOrDefaultAsync();
}

Match Score Comparison

public async Task<Dictionary<string, TeamScore>> GetMatchScores(
    int roundId,
    int teamRedId,
    int teamBlueId)
{
    var redScores = await context.ScoreResults
        .Where(s => s.RoundId == roundId && s.UserId == teamRedId)
        .ToListAsync();
    
    var blueScores = await context.ScoreResults
        .Where(s => s.RoundId == roundId && s.UserId == teamBlueId)
        .ToListAsync();
    
    return new Dictionary<string, TeamScore>
    {
        ["Red"] = new TeamScore
        {
            TotalScore = redScores.Sum(s => s.Score),
            MapsWon = redScores.Count(s => 
                s.Score > blueScores.FirstOrDefault(b => b.Slot == s.Slot)?.Score ?? 0)
        },
        ["Blue"] = new TeamScore
        {
            TotalScore = blueScores.Sum(s => s.Score),
            MapsWon = blueScores.Count(s => 
                s.Score > redScores.FirstOrDefault(r => r.Slot == s.Slot)?.Score ?? 0)
        }
    };
}

public class TeamScore
{
    public int TotalScore { get; set; }
    public int MapsWon { get; set; }
}

Player Statistics

public async Task<PlayerStats> GetPlayerStats(int userId, int roundId)
{
    var scores = await context.ScoreResults
        .Where(s => s.UserId == userId && s.RoundId == roundId)
        .ToListAsync();
    
    return new PlayerStats
    {
        MapsPlayed = scores.Count,
        TotalScore = scores.Sum(s => s.Score),
        AverageAccuracy = scores.Average(s => s.Accuracy),
        HighestCombo = scores.Max(s => s.MaxCombo),
        GradeDistribution = scores.GroupBy(s => s.Grade)
            .ToDictionary(g => g.Key, g => g.Count())
    };
}

public class PlayerStats
{
    public int MapsPlayed { get; set; }
    public int TotalScore { get; set; }
    public double AverageAccuracy { get; set; }
    public int HighestCombo { get; set; }
    public Dictionary<string, int> GradeDistribution { get; set; }
}

Grade Determination

The Grade field follows osu! standard ranking:
GradeCriteria
SS100% accuracy
SOver 90% accuracy with no misses
AOver 80% accuracy with no misses, or over 90% accuracy
BOver 70% accuracy with no misses, or over 80% accuracy
COver 60% accuracy
DBelow 60% accuracy
FFailed the map
The exact grade calculation may vary based on game mode and scoring version.

Common Queries

Duplicate Detection

Preventing duplicate score entries:
public async Task<bool> ScoreExists(int roundId, int userId, string slot)
{
    return await context.ScoreResults
        .AnyAsync(s => s.RoundId == roundId && s.UserId == userId && s.Slot == slot);
}

Score Update (Best Score Only)

If you want to keep only the best score per user per slot:
public async Task UpsertScore(ScoreResults newScore)
{
    var existing = await context.ScoreResults
        .FirstOrDefaultAsync(s => 
            s.RoundId == newScore.RoundId && 
            s.UserId == newScore.UserId && 
            s.Slot == newScore.Slot);
    
    if (existing != null)
    {
        if (newScore.Score > existing.Score)
        {
            existing.Score = newScore.Score;
            existing.Accuracy = newScore.Accuracy;
            existing.MaxCombo = newScore.MaxCombo;
            existing.Grade = newScore.Grade;
        }
    }
    else
    {
        context.ScoreResults.Add(newScore);
    }
    
    await context.SaveChangesAsync();
}
  • Round - Defines the map pool and slots
  • User - The player who achieved the score
  • MatchRoom - Match context (via MpLinkId)
  • QualifierRoom - Qualifier context (via MpLinkId)

Build docs developers (and LLMs) love