From a965fbf944bd93d062135f0d5b13d9edb50b42c1 Mon Sep 17 00:00:00 2001 From: Simon <63975668+Simyon264@users.noreply.github.com> Date: Mon, 11 Nov 2024 04:46:31 +0100 Subject: [PATCH] DB leaderboards (#66) * DB leaderboards initial commit * Clarify message * uuuhhh AMONG US --- .../20241111013501_DbLeaderboards.Designer.cs | 718 ++++++++++++++++++ .../20241111013501_DbLeaderboards.cs | 70 ++ .../ReplayDbContextModelSnapshot.cs | 68 ++ .../Data/Models/LeaderboardDefinition.cs | 27 + .../Data/Models/LeaderboardPosition.cs | 54 ++ ReplayBrowser/Data/ReplayDbContext.cs | 3 + ReplayBrowser/Models/LeaderboardData.cs | 7 - ReplayBrowser/Pages/Leaderboard.razor | 24 +- ReplayBrowser/Services/LeaderboardService.cs | 290 +++++-- 9 files changed, 1177 insertions(+), 84 deletions(-) create mode 100644 ReplayBrowser/Data/Migrations/20241111013501_DbLeaderboards.Designer.cs create mode 100644 ReplayBrowser/Data/Migrations/20241111013501_DbLeaderboards.cs create mode 100644 ReplayBrowser/Data/Models/LeaderboardDefinition.cs create mode 100644 ReplayBrowser/Data/Models/LeaderboardPosition.cs diff --git a/ReplayBrowser/Data/Migrations/20241111013501_DbLeaderboards.Designer.cs b/ReplayBrowser/Data/Migrations/20241111013501_DbLeaderboards.Designer.cs new file mode 100644 index 0000000..cd9b81b --- /dev/null +++ b/ReplayBrowser/Data/Migrations/20241111013501_DbLeaderboards.Designer.cs @@ -0,0 +1,718 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; +using ReplayBrowser.Data; + +#nullable disable + +namespace Server.Migrations +{ + [DbContext(typeof(ReplayDbContext))] + [Migration("20241111013501_DbLeaderboards")] + partial class DbLeaderboards + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.2") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.Account", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property>("FavoriteReplays") + .IsRequired() + .HasColumnType("integer[]"); + + b.Property("Guid") + .HasColumnType("uuid"); + + b.Property("IsAdmin") + .HasColumnType("boolean"); + + b.Property("Protected") + .HasColumnType("boolean"); + + b.Property>("SavedProfiles") + .IsRequired() + .HasColumnType("uuid[]"); + + b.Property("SettingsId") + .HasColumnType("integer"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Guid") + .IsUnique(); + + b.HasIndex("SettingsId"); + + b.HasIndex("Username"); + + b.ToTable("Accounts"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.AccountSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property>("Friends") + .IsRequired() + .HasColumnType("uuid[]"); + + b.Property("RedactInformation") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("AccountSettings"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.HistoryEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("integer"); + + b.Property("Action") + .IsRequired() + .HasColumnType("text"); + + b.Property("Details") + .HasColumnType("text"); + + b.Property("Time") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("HistoryEntry"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.Webhook", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AccountId") + .HasColumnType("integer"); + + b.Property("Servers") + .IsRequired() + .HasColumnType("text"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("AccountId"); + + b.ToTable("Webhook"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.WebhookHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ResponseBody") + .IsRequired() + .HasColumnType("text"); + + b.Property("ResponseCode") + .HasColumnType("integer"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone"); + + b.Property("WebhookId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("WebhookId"); + + b.ToTable("WebhookHistory"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.CharacterData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CharacterName") + .IsRequired() + .HasColumnType("text"); + + b.Property("CollectedPlayerDataPlayerGuid") + .HasColumnType("uuid"); + + b.Property("LastPlayed") + .HasColumnType("timestamp with time zone"); + + b.Property("RoundsPlayed") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CollectedPlayerDataPlayerGuid"); + + b.ToTable("CharacterData"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.CollectedPlayerData", b => + { + b.Property("PlayerGuid") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("GeneratedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsWatched") + .HasColumnType("boolean"); + + b.Property("LastSeen") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayerDataId") + .HasColumnType("integer"); + + b.Property("TotalAntagRoundsPlayed") + .HasColumnType("integer"); + + b.Property("TotalEstimatedPlaytime") + .HasColumnType("interval"); + + b.Property("TotalRoundsPlayed") + .HasColumnType("integer"); + + b.HasKey("PlayerGuid"); + + b.HasIndex("PlayerDataId"); + + b.HasIndex("PlayerGuid") + .IsUnique(); + + b.ToTable("PlayerProfiles"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.GdprRequest", b => + { + b.Property("Guid") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.HasKey("Guid"); + + b.ToTable("GdprRequests"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.JobCountData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CollectedPlayerDataPlayerGuid") + .HasColumnType("uuid"); + + b.Property("JobPrototype") + .IsRequired() + .HasColumnType("text"); + + b.Property("LastPlayed") + .HasColumnType("timestamp with time zone"); + + b.Property("RoundsPlayed") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CollectedPlayerDataPlayerGuid"); + + b.ToTable("JobCountData"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.JobDepartment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Department") + .IsRequired() + .HasColumnType("text"); + + b.Property("Job") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Job") + .IsUnique(); + + b.ToTable("JobDepartments"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.LeaderboardDefinition", b => + { + b.Property("Name") + .HasColumnType("text"); + + b.Property("ExtraInfo") + .HasColumnType("text"); + + b.Property("NameColumn") + .IsRequired() + .HasColumnType("text"); + + b.Property("TrackedData") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Name"); + + b.ToTable("LeaderboardDefinitions"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.LeaderboardPosition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Count") + .HasColumnType("integer"); + + b.Property("LeaderboardDefinitionName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlayerGuid") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property>("Servers") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("LeaderboardDefinitionName"); + + b.ToTable("Leaderboards"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Notice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Title") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Notices"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.ParsedReplay", b => + { + b.Property("Name") + .HasColumnType("text"); + + b.HasKey("Name"); + + b.ToTable("ParsedReplays"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Antag") + .HasColumnType("boolean"); + + b.Property>("AntagPrototypes") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("EffectiveJobId") + .HasColumnType("integer"); + + b.Property>("JobPrototypes") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("ParticipantId") + .HasColumnType("integer"); + + b.Property("PlayerIcName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EffectiveJobId"); + + b.HasIndex("ParticipantId"); + + b.HasIndex("PlayerIcName"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.PlayerData", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PlayerGuid") + .HasColumnType("uuid"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("PlayerData"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Replay", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Duration") + .IsRequired() + .HasColumnType("text"); + + b.Property("EndTick") + .HasColumnType("integer"); + + b.Property("EndTime") + .IsRequired() + .HasColumnType("text"); + + b.Property("FileCount") + .HasColumnType("integer"); + + b.Property("Gamemode") + .IsRequired() + .HasColumnType("text"); + + b.Property("Link") + .IsRequired() + .HasColumnType("text"); + + b.Property("Map") + .HasColumnType("text"); + + b.Property>("Maps") + .HasColumnType("text[]"); + + b.Property("RoundEndText") + .HasColumnType("text"); + + b.Property("RoundEndTextSearchVector") + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("tsvector") + .HasAnnotation("Npgsql:TsVectorConfig", "english") + .HasAnnotation("Npgsql:TsVectorProperties", new[] { "RoundEndText" }); + + b.Property("RoundId") + .HasColumnType("integer"); + + b.Property("ServerId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ServerName") + .HasColumnType("text"); + + b.Property("Size") + .HasColumnType("integer"); + + b.Property("UncompressedSize") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("Gamemode"); + + b.HasIndex("Map"); + + b.HasIndex("RoundEndTextSearchVector"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("RoundEndTextSearchVector"), "GIN"); + + b.HasIndex("ServerId"); + + b.HasIndex("ServerName"); + + b.ToTable("Replays"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.ReplayParticipant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PlayerGuid") + .HasColumnType("uuid"); + + b.Property("ReplayId") + .HasColumnType("integer"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ReplayId"); + + b.HasIndex("Username"); + + b.HasIndex("PlayerGuid", "ReplayId") + .IsUnique(); + + b.ToTable("ReplayParticipants"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.ServerToken", b => + { + b.Property("Token") + .HasColumnType("text"); + + b.HasKey("Token"); + + b.HasIndex("Token"); + + b.ToTable("ServerTokens"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.Account", b => + { + b.HasOne("ReplayBrowser.Data.Models.Account.AccountSettings", "Settings") + .WithMany() + .HasForeignKey("SettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Settings"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.HistoryEntry", b => + { + b.HasOne("ReplayBrowser.Data.Models.Account.Account", "Account") + .WithMany("History") + .HasForeignKey("AccountId"); + + b.Navigation("Account"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.Webhook", b => + { + b.HasOne("ReplayBrowser.Data.Models.Account.Account", null) + .WithMany("Webhooks") + .HasForeignKey("AccountId"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.WebhookHistory", b => + { + b.HasOne("ReplayBrowser.Data.Models.Account.Webhook", "Webhook") + .WithMany("Logs") + .HasForeignKey("WebhookId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Webhook"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.CharacterData", b => + { + b.HasOne("ReplayBrowser.Data.Models.CollectedPlayerData", null) + .WithMany("Characters") + .HasForeignKey("CollectedPlayerDataPlayerGuid"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.CollectedPlayerData", b => + { + b.HasOne("ReplayBrowser.Data.Models.PlayerData", "PlayerData") + .WithMany() + .HasForeignKey("PlayerDataId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlayerData"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.JobCountData", b => + { + b.HasOne("ReplayBrowser.Data.Models.CollectedPlayerData", null) + .WithMany("JobCount") + .HasForeignKey("CollectedPlayerDataPlayerGuid"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.LeaderboardPosition", b => + { + b.HasOne("ReplayBrowser.Data.Models.LeaderboardDefinition", "LeaderboardDefinition") + .WithMany() + .HasForeignKey("LeaderboardDefinitionName") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LeaderboardDefinition"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Player", b => + { + b.HasOne("ReplayBrowser.Data.Models.JobDepartment", "EffectiveJob") + .WithMany() + .HasForeignKey("EffectiveJobId"); + + b.HasOne("ReplayBrowser.Data.Models.ReplayParticipant", "Participant") + .WithMany("Players") + .HasForeignKey("ParticipantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EffectiveJob"); + + b.Navigation("Participant"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.ReplayParticipant", b => + { + b.HasOne("ReplayBrowser.Data.Models.Replay", "Replay") + .WithMany("RoundParticipants") + .HasForeignKey("ReplayId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Replay"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.Account", b => + { + b.Navigation("History"); + + b.Navigation("Webhooks"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Account.Webhook", b => + { + b.Navigation("Logs"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.CollectedPlayerData", b => + { + b.Navigation("Characters"); + + b.Navigation("JobCount"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.Replay", b => + { + b.Navigation("RoundParticipants"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.ReplayParticipant", b => + { + b.Navigation("Players"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ReplayBrowser/Data/Migrations/20241111013501_DbLeaderboards.cs b/ReplayBrowser/Data/Migrations/20241111013501_DbLeaderboards.cs new file mode 100644 index 0000000..6fc92b9 --- /dev/null +++ b/ReplayBrowser/Data/Migrations/20241111013501_DbLeaderboards.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Server.Migrations +{ + /// + public partial class DbLeaderboards : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "LeaderboardDefinitions", + columns: table => new + { + Name = table.Column(type: "text", nullable: false), + TrackedData = table.Column(type: "text", nullable: false), + ExtraInfo = table.Column(type: "text", nullable: true), + NameColumn = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_LeaderboardDefinitions", x => x.Name); + }); + + migrationBuilder.CreateTable( + name: "Leaderboards", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Servers = table.Column>(type: "text[]", nullable: false), + Count = table.Column(type: "integer", nullable: false), + Position = table.Column(type: "integer", nullable: false), + PlayerGuid = table.Column(type: "uuid", nullable: true), + Username = table.Column(type: "text", nullable: false), + LeaderboardDefinitionName = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Leaderboards", x => x.Id); + table.ForeignKey( + name: "FK_Leaderboards_LeaderboardDefinitions_LeaderboardDefinitionNa~", + column: x => x.LeaderboardDefinitionName, + principalTable: "LeaderboardDefinitions", + principalColumn: "Name", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Leaderboards_LeaderboardDefinitionName", + table: "Leaderboards", + column: "LeaderboardDefinitionName"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Leaderboards"); + + migrationBuilder.DropTable( + name: "LeaderboardDefinitions"); + } + } +} diff --git a/ReplayBrowser/Data/Migrations/ReplayDbContextModelSnapshot.cs b/ReplayBrowser/Data/Migrations/ReplayDbContextModelSnapshot.cs index 658f96c..7d12d25 100644 --- a/ReplayBrowser/Data/Migrations/ReplayDbContextModelSnapshot.cs +++ b/ReplayBrowser/Data/Migrations/ReplayDbContextModelSnapshot.cs @@ -301,6 +301,63 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("JobDepartments"); }); + modelBuilder.Entity("ReplayBrowser.Data.Models.LeaderboardDefinition", b => + { + b.Property("Name") + .HasColumnType("text"); + + b.Property("ExtraInfo") + .HasColumnType("text"); + + b.Property("NameColumn") + .IsRequired() + .HasColumnType("text"); + + b.Property("TrackedData") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Name"); + + b.ToTable("LeaderboardDefinitions"); + }); + + modelBuilder.Entity("ReplayBrowser.Data.Models.LeaderboardPosition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Count") + .HasColumnType("integer"); + + b.Property("LeaderboardDefinitionName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PlayerGuid") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property>("Servers") + .IsRequired() + .HasColumnType("text[]"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("LeaderboardDefinitionName"); + + b.ToTable("Leaderboards"); + }); + modelBuilder.Entity("ReplayBrowser.Data.Models.Notice", b => { b.Property("Id") @@ -585,6 +642,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("CollectedPlayerDataPlayerGuid"); }); + modelBuilder.Entity("ReplayBrowser.Data.Models.LeaderboardPosition", b => + { + b.HasOne("ReplayBrowser.Data.Models.LeaderboardDefinition", "LeaderboardDefinition") + .WithMany() + .HasForeignKey("LeaderboardDefinitionName") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LeaderboardDefinition"); + }); + modelBuilder.Entity("ReplayBrowser.Data.Models.Player", b => { b.HasOne("ReplayBrowser.Data.Models.JobDepartment", "EffectiveJob") diff --git a/ReplayBrowser/Data/Models/LeaderboardDefinition.cs b/ReplayBrowser/Data/Models/LeaderboardDefinition.cs new file mode 100644 index 0000000..b5ef5d7 --- /dev/null +++ b/ReplayBrowser/Data/Models/LeaderboardDefinition.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ReplayBrowser.Data.Models; + +public class LeaderboardDefinition : IEntityTypeConfiguration +{ + public required string Name { get; set; } + + /// + /// The text that will appear for the "Count" column. + /// + public required string TrackedData { get; set; } + + /// + /// Will be displayed in a small font below the name. + /// + public string? ExtraInfo { get; set; } + public string NameColumn { get; set; } = "Player Name"; + + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Name); + builder.Property(x => x.TrackedData).IsRequired(); + builder.Property(x => x.NameColumn).IsRequired(); + } +} \ No newline at end of file diff --git a/ReplayBrowser/Data/Models/LeaderboardPosition.cs b/ReplayBrowser/Data/Models/LeaderboardPosition.cs new file mode 100644 index 0000000..b01e175 --- /dev/null +++ b/ReplayBrowser/Data/Models/LeaderboardPosition.cs @@ -0,0 +1,54 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ReplayBrowser.Data.Models; + +public class LeaderboardPosition : IEntityTypeConfiguration +{ + public int Id { get; set; } + + /// + /// The servers that this position is for. + /// + public required List Servers { get; set; } = null!; + + /// + /// The number next to the position. For example "most times played" would be a count of how many times the player has played. + /// + public required int Count { get; set; } + + /// + /// The position of the player in the leaderboard. + /// + public int Position { get; set; } + + /// + /// The GUID of the player. If null, position is not for a player, but rather a general statistic. + /// + public Guid? PlayerGuid { get; set; } + + /// + /// The display value of the player or statistic. + /// + public string Username { get; set; } = null!; + + + public string LeaderboardDefinitionName { get; set; } = null!; + public LeaderboardDefinition LeaderboardDefinition { get; set; } = null!; + + + + public void Configure(EntityTypeBuilder builder) + { + builder.HasOne(lp => lp.LeaderboardDefinition) + .WithMany() + .HasForeignKey(lp => lp.LeaderboardDefinitionName); + + builder.HasKey(x => x.Id); + builder.Property(x => x.Servers).IsRequired(); + builder.Property(x => x.Count).IsRequired(); + builder.Property(x => x.Position).IsRequired(); + builder.Property(x => x.PlayerGuid).IsRequired(false); + builder.Property(x => x.Username).IsRequired(); + } +} \ No newline at end of file diff --git a/ReplayBrowser/Data/ReplayDbContext.cs b/ReplayBrowser/Data/ReplayDbContext.cs index e7f2160..ce54f79 100644 --- a/ReplayBrowser/Data/ReplayDbContext.cs +++ b/ReplayBrowser/Data/ReplayDbContext.cs @@ -49,4 +49,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) public DbSet ReplayParticipants { get; set; } public DbSet ServerTokens { get; set; } + + public DbSet Leaderboards { get; set; } + public DbSet LeaderboardDefinitions { get; set; } } \ No newline at end of file diff --git a/ReplayBrowser/Models/LeaderboardData.cs b/ReplayBrowser/Models/LeaderboardData.cs index 4d7bc41..9534829 100644 --- a/ReplayBrowser/Models/LeaderboardData.cs +++ b/ReplayBrowser/Models/LeaderboardData.cs @@ -3,13 +3,6 @@ namespace ReplayBrowser.Models; -public class LeaderboardData -{ - public bool IsCache { get; set; } = false; - - public List Leaderboards { get; set; } = null!; -} - public class PlayerCount { public PlayerData? Player { get; set; } diff --git a/ReplayBrowser/Pages/Leaderboard.razor b/ReplayBrowser/Pages/Leaderboard.razor index 623ef79..d1d2547 100644 --- a/ReplayBrowser/Pages/Leaderboard.razor +++ b/ReplayBrowser/Pages/Leaderboard.razor @@ -1,4 +1,5 @@ @page "/leaderboard" +@using Humanizer @using Microsoft.AspNetCore.Components.Authorization @using ReplayBrowser.Models @using ReplayBrowser.Services @@ -20,6 +21,14 @@ Leaderboard

Leaderboards

+Leaderboards update every 24 hours. +@if(LeaderboardService.IsUpdating) { + +} + @if(RequestedPrivate) {