diff --git a/PlaytimeHistory.cs b/PlaytimeHistory.cs index e6de1e7..74528a2 100644 --- a/PlaytimeHistory.cs +++ b/PlaytimeHistory.cs @@ -21,7 +21,7 @@ namespace PlaytimeHistory; public class PlaytimeHistory : BasePlugin, IPluginConfig { public override string ModuleName => "PlaytimeHistory"; - public override string ModuleVersion => "0.1.2"; + public override string ModuleVersion => "0.1.3"; public override string ModuleAuthor => "astra"; public override string ModuleDescription => "Records playtime per session, team and alive/dead state"; @@ -50,7 +50,10 @@ public class PlaytimeHistory : BasePlugin, IPluginConfig { public required ulong SteamId; public required string SessionKey; - public required MatchStats? Baseline; + // The map's running stats as last read, and this session's total so far (null until they've + // been read once). Read every second, so a close that can't read them still has them. + public MatchStats? LastStats; + public MatchStats? Stats; public string? SegmentKey; public DateTime SegmentStart; public int Team; @@ -220,7 +223,10 @@ public class PlaytimeHistory : BasePlugin, IPluginConfig if (tracked == null) OpenSession(slot, player!, now); else + { UpdateState(tracked, player!, now); + SampleStats(tracked, player!); + } } foreach (var slot in _tracked.Keys.Where(s => !seen.Contains(s)).ToList()) CloseSession(slot, _tracked[slot], EndKind.Disconnect, now, null, null); @@ -247,7 +253,8 @@ public class PlaytimeHistory : BasePlugin, IPluginConfig private void OpenSession(int slot, CCSPlayerController player, DateTime now) { var location = LookupLocation(player.IpAddress); - var tracked = new Tracked { SteamId = player.SteamID, SessionKey = NewKey(), Baseline = ReadStats(player) }; + var tracked = new Tracked { SteamId = player.SteamID, SessionKey = NewKey() }; + SampleStats(tracked, player); _tracked[slot] = tracked; _writer!.Enqueue(new SessionOpen(tracked.SessionKey, tracked.SteamId, Config.ServerId, player.PlayerName, now, _map, _tracked.Count, Server.MaxPlayers, location?.CountryIso, location?.CountryName, location?.City, ModuleVersion)); @@ -284,7 +291,9 @@ public class PlaytimeHistory : BasePlugin, IPluginConfig if (tracked.SegmentKey != null) _writer!.Enqueue(new SegmentClose(tracked.SegmentKey, now, Config.CountSpectatorTime)); - var stats = player != null ? Delta(ReadStats(player), tracked.Baseline) : null; + if (player != null) + SampleStats(tracked, player); + var stats = tracked.Stats; var ping = tracked.PingSamples > 0 ? new PingStats((int)(tracked.PingSum / tracked.PingSamples), tracked.PingMin, tracked.PingMax, tracked.PingSamples) : null; @@ -310,24 +319,41 @@ public class PlaytimeHistory : BasePlugin, IPluginConfig private static MatchStats? ReadStats(CCSPlayerController player) { - var stats = player.ActionTrackingServices?.MatchStats; - if (stats == null) + try + { + var stats = player.ActionTrackingServices?.MatchStats; + if (stats == null) + return null; + return new MatchStats(stats.Kills, stats.Deaths, stats.Assists, stats.HeadShotKills, stats.Damage, player.MVPs, player.Score); + } + catch (Exception) + { return null; - return new MatchStats(stats.Kills, stats.Deaths, stats.Assists, stats.HeadShotKills, stats.Damage, player.MVPs, player.Score); + } } - // This session's share of the map's running totals. Totals that went down were reset, so they - // count from zero. - private static MatchStats? Delta(MatchStats? now, MatchStats? baseline) + // Adds what the map's running stats gained since the last read to the session's total. The first + // read is only the starting point: what's there already happened before this session. Kills and + // score can go down (a teamkill), but deaths and damage only go down when the game resets the + // stats (the match starting after warmup, say); then all of the new values count. + private static void SampleStats(Tracked tracked, CCSPlayerController player) { + var now = ReadStats(player); if (now == null) - return null; - if (baseline == null) - return now; - static int D(int n, int b) => n >= b ? n - b : n; - return new MatchStats(D(now.Kills, baseline.Kills), D(now.Deaths, baseline.Deaths), D(now.Assists, baseline.Assists), - D(now.Headshots, baseline.Headshots), D(now.Damage, baseline.Damage), D(now.Mvp, baseline.Mvp), - D(now.Score, baseline.Score)); + return; + var last = tracked.LastStats; + tracked.LastStats = now; + if (last == null) + { + tracked.Stats ??= new MatchStats(0, 0, 0, 0, 0, 0, 0); + return; + } + if (now.Deaths < last.Deaths || now.Damage < last.Damage) + last = new MatchStats(0, 0, 0, 0, 0, 0, 0); + var s = tracked.Stats!; + tracked.Stats = new MatchStats(s.Kills + now.Kills - last.Kills, s.Deaths + now.Deaths - last.Deaths, + s.Assists + now.Assists - last.Assists, s.Headshots + now.Headshots - last.Headshots, + s.Damage + now.Damage - last.Damage, s.Mvp + now.Mvp - last.Mvp, s.Score + now.Score - last.Score); } private Geo.Location? LookupLocation(string? address)