Decompiled source of HoldfastCensusLite v1.7.0

plugins/HoldfastCensusLite.dll

Decompiled a day ago
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";
			}
		}
	}
}