From 80f523d6e9733c4bddd49581aac640a3b3a325cb Mon Sep 17 00:00:00 2001
From: Simon <63975668+Simyon264@users.noreply.github.com>
Date: Thu, 29 Aug 2024 02:13:27 +0200
Subject: [PATCH 1/5] Replay events pt. 1
---
.../20240828233655_ReplayEvents.Designer.cs | 853 ++++++++++++++++++
.../Migrations/20240828233655_ReplayEvents.cs | 112 +++
.../ReplayDbContextModelSnapshot.cs | 215 +++++
ReplayBrowser/Data/Models/Replay.cs | 10 +-
ReplayBrowser/Data/Models/ReplayDbEvent.cs | 65 ++
ReplayBrowser/Helpers/ReplayHelper.cs | 1 +
.../AlertLevelChangedReplayEvent.cs | 8 +
.../EventTypes/CargoObjectSoldReplayEvent.cs | 13 +
.../CargoProductsOrderedReplayEvent.cs | 19 +
.../EventTypes/ChatAnnouncementReplayEvent.cs | 10 +
.../EventTypes/ChatMessageReplayEvent.cs | 12 +
.../EventTypes/GenericObjectEvent.cs | 10 +
.../EventTypes/GenericPlayerEvent.cs | 16 +
.../MobStateChangedNPCReplayEvent.cs | 15 +
.../MobStateChangedPlayerReplayEvent.cs | 15 +
.../NewsArticlePublishedReplayEvent.cs | 14 +
.../EventTypes/ReplayExplosionEvent.cs | 22 +
.../EventTypes/ShuttleReplayEvent.cs | 10 +
.../EventTypes/StoreBuyReplayEvent.cs | 12 +
.../TechnologyUnlockedReplayEvent.cs | 14 +
.../Models/Ingested/ReplayEvents/MobState.cs | 9 +
.../Ingested/ReplayEvents/ReplayEvent.cs | 62 ++
.../ReplayEvents/ReplayEventPlayer.cs | 42 +
.../ReplayEvents/ReplayEventSeverity.cs | 24 +
.../Ingested/ReplayEvents/ReplayEventType.cs | 57 ++
.../ReplayEvents/ReplayTypeResolver.cs | 38 +
.../ReplayParser/ReplayParserService.cs | 63 +-
27 files changed, 1733 insertions(+), 8 deletions(-)
create mode 100644 ReplayBrowser/Data/Migrations/20240828233655_ReplayEvents.Designer.cs
create mode 100644 ReplayBrowser/Data/Migrations/20240828233655_ReplayEvents.cs
create mode 100644 ReplayBrowser/Data/Models/ReplayDbEvent.cs
create mode 100644 ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/AlertLevelChangedReplayEvent.cs
create mode 100644 ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/CargoObjectSoldReplayEvent.cs
create mode 100644 ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/CargoProductsOrderedReplayEvent.cs
create mode 100644 ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/ChatAnnouncementReplayEvent.cs
create mode 100644 ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/ChatMessageReplayEvent.cs
create mode 100644 ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/GenericObjectEvent.cs
create mode 100644 ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/GenericPlayerEvent.cs
create mode 100644 ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/MobStateChangedNPCReplayEvent.cs
create mode 100644 ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/MobStateChangedPlayerReplayEvent.cs
create mode 100644 ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/NewsArticlePublishedReplayEvent.cs
create mode 100644 ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/ReplayExplosionEvent.cs
create mode 100644 ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/ShuttleReplayEvent.cs
create mode 100644 ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/StoreBuyReplayEvent.cs
create mode 100644 ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/TechnologyUnlockedReplayEvent.cs
create mode 100644 ReplayBrowser/Models/Ingested/ReplayEvents/MobState.cs
create mode 100644 ReplayBrowser/Models/Ingested/ReplayEvents/ReplayEvent.cs
create mode 100644 ReplayBrowser/Models/Ingested/ReplayEvents/ReplayEventPlayer.cs
create mode 100644 ReplayBrowser/Models/Ingested/ReplayEvents/ReplayEventSeverity.cs
create mode 100644 ReplayBrowser/Models/Ingested/ReplayEvents/ReplayEventType.cs
create mode 100644 ReplayBrowser/Models/Ingested/ReplayEvents/ReplayTypeResolver.cs
diff --git a/ReplayBrowser/Data/Migrations/20240828233655_ReplayEvents.Designer.cs b/ReplayBrowser/Data/Migrations/20240828233655_ReplayEvents.Designer.cs
new file mode 100644
index 0000000..efb5048
--- /dev/null
+++ b/ReplayBrowser/Data/Migrations/20240828233655_ReplayEvents.Designer.cs
@@ -0,0 +1,853 @@
+//
+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("20240828233655_ReplayEvents")]
+ partial class ReplayEvents
+ {
+ ///
+ 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.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.ReplayDbEvent", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClassType")
+ .IsRequired()
+ .HasMaxLength(34)
+ .HasColumnType("character varying(34)");
+
+ b.Property("EventType")
+ .HasColumnType("integer");
+
+ b.Property("NearestBeacon")
+ .HasColumnType("text");
+
+ b.Property("Position")
+ .HasColumnType("point");
+
+ b.Property("ReplayId")
+ .HasColumnType("integer");
+
+ b.Property("Severity")
+ .HasColumnType("integer");
+
+ b.Property("Time")
+ .IsRequired()
+ .HasColumnType("double precision");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EventType");
+
+ b.HasIndex("ReplayId");
+
+ b.HasIndex("Severity");
+
+ b.HasIndex("Time");
+
+ b.ToTable("ReplayDbEvent");
+
+ b.HasDiscriminator("ClassType").HasValue("ReplayDbEvent");
+
+ b.UseTphMappingStrategy();
+ });
+
+ 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.Models.Ingested.ReplayEvents.ReplayEventPlayer", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AntagPrototypes")
+ .HasColumnType("text[]");
+
+ b.Property("JobPrototypes")
+ .HasColumnType("text[]");
+
+ b.Property("PlayerGuid")
+ .HasColumnType("uuid");
+
+ b.Property("PlayerIcName")
+ .HasColumnType("text");
+
+ b.Property("PlayerOocName")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("ReplayEventPlayer");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.AlertLevelChangedReplayEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("AlertLevelChangedReplayEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.CargoObjectSoldReplayEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("CargoObjectSoldReplayEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.CargoProductsOrderedReplayEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("CargoProductsOrderedReplayEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.ChatAnnouncementReplayEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("ChatAnnouncementReplayEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.ChatMessageReplayEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("ChatMessageReplayEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.GenericObjectEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("GenericObjectEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.GenericPlayerEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.Property("OriginId")
+ .HasColumnType("integer");
+
+ b.Property("TargetId")
+ .HasColumnType("integer");
+
+ b.HasIndex("OriginId");
+
+ b.HasIndex("TargetId");
+
+ b.HasDiscriminator().HasValue("GenericPlayerEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.MobStateChangedNPCReplayEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("MobStateChangedNPCReplayEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.MobStateChangedPlayerReplayEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("MobStateChangedPlayerReplayEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.NewsArticlePublishedReplayEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("NewsArticlePublishedReplayEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.ReplayExplosionEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("ReplayExplosionEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.ShuttleReplayEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("ShuttleReplayEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.StoreBuyReplayEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("StoreBuyReplayEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.TechnologyUnlockedReplayEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("TechnologyUnlockedReplayEvent");
+ });
+
+ 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.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.ReplayDbEvent", b =>
+ {
+ b.HasOne("ReplayBrowser.Data.Models.Replay", "Replay")
+ .WithMany("Events")
+ .HasForeignKey("ReplayId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Replay");
+ });
+
+ 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.Models.Ingested.ReplayEvents.EventTypes.GenericPlayerEvent", b =>
+ {
+ b.HasOne("ReplayBrowser.Models.Ingested.ReplayEvents.ReplayEventPlayer", "Origin")
+ .WithMany()
+ .HasForeignKey("OriginId");
+
+ b.HasOne("ReplayBrowser.Models.Ingested.ReplayEvents.ReplayEventPlayer", "Target")
+ .WithMany()
+ .HasForeignKey("TargetId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Origin");
+
+ b.Navigation("Target");
+ });
+
+ 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("Events");
+
+ b.Navigation("RoundParticipants");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Data.Models.ReplayParticipant", b =>
+ {
+ b.Navigation("Players");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/ReplayBrowser/Data/Migrations/20240828233655_ReplayEvents.cs b/ReplayBrowser/Data/Migrations/20240828233655_ReplayEvents.cs
new file mode 100644
index 0000000..e32fa68
--- /dev/null
+++ b/ReplayBrowser/Data/Migrations/20240828233655_ReplayEvents.cs
@@ -0,0 +1,112 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using NpgsqlTypes;
+
+#nullable disable
+
+namespace Server.Migrations
+{
+ ///
+ public partial class ReplayEvents : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "ReplayEventPlayer",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ PlayerOocName = table.Column(type: "text", nullable: true),
+ PlayerIcName = table.Column(type: "text", nullable: true),
+ PlayerGuid = table.Column(type: "uuid", nullable: true),
+ JobPrototypes = table.Column(type: "text[]", nullable: true),
+ AntagPrototypes = table.Column(type: "text[]", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_ReplayEventPlayer", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "ReplayDbEvent",
+ columns: table => new
+ {
+ Id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ ReplayId = table.Column(type: "integer", nullable: false),
+ ClassType = table.Column(type: "character varying(34)", maxLength: 34, nullable: false),
+ TargetId = table.Column(type: "integer", nullable: true),
+ OriginId = table.Column(type: "integer", nullable: true),
+ Time = table.Column(type: "double precision", nullable: false),
+ Severity = table.Column(type: "integer", nullable: false),
+ EventType = table.Column(type: "integer", nullable: false),
+ NearestBeacon = table.Column(type: "text", nullable: true),
+ Position = table.Column(type: "point", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_ReplayDbEvent", x => x.Id);
+ table.ForeignKey(
+ name: "FK_ReplayDbEvent_ReplayEventPlayer_OriginId",
+ column: x => x.OriginId,
+ principalTable: "ReplayEventPlayer",
+ principalColumn: "Id");
+ table.ForeignKey(
+ name: "FK_ReplayDbEvent_ReplayEventPlayer_TargetId",
+ column: x => x.TargetId,
+ principalTable: "ReplayEventPlayer",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_ReplayDbEvent_Replays_ReplayId",
+ column: x => x.ReplayId,
+ principalTable: "Replays",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ReplayDbEvent_EventType",
+ table: "ReplayDbEvent",
+ column: "EventType");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ReplayDbEvent_OriginId",
+ table: "ReplayDbEvent",
+ column: "OriginId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ReplayDbEvent_ReplayId",
+ table: "ReplayDbEvent",
+ column: "ReplayId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ReplayDbEvent_Severity",
+ table: "ReplayDbEvent",
+ column: "Severity");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ReplayDbEvent_TargetId",
+ table: "ReplayDbEvent",
+ column: "TargetId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_ReplayDbEvent_Time",
+ table: "ReplayDbEvent",
+ column: "Time");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "ReplayDbEvent");
+
+ migrationBuilder.DropTable(
+ name: "ReplayEventPlayer");
+ }
+ }
+}
diff --git a/ReplayBrowser/Data/Migrations/ReplayDbContextModelSnapshot.cs b/ReplayBrowser/Data/Migrations/ReplayDbContextModelSnapshot.cs
index 7f62be0..3196c89 100644
--- a/ReplayBrowser/Data/Migrations/ReplayDbContextModelSnapshot.cs
+++ b/ReplayBrowser/Data/Migrations/ReplayDbContextModelSnapshot.cs
@@ -480,6 +480,55 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.ToTable("Replays");
});
+ modelBuilder.Entity("ReplayBrowser.Data.Models.ReplayDbEvent", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ClassType")
+ .IsRequired()
+ .HasMaxLength(34)
+ .HasColumnType("character varying(34)");
+
+ b.Property("EventType")
+ .HasColumnType("integer");
+
+ b.Property("NearestBeacon")
+ .HasColumnType("text");
+
+ b.Property("Position")
+ .HasColumnType("point");
+
+ b.Property("ReplayId")
+ .HasColumnType("integer");
+
+ b.Property("Severity")
+ .HasColumnType("integer");
+
+ b.Property("Time")
+ .IsRequired()
+ .HasColumnType("double precision");
+
+ b.HasKey("Id");
+
+ b.HasIndex("EventType");
+
+ b.HasIndex("ReplayId");
+
+ b.HasIndex("Severity");
+
+ b.HasIndex("Time");
+
+ b.ToTable("ReplayDbEvent");
+
+ b.HasDiscriminator("ClassType").HasValue("ReplayDbEvent");
+
+ b.UseTphMappingStrategy();
+ });
+
modelBuilder.Entity("ReplayBrowser.Data.Models.ReplayParticipant", b =>
{
b.Property("Id")
@@ -510,6 +559,142 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.ToTable("ReplayParticipants");
});
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.ReplayEventPlayer", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AntagPrototypes")
+ .HasColumnType("text[]");
+
+ b.Property("JobPrototypes")
+ .HasColumnType("text[]");
+
+ b.Property("PlayerGuid")
+ .HasColumnType("uuid");
+
+ b.Property("PlayerIcName")
+ .HasColumnType("text");
+
+ b.Property("PlayerOocName")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("ReplayEventPlayer");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.AlertLevelChangedReplayEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("AlertLevelChangedReplayEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.CargoObjectSoldReplayEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("CargoObjectSoldReplayEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.CargoProductsOrderedReplayEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("CargoProductsOrderedReplayEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.ChatAnnouncementReplayEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("ChatAnnouncementReplayEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.ChatMessageReplayEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("ChatMessageReplayEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.GenericObjectEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("GenericObjectEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.GenericPlayerEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.Property("OriginId")
+ .HasColumnType("integer");
+
+ b.Property("TargetId")
+ .HasColumnType("integer");
+
+ b.HasIndex("OriginId");
+
+ b.HasIndex("TargetId");
+
+ b.HasDiscriminator().HasValue("GenericPlayerEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.MobStateChangedNPCReplayEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("MobStateChangedNPCReplayEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.MobStateChangedPlayerReplayEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("MobStateChangedPlayerReplayEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.NewsArticlePublishedReplayEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("NewsArticlePublishedReplayEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.ReplayExplosionEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("ReplayExplosionEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.ShuttleReplayEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("ShuttleReplayEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.StoreBuyReplayEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("StoreBuyReplayEvent");
+ });
+
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.TechnologyUnlockedReplayEvent", b =>
+ {
+ b.HasBaseType("ReplayBrowser.Data.Models.ReplayDbEvent");
+
+ b.HasDiscriminator().HasValue("TechnologyUnlockedReplayEvent");
+ });
+
modelBuilder.Entity("ReplayBrowser.Data.Models.Account.Account", b =>
{
b.HasOne("ReplayBrowser.Data.Models.Account.AccountSettings", "Settings")
@@ -590,6 +775,17 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Navigation("Participant");
});
+ modelBuilder.Entity("ReplayBrowser.Data.Models.ReplayDbEvent", b =>
+ {
+ b.HasOne("ReplayBrowser.Data.Models.Replay", "Replay")
+ .WithMany("Events")
+ .HasForeignKey("ReplayId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Replay");
+ });
+
modelBuilder.Entity("ReplayBrowser.Data.Models.ReplayParticipant", b =>
{
b.HasOne("ReplayBrowser.Data.Models.Replay", "Replay")
@@ -601,6 +797,23 @@ protected override void BuildModel(ModelBuilder modelBuilder)
b.Navigation("Replay");
});
+ modelBuilder.Entity("ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes.GenericPlayerEvent", b =>
+ {
+ b.HasOne("ReplayBrowser.Models.Ingested.ReplayEvents.ReplayEventPlayer", "Origin")
+ .WithMany()
+ .HasForeignKey("OriginId");
+
+ b.HasOne("ReplayBrowser.Models.Ingested.ReplayEvents.ReplayEventPlayer", "Target")
+ .WithMany()
+ .HasForeignKey("TargetId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Origin");
+
+ b.Navigation("Target");
+ });
+
modelBuilder.Entity("ReplayBrowser.Data.Models.Account.Account", b =>
{
b.Navigation("History");
@@ -622,6 +835,8 @@ protected override void BuildModel(ModelBuilder modelBuilder)
modelBuilder.Entity("ReplayBrowser.Data.Models.Replay", b =>
{
+ b.Navigation("Events");
+
b.Navigation("RoundParticipants");
});
diff --git a/ReplayBrowser/Data/Models/Replay.cs b/ReplayBrowser/Data/Models/Replay.cs
index 95e3b6e..dd0f198 100644
--- a/ReplayBrowser/Data/Models/Replay.cs
+++ b/ReplayBrowser/Data/Models/Replay.cs
@@ -1,10 +1,12 @@
using System.ComponentModel.DataAnnotations.Schema;
+using System.Reflection;
using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using NpgsqlTypes;
using ReplayBrowser.Models;
using ReplayBrowser.Models.Ingested;
+using ReplayBrowser.Models.Ingested.ReplayEvents;
using YamlDotNet.Serialization;
namespace ReplayBrowser.Data.Models;
@@ -35,6 +37,8 @@ public class Replay : IEntityTypeConfiguration
public int UncompressedSize { get; set; }
public required string EndTime { get; set; }
+ public List? Events { get; set; }
+
[JsonIgnore]
public NpgsqlTsVector RoundEndTextSearchVector { get; set; } = null!;
@@ -86,7 +90,7 @@ public ReplayResult ToResult()
};
}
- public static Replay FromYaml(YamlReplay replay, string link)
+ public static Replay FromYaml(YamlReplay replay, List? replayEvents, string link)
{
var participants = replay.RoundEndPlayers?
.GroupBy(p => p.PlayerGuid)
@@ -114,7 +118,9 @@ public static Replay FromYaml(YamlReplay replay, string link)
FileCount = replay.FileCount,
Size = replay.Size,
UncompressedSize = replay.UncompressedSize,
- EndTime = replay.EndTime
+ EndTime = replay.EndTime,
+
+ Events = replayEvents
};
}
diff --git a/ReplayBrowser/Data/Models/ReplayDbEvent.cs b/ReplayBrowser/Data/Models/ReplayDbEvent.cs
new file mode 100644
index 0000000..3a7c3c8
--- /dev/null
+++ b/ReplayBrowser/Data/Models/ReplayDbEvent.cs
@@ -0,0 +1,65 @@
+using System.Numerics;
+using System.Reflection;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata.Builders;
+using Newtonsoft.Json;
+using ReplayBrowser.Models.Ingested.ReplayEvents;
+using ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes;
+
+namespace ReplayBrowser.Data.Models;
+
+public class ReplayDbEvent : ReplayEvent, IEntityTypeConfiguration
+{
+ [JsonIgnore]
+ public int Id { get; set; }
+
+ [JsonIgnore]
+ public int ReplayId { get; set; }
+
+ [JsonIgnore]
+ public Replay Replay { get; set; } = null!;
+
+ public void Configure(EntityTypeBuilder builder)
+ {
+ //var eventTypes = Assembly.GetExecutingAssembly()
+ // .GetTypes()
+ // .Where(t => t.IsSubclassOf(typeof(ReplayEvent)))
+ // .ToDictionary(t => t.Name, t => t);
+
+ builder.HasDiscriminator("ClassType")
+ .HasValue(nameof(ReplayDbEvent))
+ .HasValue(nameof(AlertLevelChangedReplayEvent))
+ .HasValue(nameof(CargoObjectSoldReplayEvent))
+ .HasValue(nameof(CargoProductsOrderedReplayEvent))
+ .HasValue(nameof(ChatAnnouncementReplayEvent))
+ .HasValue(nameof(ChatMessageReplayEvent))
+ .HasValue(nameof(GenericObjectEvent))
+ .HasValue(nameof(GenericPlayerEvent))
+ .HasValue(nameof(MobStateChangedNPCReplayEvent))
+ .HasValue(nameof(MobStateChangedPlayerReplayEvent))
+ .HasValue(nameof(NewsArticlePublishedReplayEvent))
+ .HasValue(nameof(ReplayExplosionEvent))
+ .HasValue(nameof(ShuttleReplayEvent))
+ .HasValue(nameof(StoreBuyReplayEvent))
+ .HasValue(nameof(TechnologyUnlockedReplayEvent));
+
+ builder.Property(e => e.Position).IsRequired();
+ builder.Property(e => e.Severity).IsRequired();
+ builder.Property(e => e.Time).IsRequired();
+ builder.Property(e => e.EventType).IsRequired();
+
+ builder.HasIndex(e => e.ReplayId);
+ builder.HasIndex(e => e.EventType);
+ builder.HasIndex(e => e.Time);
+ builder.HasIndex(e => e.Severity);
+
+ builder.HasOne(e => e.Replay)
+ .WithMany(r => r.Events)
+ .HasForeignKey(e => e.ReplayId);
+
+ builder.Property(e => e.Position)
+ .HasConversion(
+ v => new NpgsqlTypes.NpgsqlPoint(v.X, v.Y),
+ v => new Vector2((float)v.X, (float)v.Y));
+ }
+}
\ No newline at end of file
diff --git a/ReplayBrowser/Helpers/ReplayHelper.cs b/ReplayBrowser/Helpers/ReplayHelper.cs
index 5b60874..8bd82f2 100644
--- a/ReplayBrowser/Helpers/ReplayHelper.cs
+++ b/ReplayBrowser/Helpers/ReplayHelper.cs
@@ -206,6 +206,7 @@ public async Task GetTotalReplayCount()
.AsNoTracking()
.Include(r => r.RoundParticipants!)
.ThenInclude(p => p.Players)
+ .Include(r => r.Events)
.FirstOrDefaultAsync(r => r.Id == id);
if (replay == null)
diff --git a/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/AlertLevelChangedReplayEvent.cs b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/AlertLevelChangedReplayEvent.cs
new file mode 100644
index 0000000..893a61e
--- /dev/null
+++ b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/AlertLevelChangedReplayEvent.cs
@@ -0,0 +1,8 @@
+using ReplayBrowser.Data.Models;
+
+namespace ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes;
+
+public class AlertLevelChangedReplayEvent : ReplayDbEvent
+{
+ public string AlertLevel;
+}
\ No newline at end of file
diff --git a/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/CargoObjectSoldReplayEvent.cs b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/CargoObjectSoldReplayEvent.cs
new file mode 100644
index 0000000..ebdc11c
--- /dev/null
+++ b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/CargoObjectSoldReplayEvent.cs
@@ -0,0 +1,13 @@
+using ReplayBrowser.Data.Models;
+
+namespace ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes;
+
+public class CargoObjectSoldReplayEvent : ReplayDbEvent
+{
+ ///
+ /// The amount of money the objects were sold for
+ ///
+ public double Amount;
+
+ public int ObjectsSold;
+}
\ No newline at end of file
diff --git a/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/CargoProductsOrderedReplayEvent.cs b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/CargoProductsOrderedReplayEvent.cs
new file mode 100644
index 0000000..3f86de0
--- /dev/null
+++ b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/CargoProductsOrderedReplayEvent.cs
@@ -0,0 +1,19 @@
+using ReplayBrowser.Data.Models;
+
+namespace ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes;
+
+public class CargoProductsOrderedReplayEvent : ReplayDbEvent
+{
+ public ReplayEventPlayer ApprovedBy;
+
+ public CargoReplayProduct Product;
+}
+
+public class CargoReplayProduct
+{
+ public string ProductId;
+
+ public string Reason = "";
+
+ public ReplayEventPlayer OrderedBy;
+}
\ No newline at end of file
diff --git a/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/ChatAnnouncementReplayEvent.cs b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/ChatAnnouncementReplayEvent.cs
new file mode 100644
index 0000000..677bd11
--- /dev/null
+++ b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/ChatAnnouncementReplayEvent.cs
@@ -0,0 +1,10 @@
+using ReplayBrowser.Data.Models;
+
+namespace ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes;
+
+public class ChatAnnouncementReplayEvent : ReplayDbEvent
+{
+ public string Message;
+
+ public string Sender;
+}
\ No newline at end of file
diff --git a/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/ChatMessageReplayEvent.cs b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/ChatMessageReplayEvent.cs
new file mode 100644
index 0000000..b0eff89
--- /dev/null
+++ b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/ChatMessageReplayEvent.cs
@@ -0,0 +1,12 @@
+using ReplayBrowser.Data.Models;
+
+namespace ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes;
+
+public class ChatMessageReplayEvent : ReplayDbEvent
+{
+ public string Message;
+
+ public ReplayEventPlayer Sender;
+
+ public string Type;
+}
\ No newline at end of file
diff --git a/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/GenericObjectEvent.cs b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/GenericObjectEvent.cs
new file mode 100644
index 0000000..56dc83e
--- /dev/null
+++ b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/GenericObjectEvent.cs
@@ -0,0 +1,10 @@
+using ReplayBrowser.Data.Models;
+
+namespace ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes;
+
+public class GenericObjectEvent : ReplayDbEvent
+{
+ public string Target;
+
+ public string? Origin;
+}
\ No newline at end of file
diff --git a/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/GenericPlayerEvent.cs b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/GenericPlayerEvent.cs
new file mode 100644
index 0000000..e29b8b2
--- /dev/null
+++ b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/GenericPlayerEvent.cs
@@ -0,0 +1,16 @@
+using ReplayBrowser.Data.Models;
+
+namespace ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes;
+
+public class GenericPlayerEvent : ReplayDbEvent
+{
+ ///
+ /// The player info associated with this event. This who this event is about.
+ ///
+ public ReplayEventPlayer Target { get; set; }
+
+ ///
+ /// The source of the event. Who was the cause for the Target being affected? Can be null.
+ ///
+ public ReplayEventPlayer? Origin { get; set; }
+}
\ No newline at end of file
diff --git a/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/MobStateChangedNPCReplayEvent.cs b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/MobStateChangedNPCReplayEvent.cs
new file mode 100644
index 0000000..db7327f
--- /dev/null
+++ b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/MobStateChangedNPCReplayEvent.cs
@@ -0,0 +1,15 @@
+using ReplayBrowser.Data.Models;
+
+namespace ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes;
+
+///
+/// Represents a non-player controlled mob changing mob states.
+///
+public class MobStateChangedNPCReplayEvent : ReplayDbEvent
+{
+ public string Target;
+
+ public MobState OldState;
+
+ public MobState NewState;
+}
\ No newline at end of file
diff --git a/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/MobStateChangedPlayerReplayEvent.cs b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/MobStateChangedPlayerReplayEvent.cs
new file mode 100644
index 0000000..d4b06e6
--- /dev/null
+++ b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/MobStateChangedPlayerReplayEvent.cs
@@ -0,0 +1,15 @@
+using ReplayBrowser.Data.Models;
+
+namespace ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes;
+
+///
+/// Represents a player controlled mob changing mob states.
+///
+public class MobStateChangedPlayerReplayEvent : ReplayDbEvent
+{
+ public ReplayEventPlayer Target;
+
+ public MobState OldState;
+
+ public MobState NewState;
+}
\ No newline at end of file
diff --git a/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/NewsArticlePublishedReplayEvent.cs b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/NewsArticlePublishedReplayEvent.cs
new file mode 100644
index 0000000..e5e927f
--- /dev/null
+++ b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/NewsArticlePublishedReplayEvent.cs
@@ -0,0 +1,14 @@
+using ReplayBrowser.Data.Models;
+
+namespace ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes;
+
+public class NewsArticlePublishedReplayEvent : ReplayDbEvent
+{
+ public string Title;
+
+ public string Content;
+
+ public string? Author;
+
+ public TimeSpan ShareTime;
+}
\ No newline at end of file
diff --git a/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/ReplayExplosionEvent.cs b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/ReplayExplosionEvent.cs
new file mode 100644
index 0000000..faf360c
--- /dev/null
+++ b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/ReplayExplosionEvent.cs
@@ -0,0 +1,22 @@
+using ReplayBrowser.Data.Models;
+
+namespace ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes;
+
+public class ReplayExplosionEvent : ReplayDbEvent
+{
+ public ReplayEventPlayer? Source;
+
+ public float Intensity;
+
+ public float Slope;
+
+ public float MaxTileIntensity;
+
+ public float TileBreakScale;
+
+ public int MaxTileBreak;
+
+ public bool CanCreateVacuum;
+
+ public string Type;
+}
\ No newline at end of file
diff --git a/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/ShuttleReplayEvent.cs b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/ShuttleReplayEvent.cs
new file mode 100644
index 0000000..2e4d2f7
--- /dev/null
+++ b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/ShuttleReplayEvent.cs
@@ -0,0 +1,10 @@
+using ReplayBrowser.Data.Models;
+
+namespace ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes;
+
+public class ShuttleReplayEvent : ReplayDbEvent
+{
+ public int? Countdown;
+
+ public ReplayEventPlayer? Source;
+}
\ No newline at end of file
diff --git a/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/StoreBuyReplayEvent.cs b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/StoreBuyReplayEvent.cs
new file mode 100644
index 0000000..774b02a
--- /dev/null
+++ b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/StoreBuyReplayEvent.cs
@@ -0,0 +1,12 @@
+using ReplayBrowser.Data.Models;
+
+namespace ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes;
+
+public class StoreBuyReplayEvent : ReplayDbEvent
+{
+ public ReplayEventPlayer Buyer;
+
+ public string Item;
+
+ public int Cost;
+}
\ No newline at end of file
diff --git a/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/TechnologyUnlockedReplayEvent.cs b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/TechnologyUnlockedReplayEvent.cs
new file mode 100644
index 0000000..b083c02
--- /dev/null
+++ b/ReplayBrowser/Models/Ingested/ReplayEvents/EventTypes/TechnologyUnlockedReplayEvent.cs
@@ -0,0 +1,14 @@
+using ReplayBrowser.Data.Models;
+
+namespace ReplayBrowser.Models.Ingested.ReplayEvents.EventTypes;
+
+public class TechnologyUnlockedReplayEvent : ReplayDbEvent
+{
+ public string Name;
+
+ public string Discipline;
+
+ public int Tier;
+
+ public ReplayEventPlayer Player;
+}
\ No newline at end of file
diff --git a/ReplayBrowser/Models/Ingested/ReplayEvents/MobState.cs b/ReplayBrowser/Models/Ingested/ReplayEvents/MobState.cs
new file mode 100644
index 0000000..a4bf8f1
--- /dev/null
+++ b/ReplayBrowser/Models/Ingested/ReplayEvents/MobState.cs
@@ -0,0 +1,9 @@
+namespace ReplayBrowser.Models.Ingested.ReplayEvents;
+
+public enum MobState : byte
+{
+ Invalid = 0,
+ Alive = 1,
+ Critical = 2,
+ Dead = 3,
+}
\ No newline at end of file
diff --git a/ReplayBrowser/Models/Ingested/ReplayEvents/ReplayEvent.cs b/ReplayBrowser/Models/Ingested/ReplayEvents/ReplayEvent.cs
new file mode 100644
index 0000000..395cf89
--- /dev/null
+++ b/ReplayBrowser/Models/Ingested/ReplayEvents/ReplayEvent.cs
@@ -0,0 +1,62 @@
+using System.ComponentModel.DataAnnotations.Schema;
+using System.Numerics;
+using Newtonsoft.Json;
+using YamlDotNet.Serialization;
+
+namespace ReplayBrowser.Models.Ingested.ReplayEvents;
+
+public class ReplayEvent
+{
+ ///
+ /// Represents the seconds since the start of the round when the event occurred.
+ ///
+ public double? Time { get; set; }
+
+ ///
+ /// How severe the event is.
+ ///
+ public ReplayEventSeverity Severity { get; set; }
+
+ ///
+ /// The type of event that occurred.
+ ///
+ [JsonIgnore] // This is not needed in the JSON.
+ public ReplayEventType EventType { get; set; }
+
+ ///
+ /// The type of event that occurred. For serialization purposes.
+ ///
+ [NotMapped]
+ [YamlIgnore]
+ public string EventTypeString
+ {
+ get => EventType.ToString();
+ set => EventType = Enum.Parse(value);
+ }
+
+ ///
+ /// The nearest beacon to the event.
+ ///
+ public string? NearestBeacon { get; set; }
+
+ ///
+ /// The exact position of the event.
+ ///
+ [YamlIgnore]
+ public Vector2 Position { get; set; }
+
+ ///
+ /// The exact position of the event. This is needed because Robust stores null vecotrs as 0,0 and also YAML cannot parse string to Vector2.
+ ///
+ [NotMapped] // This is not a database column.
+ [YamlMember(Alias = "position")]
+ public string PositionString
+ {
+ get => $"{Position.X},{Position.Y}";
+ set
+ {
+ var parts = value.Split(',');
+ Position = new Vector2(float.Parse(parts[0]), float.Parse(parts[1]));
+ }
+ }
+}
\ No newline at end of file
diff --git a/ReplayBrowser/Models/Ingested/ReplayEvents/ReplayEventPlayer.cs b/ReplayBrowser/Models/Ingested/ReplayEvents/ReplayEventPlayer.cs
new file mode 100644
index 0000000..6e8dee4
--- /dev/null
+++ b/ReplayBrowser/Models/Ingested/ReplayEvents/ReplayEventPlayer.cs
@@ -0,0 +1,42 @@
+using Newtonsoft.Json;
+using YamlDotNet.Serialization;
+
+namespace ReplayBrowser.Models.Ingested.ReplayEvents;
+
+///
+/// Represents a player in a replay event.
+///
+public class ReplayEventPlayer
+{
+ ///
+ /// DB primary key.
+ ///
+ [JsonIgnore]
+ [YamlIgnore]
+ public int Id { get; set; }
+
+ ///
+ /// The username of the player.
+ ///
+ public string? PlayerOocName { get; set; }
+
+ ///
+ /// The character name of the entity the player is playing as.
+ ///
+ public string? PlayerIcName { get; set; }
+
+ ///
+ /// The GUID of the player. Null if this was not a connected player who caused the event. (e.g. a NPC)
+ ///
+ public Guid? PlayerGuid { get; set; }
+
+ ///
+ /// The job(s) the player was playing as when the event occurred.
+ ///
+ public string[]? JobPrototypes { get; set; }
+
+ ///
+ /// The antag role(s) the player was playing as when the event occurred.
+ ///
+ public string[]? AntagPrototypes { get; set; }
+}
\ No newline at end of file
diff --git a/ReplayBrowser/Models/Ingested/ReplayEvents/ReplayEventSeverity.cs b/ReplayBrowser/Models/Ingested/ReplayEvents/ReplayEventSeverity.cs
new file mode 100644
index 0000000..ed31139
--- /dev/null
+++ b/ReplayBrowser/Models/Ingested/ReplayEvents/ReplayEventSeverity.cs
@@ -0,0 +1,24 @@
+namespace ReplayBrowser.Models.Ingested.ReplayEvents;
+
+public enum ReplayEventSeverity
+{
+ ///
+ /// Low relevance, such as a player joining or leaving
+ ///
+ Low,
+
+ ///
+ /// Medium relevance, such as a chat message
+ ///
+ Medium,
+
+ ///
+ /// High relevance, so an announcment or a shuttle call
+ ///
+ High,
+
+ ///
+ /// Severe relevance, such as the station nuke being armed
+ ///
+ Critical
+}
\ No newline at end of file
diff --git a/ReplayBrowser/Models/Ingested/ReplayEvents/ReplayEventType.cs b/ReplayBrowser/Models/Ingested/ReplayEvents/ReplayEventType.cs
new file mode 100644
index 0000000..d95d55f
--- /dev/null
+++ b/ReplayBrowser/Models/Ingested/ReplayEvents/ReplayEventType.cs
@@ -0,0 +1,57 @@
+namespace ReplayBrowser.Models.Ingested.ReplayEvents;
+
+///
+/// Represents the type of event that occurred in a replay.
+///
+public enum ReplayEventType
+{
+ #region Out of character events
+
+ PlayerJoin,
+ PlayerLeave,
+
+ #endregion
+
+ #region Gameflow
+
+ GameRuleStarted,
+ GameRuleEnded,
+ RoundEnded,
+
+ #endregion
+
+ #region In character events
+
+ CargoProductOrdered,
+ CargoProductSold,
+
+ MobStateChanged,
+
+ MobSlipped,
+ MobStunned,
+
+ NukeArmed,
+ NukeDetonated,
+ NukeDefused,
+
+ PowerEngineSpawned, // Tesla or Singularity
+ ContainmentFieldDisengaged,
+
+ ItemBoughtFromStore, // Item bought from an (for example) uplink.
+
+ Explosion,
+
+ AnnouncementSent, // Comms console
+ ChatMessageSent,
+ AlertLevelChanged,
+ NewsArticlePublished,
+
+ TechnologyUnlocked,
+
+ EvacuationShuttleCalled,
+ EvacuationShuttleDocked,
+ EvacuationShuttleDockedCentCom,
+ EvacuationShuttleDeparted,
+ EvacuationShuttleRecalled,
+ #endregion
+}
\ No newline at end of file
diff --git a/ReplayBrowser/Models/Ingested/ReplayEvents/ReplayTypeResolver.cs b/ReplayBrowser/Models/Ingested/ReplayEvents/ReplayTypeResolver.cs
new file mode 100644
index 0000000..3f5ae5f
--- /dev/null
+++ b/ReplayBrowser/Models/Ingested/ReplayEvents/ReplayTypeResolver.cs
@@ -0,0 +1,38 @@
+using System.Reflection;
+using ReplayBrowser.Data.Models;
+using YamlDotNet.Core.Events;
+using YamlDotNet.Serialization;
+using YamlDotNet.Serialization.BufferedDeserialization.TypeDiscriminators;
+
+namespace ReplayBrowser.Models.Ingested.ReplayEvents;
+
+///
+/// Resolves the type of a replay event based on its name.
+///
+public class ReplayTypeResolver : INodeTypeResolver
+{
+ private readonly Dictionary _typeMappings;
+
+ public ReplayTypeResolver()
+ {
+ _typeMappings = Assembly.GetExecutingAssembly()
+ .GetTypes()
+ .Where(t => t.IsSubclassOf(typeof(ReplayDbEvent)))
+ .ToDictionary(t => t.Name, t => t);
+ }
+
+ public bool Resolve(NodeEvent? nodeEvent, ref Type currentType)
+ {
+ if (nodeEvent is Scalar scalar && scalar.Value.StartsWith("!type:"))
+ {
+ var typeName = scalar.Value.Substring(6); // Extract the type name after '!type:'
+ if (_typeMappings.TryGetValue(typeName, out var resolvedType))
+ {
+ currentType = resolvedType;
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/ReplayBrowser/Services/ReplayParser/ReplayParserService.cs b/ReplayBrowser/Services/ReplayParser/ReplayParserService.cs
index fa660d0..50864c6 100644
--- a/ReplayBrowser/Services/ReplayParser/ReplayParserService.cs
+++ b/ReplayBrowser/Services/ReplayParser/ReplayParserService.cs
@@ -1,6 +1,7 @@
using System.Collections.Concurrent;
using System.Globalization;
using System.IO.Compression;
+using System.Reflection;
using System.Text;
using Microsoft.EntityFrameworkCore;
using ReplayBrowser.Data;
@@ -8,6 +9,7 @@
using ReplayBrowser.Helpers;
using ReplayBrowser.Models;
using ReplayBrowser.Models.Ingested;
+using ReplayBrowser.Models.Ingested.ReplayEvents;
using ReplayBrowser.Services.ReplayParser.Providers;
using Serilog;
using YamlDotNet.Serialization;
@@ -191,8 +193,16 @@ private async Task ConsumeQueue(CancellationToken token)
}
catch (FormatException)
{
- var date = DateTime.ParseExact(match.Groups[1].Value, "yyyy-MM-dd", CultureInfo.InvariantCulture);
- parsedReplay.Date = date.ToUniversalTime();
+ try
+ {
+ var date = DateTime.ParseExact(match.Groups[1].Value, "yyyy-MM-dd", CultureInfo.InvariantCulture);
+ parsedReplay.Date = date.ToUniversalTime();
+ }
+ catch (FormatException e)
+ {
+ // fuck this
+ parsedReplay.Date = DateTime.UtcNow;
+ }
}
}
@@ -270,17 +280,42 @@ private Replay ParseReplay(Stream stream, string replayLink)
throw new Exception("Replay is not valid.");
}
- var replay = Replay.FromYaml(yamlReplay, replayLink);
+ var eventFile = zipArchive.GetEntry("_replay/events.yml");
+ List? replayEvents = null;
+ if (eventFile != null)
+ {
+ var eventStream = eventFile.Open();
+ var eventReader = new StreamReader(eventStream);
+ var eventDeserializerBuilder = new DeserializerBuilder()
+ .IgnoreUnmatchedProperties()
+ .WithNamingConvention(CamelCaseNamingConvention.Instance);
+
+ var eventTypes = Assembly.GetExecutingAssembly()
+ .GetTypes()
+ .Where(t => t.IsSubclassOf(typeof(ReplayDbEvent)) && !t.IsAbstract);
+
+ foreach (var type in eventTypes)
+ {
+ var tagName = $"!type:{type.Name}";
+ eventDeserializerBuilder = eventDeserializerBuilder.WithTagMapping(tagName, type);
+ }
+
+ var eventDeserializer = eventDeserializerBuilder.Build();
+
+ replayEvents = eventDeserializer.Deserialize?>(eventReader);
+ }
+
+ var replay = Replay.FromYaml(yamlReplay, replayEvents, replayLink);
var replayUrls = _configuration.GetSection("ReplayUrls").Get()!;
if (replay.ServerId == Constants.UnsetServerId)
{
- replay.ServerId = replayUrls.First(x => replay.Link!.Contains(x.Url)).FallBackServerId;
+ replay.ServerId = replayUrls.FirstOrDefault(x => replay.Link!.Contains(x.Url))?.FallBackServerId ?? string.Empty;
}
if (replay.ServerName == Constants.UnsetServerName)
{
- replay.ServerName = replayUrls.First(x => replay.Link!.Contains(x.Url)).FallBackServerName;
+ replay.ServerName = replayUrls.FirstOrDefault(x => replay.Link!.Contains(x.Url))?.FallBackServerName ?? string.Empty;
}
if (yamlReplay.RoundEndPlayers == null)
@@ -315,7 +350,23 @@ private Replay ParseReplay(Stream stream, string replayLink)
public StorageUrl GetStorageUrlFromReplayLink(string replayLink)
{
var replayUrls = _configuration.GetSection("ReplayUrls").Get()!;
- var fetched = replayUrls.First(x => replayLink.Contains(x.Url));
+ var fetched = replayUrls.FirstOrDefault(x => replayLink.Contains(x.Url));
+ if (fetched == null)
+ {
+ var fallback = new StorageUrl
+ {
+ Provider = "Unknown",
+ Url = replayLink,
+ ReplayRegex = "",
+ ServerNameRegex = "",
+ FallBackServerId = Constants.UnsetServerId,
+ FallBackServerName = Constants.UnsetServerName,
+ };
+
+ fallback.CompileRegex();
+ return fallback;
+ }
+
fetched.CompileRegex();
return fetched;
}
From 5cea37eb2816ce915c6c5086847048610e783681 Mon Sep 17 00:00:00 2001
From: Simon <63975668+Simyon264@users.noreply.github.com>
Date: Thu, 29 Aug 2024 02:45:54 +0200
Subject: [PATCH 2/5] :3
---
ReplayBrowser/Controllers/ReplayController.cs | 13 +-
...829003832_ReplayEventsSubtypes.Designer.cs | 1022 +++++++++++++++++
.../20240829003832_ReplayEventsSubtypes.cs | 560 +++++++++
.../ReplayDbContextModelSnapshot.cs | 215 +++-
ReplayBrowser/Data/Models/ReplayDbEvent.cs | 2 +-
ReplayBrowser/Helpers/ReplayHelper.cs | 22 +-
.../AlertLevelChangedReplayEvent.cs | 11 +-
.../EventTypes/CargoObjectSoldReplayEvent.cs | 12 +-
.../CargoProductsOrderedReplayEvent.cs | 25 +-
.../EventTypes/ChatAnnouncementReplayEvent.cs | 14 +-
.../EventTypes/ChatMessageReplayEvent.cs | 21 +-
.../EventTypes/GenericObjectEvent.cs | 14 +-
.../EventTypes/GenericPlayerEvent.cs | 24 +-
.../MobStateChangedNPCReplayEvent.cs | 25 +-
.../MobStateChangedPlayerReplayEvent.cs | 31 +-
.../NewsArticlePublishedReplayEvent.cs | 16 +-
.../EventTypes/ReplayExplosionEvent.cs | 25 +-
.../EventTypes/ShuttleReplayEvent.cs | 19 +-
.../EventTypes/StoreBuyReplayEvent.cs | 20 +-
.../TechnologyUnlockedReplayEvent.cs | 21 +-
.../ReplayEvents/ReplayEventPlayer.cs | 21 +-
21 files changed, 2064 insertions(+), 69 deletions(-)
create mode 100644 ReplayBrowser/Data/Migrations/20240829003832_ReplayEventsSubtypes.Designer.cs
create mode 100644 ReplayBrowser/Data/Migrations/20240829003832_ReplayEventsSubtypes.cs
diff --git a/ReplayBrowser/Controllers/ReplayController.cs b/ReplayBrowser/Controllers/ReplayController.cs
index 21efb8c..8767c36 100644
--- a/ReplayBrowser/Controllers/ReplayController.cs
+++ b/ReplayBrowser/Controllers/ReplayController.cs
@@ -2,6 +2,7 @@
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
+using Newtonsoft.Json;
using ReplayBrowser.Data;
using ReplayBrowser.Data.Models;
using ReplayBrowser.Helpers;
@@ -33,13 +34,21 @@ public ReplayController(ReplayDbContext dbContext, AccountService accountService
public async Task GetReplay(int replayId)
{
var authState = new AuthenticationState(HttpContext.User);
- var replay = await _replayHelper.GetReplay(replayId, authState);
+ var replay = await _replayHelper.GetFullReplay(replayId, authState);
if (replay == null)
{
return NotFound();
}
- return Ok(replay);
+ var settings = new JsonSerializerSettings
+ {
+ ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
+ Formatting = Formatting.Indented,
+ TypeNameHandling = TypeNameHandling.None,
+ TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple
+ };
+
+ return Ok(JsonConvert.SerializeObject(replay, settings));
}
///
diff --git a/ReplayBrowser/Data/Migrations/20240829003832_ReplayEventsSubtypes.Designer.cs b/ReplayBrowser/Data/Migrations/20240829003832_ReplayEventsSubtypes.Designer.cs
new file mode 100644
index 0000000..fd994ed
--- /dev/null
+++ b/ReplayBrowser/Data/Migrations/20240829003832_ReplayEventsSubtypes.Designer.cs
@@ -0,0 +1,1022 @@
+//
+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("20240829003832_ReplayEventsSubtypes")]
+ partial class ReplayEventsSubtypes
+ {
+ ///
+ 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.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