0.2.0: log kills, bomb plants and defuses as they happen; leave warmup out of match stats
This commit is contained in:
parent
905acd8f1d
commit
bba0471f20
5 changed files with 144 additions and 9 deletions
|
|
@ -14,14 +14,16 @@ namespace PlaytimeHistory;
|
|||
// Records every player's time on the server in its own MySQL database: sessions (join to leave, or
|
||||
// to a map change) and, inside them, segments, each a stretch where the player's team and
|
||||
// alive/dead state stayed the same. Per-hour, per-team and alive/dead playtime all come from the
|
||||
// segments with SQL (see the README). Only country and city are kept from a player's IP.
|
||||
// segments with SQL (see the README). Kills, bomb plants and defuses are logged as they happen.
|
||||
// Warmup counts as playtime, but not toward match stats or events. Only country and city are kept
|
||||
// from a player's IP.
|
||||
//
|
||||
// Threads: every field below is only touched on the game thread. Database writes are queued to the
|
||||
// Writer; command lookups run in Task.Run and hand their answer back with Server.NextFrame.
|
||||
public class PlaytimeHistory : BasePlugin, IPluginConfig<PlaytimeHistoryConfig>
|
||||
{
|
||||
public override string ModuleName => "PlaytimeHistory";
|
||||
public override string ModuleVersion => "0.1.3";
|
||||
public override string ModuleVersion => "0.2.0";
|
||||
public override string ModuleAuthor => "astra";
|
||||
public override string ModuleDescription => "Records playtime per session, team and alive/dead state";
|
||||
|
||||
|
|
@ -92,6 +94,8 @@ public class PlaytimeHistory : BasePlugin, IPluginConfig<PlaytimeHistoryConfig>
|
|||
RegisterEventHandler<EventPlayerDeath>(OnPlayerDeath);
|
||||
RegisterEventHandler<EventPlayerDisconnect>(OnPlayerDisconnect, HookMode.Pre);
|
||||
RegisterEventHandler<EventRoundEnd>(OnRoundEnd);
|
||||
RegisterEventHandler<EventBombPlanted>(OnBombPlanted);
|
||||
RegisterEventHandler<EventBombDefused>(OnBombDefused);
|
||||
|
||||
OpenGeo();
|
||||
|
||||
|
|
@ -185,6 +189,9 @@ public class PlaytimeHistory : BasePlugin, IPluginConfig<PlaytimeHistoryConfig>
|
|||
|
||||
private static string NewKey() => Guid.NewGuid().ToString("N");
|
||||
|
||||
private static bool InWarmup() =>
|
||||
Utilities.FindAllEntitiesByDesignerName<CCSGameRulesProxy>("cs_gamerules").FirstOrDefault()?.GameRules?.WarmupPeriod ?? false;
|
||||
|
||||
private static bool Trackable(CCSPlayerController? player) =>
|
||||
player is { IsValid: true, IsBot: false, IsHLTV: false } &&
|
||||
player.Connected == PlayerConnectedState.Connected && player.SteamID != 0;
|
||||
|
|
@ -208,6 +215,7 @@ public class PlaytimeHistory : BasePlugin, IPluginConfig<PlaytimeHistoryConfig>
|
|||
if (_writer == null || !_mapRunning)
|
||||
return;
|
||||
var now = Now();
|
||||
var warmup = InWarmup();
|
||||
var seen = new HashSet<int>();
|
||||
for (var slot = 0; slot < Server.MaxPlayers; slot++)
|
||||
{
|
||||
|
|
@ -225,7 +233,7 @@ public class PlaytimeHistory : BasePlugin, IPluginConfig<PlaytimeHistoryConfig>
|
|||
else
|
||||
{
|
||||
UpdateState(tracked, player!, now);
|
||||
SampleStats(tracked, player!);
|
||||
SampleStats(tracked, player!, warmup);
|
||||
}
|
||||
}
|
||||
foreach (var slot in _tracked.Keys.Where(s => !seen.Contains(s)).ToList())
|
||||
|
|
@ -254,7 +262,7 @@ public class PlaytimeHistory : BasePlugin, IPluginConfig<PlaytimeHistoryConfig>
|
|||
{
|
||||
var location = LookupLocation(player.IpAddress);
|
||||
var tracked = new Tracked { SteamId = player.SteamID, SessionKey = NewKey() };
|
||||
SampleStats(tracked, player);
|
||||
SampleStats(tracked, player, InWarmup());
|
||||
_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));
|
||||
|
|
@ -292,7 +300,7 @@ public class PlaytimeHistory : BasePlugin, IPluginConfig<PlaytimeHistoryConfig>
|
|||
_writer!.Enqueue(new SegmentClose(tracked.SegmentKey, now, Config.CountSpectatorTime));
|
||||
|
||||
if (player != null)
|
||||
SampleStats(tracked, player);
|
||||
SampleStats(tracked, player, InWarmup());
|
||||
var stats = tracked.Stats;
|
||||
var ping = tracked.PingSamples > 0
|
||||
? new PingStats((int)(tracked.PingSum / tracked.PingSamples), tracked.PingMin, tracked.PingMax, tracked.PingSamples)
|
||||
|
|
@ -335,15 +343,16 @@ public class PlaytimeHistory : BasePlugin, IPluginConfig<PlaytimeHistoryConfig>
|
|||
// 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)
|
||||
// stats (the match starting after warmup, say); then all of the new values count. During warmup
|
||||
// the reads only move the starting point along: warmup counts as playtime, not toward stats.
|
||||
private static void SampleStats(Tracked tracked, CCSPlayerController player, bool warmup)
|
||||
{
|
||||
var now = ReadStats(player);
|
||||
if (now == null)
|
||||
return;
|
||||
var last = tracked.LastStats;
|
||||
tracked.LastStats = now;
|
||||
if (last == null)
|
||||
if (last == null || warmup)
|
||||
{
|
||||
tracked.Stats ??= new MatchStats(0, 0, 0, 0, 0, 0, 0);
|
||||
return;
|
||||
|
|
@ -452,9 +461,49 @@ public class PlaytimeHistory : BasePlugin, IPluginConfig<PlaytimeHistoryConfig>
|
|||
private HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
|
||||
{
|
||||
UpdateNextFrame(@event.Userid);
|
||||
RecordKill(@event);
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
// Logs a kill by a tracked player. Not suicides, team kills or anything in warmup.
|
||||
private void RecordKill(EventPlayerDeath @event)
|
||||
{
|
||||
var killer = @event.Attacker;
|
||||
var victim = @event.Userid;
|
||||
if (_writer == null || !_mapRunning || !Trackable(killer) || victim is not { IsValid: true } ||
|
||||
victim.Slot == killer!.Slot || victim.Team == killer.Team || InWarmup())
|
||||
return;
|
||||
var bot = victim.IsBot;
|
||||
_writer.Enqueue(new EventRecord(NewKey(), Config.ServerId, SessionKeyOf(killer), killer.SteamID, EventKind.Kill, Now(),
|
||||
_map, (int)killer.Team, Weapon: @event.Weapon, Headshot: @event.Headshot, Noscope: @event.Noscope,
|
||||
Thrusmoke: @event.Thrusmoke, Wallbang: @event.Penetrated > 0, Blind: @event.Attackerblind,
|
||||
VictimSteamId: bot || victim.SteamID == 0 ? null : victim.SteamID, VictimBot: bot));
|
||||
}
|
||||
|
||||
private HookResult OnBombPlanted(EventBombPlanted @event, GameEventInfo info)
|
||||
{
|
||||
RecordBomb(@event.Userid, EventKind.BombPlanted, @event.Site);
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
private HookResult OnBombDefused(EventBombDefused @event, GameEventInfo info)
|
||||
{
|
||||
RecordBomb(@event.Userid, EventKind.BombDefused, @event.Site);
|
||||
return HookResult.Continue;
|
||||
}
|
||||
|
||||
private void RecordBomb(CCSPlayerController? player, EventKind kind, int site)
|
||||
{
|
||||
if (_writer == null || !_mapRunning || !Trackable(player) || InWarmup())
|
||||
return;
|
||||
_writer.Enqueue(new EventRecord(NewKey(), Config.ServerId, SessionKeyOf(player!), player!.SteamID, kind, Now(),
|
||||
_map, (int)player.Team, Site: site));
|
||||
}
|
||||
|
||||
// The session open for this player, if it's theirs.
|
||||
private string? SessionKeyOf(CCSPlayerController player) =>
|
||||
_tracked.TryGetValue(player.Slot, out var tracked) && tracked.SteamId == player.SteamID ? tracked.SessionKey : null;
|
||||
|
||||
// The controller and pawn have caught up with the event by the next frame.
|
||||
private void UpdateNextFrame(CCSPlayerController? player)
|
||||
{
|
||||
|
|
@ -466,6 +515,8 @@ public class PlaytimeHistory : BasePlugin, IPluginConfig<PlaytimeHistoryConfig>
|
|||
|
||||
private HookResult OnRoundEnd(EventRoundEnd @event, GameEventInfo info)
|
||||
{
|
||||
if (InWarmup())
|
||||
return HookResult.Continue;
|
||||
foreach (var tracked in _tracked.Values)
|
||||
if (Teams.Playing(tracked.Team))
|
||||
tracked.Rounds++;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue