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
The unique identifier for this score entry.Column: id (Primary Key)
Foreign key to the Round this score belongs to.Column: round_idRelationship: Many-to-one with Round
Foreign key to the User (player) who achieved this score.Column: user_idRelationship: Many-to-one with User
The map slot identifier from the round’s map pool (e.g., “NM1”, “HD2”, “DT3”).Column: slot
The raw score achieved by the player on this beatmap.In ScoreV1 format (standard osu! scoring).Column: score
The accuracy percentage achieved (0-100).Example: 98.5 represents 98.5% accuracyColumn: accuracy
The maximum combo achieved during this play.Column: max_combo
The rank grade achieved (SS, S, A, B, C, D, F).Column: grade
Navigation Properties
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.
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:
- Bancho Match History API -
https://osu.ppy.sh/community/matches/{MpLinkId}
- IRC Events - Real-time score updates from BanchoBot
Parsing Workflow
- Match completes a beatmap
- System fetches match data from osu! API
- 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:
| Grade | Criteria |
|---|
| SS | 100% accuracy |
| S | Over 90% accuracy with no misses |
| A | Over 80% accuracy with no misses, or over 90% accuracy |
| B | Over 70% accuracy with no misses, or over 80% accuracy |
| C | Over 60% accuracy |
| D | Below 60% accuracy |
| F | Failed 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)