Skip to main content
The user system consists of three related entities: User (core identity), OsuUser (cached osu! data), and Player (tournament participant). This separation allows flexible management of user accounts and tournament-specific information.

User Model

Class Definition

Namespace: ss.Internal.Management.Server.AutoRef Table: users Source: /home/daytona/workspace/source/ss.Integrated.Management.Server/Database/Models.cs:269
[Table("users")]
public class User
{
    // Properties...
}

Properties

Id
int
required
The unique identifier for the user.Column: id (Primary Key)
OsuID
int
required
The osu! user ID from https://osu.ppy.sh/users/{OsuID}Column: osu_idRelationship: Foreign key to OsuUser
DiscordID
string?
The Discord user ID (snowflake) linking this user to their Discord account.Nullable to allow users who haven’t linked Discord yet.Column: discord_id
OsuData
OsuUser
Navigation property to the cached osu! profile data.
[ForeignKey("OsuID")]
public virtual OsuUser OsuData { get; set; }
DisplayName
string
Computed property that returns the osu! username or “Desconocido” (Unknown) if not available.Not stored in the database (marked with [NotMapped]).
[NotMapped]
public string DisplayName => OsuData.Username ?? "Desconocido";

OsuUser Model

Class Definition

Namespace: ss.Internal.Management.Server.AutoRef Table: osu_user Source: /home/daytona/workspace/source/ss.Integrated.Management.Server/Database/Models.cs:288
[Table("osu_user")]
public class OsuUser
{
    // Properties...
}

Properties

Id
int
required
The osu! user ID. Must match a User’s OsuID for the 1:1 relationship.Column: id (Primary Key)
Username
string
required
The osu! username at the time of last cache update.Column: username
GlobalRank
int
required
The user’s global ranking at the time of last cache update.Column: global_rank
CountryRank
int
required
The user’s country ranking at the time of last cache update.Column: country_rank

Purpose

The OsuUser table serves as a cache to avoid excessive API calls to the osu! servers. It stores a snapshot of the user’s profile data that can be refreshed periodically.

Player Model

Class Definition

Namespace: ss.Internal.Management.Server.AutoRef Table: players Source: /home/daytona/workspace/source/ss.Integrated.Management.Server/Database/Models.cs:306
[Table("players")]
public class Player
{
    // Properties...
}

Properties

Id
int
required
The unique identifier for the player entry.Column: id (Primary Key)
UserId
int
required
Foreign key to the User this player record represents.Column: user_idRelationship: Many-to-one with User
RegisteredAt
DateTime
required
The timestamp when the player registered for the tournament.Column: registered_at
Availability
string
required
JSON-stored availability schedule for the player.Format is implementation-dependent but typically stores time slots when the player is available.Column: availabilityStorage: JSON string
QualifierRoomId
string?
Foreign key to the QualifierRoom the player is assigned to.Nullable if the player hasn’t been assigned to a qualifier lobby yet.Column: qualifier_room_idRelationship: Many-to-one with QualifierRoom
User
User
Navigation property to the User entity.
[ForeignKey("UserId")]
public virtual User User { get; set; }
QualifierRoom
QualifierRoom?
Navigation property to the assigned QualifierRoom.
[ForeignKey("QualifierRoomId")]
public virtual QualifierRoom? QualifierRoom { get; set; }

Entity Relationships

Usage Examples

Registering a New User

// First, cache the osu! user data
var osuUser = new OsuUser
{
    Id = 7562902,
    Username = "WhiteCat",
    GlobalRank = 1,
    CountryRank = 1
};

context.OsuUsers.Add(osuUser);

// Then create the User record
var user = new User
{
    OsuID = 7562902,
    DiscordID = "123456789012345678"
};

context.Users.Add(user);
await context.SaveChangesAsync();

Registering a Player for Tournament

var player = new Player
{
    UserId = user.Id,
    RegisteredAt = DateTime.UtcNow,
    Availability = "[]"  // Empty availability initially
};

context.Players.Add(player);
await context.SaveChangesAsync();

Assigning Player to Qualifier Lobby

var player = await context.Players
    .Include(p => p.User)
        .ThenInclude(u => u.OsuData)
    .FirstOrDefaultAsync(p => p.UserId == 42);

player.QualifierRoomId = "Q1-SAT-1400";

await context.SaveChangesAsync();

Console.WriteLine($"{player.User.DisplayName} assigned to {player.QualifierRoomId}");

Querying Players with Full Data

var players = await context.Players
    .Include(p => p.User)
        .ThenInclude(u => u.OsuData)
    .Include(p => p.QualifierRoom)
    .OrderByDescending(p => p.User.OsuData.GlobalRank)
    .ToListAsync();

foreach (var player in players)
{
    Console.WriteLine($"{player.User.DisplayName} (#{player.User.OsuData.GlobalRank})");
    Console.WriteLine($"  Registered: {player.RegisteredAt:yyyy-MM-dd}");
    Console.WriteLine($"  Qualifier: {player.QualifierRoom?.Id ?? "Not assigned"}");
}

Updating osu! Profile Cache

public async Task UpdateOsuCache(int osuId)
{
    // Fetch from osu! API (pseudo-code)
    var apiData = await osuApiClient.GetUser(osuId);
    
    var osuUser = await context.OsuUsers.FindAsync(osuId);
    if (osuUser != null)
    {
        osuUser.Username = apiData.Username;
        osuUser.GlobalRank = apiData.Statistics.GlobalRank;
        osuUser.CountryRank = apiData.Statistics.CountryRank;
        
        await context.SaveChangesAsync();
    }
}

Finding User by Discord ID

public async Task<User?> GetUserByDiscord(string discordId)
{
    return await context.Users
        .Include(u => u.OsuData)
        .FirstOrDefaultAsync(u => u.DiscordID == discordId);
}

Checking Tournament Registration

public async Task<bool> IsRegistered(int userId)
{
    return await context.Players.AnyAsync(p => p.UserId == userId);
}

public async Task<Player?> GetPlayerData(int userId)
{
    return await context.Players
        .Include(p => p.User)
            .ThenInclude(u => u.OsuData)
        .Include(p => p.QualifierRoom)
        .FirstOrDefaultAsync(p => p.UserId == userId);
}

Design Rationale

Separation of Concerns

  1. User: Core identity linking osu! and Discord accounts
    • Can exist independently of tournament participation
    • Reusable across multiple tournaments
  2. OsuUser: Cached profile data
    • 1:1 relationship with User
    • Reduces API calls to osu! servers
    • Can be refreshed periodically
  3. Player: Tournament-specific data
    • Only exists when user registers for tournament
    • Stores availability and qualifier assignment
    • Separate from core user identity

Benefits

  • Users can be registered in the system before tournament registration opens
  • osu! profile data can be updated without affecting user identity
  • Player-specific data is isolated from general user information
  • System can support multiple tournaments by extending the Player model
  • MatchRoom - References User for team assignments
  • QualifierRoom - References User for request tracking
  • Player - References QualifierRoom for lobby assignment
  • ScoreResults - References User for score tracking
  • RefereeInfo - Similar identity model for referees

Build docs developers (and LLMs) love