using System.Text.Json.Serialization; namespace PlaytimeHistory; // Every database write, as data, so the writer can run them in order off the game thread and keep // them in the spool file while the database is down. Each one is safe to run twice: inserts are // keyed (session_key, segment_key) and closes only touch rows still open, so a write that's sent // again after a lost reply changes nothing. [JsonPolymorphic(TypeDiscriminatorPropertyName = "op")] [JsonDerivedType(typeof(ServerSeen), "server_seen")] [JsonDerivedType(typeof(RecoverStale), "recover_stale")] [JsonDerivedType(typeof(SessionOpen), "session_open")] [JsonDerivedType(typeof(SessionClose), "session_close")] [JsonDerivedType(typeof(SegmentOpen), "segment_open")] [JsonDerivedType(typeof(SegmentRestate), "segment_restate")] [JsonDerivedType(typeof(SegmentClose), "segment_close")] [JsonDerivedType(typeof(Heartbeat), "heartbeat")] [JsonDerivedType(typeof(OnlineSnapshot), "online_snapshot")] [JsonDerivedType(typeof(EventRecord), "event")] public abstract record Op; public sealed record ServerSeen(int ServerId, string Address, string Hostname, DateTime At) : Op; // Closes whatever this server left open (a crash, or a kill without an unload) at its last heartbeat. public sealed record RecoverStale(int ServerId, bool CountSpectatorTime) : Op; public sealed record SessionOpen( string SessionKey, ulong SteamId, int ServerId, string Nickname, DateTime StartedAt, string Map, int PlayersOnline, int MaxPlayers, string? CountryIso, string? CountryName, string? City, string PluginVersion) : Op; public sealed record SessionClose( string SessionKey, ulong SteamId, DateTime EndedAt, EndKind EndKind, string Map, int? DisconnectReason, string? DisconnectReasonName, MatchStats? Stats, int RoundsPlayed, PingStats? Ping) : Op; public sealed record SegmentOpen( string SegmentKey, string SessionKey, ulong SteamId, int ServerId, string Map, int Team, bool? Alive, DateTime StartedAt) : Op; // The state changed within the second the segment started: change it instead of leaving a // zero-length segment behind. public sealed record SegmentRestate(string SegmentKey, int Team, bool? Alive) : Op; public sealed record SegmentClose(string SegmentKey, DateTime EndedAt, bool CountSpectatorTime) : Op; public sealed record Heartbeat(int ServerId, DateTime At) : Op; public sealed record OnlineSnapshot(int ServerId, DateTime At, int Players, int Bots, int MaxPlayers, string Map) : Op; // A kill, bomb plant or bomb defuse, as it happens. The kill-only fields are null otherwise. public sealed record EventRecord( string EventKey, int ServerId, string? SessionKey, ulong SteamId, EventKind Kind, DateTime At, string Map, int Team, string? Weapon = null, bool? Headshot = null, bool? Noscope = null, bool? Thrusmoke = null, bool? Wallbang = null, bool? Blind = null, ulong? VictimSteamId = null, bool? VictimBot = null, int? Site = null) : Op; public sealed record MatchStats(int Kills, int Deaths, int Assists, int Headshots, int Damage, int Mvp, int Score); public sealed record PingStats(int Avg, int Min, int Max, int Samples); public enum EndKind { Open = 0, Disconnect = 1, MapChange = 2, Unload = 3, Stale = 4, } public enum EventKind { Kill = 1, BombPlanted = 2, BombDefused = 3, } public static class Teams { public const int None = 0; public const int Spectator = 1; public const int T = 2; public const int CT = 3; public static bool Playing(int team) => team is T or CT; }