using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using HoldfastCensus.Aggregation;
using HoldfastCensus.Events;
using HoldfastCensus.Hooks;
using HoldfastCensus.Io;
using HoldfastCensus.Model;
using HoldfastGame;
using HoldfastSharedMethods;
using Microsoft.CodeAnalysis;
using Steamworks;
using UnityEngine;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyCompany("HoldfastCensusLite")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.7.0.0")]
[assembly: AssemblyInformationalVersion("1.7.0+b21cc4c77b4802d65a7e096830e9c9bd85b1b4a4")]
[assembly: AssemblyProduct("HoldfastCensusLite")]
[assembly: AssemblyTitle("HoldfastCensusLite")]
[assembly: AssemblyVersion("1.7.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
[CompilerGenerated]
[Embedded]
internal sealed class EmbeddedAttribute : Attribute
{
}
}
namespace System.Runtime.CompilerServices
{
[CompilerGenerated]
[Embedded]
[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
internal sealed class RefSafetyRulesAttribute : Attribute
{
public readonly int Version;
public RefSafetyRulesAttribute(int P_0)
{
Version = P_0;
}
}
}
namespace HoldfastCensus
{
[BepInPlugin("dev.gmrrh.census.lite", "Holdfast Census Lite", "1.7.0")]
[BepInProcess("Holdfast NaW.exe")]
public sealed class HoldfastCensusPlugin : BaseUnityPlugin
{
public const string Guid = "dev.gmrrh.census.lite";
public const string Name = "Holdfast Census Lite";
internal static readonly bool SelfOnlyBuild = true;
public const string Version = "1.7.0";
private PayloadSender _sender;
private DiskCache _cache;
private EventJournal _journal;
private Coroutine _flushRoutine;
private bool _disabledNoticeLogged;
private bool _steamIdJournaled;
private int _frame;
internal static HoldfastCensusPlugin Instance { get; private set; }
internal static EventListener Listener { get; private set; }
internal SessionManager Tracker { get; private set; }
internal TrackerConfig Cfg { get; private set; }
private void Awake()
{
//IL_013d: Unknown result type (might be due to invalid IL or missing references)
//IL_0143: Expected O, but got Unknown
Instance = this;
Cfg = TrackerConfig.Create(((BaseUnityPlugin)this).Config);
Listener = new EventListener();
if (!Cfg.Enabled.Value)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)"[Census] Disabled by config — nothing will be captured.");
return;
}
if (string.IsNullOrEmpty(Cfg.ReporterSecret.Value))
{
((BaseUnityPlugin)this).Logger.LogWarning((object)"[Census] ReporterSecret missing - reports will be rejected; get your personal secret from the 2.BA admin");
}
Tracker = new SessionManager("1.7.0");
Tracker.PayloadSink = HandlePayload;
_cache = new DiskCache(Path.Combine(Paths.PluginPath, "dev.gmrrh.census.lite", "cache"), Cfg.CacheMaxFiles.Value);
_sender = new PayloadSender(Cfg, _cache);
if (Cfg.TestMode.Value)
{
_journal = new EventJournal(Path.Combine(_cache.BaseDirectory, "dumps"));
((BaseUnityPlugin)this).Logger.LogWarning((object)("[Census] TEST MODE — every event is journaled to " + (_journal.FilePath ?? "<unavailable>") + "; network sending is DISABLED."));
}
Harmony harmony = new Harmony("dev.gmrrh.census.lite");
TryPatch(harmony, typeof(InitializeOnMapPatch), "map-init anchor (round details)");
TryPatch(harmony, typeof(ModsLoadedPatch), "mods-loaded anchor (event registration)");
TryPatch(harmony, typeof(ScorePatch), "scoreboard snapshot");
TryPatch(harmony, typeof(DamagePatch), "damage attribution (optional)");
TryPatch(harmony, typeof(DisconnectPatch), "client disconnect (local-left flush)");
TryPatch(harmony, typeof(LeaderboardPatch), "round-end leaderboard rank");
TryPatch(harmony, typeof(KillDistancePatch), "non-melee kill distance");
TrySetSteamIdFromSteamworks();
((BaseUnityPlugin)this).Logger.LogInfo((object)("[Census] Payload signing self-test: " + (PayloadSigner.SelfTest() ? "ok" : "FAILED — payloads would be sent unsigned.")));
((BaseUnityPlugin)this).Logger.LogInfo((object)("[Census] Holdfast Census Lite v1.7.0" + (SelfOnlyBuild ? " (self-only)" : "") + " ready. Endpoint: " + Cfg.EndpointUrl.Value));
}
private void TryPatch(Harmony harmony, Type patchClass, string label)
{
try
{
Harmony.CreateAndPatchAll(patchClass, "dev.gmrrh.census.lite");
((BaseUnityPlugin)this).Logger.LogInfo((object)("[Census] Hook applied: " + label));
Journal("hook applied: " + label);
}
catch (Exception ex)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)("[Census] Hook FAILED (" + label + "): " + ex.Message + " — related stats will be missing."));
Journal("HOOK FAILED: " + label + " — " + ex.Message);
}
}
private void Update()
{
if (Tracker == null)
{
return;
}
if (!string.IsNullOrEmpty(Tracker.ReporterSteamId))
{
if (!_steamIdJournaled)
{
_steamIdJournaled = true;
Journal("reporter steam id resolved: " + Tracker.ReporterSteamId);
}
}
else if (++_frame % 120 == 0)
{
TrySetSteamIdFromSteamworks();
}
}
private void TrySetSteamIdFromSteamworks()
{
//IL_0000: Unknown result type (might be due to invalid IL or missing references)
//IL_0005: Unknown result type (might be due to invalid IL or missing references)
//IL_000f: Unknown result type (might be due to invalid IL or missing references)
//IL_001d: Unknown result type (might be due to invalid IL or missing references)
try
{
CSteamID steamID = SteamUser.GetSteamID();
if (((CSteamID)(ref steamID)).IsValid() && steamID.m_SteamID != 0L)
{
Tracker.SetReporterSteamId(steamID.m_SteamID);
}
}
catch (Exception ex)
{
((BaseUnityPlugin)this).Logger.LogDebug((object)("[Census] Steam id not available yet: " + ex.Message));
}
}
internal void OnMapInitialized(bool statsTrackingEnabled)
{
if (!statsTrackingEnabled && !_disabledNoticeLogged)
{
_disabledNoticeLogged = true;
((BaseUnityPlugin)this).Logger.LogInfo((object)"[Census] This server has StatsTrackingEnabled=false — capture paused for this map.");
}
else if (statsTrackingEnabled)
{
_disabledNoticeLogged = false;
}
}
internal void ScheduleFlush(float delaySeconds, string cause)
{
try
{
if (_flushRoutine != null)
{
((MonoBehaviour)this).StopCoroutine(_flushRoutine);
}
_flushRoutine = ((MonoBehaviour)this).StartCoroutine(FlushAfter(delaySeconds, cause));
}
catch (Exception ex)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)("[Census] Flush scheduling failed (" + ex.Message + ") — flushing immediately."));
FlushNow(cause);
}
}
private IEnumerator FlushAfter(float delay, string cause)
{
yield return (object)new WaitForSeconds(delay);
_flushRoutine = null;
FlushNow(cause);
}
internal void FlushNow(string cause)
{
try
{
Tracker?.Flush(cause);
}
catch (Exception arg)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)$"[Census] Flush failed: {arg}");
}
}
private void HandlePayload(string json, int playerCount, string cause)
{
try
{
string text = null;
if (Cfg.DebugDumpPayload.Value || Cfg.TestMode.Value)
{
text = Dump(json, cause);
}
if (Cfg.TestMode.Value)
{
Journal(string.Format("payload built (cause={0}, players={1}, {2} bytes) -> {3}; network send skipped (test mode)", cause, playerCount, json.Length, text ?? "dump failed"));
}
else if (playerCount < Cfg.MinPlayers.Value)
{
((BaseUnityPlugin)this).Logger.LogInfo((object)$"[Census] Payload dropped (players={playerCount} < min={Cfg.MinPlayers.Value}, cause={cause}).");
}
else
{
_sender?.Enqueue(json);
((BaseUnityPlugin)this).Logger.LogInfo((object)$"[Census] Round payload queued (cause={cause}, players={playerCount}, {json.Length} bytes).");
}
}
catch (Exception ex)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)("[Census] Payload handling failed: " + ex.Message));
}
}
private string Dump(string json, string cause)
{
try
{
string text = Path.Combine(_cache.BaseDirectory, "dumps");
Directory.CreateDirectory(text);
string text2 = Path.Combine(text, $"dump_{DateTime.UtcNow:yyyyMMdd_HHmmss}_{Sanitize(cause)}.json");
File.WriteAllText(text2, json);
return text2;
}
catch
{
return null;
}
}
private static string Sanitize(string s)
{
return string.Concat((s ?? "x").Split(Path.GetInvalidFileNameChars()));
}
private void OnApplicationQuit()
{
try
{
FlushNow("application-quit");
}
catch
{
}
try
{
_sender?.Dispose();
}
catch
{
}
try
{
_journal?.Close();
}
catch
{
}
}
internal static void Journal(string message)
{
Instance?._journal?.Write(message);
}
internal void LogEvent(string message)
{
Journal(message);
if (Cfg != null && (Cfg.VerboseEvents.Value || Cfg.TestMode.Value))
{
((BaseUnityPlugin)this).Logger.LogInfo((object)("[Census][evt] " + message));
}
}
internal static void LogHandled(string context, Exception e)
{
HoldfastCensusPlugin instance = Instance;
if (instance != null)
{
((BaseUnityPlugin)instance).Logger.LogWarning((object)("[Census] Handled error in " + context + ": " + e.Message));
}
}
internal static void LogHttp(string message)
{
HoldfastCensusPlugin instance = Instance;
if (instance != null)
{
((BaseUnityPlugin)instance).Logger.LogInfo((object)("[Census] " + message));
}
}
internal static void LogInfo(string message)
{
HoldfastCensusPlugin instance = Instance;
if (instance != null)
{
((BaseUnityPlugin)instance).Logger.LogInfo((object)message);
}
}
internal static void LogWarn(string message)
{
HoldfastCensusPlugin instance = Instance;
if (instance != null)
{
((BaseUnityPlugin)instance).Logger.LogWarning((object)message);
}
}
internal static void LogError(string message)
{
HoldfastCensusPlugin instance = Instance;
if (instance != null)
{
((BaseUnityPlugin)instance).Logger.LogError((object)message);
}
}
}
internal sealed class TrackerConfig
{
public ConfigEntry<bool> Enabled;
public ConfigEntry<string> EndpointUrl;
public ConfigEntry<string> IngestToken;
public ConfigEntry<string> BearerToken;
public ConfigEntry<string> ReporterSecret;
public ConfigEntry<int> MinPlayers;
public ConfigEntry<bool> SkipServersWithStatsTrackingDisabled;
public ConfigEntry<bool> UseServerBlacklist;
public ConfigEntry<string> ServerBlacklist;
public ConfigEntry<bool> UseServerWhitelist;
public ConfigEntry<string> ServerWhitelist;
public ConfigEntry<int> CacheMaxFiles;
public ConfigEntry<bool> DebugDumpPayload;
public ConfigEntry<bool> TestMode;
public ConfigEntry<bool> VerboseEvents;
public static TrackerConfig Create(ConfigFile file)
{
return new TrackerConfig
{
Enabled = file.Bind<bool>("Tracker", "Enabled", true, "Master switch for capture and reporting."),
EndpointUrl = file.Bind<string>("Tracker", "EndpointUrl", "http://127.0.0.1:8000/api/ingest/mod", "stats backend ingest endpoint the round payloads are POSTed to."),
IngestToken = file.Bind<string>("Tracker", "IngestToken", "", "Shared ingest token, sent as 'X-Ingest-Token' header. Leave empty for local testing."),
BearerToken = file.Bind<string>("Tracker", "BearerToken", "", "Optional JWT, sent as 'Authorization: Bearer <token>' in addition to the ingest token."),
ReporterSecret = file.Bind<string>("Tracker", "ReporterSecret", "", "Personal per-reporter ingest secret, sent as 'X-Reporter-Secret' header. Obtained from the stats backend admin; binds your Steam ID so reports cannot be forged. Leave empty for unauthenticated backends."),
MinPlayers = file.Bind<int>("Tracker", "MinPlayers", 1, "Rounds with fewer joined players are not reported."),
SkipServersWithStatsTrackingDisabled = file.Bind<bool>("Tracker", "SkipServersWithStatsTrackingDisabled", false, "If true, nothing is captured on servers that disabled stats tracking (many official AGS servers do). Default false: capture anyway — the payload records the server's flag."),
UseServerBlacklist = file.Bind<bool>("Tracker", "UseServerBlacklist", false, "If true, no round is captured or reported on servers whose name matches an entry in ServerBlacklist. The blacklist wins over the whitelist."),
ServerBlacklist = file.Bind<string>("Tracker", "ServerBlacklist", "", "Comma-separated server-name fragments; a server is blocked when its full name CONTAINS any entry, ignoring case (e.g. 'EU23,2.BA Event'). Effective when UseServerBlacklist is true; an empty list blocks nothing."),
UseServerWhitelist = file.Bind<bool>("Tracker", "UseServerWhitelist", false, "If true, rounds are captured ONLY on servers whose name matches an entry in ServerWhitelist. Leave false to record on every server."),
ServerWhitelist = file.Bind<string>("Tracker", "ServerWhitelist", "", "Comma-separated server-name fragments; a server is allowed when its full name CONTAINS any entry, ignoring case. An EMPTY list with the whitelist enabled allows every server — fill it or leave the whitelist off."),
CacheMaxFiles = file.Bind<int>("Tracker", "CacheMaxFiles", 200, "Maximum number of unsent payloads kept on disk when the backend is unreachable."),
DebugDumpPayload = file.Bind<bool>("Debug", "DebugDumpPayload", true, "Write every payload JSON to BepInEx/plugins/dev.gmrrh.census.lite/cache/dumps before sending."),
TestMode = file.Bind<bool>("Debug", "TestMode", false, "Test mode: write EVERY captured event to a journal file (cache/dumps/testrun_*.log), always dump payloads, and disable network sending. Turn off for real reporting."),
VerboseEvents = file.Bind<bool>("Debug", "VerboseEvents", false, "Log every captured game event to the BepInEx log (very spammy, debugging only). Implied by TestMode.")
};
}
public bool ShouldRecordServer(string serverName, out string reason)
{
reason = "";
serverName = serverName ?? "";
if (UseServerBlacklist != null && UseServerBlacklist.Value)
{
foreach (string item in SplitList(ServerBlacklist))
{
if (serverName.IndexOf(item, StringComparison.OrdinalIgnoreCase) >= 0)
{
reason = "matches blacklist entry '" + item + "'";
return false;
}
}
}
if (UseServerWhitelist != null && UseServerWhitelist.Value)
{
List<string> list = SplitList(ServerWhitelist);
if (list.Count == 0)
{
return true;
}
foreach (string item2 in list)
{
if (serverName.IndexOf(item2, StringComparison.OrdinalIgnoreCase) >= 0)
{
reason = "whitelisted by '" + item2 + "'";
return true;
}
}
reason = "matches no whitelist entry";
return false;
}
return true;
}
private static List<string> SplitList(ConfigEntry<string> entry)
{
List<string> list = new List<string>();
if (entry == null || string.IsNullOrEmpty(entry.Value))
{
return list;
}
string[] array = entry.Value.Split(new char[2] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries);
for (int i = 0; i < array.Length; i++)
{
string text = array[i].Trim();
if (text.Length > 0)
{
list.Add(text);
}
}
return list;
}
}
}
namespace HoldfastCensus.Model
{
internal sealed class KillRecord
{
public int KillerPlayerId;
public int VictimPlayerId;
public string KillerSteamId = "";
public string VictimSteamId = "";
public string KillerName = "";
public string VictimName = "";
public string Reason = "";
public int ReasonValue;
public string Details = "";
public string Category = "other";
public bool Teamkill;
public float? Distance;
public DateTime Utc;
}
internal sealed class ScoreboardRow
{
public int PlayerId;
public int Kills;
public int Deaths;
public int Score;
public int Killstreak;
}
internal sealed class PlayerEntry
{
public int PlayerId;
public string SteamId = "";
public string Name = "";
public string RegimentTag = "";
public bool IsBot;
public string Faction = "";
public DateTime JoinedUtc;
public bool Left;
}
internal sealed class PlayerStats
{
public int PlayerId;
public long ShotsFired;
public long DryShots;
public long ShotsWithPlayerHits;
public long ProjectilesHitPlayers;
public long ProjectilesHitObjects;
public long HitsFromDamage;
public long MeleeHits;
public long ArtyHits;
public double DamageDealtAll;
public double DamageReceived;
public double AttributedDamage;
public long AttributedDamageEvents;
public long Kills;
public long Deaths;
public long Teamkills;
public long Suicides;
public long FirearmKills;
public long MeleeKills;
public long ArtyKills;
public long EnvironmentDeaths;
public long ScoreFromActions;
public long SnapshotKills = -1L;
public long SnapshotDeaths = -1L;
public long SnapshotScore = -1L;
public long SnapshotKillstreak = -1L;
public int LeaderboardPlace;
public readonly HashSet<string> ClassesPlayed = new HashSet<string>();
public readonly Dictionary<string, long> WeaponUse = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase);
}
}
namespace HoldfastCensus.Io
{
internal sealed class DiskCache
{
private readonly string _dir;
private readonly int _maxFiles;
private readonly object _io = new object();
public string BaseDirectory => _dir;
public DiskCache(string dir, int maxFiles)
{
_dir = dir;
_maxFiles = Math.Max(1, maxFiles);
try
{
Directory.CreateDirectory(_dir);
}
catch
{
}
}
public string Save(string json)
{
lock (_io)
{
string path = "pending_" + DateTime.UtcNow.ToString("yyyyMMdd_HHmmssfff") + "_" + Guid.NewGuid().ToString("N").Substring(0, 6) + ".json";
string text = Path.Combine(_dir, path);
File.WriteAllText(text, json);
PruneLocked();
return text;
}
}
public List<string> Oldest(int max)
{
lock (_io)
{
try
{
string[] files = Directory.GetFiles(_dir, "pending_*.json");
Array.Sort(files);
return files.Take(Math.Max(0, max)).ToList();
}
catch
{
return new List<string>();
}
}
}
public void Delete(string path)
{
try
{
lock (_io)
{
File.Delete(path);
}
}
catch
{
}
}
private void PruneLocked()
{
try
{
string[] files = Directory.GetFiles(_dir, "pending_*.json");
if (files.Length > _maxFiles)
{
Array.Sort(files);
for (int i = 0; i < files.Length - _maxFiles; i++)
{
File.Delete(files[i]);
}
}
}
catch
{
}
}
}
internal sealed class EventJournal
{
private readonly StreamWriter _writer;
public string FilePath { get; }
public EventJournal(string directory)
{
try
{
Directory.CreateDirectory(directory);
FilePath = Path.Combine(directory, "testrun_" + DateTime.UtcNow.ToString("yyyyMMdd_HHmmss") + ".log");
_writer = new StreamWriter(FilePath, append: false)
{
AutoFlush = true
};
Write("journal opened — test mode active");
}
catch
{
FilePath = null;
}
}
public void Write(string message)
{
try
{
_writer?.WriteLine(DateTime.UtcNow.ToString("HH:mm:ss.fff") + " " + message);
}
catch
{
}
}
public void Close()
{
try
{
Write("journal closed");
_writer?.Dispose();
}
catch
{
}
}
}
internal sealed class JsonWriter
{
private readonly StringBuilder _sb = new StringBuilder(16384);
private bool[] _needComma = new bool[32];
private int _depth;
public JsonWriter BeginObject()
{
PreValue();
_sb.Append('{');
Push();
return this;
}
public JsonWriter EndObject()
{
_sb.Append('}');
Pop();
return this;
}
public JsonWriter BeginArray()
{
PreValue();
_sb.Append('[');
Push();
return this;
}
public JsonWriter EndArray()
{
_sb.Append(']');
Pop();
return this;
}
public JsonWriter Key(string name)
{
PreValue();
_sb.Append('"').Append(Escape(name)).Append("\":");
_needComma[_depth] = false;
return this;
}
public JsonWriter Str(string value)
{
PreValue();
if (value == null)
{
_sb.Append("null");
}
else
{
_sb.Append('"').Append(Escape(value)).Append('"');
}
_needComma[_depth] = true;
return this;
}
public JsonWriter Int(long value)
{
PreValue();
_sb.Append(value.ToString(CultureInfo.InvariantCulture));
_needComma[_depth] = true;
return this;
}
public JsonWriter Dbl(double value)
{
PreValue();
if (double.IsNaN(value) || double.IsInfinity(value))
{
_sb.Append("null");
}
else
{
_sb.Append(value.ToString("R", CultureInfo.InvariantCulture));
}
_needComma[_depth] = true;
return this;
}
public JsonWriter Bool(bool value)
{
PreValue();
_sb.Append(value ? "true" : "false");
_needComma[_depth] = true;
return this;
}
public override string ToString()
{
return _sb.ToString();
}
private void Push()
{
_depth++;
if (_depth >= _needComma.Length)
{
Array.Resize(ref _needComma, _depth * 2);
}
_needComma[_depth] = false;
}
private void Pop()
{
_depth--;
if (_depth >= 0)
{
_needComma[_depth] = true;
}
}
private void PreValue()
{
if (_needComma[_depth])
{
_sb.Append(',');
}
}
private static string Escape(string s)
{
StringBuilder stringBuilder = new StringBuilder(s.Length + 8);
foreach (char c in s)
{
switch (c)
{
case '"':
stringBuilder.Append("\\\"");
continue;
case '\\':
stringBuilder.Append("\\\\");
continue;
case '\n':
stringBuilder.Append("\\n");
continue;
case '\r':
stringBuilder.Append("\\r");
continue;
case '\t':
stringBuilder.Append("\\t");
continue;
}
if (c < ' ')
{
StringBuilder stringBuilder2 = stringBuilder.Append("\\u");
int num = c;
stringBuilder2.Append(num.ToString("x4", CultureInfo.InvariantCulture));
}
else
{
stringBuilder.Append(c);
}
}
return stringBuilder.ToString();
}
}
internal sealed class PayloadSender : IDisposable
{
private readonly TrackerConfig _cfg;
private readonly DiskCache _cache;
private readonly BlockingCollection<string> _queue = new BlockingCollection<string>(new ConcurrentQueue<string>());
private readonly Thread _thread;
private volatile bool _disposed;
private bool _unsignedWarned;
private bool _secretWarned;
public PayloadSender(TrackerConfig cfg, DiskCache cache)
{
_cfg = cfg;
_cache = cache;
try
{
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
}
catch
{
}
_thread = new Thread(WorkerLoop)
{
IsBackground = true,
Name = "Census-Sender"
};
_thread.Start();
}
public void Enqueue(string json)
{
try
{
_queue.TryAdd(json);
}
catch
{
}
}
private void WorkerLoop()
{
while (!_disposed)
{
string text = null;
try
{
text = _queue.Take();
}
catch (ObjectDisposedException)
{
break;
}
catch (InvalidOperationException)
{
break;
}
if (text == null)
{
continue;
}
if (!TrySend(text))
{
try
{
_cache.Save(text);
}
catch
{
}
}
DrainCache();
}
}
internal bool TrySend(string json)
{
try
{
HttpWebRequest httpWebRequest = (HttpWebRequest)WebRequest.Create(_cfg.EndpointUrl.Value);
httpWebRequest.Method = "POST";
httpWebRequest.ContentType = "application/json";
httpWebRequest.Timeout = 8000;
httpWebRequest.ReadWriteTimeout = 8000;
httpWebRequest.Proxy = null;
string value = _cfg.IngestToken.Value;
if (!string.IsNullOrEmpty(value))
{
httpWebRequest.Headers.Add("X-Ingest-Token", value);
}
string value2 = _cfg.BearerToken.Value;
if (!string.IsNullOrEmpty(value2))
{
httpWebRequest.Headers.Add("Authorization", "Bearer " + value2);
}
string value3 = ((_cfg.ReporterSecret != null) ? _cfg.ReporterSecret.Value : null);
if (!string.IsNullOrEmpty(value3))
{
httpWebRequest.Headers.Add("X-Reporter-Secret", value3);
}
else if (!_secretWarned)
{
_secretWarned = true;
HoldfastCensusPlugin.LogWarn("[Census] ReporterSecret missing - reports will be rejected; get your personal secret from the 2.BA admin");
}
byte[] bytes = Encoding.UTF8.GetBytes(json);
string text = PayloadSigner.Sign(bytes);
if (text != null)
{
httpWebRequest.Headers.Add("X-Census-Signature", text);
}
else if (!_unsignedWarned)
{
_unsignedWarned = true;
HoldfastCensusPlugin.LogWarn("[Census] Payload signing unavailable — payloads are sent unsigned and will be flagged by the backend.");
}
httpWebRequest.ContentLength = bytes.Length;
using (Stream stream = httpWebRequest.GetRequestStream())
{
stream.Write(bytes, 0, bytes.Length);
}
try
{
using HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse();
int statusCode = (int)httpWebResponse.StatusCode;
return statusCode >= 200 && statusCode < 300;
}
catch (WebException ex)
{
if (ex.Response is HttpWebResponse httpWebResponse2)
{
int statusCode2 = (int)httpWebResponse2.StatusCode;
if (statusCode2 == 409)
{
return true;
}
string json2 = "";
try
{
using Stream stream2 = httpWebResponse2.GetResponseStream();
if (stream2 != null)
{
using StreamReader streamReader = new StreamReader(stream2, Encoding.UTF8);
json2 = streamReader.ReadToEnd();
}
}
catch
{
}
if (statusCode2 == 401 || statusCode2 == 403)
{
string text2 = ExtractJsonMessage(json2);
string permanentErrorExplanation = GetPermanentErrorExplanation(statusCode2, text2);
HoldfastCensusPlugin.LogError(string.Format("[Census] Ingest rejected payload (HTTP {0}{1}) — {2}. Dropped, not retried.", statusCode2, string.IsNullOrEmpty(text2) ? "" : (": " + text2), permanentErrorExplanation));
return true;
}
HoldfastCensusPlugin.LogHttp($"ingest responded HTTP {statusCode2} — payload cached for retry.");
}
return false;
}
}
catch (Exception e)
{
HoldfastCensusPlugin.LogHandled("http", e);
return false;
}
}
private static string GetPermanentErrorExplanation(int code, string message)
{
if (!string.IsNullOrEmpty(message))
{
if (message.IndexOf("invalid reporter secret", StringComparison.OrdinalIgnoreCase) >= 0)
{
return "ReporterSecret invalid/rotated - ask an admin for a new one";
}
if (message.IndexOf("reporter secret not configured", StringComparison.OrdinalIgnoreCase) >= 0)
{
return "admin hasn't issued a secret yet";
}
if (message.IndexOf("reporter secret missing", StringComparison.OrdinalIgnoreCase) >= 0)
{
return "ReporterSecret missing - reports will be rejected; get your personal secret from the 2.BA admin";
}
if (message.IndexOf("reporter blocked", StringComparison.OrdinalIgnoreCase) >= 0)
{
return "reporter blocked by admin";
}
if (message.IndexOf("invalid ingest token", StringComparison.OrdinalIgnoreCase) >= 0)
{
return "invalid IngestToken in config";
}
if (message.IndexOf("signature", StringComparison.OrdinalIgnoreCase) >= 0)
{
return "signature rejected by backend";
}
}
if (code == 403)
{
return "reporter blocked by admin";
}
return "permanent configuration error";
}
private static string ExtractJsonMessage(string json)
{
if (string.IsNullOrEmpty(json))
{
return "";
}
int num = json.IndexOf("\"message\"", StringComparison.OrdinalIgnoreCase);
if (num < 0)
{
return "";
}
int num2 = json.IndexOf(':', num + 9);
if (num2 < 0)
{
return "";
}
int num3 = json.IndexOf('"', num2 + 1);
if (num3 < 0)
{
return "";
}
int num4 = -1;
bool flag = false;
for (int i = num3 + 1; i < json.Length; i++)
{
char c = json[i];
if (flag)
{
flag = false;
continue;
}
switch (c)
{
case '\\':
flag = true;
continue;
case '"':
break;
default:
continue;
}
num4 = i;
break;
}
if (num4 > num3)
{
return json.Substring(num3 + 1, num4 - num3 - 1).Trim();
}
return "";
}
private void DrainCache()
{
try
{
foreach (string item in _cache.Oldest(5))
{
string json;
try
{
json = File.ReadAllText(item);
}
catch
{
continue;
}
if (TrySend(json))
{
_cache.Delete(item);
continue;
}
break;
}
}
catch
{
}
}
public void Dispose()
{
_disposed = true;
try
{
_queue.CompleteAdding();
}
catch
{
}
}
}
internal static class PayloadSigner
{
private const string KeyId = "v1";
private static readonly byte[] DomainPrefix = Encoding.UTF8.GetBytes("census.mod.round.v1\n");
private static readonly byte[] KeyPartA = new byte[32]
{
230, 181, 182, 175, 222, 255, 115, 41, 58, 2,
148, 162, 74, 125, 102, 110, 4, 180, 93, 156,
114, 21, 254, 246, 92, 148, 113, 229, 174, 190,
217, 31
};
private static readonly byte[] KeyPartB = new byte[32]
{
203, 107, 80, 243, 45, 176, 203, 203, 182, 173,
72, 103, 35, 91, 199, 35, 176, 16, 146, 224,
246, 244, 16, 202, 13, 250, 25, 84, 197, 43,
234, 253
};
private static volatile byte[] _key;
private static byte[] Key
{
get
{
byte[] array = _key;
if (array == null)
{
array = new byte[KeyPartA.Length];
for (int i = 0; i < KeyPartA.Length; i++)
{
array[i] = (byte)(KeyPartA[i] ^ KeyPartB[i]);
}
_key = array;
}
return array;
}
}
internal static string Sign(byte[] body)
{
if (body == null || body.Length == 0)
{
return null;
}
try
{
byte[] array = new byte[DomainPrefix.Length + body.Length];
Buffer.BlockCopy(DomainPrefix, 0, array, 0, DomainPrefix.Length);
Buffer.BlockCopy(body, 0, array, DomainPrefix.Length, body.Length);
using HMACSHA256 hMACSHA = new HMACSHA256(Key);
return "v1=" + ToHexLower(hMACSHA.ComputeHash(array));
}
catch
{
return null;
}
}
internal static bool SelfTest()
{
try
{
string text = Sign(Encoding.UTF8.GetBytes("census payload signer self-test"));
string text2 = Sign(Encoding.UTF8.GetBytes("census payload signer self-test"));
return text != null && text == text2 && text.Length == "v1".Length + 1 + 64;
}
catch
{
return false;
}
}
private static string ToHexLower(byte[] bytes)
{
StringBuilder stringBuilder = new StringBuilder(bytes.Length * 2);
foreach (byte b in bytes)
{
stringBuilder.Append(b.ToString("x2"));
}
return stringBuilder.ToString();
}
}
}
namespace HoldfastCensus.Hooks
{
[HarmonyPatch]
internal static class DamagePatch
{
private static bool _probed;
[HarmonyPostfix]
[HarmonyPatch(typeof(PlayerDamageManager), "ProcessPlayerHealthChanged")]
private static void HealthChangedPostfix(RoundPlayer __0, PlayerHealthChangedEventArgs __1)
{
//IL_0028: Unknown result type (might be due to invalid IL or missing references)
//IL_002d: Unknown result type (might be due to invalid IL or missing references)
try
{
SessionManager sessionManager = HoldfastCensusPlugin.Instance?.Tracker;
if (sessionManager == null || __0 == null || __1 == null)
{
return;
}
int networkPlayerID = __0.NetworkPlayerID;
string text = ((object)((EntityHealthChangedEventArgs<PlayerHealthChangedPacket>)(object)__1).ChangeReason/*cast due to .constrained prefix*/).ToString();
object obj = null;
try
{
PlayerHealthChangedPacket packet = ((EntityHealthChangedEventArgs<PlayerHealthChangedPacket>)(object)__1).Packet;
obj = ((packet != null) ? packet.HealthChangedData : null);
}
catch
{
}
if (!_probed)
{
_probed = true;
HoldfastCensusPlugin.Journal("health-args probe: reason=" + text + " data=" + ((obj != null) ? obj.GetType().Name : "none"));
}
int num = 0;
ShotByFirearmPlayerHealthChangedData val = (ShotByFirearmPlayerHealthChangedData)((obj is ShotByFirearmPlayerHealthChangedData) ? obj : null);
if (val == null)
{
ShotByFirearmVehicleHealthChangedData val2 = (ShotByFirearmVehicleHealthChangedData)((obj is ShotByFirearmVehicleHealthChangedData) ? obj : null);
if (val2 == null)
{
HitByMeleeWeaponPlayerHealthChangedData val3 = (HitByMeleeWeaponPlayerHealthChangedData)((obj is HitByMeleeWeaponPlayerHealthChangedData) ? obj : null);
if (val3 == null)
{
HitByMeleeWeaponVehicleHealthChangedData val4 = (HitByMeleeWeaponVehicleHealthChangedData)((obj is HitByMeleeWeaponVehicleHealthChangedData) ? obj : null);
if (val4 == null)
{
DirectCannonballHitPlayerHealthChangedData val5 = (DirectCannonballHitPlayerHealthChangedData)((obj is DirectCannonballHitPlayerHealthChangedData) ? obj : null);
if (val5 == null)
{
WithinCannonballRadiusPlayerHealthChangedData val6 = (WithinCannonballRadiusPlayerHealthChangedData)((obj is WithinCannonballRadiusPlayerHealthChangedData) ? obj : null);
if (val6 == null)
{
ShotByArtilleryVehicleHealthChangedData val7 = (ShotByArtilleryVehicleHealthChangedData)((obj is ShotByArtilleryVehicleHealthChangedData) ? obj : null);
if (val7 == null)
{
TrampledPlayerHealthChangedData val8 = (TrampledPlayerHealthChangedData)((obj is TrampledPlayerHealthChangedData) ? obj : null);
if (val8 != null)
{
num = val8.AttackingRiderID;
}
}
else
{
num = val7.FiringPlayerID;
}
}
else
{
num = val6.FiringPlayerID;
}
}
else
{
num = val5.FiringPlayerID;
}
}
else
{
num = val4.AttackingPlayerID;
}
}
else
{
num = val3.AttackingPlayerID;
}
}
else
{
num = val2.FiringPlayerID;
}
}
else
{
num = val.FiringPlayerID;
}
if (num > 0 && num != networkPlayerID)
{
HoldfastCensusPlugin.Journal($"dmg-attr #{num}>#{networkPlayerID} ({text})");
sessionManager.DamageAttribution(num, networkPlayerID, text);
}
}
catch (Exception e)
{
HoldfastCensusPlugin.LogHandled("damage-detail", e);
}
}
}
[HarmonyPatch]
internal static class DisconnectPatch
{
[HarmonyPostfix]
[HarmonyPatch(typeof(ClientConnectionManager), "uLink_OnDisconnectedFromServer")]
private static void LinkDisconnectedPostfix()
{
try
{
HoldfastCensusPlugin instance = HoldfastCensusPlugin.Instance;
if (instance?.Tracker != null && instance.Tracker.HasLiveSession)
{
HoldfastCensusPlugin.Journal("client connection to server ended — scheduling local-left flush");
instance.ScheduleFlush(1.5f, "local-left");
}
}
catch (Exception e)
{
HoldfastCensusPlugin.LogHandled("disconnect(uLink)", e);
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(ClientConnectionManager), "DisconnectFromServerGracefullyInGame")]
private static void GracefulDisconnectPostfix()
{
try
{
HoldfastCensusPlugin instance = HoldfastCensusPlugin.Instance;
if (instance?.Tracker != null && instance.Tracker.HasLiveSession)
{
HoldfastCensusPlugin.Journal("graceful in-game disconnect — scheduling local-left flush");
instance.ScheduleFlush(1.5f, "local-left");
}
}
catch (Exception e)
{
HoldfastCensusPlugin.LogHandled("disconnect(graceful)", e);
}
}
}
[HarmonyPatch]
internal static class KillDistancePatch
{
[HarmonyPrefix]
[HarmonyPatch(typeof(ClientRoundScoreManager), "_ExecuteScoreableActionPacket")]
private static void ExecuteScoreableActionPrefix(ScorableAction __0)
{
//IL_0020: Unknown result type (might be due to invalid IL or missing references)
try
{
PlayerKilledPlayerScorableAction val = (PlayerKilledPlayerScorableAction)(object)((__0 is PlayerKilledPlayerScorableAction) ? __0 : null);
if (val != null)
{
int ownerID = ((ScorableAction)val).OwnerID;
int killedPlayerID = val.KilledPlayerID;
float distance = val.Distance;
if (IsNonMelee(val.KilledMethod))
{
HoldfastCensusPlugin.Instance?.Tracker?.RecordScoredKillDistance(ownerID, killedPlayerID, distance);
}
}
}
catch (Exception e)
{
HoldfastCensusPlugin.LogHandled("kill-distance", e);
}
}
private static bool IsNonMelee(KilledMethod method)
{
//IL_0000: Unknown result type (might be due to invalid IL or missing references)
//IL_0002: Invalid comparison between Unknown and I4
//IL_0004: Unknown result type (might be due to invalid IL or missing references)
//IL_0006: Unknown result type (might be due to invalid IL or missing references)
//IL_0008: Invalid comparison between Unknown and I4
if ((int)method == 1 || method - 3 <= 4)
{
return true;
}
return false;
}
}
[HarmonyPatch]
internal static class LeaderboardPatch
{
[HarmonyPostfix]
[HarmonyPatch(typeof(PlayerEndOfMatchList), "_SortEntries")]
private static void SortEntriesPostfix(PlayerEndOfMatchList __instance)
{
try
{
if (!((Object)(object)__instance == (Object)null))
{
int value = Traverse.Create((object)__instance).Field("currentLocalPlayerRank").GetValue<int>();
if (value > 0)
{
HoldfastCensusPlugin.Instance?.Tracker?.SetLeaderboardPlace(value);
HoldfastCensusPlugin.Journal($"leaderboard place captured: #{value}");
}
}
}
catch (Exception e)
{
HoldfastCensusPlugin.LogHandled("leaderboard_place", e);
}
}
}
internal static class TrackerBootstrap
{
private static readonly FieldInfo SharedListField = AccessTools.Field(typeof(BaseModLoaderManager), "holdfastSharedMethodInstances");
private static bool _listWarned;
private static float _nextSceneScan;
public static void EnsureRegistered(BaseModLoaderManager loader)
{
try
{
if ((Object)(object)loader == (Object)null || !(loader is ClientModLoaderManager))
{
return;
}
if (!(SharedListField?.GetValue(loader) is IList list))
{
if (!_listWarned)
{
_listWarned = true;
HoldfastCensusPlugin.LogWarn("[Census] Field 'holdfastSharedMethodInstances' not found — event capture inactive (game update?).");
}
return;
}
EventListener listener = HoldfastCensusPlugin.Listener;
if (listener != null && !list.Contains(listener))
{
list.Add(listener);
HoldfastCensusPlugin.LogInfo("[Census] Event listener registered into client mod loader.");
HoldfastCensusPlugin.Journal("event listener registered into client mod loader");
}
}
catch (Exception e)
{
HoldfastCensusPlugin.LogHandled("register", e);
}
}
public static void EnsureRegisteredFromScene()
{
try
{
if (!(Time.unscaledTime < _nextSceneScan))
{
_nextSceneScan = Time.unscaledTime + 5f;
Object obj = Object.FindObjectOfType(typeof(ClientModLoaderManager));
BaseModLoaderManager val = (BaseModLoaderManager)(object)((obj is BaseModLoaderManager) ? obj : null);
if ((Object)(object)val != (Object)null)
{
EnsureRegistered(val);
}
}
}
catch (Exception e)
{
HoldfastCensusPlugin.LogHandled("register-scan", e);
}
}
public static void OnMapInit(BaseModLoaderManager loader, RoundGameDetails details)
{
//IL_0160: Unknown result type (might be due to invalid IL or missing references)
//IL_0165: Unknown result type (might be due to invalid IL or missing references)
//IL_0175: Unknown result type (might be due to invalid IL or missing references)
//IL_017a: Unknown result type (might be due to invalid IL or missing references)
//IL_018a: Unknown result type (might be due to invalid IL or missing references)
//IL_018f: Unknown result type (might be due to invalid IL or missing references)
//IL_019f: Unknown result type (might be due to invalid IL or missing references)
//IL_01a4: Unknown result type (might be due to invalid IL or missing references)
//IL_01b4: Unknown result type (might be due to invalid IL or missing references)
//IL_01b9: Unknown result type (might be due to invalid IL or missing references)
//IL_021d: Unknown result type (might be due to invalid IL or missing references)
//IL_022b: Unknown result type (might be due to invalid IL or missing references)
//IL_0239: Unknown result type (might be due to invalid IL or missing references)
//IL_0257: Unknown result type (might be due to invalid IL or missing references)
//IL_0265: Unknown result type (might be due to invalid IL or missing references)
EnsureRegistered(loader);
EnsureRegisteredFromScene();
try
{
SessionManager sessionManager = HoldfastCensusPlugin.Instance?.Tracker;
if (sessionManager != null && details != null)
{
bool statsTrackingEnabled = details.StatsTrackingEnabled;
bool valueOrDefault = HoldfastCensusPlugin.Instance?.Cfg?.SkipServersWithStatsTrackingDisabled.Value == true;
string reason = null;
bool flag = HoldfastCensusPlugin.Instance?.Cfg?.ShouldRecordServer(details.ServerName, out reason) ?? true;
if (!flag)
{
HoldfastCensusPlugin.LogInfo("[Census] Server filter: '" + details.ServerName + "' blocked (" + reason + ") — recording disabled for this map.");
HoldfastCensusPlugin.Journal("server filter: '" + details.ServerName + "' blocked (" + reason + ") — recording disabled for this map");
}
if (!statsTrackingEnabled && valueOrDefault)
{
sessionManager.Flush("map-change-tracking-disabled");
}
else if (sessionManager.HasLiveSession)
{
sessionManager.Flush("map-change");
}
sessionManager.MapDetails(details.RoundIdentifier, details.ServerName, ((object)details.Map/*cast due to .constrained prefix*/).ToString(), ((object)details.AttackingFaction/*cast due to .constrained prefix*/).ToString(), ((object)details.DefendingFaction/*cast due to .constrained prefix*/).ToString(), ((object)details.GameplayMode/*cast due to .constrained prefix*/).ToString(), ((object)details.GameType/*cast due to .constrained prefix*/).ToString(), details.FriendlyFire, statsTrackingEnabled, valueOrDefault, flag);
HoldfastCensusPlugin.Instance?.OnMapInitialized(statsTrackingEnabled);
if (!statsTrackingEnabled && !valueOrDefault)
{
HoldfastCensusPlugin.Journal("server has stats tracking disabled — capturing anyway (config default)");
}
HoldfastCensusPlugin.Journal($"map init: roundId={details.RoundIdentifier} server='{details.ServerName}' map={details.Map} mode={details.GameplayMode} type={details.GameType} " + $"atk={details.AttackingFaction} def={details.DefendingFaction} ff={details.FriendlyFire} statsTracking={statsTrackingEnabled}");
}
}
catch (Exception e)
{
HoldfastCensusPlugin.LogHandled("map-init", e);
}
}
}
[HarmonyPatch]
internal static class InitializeOnMapPatch
{
[HarmonyPostfix]
[HarmonyPatch(typeof(BaseModLoaderManager), "InitializeOnMap")]
private static void InitializeOnMapPostfix(BaseModLoaderManager __instance, RoundGameDetails __0)
{
TrackerBootstrap.OnMapInit(__instance, __0);
}
}
[HarmonyPatch]
internal static class ModsLoadedPatch
{
[HarmonyPostfix]
[HarmonyPatch(typeof(ClientModLoaderManager), "_OnModLoadedComplete")]
private static void ModsLoadedPostfix(BaseModLoaderManager __instance)
{
TrackerBootstrap.EnsureRegistered(__instance);
}
}
[HarmonyPatch]
internal static class ScorePatch
{
[HarmonyPostfix]
[HarmonyPatch(typeof(ClientRoundScoreManager), "ApplySnapshotData")]
private static void ApplySnapshotPostfix(RoundScoreSnapshot __0)
{
try
{
TrackerBootstrap.EnsureRegisteredFromScene();
Dictionary<int, PlayerCurrentScores> dictionary = ((__0 != null) ? __0.PlayerScoresDictionary : null);
if (dictionary == null || dictionary.Count == 0)
{
return;
}
List<ScoreboardRow> list = new List<ScoreboardRow>(dictionary.Count);
foreach (KeyValuePair<int, PlayerCurrentScores> item in dictionary)
{
PlayerCurrentScores value = item.Value;
if (value != null)
{
list.Add(new ScoreboardRow
{
PlayerId = value.PlayerID,
Kills = value.TotalPlayerKills,
Deaths = value.TotalPlayerDeaths,
Score = value.TotalScore,
Killstreak = value.KillStreak
});
}
}
HoldfastCensusPlugin.Instance?.Tracker?.Snapshot(list);
HoldfastCensusPlugin.Journal($"scoreboard snapshot applied ({list.Count} rows)");
}
catch (Exception e)
{
HoldfastCensusPlugin.LogHandled("scoreboard", e);
}
}
}
}
namespace HoldfastCensus.Events
{
internal sealed class EventListener : IHoldfastSharedMethods
{
private static bool _shotInfoSeen;
private static HoldfastCensusPlugin P => HoldfastCensusPlugin.Instance;
private static SessionManager T => P?.Tracker;
private static void Safe(string name, Action a)
{
try
{
a();
}
catch (Exception e)
{
HoldfastCensusPlugin.LogHandled("event:" + name, e);
}
}
private static string S(object o)
{
return o?.ToString() ?? "";
}
private unsafe static string Fac(FactionCountry f)
{
//IL_0000: Unknown result type (might be due to invalid IL or missing references)
//IL_0001: Unknown result type (might be due to invalid IL or missing references)
FactionCountry val = (FactionCountry)f;
return ((object)(*(FactionCountry*)(&val))/*cast due to .constrained prefix*/).ToString();
}
private unsafe static string Mode(GameplayMode m)
{
//IL_0000: Unknown result type (might be due to invalid IL or missing references)
//IL_0001: Unknown result type (might be due to invalid IL or missing references)
GameplayMode val = (GameplayMode)m;
return ((object)(*(GameplayMode*)(&val))/*cast due to .constrained prefix*/).ToString();
}
private unsafe static string GType(GameType t)
{
//IL_0000: Unknown result type (might be due to invalid IL or missing references)
//IL_0001: Unknown result type (might be due to invalid IL or missing references)
GameType val = (GameType)t;
return ((object)(*(GameType*)(&val))/*cast due to .constrained prefix*/).ToString();
}
public void OnRoundDetails(int roundId, string serverName, string mapName, FactionCountry attackingFaction, FactionCountry defendingFaction, GameplayMode gameplayMode, GameType gameType)
{
//IL_001c: Unknown result type (might be due to invalid IL or missing references)
//IL_001e: Unknown result type (might be due to invalid IL or missing references)
//IL_0024: Unknown result type (might be due to invalid IL or missing references)
//IL_0026: Unknown result type (might be due to invalid IL or missing references)
//IL_002c: Unknown result type (might be due to invalid IL or missing references)
//IL_002e: Unknown result type (might be due to invalid IL or missing references)
//IL_0034: Unknown result type (might be due to invalid IL or missing references)
//IL_0036: Unknown result type (might be due to invalid IL or missing references)
Safe("OnRoundDetails", delegate
{
//IL_004e: Unknown result type (might be due to invalid IL or missing references)
//IL_0059: Unknown result type (might be due to invalid IL or missing references)
//IL_0064: Unknown result type (might be due to invalid IL or missing references)
//IL_006f: Unknown result type (might be due to invalid IL or missing references)
P?.LogEvent($"round {roundId} '{serverName}' map={mapName}");
T?.RoundDetails(roundId, serverName, mapName, Fac(attackingFaction), Fac(defendingFaction), Mode(gameplayMode), GType(gameType));
});
}
public void OnRoundEndFactionWinner(FactionCountry factionCountry, FactionRoundWinnerReason reason)
{
//IL_0007: Unknown result type (might be due to invalid IL or missing references)
//IL_0008: Unknown result type (might be due to invalid IL or missing references)
//IL_000e: Unknown result type (might be due to invalid IL or missing references)
//IL_000f: Unknown result type (might be due to invalid IL or missing references)
Safe("OnRoundEndFactionWinner", delegate
{
//IL_0011: Unknown result type (might be due to invalid IL or missing references)
//IL_001c: Unknown result type (might be due to invalid IL or missing references)
//IL_003c: Unknown result type (might be due to invalid IL or missing references)
//IL_0047: Unknown result type (might be due to invalid IL or missing references)
P?.LogEvent($"round end winner={Fac(factionCountry)} reason={reason}");
T?.RoundEnd(Fac(factionCountry), S(reason));
P?.ScheduleFlush(2.5f, "round-end");
});
}
public void OnRoundEndPlayerWinner(int playerId)
{
Safe("OnRoundEndPlayerWinner", delegate
{
});
}
public void OnIsClient(bool isClient, ulong steamId)
{
Safe("OnIsClient", delegate
{
if (isClient && steamId != 0L)
{
P?.LogEvent($"is-client steam={steamId}");
T?.SetReporterSteamId(steamId);
}
});
}
public void OnIsServer(bool isServer)
{
Safe("OnIsServer", delegate
{
});
}
public void OnPlayerJoined(int playerId, ulong steamId, string playerName, string regimentTag, bool isBot)
{
Safe("OnPlayerJoined", delegate
{
P?.LogEvent($"joined #{playerId} {playerName} [{regimentTag}] bot={isBot} steam={steamId}");
T?.PlayerJoined(playerId, steamId, playerName, regimentTag, isBot);
});
}
public void OnPlayerLeft(int playerId)
{
Safe("OnPlayerLeft", delegate
{
P?.LogEvent($"left #{playerId}");
SessionManager t = T;
if (t != null && t.PlayerLeft(playerId))
{
P?.ScheduleFlush(1.5f, "local-left");
}
});
}
public void OnPlayerSpawned(int playerId, int actorNumber, FactionCountry factionCountry, PlayerClass playerClass, int spawnSectionId, GameObject playerGameObject)
{
//IL_000e: Unknown result type (might be due to invalid IL or missing references)
//IL_000f: Unknown result type (might be due to invalid IL or missing references)
//IL_0015: Unknown result type (might be due to invalid IL or missing references)
//IL_0017: Unknown result type (might be due to invalid IL or missing references)
Safe("OnPlayerSpawned", delegate
{
//IL_001c: Unknown result type (might be due to invalid IL or missing references)
//IL_0027: Unknown result type (might be due to invalid IL or missing references)
//IL_004c: Unknown result type (might be due to invalid IL or missing references)
//IL_0057: Unknown result type (might be due to invalid IL or missing references)
P?.LogEvent($"spawned #{playerId} faction={Fac(factionCountry)} class={playerClass}");
T?.PlayerSpawned(playerId, Fac(factionCountry), S(playerClass));
});
}
public void OnPlayerHurt(int playerId, byte oldHp, byte newHp, EntityHealthChangedReason reason)
{
//IL_001c: Unknown result type (might be due to invalid IL or missing references)
//IL_001e: Unknown result type (might be due to invalid IL or missing references)
Safe("OnPlayerHurt", delegate
{
//IL_0043: Unknown result type (might be due to invalid IL or missing references)
P?.LogEvent($"hurt #{playerId} hp {oldHp}->{newHp} ({reason})");
T?.PlayerHurt(playerId, oldHp, newHp);
});
}
public void OnPlayerKilledPlayer(int killerPlayerId, int victimPlayerId, EntityHealthChangedReason reason, string additionalDetails)
{
//IL_0015: Unknown result type (might be due to invalid IL or missing references)
//IL_0016: Unknown result type (might be due to invalid IL or missing references)
Safe("OnPlayerKilledPlayer", delegate
{
//IL_0035: Unknown result type (might be due to invalid IL or missing references)
//IL_006a: Unknown result type (might be due to invalid IL or missing references)
//IL_007a: Unknown result type (might be due to invalid IL or missing references)
//IL_008a: Expected I4, but got Unknown
P?.LogEvent($"kill #{killerPlayerId}>#{victimPlayerId} {reason} '{additionalDetails}'");
T?.KilledPlayer(killerPlayerId, victimPlayerId, S(reason), (int)reason, additionalDetails);
});
}
public void OnScorableAction(int playerId, int score, ScorableActionType reason)
{
//IL_0015: Unknown result type (might be due to invalid IL or missing references)
//IL_0016: Unknown result type (might be due to invalid IL or missing references)
Safe("OnScorableAction", delegate
{
//IL_0027: Unknown result type (might be due to invalid IL or missing references)
P?.LogEvent($"score #{playerId} +{score} ({reason})");
T?.ScorableAction(playerId, score);
});
}
public void OnPlayerShoot(int playerId, bool dryShot)
{
Safe("OnPlayerShoot", delegate
{
P?.LogEvent($"shoot #{playerId} dry={dryShot}");
T?.PlayerShot(playerId, dryShot);
});
}
public void OnShotInfo(int playerId, int shotCount, Vector3[][] shotsPointsPositions, float[] trajectileDistances, float[] distanceFromFiringPositions, float[] horizontalDeviationAngles, float[] maxHorizontalDeviationAngles, float[] muzzleVelocities, float[] gravities, float[] damageHitBaseDamages, float[] damageRangeUnitValues, float[] damagePostTraitAndBuffValues, float[] totalDamages, Vector3[] hitPositions, Vector3[] hitDirections, int[] hitPlayerIds, int[] hitDamageableObjectIds, int[] hitShipIds, int[] hitVehicleIds)
{
Safe("OnShotInfo", delegate
{
if (!_shotInfoSeen)
{
_shotInfoSeen = true;
P?.LogEvent("OnShotInfo received — full ballistics event is active on this server");
}
int[] array = hitPlayerIds;
int num = ((array != null) ? array.Length : 0);
int[] array2 = hitDamageableObjectIds;
int num2 = ((array2 != null) ? array2.Length : 0);
int[] array3 = hitShipIds;
int num3 = num2 + ((array3 != null) ? array3.Length : 0);
int[] array4 = hitVehicleIds;
int num4 = num3 + ((array4 != null) ? array4.Length : 0);
double num5 = 0.0;
if (totalDamages != null)
{
for (int i = 0; i < totalDamages.Length; i++)
{
num5 += (double)totalDamages[i];
}
}
if (num > 0 || num4 > 0)
{
P?.LogEvent($"shotinfo #{playerId} shotCount={shotCount} playerHits={num} objectHits={num4} dmg={num5:F1}");
}
T?.ShotInfo(playerId, num, num4, num5);
});
}
public void OnPlayerWeaponSwitch(int playerId, string weapon)
{
Safe("OnPlayerWeaponSwitch", delegate
{
P?.LogEvent($"weapon #{playerId} -> {weapon}");
T?.WeaponSwitch(playerId, weapon);
});
}
public void OnSyncValueState(int value)
{
}
public void OnUpdateSyncedTime(double time)
{
}
public void OnUpdateElapsedTime(float time)
{
}
public void OnUpdateTimeRemaining(float time)
{
}
public void PassConfigVariables(string[] value)
{
}
public void OnPlayerBlock(int playerId, int blockedPlayerId)
{
}
public void OnPlayerMeleeStartSecondaryAttack(int playerId)
{
}
public void OnPlayerStartCarry(int playerId, CarryableObjectType carryableObjectType)
{
}
public void OnPlayerEndCarry(int playerId)
{
}
public void OnPlayerShout(int playerId, CharacterVoicePhrase phrase)
{
}
public void OnConsoleCommand(string command, string arg, bool success)
{
}
public void OnRCLogin(int playerId, string password, bool success)
{
}
public void OnRCCommand(int playerId, string command, string arg, bool success)
{
}
public void OnTextMessage(int playerId, TextChatChannel channel, string message)
{
}
public void OnAdminPlayerAction(int adminPlayerId, int targetPlayerId, ServerAdminAction action, string reason)
{
}
public void OnDamageableObjectDamaged(GameObject gameObject, int playerActorNumber, int damageableObjectId, int oldHealth, int newHealth)
{
}
public void OnInteractableObjectInteraction(int playerId, int interactableObjectId, GameObject gameObject, InteractionActivationType interactionActivationType, int nextInteractionState)
{
}
public void OnEmplacementPlaced(int playerId, GameObject gameObject, EmplacementType emplacementType)
{
}
public void OnEmplacementConstructed(int playerId)
{
}
public void OnCapturePointCaptured(int capturePoint)
{
}
public void OnCapturePointOwnerChanged(int capturePoint, FactionCountry factionCountry)
{
}
public void OnCapturePointDataUpdated(int capturePoint, int flagIndex, int players)
{
}
public void OnBuffStart(int playerId, BuffType buff)
{
}
public void OnBuffStop(int playerId, BuffType buff)
{
}
public void OnVehicleSpawned(int vehicleId, FactionCountry factionCountry, PlayerClass playerClass, GameObject vehicle, int playerActorNumber)
{
}
public void OnVehicleHurt(int vehicleId, byte oldHealth, byte newHealth, EntityHealthChangedReason reason)
{
}
public void OnPlayerKilledVehicle(int playerId, int vehicleId, EntityHealthChangedReason reason, string details)
{
}
public void OnShipSpawned(int shipId, GameObject ship, FactionCountry factionCountry, ShipType shipType, int captainPlayerId)
{
}
public void OnShipDamaged(int shipId, int damage, int health)
{
}
}
}
namespace HoldfastCensus.Aggregation
{
internal sealed class PlayerRegistry
{
private readonly Dictionary<int, PlayerEntry> _players = new Dictionary<int, PlayerEntry>();
public IReadOnlyDictionary<int, PlayerEntry> Players => _players;
public int Count => _players.Count;
public void Reset()
{
_players.Clear();
}
public PlayerEntry Joined(int playerId, ulong steamId, string name, string regimentTag, bool isBot)
{
PlayerEntry orAdd = GetOrAdd(playerId);
if (steamId != 0L)
{
orAdd.SteamId = steamId.ToString();
}
if (!string.IsNullOrEmpty(name))
{
orAdd.Name = name;
}
if (regimentTag != null)
{
orAdd.RegimentTag = regimentTag;
}
orAdd.IsBot |= isBot;
orAdd.Left = false;
return orAdd;
}
public PlayerEntry GetOrAdd(int playerId)
{
if (!_players.TryGetValue(playerId, out var value))
{
value = new PlayerEntry
{
PlayerId = playerId,
JoinedUtc = DateTime.UtcNow
};
_players[playerId] = value;
}
return value;
}
public PlayerEntry Get(int playerId)
{
if (!_players.TryGetValue(playerId, out var value))
{
return null;
}
return value;
}
public void Left(int playerId)
{
if (_players.TryGetValue(playerId, out var value))
{
value.Left = true;
}
}
}
internal sealed class RoundSession
{
public int RoundId = -1;
public string ServerName = "";
public string MapName = "";
public string AttackingFaction = "";
public string DefendingFaction = "";
public string GameplayMode = "";
public string GameType = "";
public bool FriendlyFire;
public bool StatsTrackingEnabled = true;
public DateTime StartedUtc = DateTime.UtcNow;
public DateTime? EndedUtc;
public string WinnerSide = "";
public string WinnerReason = "";
public bool Flushed;
public int LeaderboardPlace;
public readonly Dictionary<int, PlayerStats> Stats = new Dictionary<int, PlayerStats>();
public readonly List<KillRecord> Kills = new List<KillRecord>();
public List<ScoreboardRow> Scoreboard;
public PlayerStats GetStats(int playerId)
{
if (!Stats.TryGetValue(playerId, out var value))
{
value = new PlayerStats
{
PlayerId = playerId
};
Stats[playerId] = value;
}
return value;
}
public void UpdateMeta(string serverName, string mapName, string attacking, string defending, string mode, string gameType)
{
if (!string.IsNullOrEmpty(serverName))
{
ServerName = serverName;
}
if (!string.IsNullOrEmpty(mapName))
{
MapName = mapName;
}
if (!string.IsNullOrEmpty(attacking))
{
AttackingFaction = attacking;
}
if (!string.IsNullOrEmpty(defending))
{
DefendingFaction = defending;
}
if (!string.IsNullOrEmpty(mode))
{
GameplayMode = mode;
}
if (!string.IsNullOrEmpty(gameType))
{
GameType = gameType;
}
}
public string BuildPayloadJson(string modVersion, string reporterSteamId, string sessionId, string cause, PlayerRegistry registry, int localPlayerId)
{
bool selfOnlyBuild = HoldfastCensusPlugin.SelfOnlyBuild;
JsonWriter jsonWriter = new JsonWriter();
jsonWriter.BeginObject();
jsonWriter.Key("schema").Str("census.mod.round.v1");
if (selfOnlyBuild)
{
jsonWriter.Key("self_only").Bool(value: true);
}
jsonWriter.Key("mod_version").Str(modVersion);
jsonWriter.Key("reporter_steam_id").Str(reporterSteamId ?? "");
jsonWriter.Key("session_id").Str(sessionId);
jsonWriter.Key("flush_cause").Str(cause ?? "");
jsonWriter.Key("player_count").Int(selfOnlyBuild ? ((localPlayerId > 0) ? 1 : 0) : registry.Count);
if (selfOnlyBuild)
{
jsonWriter.Key("server_player_count").Int(registry.Count);
}
if (cause == "round-end")
{
int num = LeaderboardPlace;
if (num <= 0 && localPlayerId > 0 && Scoreboard != null)
{
num = ComputeFallbackLeaderboardPlace(localPlayerId, registry);
}
if (num > 0)
{
jsonWriter.Key("leaderboard_place").Int(num);
if (localPlayerId > 0)
{
GetStats(localPlayerId).LeaderboardPlace = num;
}
}
}
jsonWriter.Key("round").BeginObject();
jsonWriter.Key("round_id").Int(RoundId);
jsonWriter.Key("server_name").Str(ServerName);
jsonWriter.Key("map_name").Str(MapName);
jsonWriter.Key("attacking_faction").Str(AttackingFaction);
jsonWriter.Key("defending_faction").Str(DefendingFaction);
jsonWriter.Key("gameplay_mode").Str(GameplayMode);
jsonWriter.Key("game_type").Str(GameType);
jsonWriter.Key("friendly_fire").Bool(FriendlyFire);
jsonWriter.Key("stats_tracking_enabled").Bool(StatsTrackingEnabled);
jsonWriter.Key("round_start_utc").Str(StartedUtc.ToString("o"));
jsonWriter.Key("round_end_utc").Str((EndedUtc ?? DateTime.UtcNow).ToString("o"));
jsonWriter.Key("winner_side").Str(WinnerSide);
jsonWriter.Key("winner_reason").Str(WinnerReason);
jsonWriter.EndObject();
List<int> list;
if (selfOnlyBuild)
{
list = ((localPlayerId > 0) ? new List<int> { localPlayerId } : new List<int>());
}
else
{
HashSet<int> hashSet = new HashSet<int>(registry.Players.Keys);
hashSet.UnionWith(Stats.Keys);
if (Scoreboard != null)
{
foreach (ScoreboardRow item in Scoreboard)
{
hashSet.Add(item.PlayerId);
}
}
list = new List<int>(hashSet);
list.Sort();
}
jsonWriter.Key("players").BeginArray();
foreach (int item2 in list)
{
PlayerEntry e = registry.Get(item2);
Stats.TryGetValue(item2, out var value);
WritePlayer(jsonWriter, item2, e, value);
}
jsonWriter.EndArray();
jsonWriter.Key("kills").BeginArray();
foreach (KillRecord kill in Kills)
{
string value2 = ((!string.IsNullOrEmpty(kill.KillerSteamId)) ? kill.KillerSteamId : ((kill.KillerPlayerId <= 0) ? "" : (registry?.Get(kill.KillerPlayerId)?.SteamId ?? "")));
string value3 = ((!string.IsNullOrEmpty(kill.VictimSteamId)) ? kill.VictimSteamId : (registry?.Get(kill.VictimPlayerId)?.SteamId ?? ""));
string value4 = ((!string.IsNullOrEmpty(kill.KillerName)) ? kill.KillerName : ((kill.KillerPlayerId <= 0) ? "" : (registry?.Get(kill.KillerPlayerId)?.Name ?? "")));
string value5 = ((!string.IsNullOrEmpty(kill.VictimName)) ? kill.VictimName : (registry?.Get(kill.VictimPlayerId)?.Name ?? ""));
jsonWriter.BeginObject();
jsonWriter.Key("killer_player_id").Int(kill.KillerPlayerId);
jsonWriter.Key("victim_player_id").Int(kill.VictimPlayerId);
jsonWriter.Key("killer_steam_id").Str(value2);
jsonWriter.Key("victim_steam_id").Str(value3);
jsonWriter.Key("killer_name").Str(value4);
jsonWriter.Key("victim_name").Str(value5);
jsonWriter.Key("reason").Str(kill.Reason);
jsonWriter.Key("reason_value").Int(kill.ReasonValue);
jsonWriter.Key("category").Str(kill.Category);
jsonWriter.Key("teamkill").Bool(kill.Teamkill);
if (kill.Distance.HasValue)
{
jsonWriter.Key("distance").Dbl(Math.Round(kill.Distance.Value, 1));
}
jsonWriter.Key("details").Str(kill.Details);
jsonWriter.Key("utc").Str(kill.Utc.ToString("o"));
jsonWriter.EndObject();
}
jsonWriter.EndArray();
jsonWriter.Key("scoreboard").BeginObject();
jsonWriter.Key("source").Str("RoundScoreSnapshot");
jsonWriter.Key("rows").BeginArray();
if (Scoreboard != null)
{
foreach (ScoreboardRow item3 in Scoreboard)
{
jsonWriter.BeginObject();
jsonWriter.Key("player_id").Int(item3.PlayerId);
jsonWriter.Key("kills").Int(item3.Kills);
jsonWriter.Key("deaths").Int(item3.Deaths);
jsonWriter.Key("score").Int(item3.Score);
jsonWriter.Key("killstreak").Int(item3.Killstreak);
jsonWriter.EndObject();
}
}
jsonWriter.EndArray();
jsonWriter.EndObject();
jsonWriter.EndObject();
return jsonWriter.ToString();
}
private static void WritePlayer(JsonWriter w, int id, PlayerEntry e, PlayerStats s)
{
w.BeginObject();
w.Key("player_id").Int(id);
w.Key("steam_id").Str(e?.SteamId ?? "");
w.Key("name").Str(e?.Name ?? "");
w.Key("regiment_tag").Str(e?.RegimentTag ?? "");
w.Key("is_bot").Bool(e?.IsBot ?? false);
w.Key("faction").Str(e?.Faction ?? "");
w.Key("joined_utc").Str((e != null) ? e.JoinedUtc.ToString("o") : "");
w.Key("left").Bool(e?.Left ?? false);
w.Key("classes_played").BeginArray();
if (s != null)
{
foreach (string item in s.ClassesPlayed)
{
w.Str(item);
}
}
w.EndArray();
w.Key("weapons_used").BeginObject();
if (s != null)
{
foreach (KeyValuePair<string, long> item2 in s.WeaponUse)
{
w.Key(item2.Key).Int(item2.Value);
}
}
w.EndObject();
if (s == null)
{
w.Key("activity").Bool(value: false);
w.EndObject();
return;
}
w.Key("activity").Bool(value: true);
w.Key("shots_fired").Int(s.ShotsFired);
w.Key("dry_shots").Int(s.DryShots);
w.Key("shots_with_player_hits").Int(s.ShotsWithPlayerHits);
w.Key("projectiles_hit_players").Int(s.ProjectilesHitPlayers);
w.Key("projectiles_hit_objects").Int(s.ProjectilesHitObjects);
w.Key("hits_from_damage").Int(s.HitsFromDamage);
w.Key("melee_hits").Int(s.MeleeHits);
w.Key("arty_hits").Int(s.ArtyHits);
w.Key("damage_dealt_all").Dbl(s.DamageDealtAll);
w.Key("damage_received").Dbl(s.DamageReceived);
w.Key("attributed_damage").Dbl(s.AttributedDamage);
w.Key("attributed_damage_events").Int(s.AttributedDamageEvents);
w.Key("kills").Int(s.Kills);
w.Key("deaths").Int(s.Deaths);
w.Key("teamkills").Int(s.Teamkills);
w.Key("suicides").Int(s.Suicides);
w.Key("firearm_kills").Int(s.FirearmKills);
w.Key("melee_kills").Int(s.MeleeKills);
w.Key("arty_kills").Int(s.ArtyKills);
w.Key("environment_deaths").Int(s.EnvironmentDeaths);
w.Key("score_from_actions").Int(s.ScoreFromActions);
w.Key("snapshot_kills").Int(s.SnapshotKills);
w.Key("snapshot_deaths").Int(s.SnapshotDeaths);
w.Key("snapshot_score").Int(s.SnapshotScore);
w.Key("snapshot_killstreak").Int(s.SnapshotKillstreak);
if (s.LeaderboardPlace > 0)
{
w.Key("leaderboard_place").Int(s.LeaderboardPlace);
}
w.EndObject();
}
private int ComputeFallbackLeaderboardPlace(int localPlayerId, PlayerRegistry registry)
{
if (Scoreboard == null || Scoreboard.Count == 0 || registry == null)
{
return 0;
}
PlayerEntry playerEntry = registry.Get(localPlayerId);
if (playerEntry == null || string.IsNullOrEmpty(playerEntry.Faction))
{
return 0;
}
List<ScoreboardRow> list = new List<ScoreboardRow>();
foreach (ScoreboardRow item in Scoreboard)
{
PlayerEntry playerEntry2 = registry.Get(item.PlayerId);
if (playerEntry2 != null && playerEntry2.Faction == playerEntry.Faction)
{
list.Add(item);
}
}
if (list.Count == 0)
{
return 0;
}
list.Sort(delegate(ScoreboardRow a, ScoreboardRow b)
{
int num2 = b.Score.CompareTo(a.Score);
if (num2 != 0)
{
return num2;
}
num2 = b.Kills.CompareTo(a.Kills);
if (num2 != 0)
{
return num2;
}
num2 = a.Deaths.CompareTo(b.Deaths);
return (num2 != 0) ? num2 : a.PlayerId.CompareTo(b.PlayerId);
});
int num = list.FindIndex((ScoreboardRow r) => r.PlayerId == localPlayerId);
if (num < 0)
{
return 0;
}
return num + 1;
}
}
internal sealed class SessionManager
{
private struct HurtDelta
{
public byte OldHp;
public byte NewHp;
}
private struct PendingDistance
{
public float Distance;
public DateTime Utc;
}
public const int MaxKillRecords = 5000;
private readonly object _sync = new object();
private readonly string _modVersion;
private RoundSession _current;
private bool _trackingDisabledOnServer;
private bool _serverFiltered;
private readonly Dictionary<int, HurtDelta> _pendingHurt = new Dictionary<int, HurtDelta>();
private readonly Dictionary<long, PendingDistance> _pendingDistances = new Dictionary<long, PendingDistance>();
public string ReporterSteamId = "";
public readonly string SessionId = Guid.NewGuid().ToString("N");
public int LocalPlayerId = -1;
private int _lastLocalPlayerId = -1;
public Action<string, int, string> PayloadSink;
public PlayerRegistry Registry { get; } = new PlayerRegistry();
private int ReportPlayerId
{
get
{
if (LocalPlayerId <= 0)
{
return _lastLocalPlayerId;
}
return LocalPlayerId;
}
}
public bool TrackingDisabled
{
get
{
lock (_sync)
{
return _trackingDisabledOnServer;
}
}
}
public bool HasLiveSession
{
get
{
lock (_sync)
{
return _current != null && !_current.Flushed;
}
}
}
private static long DistanceKey(int killerId, int victimId)
{
return ((long)killerId << 32) | (uint)victimId;
}
public SessionManager(string modVersion)
{
_modVersion = modVersion;
}
public void SetReporterSteamId(ulong steamId)
{
if (steamId == 0L)
{
return;
}
lock (_sync)
{
if (string.IsNullOrEmpty(ReporterSteamId))
{
ReporterSteamId = steamId.ToString();
ResolveLocalPlayerLocked();
}
}
}
private void ResolveLocalPlayerLocked()
{
if (LocalPlayerId > 0 || string.IsNullOrEmpty(ReporterSteamId))
{
return;
}
foreach (KeyValuePair<int, PlayerEntry> player in Registry.Players)
{
if (player.Value.SteamId == ReporterSteamId)
{
LocalPlayerId = player.Key;
_lastLocalPlayerId = player.Key;
HoldfastCensusPlugin.Journal($"local player resolved: #{player.Key} steam={ReporterSteamId}");
break;
}
}
}
public void RoundDetails(int roundId, string serverName, string mapName, string attacking, string defending, string mode, string gameType)
{
lock (_sync)
{
if (!_trackingDisabledOnServer && !_serverFiltered)
{
if (_current != null && !_current.Flushed && (_current.RoundId == roundId || _current.RoundId == -1))
{
_current.RoundId = roundId;
_current.UpdateMeta(serverName, mapName, attacking, defending, mode, gameType);
return;
}
FlushLocked("superseded-round");
_current = new RoundSession();
_current.RoundId = roundId;
_current.UpdateMeta(serverName, mapName, attacking, defending, mode, gameType);
Registry.Reset();
}
}
}
public void MapDetails(int roundId, string serverName, string mapName, string attacking, string defending, string mode, string gameType, bool friendlyFire, bool statsTrackingEnabled, bool skipWhenDisabled, bool serverAllowed)
{
lock (_sync)
{
if (!serverAllowed)
{
_current = null;
_serverFiltered = true;
return;
}
_serverFiltered = false;
if (!statsTrackingEnabled && skipWhenDisabled)
{
FlushLocked("tracking-disabled");
_trackingDisabledOnServer = true;
return;
}
_trackingDisabledOnServer = false;
if (_current == null || _current.Flushed)
{
_current = new RoundSession();
_current.UpdateMeta(serverName, mapName, attacking, defending, mode, gameType);
_current.FriendlyFire = friendlyFire;
_current.StatsTrackingEnabled = statsTrackingEnabled;
_current.RoundId = roundId;
Registry.Reset();
_pendingHurt.Clear();
_pendingDistances.Clear();
}
else
{
_current.UpdateMeta(serverName, mapName, attacking, defending, mode, gameType);
_current.FriendlyFire = friendlyFire;
_current.StatsTrackingEnabled = statsTrackingEnabled;
_current.RoundId = roundId;
}
}
}
public void RoundEnd(string winnerFaction, string winnerReason)
{
lock (_sync)
{
if (_current != null && !_current.Flushed)
{
_current.EndedUtc = DateTime.UtcNow;
_current.WinnerSide = winnerFaction ?? "";
_current.WinnerReason = winnerReason ?? "";
}
}
}
public void SetLeaderboardPlace(int place)
{
lock (_sync)
{
if (_current != null && !_current.Flushed)
{
_current.LeaderboardPlace = place;
int reportPlayerId = ReportPlayerId;
if (reportPlayerId > 0)
{
_current.GetStats(reportPlayerId).LeaderboardPlace = place;
}
}
}
}
public void Flush(string cause)
{
lock (_sync)
{
FlushLocked(cause);
}
}
private void FlushLocked(string cause)
{
RoundSession current = _current;
if (current == null || current.Flushed)
{
return;
}
current.Flushed = true;
if (!current.EndedUtc.HasValue)
{
current.EndedUtc = DateTime.UtcNow;
}
int reportPlayerId = ReportPlayerId;
string arg = current.BuildPayloadJson(_modVersion, ReporterSteamId, SessionId, cause, Registry, reportPlayerId);
int arg2 = Registry.Count;
if (HoldfastCensusPlugin.SelfOnlyBuild && reportPlayerId <= 0)
{
arg2 = 0;
}
_current = null;
try
{
PayloadSink?.Invoke(arg, arg2, cause);
}
catch
{
}
}
public void PlayerJoined(int playerId, ulong steamId, string name, string regimentTag, bool isBot)
{
lock (_sync)
{
Registry.Joined(playerId, steamId, name, regimentTag, isBot);
if (steamId != 0L && !string.IsNullOrEmpty(ReporterSteamId) && steamId.ToString() == ReporterSteamId)
{
LocalPlayerId = playerId;
_lastLocalPlayerId = playerId;
HoldfastCensusPlugin.Journal($"local player resolved: #{playerId} steam={ReporterSteamId} (join)");
}
}
}
public bool PlayerLeft(int playerId)
{
lock (_sync)
{
PlayerEntry playerEntry = Registry.Get(playerId);
Registry.Left(playerId);
int num;
if (playerId != LocalPlayerId)
{
if (playerEntry != null && !string.IsNullOrEmpty(ReporterSteamId))
{
num = ((playerEntry.SteamId == ReporterSteamId) ? 1 : 0);
if (num != 0)
{
goto IL_005d;
}
}
else
{
num = 0;
}
goto IL_0064;
}
num = 1;
goto IL_005d;
IL_0064:
return (byte)num != 0;
IL_005d:
LocalPlayerId = -1;
goto IL_0064;
}
}
public void PlayerSpawned(int playerId, string faction, string playerClass)
{
lock (_sync)
{
PlayerEntry orAdd = Registry.GetOrAdd(playerId);
if (!string.IsNullOrEmpty(faction))
{
orAdd.Faction = faction;
}
if ((!HoldfastCensusPlugin.SelfOnlyBuild || playerId == LocalPlayerId) && !string.IsNullOrEmpty(playerClass) && _current != null && !_current.Flushed)
{
_current.GetStats(playerId).ClassesPlayed.Add(playerClass);
}
}
}
public void PlayerShot(int playerId, bool dryShot)
{
lock (_sync)
{
if (_current != null && !_current.Flushed && !_trackingDisabledOnServer && !_serverFiltered && (!HoldfastCensusPlugin.SelfOnlyBuild || playerId == LocalPlayerId))
{
PlayerStats stats = _current.GetStats(playerId);
if (dryShot)
{
stats.DryShots++;
}
else
{
stats.ShotsFired++;
}
}
}
}
public void ShotInfo(int playerId, int playerHits, int objectHits, double damageAll)
{
lock (_sync)
{
if (_current != null && !_current.Flushed && !_trackingDisabledOnServer && !_serverFiltered && (!HoldfastCensusPlugin.SelfOnlyBuild || playerId == LocalPlayerId))
{
PlayerStats stats = _current.GetStats(playerId);
stats.ProjectilesHitPlayers += playerHits;
stats.ProjectilesHitObjects += objectHits;
if (playerHits > 0)
{
stats.ShotsWithPlayerHits++;
}
stats.DamageDealtAll += damageAll;
}
}
}
public void PlayerHurt(int playerId, byte oldHp, byte newHp)
{
lock (_sync)
{
if (_current != null && !_current.Flushed && !_trackingDisabledOnServer && !_serverFiltered && newHp < oldHp)
{
if (!HoldfastCensusPlugin.SelfOnlyBuild || playerId == LocalPlayerId)
{
_current.GetStats(playerId).DamageReceived += oldHp - newHp;
}
_pendingHurt[playerId] = new HurtDelta
{
OldHp = oldHp,
NewHp = newHp
};
}
}
}
public void KilledPlayer(int killerPlayerId, int victimPlayerId, string reasonName, int reasonValue, string details)
{
lock (_sync)
{
if (_current == null || _current.Flushed || _trackingDisabledOnServer || _serverFiltered || reasonName == "None" || reasonName == "RespawningForNextRound" || (HoldfastCensusPlugin.SelfOnlyBuild && killerPlayerId != LocalPlayerId && victimPlayerId != LocalPlayerId))
{
return;
}
string killerSteamId = ((killerPlayerId <= 0) ? "" : (Registry.Get(killerPlayerId)?.SteamId ?? ""));
string victimSteamId = Registry.Get(victimPlayerId)?.SteamId ?? "";
string killerName = ((killerPlayerId <= 0) ? "" : (Registry.Get(killerPlayerId)?.Name ?? ""));
string victimName = Registry.Get(victimPlayerId)?.Name ?? "";
bool flag = false;
if (killerPlayerId > 0 && killerPlayerId != victimPlayerId)
{
PlayerEntry playerEntry = Registry.Get(killerPlayerId);
PlayerEntry playerEntry2 = Registry.Get(victimPlayerId);
flag = playerEntry != null && playerEntry2 != null && !string.IsNullOrEmpty(playerEntry.Faction) && playerEntry.Faction == playerEntry2.Faction;
}
string text = CategoryOf(reasonName);
float? distance = null;
if (text != "melee")
{
long key = DistanceKey(killerPlayerId, victimPlayerId);
if (_pendingDistances.TryGetValue(key, out var value))
{
_pendingDistances.Remove(key);
if ((DateTime.UtcNow - value.Utc).TotalSeconds < 5.0)
{
distance = value.Distance;
HoldfastCensusPlugin.Journal($"kill-dist #{killerPlayerId}>#{victimPlayerId} paired on kill -> {value.Distance:F1}m");
}
}
}
if (_current.Kills.Count < 5000)
{
_current.Kills.Add(new KillRecord
{
KillerPlayerId = killerPlayerId,
VictimPlayerId = victimPlayerId,
KillerSteamId = killerSteamId,
VictimSteamId = victimSteamId,
KillerName = killerName,
VictimName = victimName,
Reason = (reasonName ?? ""),
ReasonValue = reasonValue,
Details = (details ?? ""),
Category = text,
Teamkill = flag,
Distance = distance,
Utc = DateTime.UtcNow
});
}
PlayerStats stats = _current.GetStats(victimPlayerId);
stats.Deaths++;
if (killerPlayerId == victimPlayerId && killerPlayerId > 0)
{
stats.Suicides++;
}
else if (killerPlayerId > 0 && reasonName != "KilledByAdmin")
{
PlayerStats stats2 = _current.GetStats(killerPlayerId);
stats2.Kills++;
if (flag)
{
stats2.Teamkills++;
}
switch (CategoryOf(reasonName))
{
case "firearm":
stats2.FirearmKills++;
break;
case "melee":
stats2.MeleeKills++;
break;
case "artillery":
stats2.ArtyKills++;
break;
}
}
else if (killerPlayerId <= 0 && reasonName != "KilledByAdmin")
{
stats.EnvironmentDeaths++;
}
}
}
public void ScorableAction(int playerId, int score)
{
lock (_sync)
{
if (_current != null && !_current.Flushed && !_trackingDisabledOnServer && !_serverFiltered && (!HoldfastCensusPlugin.SelfOnlyBuild || playerId == LocalPlayerId))
{
_current.GetStats(playerId).ScoreFromActions += score;
}
}
}
public void WeaponSwitch(int playerId, string weapon)
{
lock (_sync)
{
if (_current != null && !_current.Flushed && !_trackingDisabledOnServer && !_serverFiltered && !string.IsNullOrEmpty(weapon) && (!HoldfastCensusPlugin.SelfOnlyBuild || playerId == LocalPlayerId))
{
PlayerStats stats = _current.GetStats(playerId);
stats.WeaponUse[weapon] = (stats.WeaponUse.TryGetValue(weapon, out var value) ? (value + 1) : 1);
}
}
}
public void Snapshot(List<ScoreboardRow> rows)
{
lock (_sync)
{
if (_current == null || _current.Flushed)
{
return;
}
if (HoldfastCensusPlugin.SelfOnlyBuild)
{
int local = LocalPlayerId;
rows = rows.FindAll((ScoreboardRow r) => r.PlayerId == local);
}
_current.Scoreboard = rows;
foreach (ScoreboardRow row in rows)
{
PlayerStats stats = _current.GetStats(row.PlayerId);
stats.SnapshotKills = row.Kills;
stats.SnapshotDeaths = row.Deaths;
stats.SnapshotScore = row.Score;
stats.SnapshotKillstreak = row.Killstreak;
}
}
}
public void DamageAttribution(int attackerId, int victimId, string reasonName)
{
lock (_sync)
{
if (_current != null && !_current.Flushed && !_trackingDisabledOnServer && !_serverFiltered && (!HoldfastCensusPlugin.SelfOnlyBuild || attackerId == LocalPlayerId))
{
int num = 0;
if (_pendingHurt.TryGetValue(victimId, out var value))
{
num = value.OldHp - value.NewHp;
_pendingHurt.Remove(victimId);
}
PlayerStats stats = _current.GetStats(attackerId);
stats.AttributedDamageEvents++;
stats.AttributedDamage += num;
switch (CategoryOf(reasonName))
{
case "firearm":
stats.HitsFromDamage++;
break;
case "melee":
stats.MeleeHits++;
break;
case "artillery":
stats.ArtyHits++;
break;
}
}
}
}
public void RecordScoredKillDistance(int killerId, int victimId, float distance)
{
lock (_sync)
{
if (_current == null || _current.Flushed || _trackingDisabledOnServer || _serverFiltered || (HoldfastCensusPlugin.SelfOnlyBuild && killerId != LocalPlayerId && victimId != LocalPlayerId))
{
return;
}
for (int num = _current.Kills.Count - 1; num >= 0; num--)
{
KillRecord killRecord = _current.Kills[num];
if (killRecord.KillerPlayerId == killerId && killRecord.VictimPlayerId == victimId && !killRecord.Distance.HasValue && (DateTime.UtcNow - killRecord.Utc).TotalSeconds < 5.0)
{
if (killRecord.Category != "melee")
{
killRecord.Distance = distance;
HoldfastCensusPlugin.Journal($"kill-dist #{killerId}>#{victimId} matched -> {distance:F1}m");
}
return;
}
}
if (_pendingDistances.Count > 20)
{
DateTime utcNow = DateTime.UtcNow;
List<long> list = new List<long>();
foreach (KeyValuePair<long, PendingDistance> pendingDistance in _pendingDistances)
{
if ((utcNow - pendingDistance.Value.Utc).TotalSeconds > 10.0)
{
list.Add(pendingDistance.Key);
}
}
foreach (long item in list)
{
_pendingDistances.Remove(item);
}
}
_pendingDistances[DistanceKey(killerId, victimId)] = new PendingDistance
{
Distance = distance,
Utc = DateTime.UtcNow
};
HoldfastCensusPlugin.Journal($"kill-dist #{killerId}>#{victimId} pending -> {distance:F1}m");
}
}
internal static string CategoryOf(string reason)
{
switch (reason)
{
case "ShotByFirearm":
case "VehicleShotByFirearm":
return "firearm";
case "HitByMeleeWeapon":
case "HitByMeleeSecondaryAttack":
case "TrampledPlayer":
case "VehicleHitByMeleeWeapon":
return "melee";
case "ExplosionRadius":
case "HitByCannonball":
case "RocketExplosionRadius":
case "VehicleHitByArtillery":
case "DirectCannonballHit":
return "artillery";
default:
return "other";
}
}
}
}