Decompiled source of VikingStoryteller v0.4.9

plugins/VikingStoryteller.dll

Decompiled a month ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using HarmonyLib;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: AssemblyVersion("0.0.0.0")]
namespace VikingStoryteller;

public static class CheckpointCopy
{
	private sealed class References : IEqualityComparer<object>
	{
		public new bool Equals(object a, object b)
		{
			return object.ReferenceEquals(a, b);
		}

		public int GetHashCode(object o)
		{
			return RuntimeHelpers.GetHashCode(o);
		}
	}

	private static readonly Dictionary<Type, Func<object, Dictionary<object, object>, object>> copiers = new Dictionary<Type, Func<object, Dictionary<object, object>, object>>();

	public static T Capture<T>(T value)
	{
		return (T)Copy(value, new Dictionary<object, object>(new References()));
	}

	private static object Copy(object value, Dictionary<object, object> seen)
	{
		if (value == null)
		{
			return null;
		}
		Type type = value.GetType();
		if (type.IsPrimitive || type.IsEnum || type == typeof(string) || type == typeof(decimal) || type == typeof(DateTime) || type == typeof(Guid) || type == typeof(TimeSpan))
		{
			return value;
		}
		if (seen.TryGetValue(value, out var value2))
		{
			return value2;
		}
		if (type == typeof(Dictionary<string, string>))
		{
			return CopyIndex((Dictionary<string, string>)value, seen);
		}
		if (type == typeof(Dictionary<string, double>))
		{
			return CopyIndex((Dictionary<string, double>)value, seen);
		}
		if (type == typeof(Dictionary<string, long>))
		{
			return CopyIndex((Dictionary<string, long>)value, seen);
		}
		if (type == typeof(Dictionary<string, int>))
		{
			return CopyIndex((Dictionary<string, int>)value, seen);
		}
		if (type == typeof(HashSet<string>))
		{
			HashSet<string> hashSet = (HashSet<string>)value;
			return seen[value] = new HashSet<string>(hashSet, hashSet.Comparer);
		}
		if (type == typeof(HashSet<long>))
		{
			HashSet<long> hashSet3 = (HashSet<long>)value;
			return seen[value] = new HashSet<long>(hashSet3, hashSet3.Comparer);
		}
		if (value is Array array)
		{
			if (array.Rank != 1)
			{
				throw new NotSupportedException("Multidimensional checkpoint array");
			}
			Array array2 = (Array)(seen[value] = Array.CreateInstance(type.GetElementType(), array.Length));
			for (int i = 0; i < array.Length; i++)
			{
				array2.SetValue(Copy(array.GetValue(i), seen), i);
			}
			return array2;
		}
		if (value is IDictionary dictionary)
		{
			IDictionary dictionary2 = (IDictionary)(seen[value] = (IDictionary)Activator.CreateInstance(type));
			foreach (DictionaryEntry item in dictionary)
			{
				dictionary2.Add(Copy(item.Key, seen), Copy(item.Value, seen));
			}
			return dictionary2;
		}
		if (value is IList list)
		{
			IList list2 = (IList)(seen[value] = (IList)Activator.CreateInstance(type));
			foreach (object item2 in list)
			{
				list2.Add(Copy(item2, seen));
			}
			return list2;
		}
		if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(HashSet<>))
		{
			object obj3 = (seen[value] = Activator.CreateInstance(type));
			MethodInfo method = type.GetMethod("Add");
			foreach (object item3 in (IEnumerable)value)
			{
				method.Invoke(obj3, new object[1] { Copy(item3, seen) });
			}
			return obj3;
		}
		if (type.Assembly != typeof(CheckpointCopy).Assembly || type.Namespace != "VikingStoryteller" || type.IsValueType)
		{
			throw new NotSupportedException("Non-DTO checkpoint type: " + type.FullName);
		}
		if (!copiers.TryGetValue(type, out var value3))
		{
			value3 = BuildCopier(type);
			copiers[type] = value3;
		}
		return value3(value, seen);
	}

	private static object CopyIndex<T>(Dictionary<string, T> value, Dictionary<object, object> seen)
	{
		return seen[value] = new Dictionary<string, T>(value, value.Comparer);
	}

	private static Func<object, Dictionary<object, object>, object> BuildCopier(Type type)
	{
		if (type.GetProperties(BindingFlags.Instance | BindingFlags.Public).Length != 0 || type.GetFields(BindingFlags.Instance | BindingFlags.NonPublic).Any((FieldInfo f) => !f.IsStatic))
		{
			throw new NotSupportedException("Checkpoint DTO must expose its state as public fields: " + type.FullName);
		}
		ParameterExpression parameterExpression = Expression.Parameter(typeof(object), "source");
		ParameterExpression parameterExpression2 = Expression.Parameter(typeof(Dictionary<object, object>), "seen");
		ParameterExpression parameterExpression3 = Expression.Variable(type, "clone");
		List<Expression> list = new List<Expression>();
		list.Add(Expression.Assign(parameterExpression3, Expression.Convert(Expression.Call(parameterExpression, typeof(object).GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic)), type)));
		list.Add(Expression.Call(parameterExpression2, typeof(Dictionary<object, object>).GetMethod("Add"), parameterExpression, Expression.Convert(parameterExpression3, typeof(object))));
		FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public);
		foreach (FieldInfo fieldInfo in fields)
		{
			if (!fieldInfo.FieldType.IsValueType && !(fieldInfo.FieldType == typeof(string)))
			{
				MemberExpression memberExpression = Expression.Field(parameterExpression3, fieldInfo);
				list.Add(Expression.Assign(memberExpression, Expression.Convert(Expression.Call(typeof(CheckpointCopy).GetMethod("Copy", BindingFlags.Static | BindingFlags.NonPublic), Expression.Convert(memberExpression, typeof(object)), parameterExpression2), fieldInfo.FieldType)));
			}
		}
		list.Add(Expression.Convert(parameterExpression3, typeof(object)));
		return Expression.Lambda<Func<object, Dictionary<object, object>, object>>(Expression.Block(new ParameterExpression[1] { parameterExpression3 }, list), new ParameterExpression[2] { parameterExpression, parameterExpression2 }).Compile();
	}
}
public sealed class CheckpointWriter
{
	private Task task;

	private DateTime capturedUtc;

	public bool Busy => task != null && !task.IsCompleted;

	public bool Pending => task != null;

	public void Start(string directory, Dictionary<string, object> detached, DateTime utc)
	{
		if (task != null)
		{
			throw new InvalidOperationException("Consume previous checkpoint before starting another");
		}
		capturedUtc = utc;
		task = Task.Factory.StartNew(delegate
		{
			Write(directory, detached);
		});
	}

	public bool Complete(bool wait, out DateTime checkpoint, out Exception error)
	{
		checkpoint = default(DateTime);
		error = null;
		if (task == null || (!wait && !task.IsCompleted))
		{
			return false;
		}
		try
		{
			task.GetAwaiter().GetResult();
			checkpoint = capturedUtc;
		}
		catch (Exception ex)
		{
			error = ex;
		}
		finally
		{
			task = null;
		}
		return true;
	}

	public static void Write(string directory, Dictionary<string, object> detached)
	{
		//IL_0001: Unknown result type (might be due to invalid IL or missing references)
		//IL_0008: Expected O, but got Unknown
		JsonSerializerSettings val = new JsonSerializerSettings();
		val.TypeNameHandling = (TypeNameHandling)0;
		val.NullValueHandling = (NullValueHandling)1;
		JsonSerializerSettings val2 = val;
		foreach (KeyValuePair<string, object> item in detached.OrderBy((KeyValuePair<string, object> e) => (e.Key == "status.json") ? 1 : 0))
		{
			if (Path.GetFileName(item.Key) != item.Key)
			{
				throw new InvalidDataException("Checkpoint must use a file name");
			}
			string text = Path.Combine(directory, item.Key);
			string text2 = text + ".tmp";
			try
			{
				File.WriteAllText(text2, JsonConvert.SerializeObject(item.Value, (Formatting)0, val2));
				if (File.Exists(text))
				{
					File.Replace(text2, text, null);
				}
				else
				{
					File.Move(text2, text);
				}
			}
			finally
			{
				if (File.Exists(text2))
				{
					File.Delete(text2);
				}
			}
		}
	}
}
[BepInPlugin("JacobsValheim.VikingStoryteller", "Viking Storyteller", "0.4.9")]
public class Plugin : BaseUnityPlugin
{
	private class Connection
	{
		public string token;

		public string snapshotName;

		public string sessionId;

		public float helloAt;

		public float lastHello = -999f;

		public float window;

		public float activityAt;

		public float metricsWindow;

		public float snapshotWindow;

		public float sessionAt;

		public float uiWindow;

		public int received;

		public int version;

		public int metricsReceived;

		public int snapshotsReceived;

		public int uiRequests;

		public long sequence;

		public long metricsSequence;

		public long snapshotSequence;

		public bool? afk;

		public PlayerSnapshot previousSnapshot;

		public Entity sessionPlayer;
	}

	private class Cached
	{
		public Entity entity;

		public float time;
	}

	public class Capture
	{
		public DeathReport report;

		public ZDO zdo;

		public long owner;
	}

	public class DamageContext
	{
		public bool entered;

		public HitData previous;
	}

	public class HealthCapture
	{
		public HealthReport report;

		public ZDO zdo;

		public long owner;
	}

	public class RemovalCapture
	{
		public ZDOID id;

		public StoryEvent record;
	}

	public class MetricCapture
	{
		public double before;

		public double time;

		public string player;

		public string group;

		public string name;

		public string operation;

		public string method;

		public PlayerProfile profile;

		public float x;

		public float y;

		public float z;
	}

	public class MetricsEnvelope
	{
		public int protocol = 3;

		public string world;

		public string token;

		public long sequence;

		public MetricReport report;

		public int workVersion;

		public WorkSample[] work;
	}

	public class SnapshotEnvelope
	{
		public int protocol = 3;

		public string world;

		public string token;

		public long sequence;

		public PlayerSnapshot snapshot;

		public EquipmentReport equipment;
	}

	public class StatsRequest
	{
		public int protocol = 3;

		public string world;

		public string token;

		public string id;

		public string leaderboardMetric;

		public string leaderboardPeriod;
	}

	public class StatsResponse
	{
		public int protocol = 3;

		public string world;

		public string id;

		public StatsView view;

		public LeaderboardView leaderboard;
	}

	private class LethalDot
	{
		public string victim;

		public string attacker;

		public string cause;

		public long owner;

		public HitData hit;
	}

	public class DotAdd
	{
		public long owner;

		public string attacker;

		public double remaining;

		public double amount;
	}

	public class DotTick
	{
		public Character character;

		public DotTick previous;

		public string attacker;

		public string cause;
	}

	public class DropContext
	{
		public DropContext previous;

		public string source;

		public List<ItemDrop> items = new List<ItemDrop>();
	}

	public class PickupContext
	{
		public PickupContext previous;

		public ItemDrop drop;

		public string id;

		public string player;

		public Vector3 position;

		public Inventory inventory;
	}

	public class AdditionCapture
	{
		public PickupContext context;

		public LootReport report;

		public int before;
	}

	public class LootEnvelope
	{
		public string world;

		public string token;

		public long sequence;

		public LootReport report;
	}

	public class LedgerScope
	{
		public LedgerScope previous;

		public Inventory fromInv;

		public Inventory toInv;

		public LedgerPlace from;

		public LedgerPlace to;

		public Dictionary<string, LedgerStack> beforeFrom;

		public Dictionary<string, LedgerStack> beforeTo;

		public string player;

		public string operation;

		public string integration;

		public string transaction;

		public long actorPeer;

		public bool ownerTransaction;

		public bool automated;

		public bool serverObserved;

		public bool committed;

		public ZDO serverChest;

		public int serverWidth;

		public int serverHeight;
	}

	public class LedgerEnvelope
	{
		public string world;

		public string token;

		public long sequence;

		public LedgerReport report;
	}

	private sealed class StoragePurpose
	{
		public string operation;
	}

	public sealed class CraftingStorageObservation
	{
		public Inventory bag;

		public Inventory chest;

		public LedgerPlace playerPlace;

		public LedgerPlace chestPlace;

		public Dictionary<string, LedgerStack> bagBefore;

		public Dictionary<string, LedgerStack> chestBefore;

		public string operation;

		public string transaction;

		public Player player;

		public object batch;
	}

	private const string Hello = "JVStory_Hello_v1";

	private const string Accept = "JVStory_Accept_v1";

	private const string Report = "JVStory_Death_v1";

	private const string TelemetryRpc = "JVStory_Telemetry_v2";

	private const string MetricsRpc = "JVStory_PlayerMetrics_v3";

	private const string SnapshotRpc = "JVStory_PlayerSnapshot_v3";

	private const string StatsRequestRpc = "JVStory_StatsRequest_v3";

	private const string StatsResponseRpc = "JVStory_StatsResponse_v3";

	private const string LootRpc = "VikingStory_Loot_v1";

	private const string LedgerRpc = "VikingStory_Ledger_v1";

	private const string CraftingStorageIntegration = "VikingCraftingAndQuickDeposit";

	public static Plugin Instance;

	private static readonly FieldInfo LastHit = AccessTools.Field(typeof(Character), "m_lastHit");

	private readonly Dictionary<ZRpc, Connection> connections = new Dictionary<ZRpc, Connection>();

	private readonly Dictionary<string, Cached> cache = new Dictionary<string, Cached>();

	private readonly Queue<DeathReport> outgoing = new Queue<DeathReport>();

	private readonly Queue<string> lines = new Queue<string>();

	private readonly HashSet<string> emitted = new HashSet<string>();

	private readonly Dictionary<string, Entity> roster = new Dictionary<string, Entity>();

	private StoryModel model;

	private ConfigEntry<bool> send;

	private ConfigEntry<bool> collect;

	private Harmony harmony;

	private string world;

	private string token;

	private string directory;

	private ZRpc acceptedServer;

	private float nextTick;

	private float nextSave;

	private float nextFlush;

	private readonly CheckpointWriter checkpointWriter = new CheckpointWriter();

	private double checkpointCaptureMs;

	private long accepted;

	private long rejected;

	private long dropped;

	private long failures;

	private bool hooksReady;

	private readonly string collectorRun = Guid.NewGuid().ToString("N");

	private static readonly JsonSerializerSettings JsonSettings;

	private static readonly JsonSerializerSettings WriteSettings;

	private readonly Queue<HealthReport> healthQueue = new Queue<HealthReport>();

	private readonly Queue<ActionReport> actionQueue = new Queue<ActionReport>();

	private readonly Dictionary<Character, HitData> damageContext = new Dictionary<Character, HitData>();

	private readonly ActivityClock activityClock = new ActivityClock();

	private TelemetryModel telemetry;

	private ConfigEntry<int> afkSeconds;

	private bool serverTelemetry;

	private float nextActivity;

	private long sequence;

	private long healthAccepted;

	private long activityAccepted;

	private DateTime lastInputTimer;

	private readonly MetricBuffer metricBuffer = new MetricBuffer();

	private static readonly FieldInfo RawProfileStats;

	private readonly Dictionary<string, FieldInfo> rawStatFields = new Dictionary<string, FieldInfo>();

	private readonly HashSet<string> nativeStatNames = new HashSet<string>(from n in Enum.GetNames(typeof(PlayerStatType))
		where n != "Count" && n != "None"
		select n);

	private static readonly Dictionary<string, string> NamedStatFields;

	private static readonly Dictionary<string, string> NamedStatGroups;

	private bool serverMetrics;

	private float nextMetrics;

	private long metricsSequence;

	private long metricsAccepted;

	private string metricsCharacter;

	private Harmony metricHarmony;

	private bool nativeMetricHooksReady;

	private ConfigEntry<bool> snapshotsEnabled;

	private ConfigEntry<bool> epicLootReporting;

	private readonly EpicLootAdapter epicAdapter = new EpicLootAdapter();

	private string equipmentFingerprint;

	private ConfigEntry<int> snapshotSeconds;

	private ConfigEntry<int> afkSnapshotSeconds;

	private bool lastSnapshotAfk;

	private float nextSnapshotCheck;

	private float nextSnapshot;

	private string lastSnapshotBiome;

	private string lastSnapshotPlayer;

	private long snapshotSequence;

	private long snapshotsAccepted;

	private PlayerSnapshot localPreviousSnapshot;

	private string localSnapshotName;

	private string localEquipmentPlayer;

	private ConfigEntry<KeyboardShortcut> statsKey;

	private ConfigEntry<bool> leaderboardEnabled;

	private LeaderboardView leaderboard;

	private int leaderboardMetric;

	private int leaderboardPeriod = 2;

	private static readonly string[] PeriodKeys;

	private Harmony statsUiHarmony;

	private bool statsUiReady;

	private bool statsOpen;

	private Rect statsRect;

	private Vector2 statsScroll;

	private CursorLockMode previousCursorLock;

	private bool previousCursorVisible;

	private int statsTab;

	private float nextStatsRequest;

	private string pendingStatsRequest;

	private string statsNotice = "Waiting for the server...";

	private string statsSearch = "";

	private StatsView statsView;

	private GUIStyle statsWindowStyle;

	private GUIStyle statsTitleStyle;

	private GUIStyle statsLabelStyle;

	private GUIStyle statsMutedStyle;

	private GUIStyle statsButtonStyle;

	private Texture2D statsBackground;

	private WorkModel workModel;

	private readonly WorkBuffer workBuffer = new WorkBuffer();

	private readonly Queue<string> detailLines = new Queue<string>();

	private bool serverWork;

	private long workAccepted;

	private long workSummaries;

	private readonly Dictionary<Character, int> capacityContext = new Dictionary<Character, int>();

	private ConfigEntry<bool> retentionEnabled;

	private ConfigEntry<int> detailDays;

	private ConfigEntry<int> eventDays;

	private ConfigEntry<int> archiveDays;

	private ConfigEntry<int> detailMiB;

	private ConfigEntry<int> eventMiB;

	private ConfigEntry<int> archiveMiB;

	private Task<RetentionResult> retentionTask;

	private float nextRetention;

	private DateTime savedCheckpointUtc;

	private ConfigEntry<int> historyDays;

	private ConfigEntry<int> historyMonths;

	private readonly List<StoryEvent> recentHistoryDeaths = new List<StoryEvent>();

	private readonly Dictionary<Character, HitData> dotHitContext = new Dictionary<Character, HitData>();

	private readonly Dictionary<StatusEffect, DotSource> dotSources = new Dictionary<StatusEffect, DotSource>();

	private readonly Dictionary<Character, DotTick> dotTicks = new Dictionary<Character, DotTick>();

	private static readonly FieldInfo DotCharacter;

	private static readonly FieldInfo PoisonLeft;

	private static readonly FieldInfo FireLeft;

	private static readonly FieldInfo SpiritLeft;

	private bool dotHooksReady;

	private readonly Dictionary<Character, LethalDot> lethalDots = new Dictionary<Character, LethalDot>();

	private ConfigEntry<bool> lootEnabled;

	private ConfigEntry<bool> lootMaterials;

	private Harmony lootHarmony;

	private bool lootHooksReady;

	private bool serverLoot;

	private long lootAccepted;

	private long lootDropped;

	private long lootSequence;

	private LootModel lootModel;

	private readonly Queue<LootReport> lootQueue = new Queue<LootReport>();

	private readonly Dictionary<ZRpc, long> lootSequences = new Dictionary<ZRpc, long>();

	private readonly Dictionary<ZRpc, Tuple<float, int>> lootRates = new Dictionary<ZRpc, Tuple<float, int>>();

	private readonly Dictionary<string, Tuple<float, long>> destroyedLootOwners = new Dictionary<string, Tuple<float, long>>();

	private DropContext dropContext;

	private PickupContext pickupContext;

	private ConfigEntry<bool> ledgerEnabled;

	private Harmony ledgerHarmony;

	private bool ledgerHooksReady;

	private string tidyLedger = "absent";

	private string bestLedger = "absent";

	private long ledgerAccepted;

	private long ledgerDropped;

	private long ledgerSequence;

	private readonly Dictionary<Inventory, Container> ledgerContainers = new Dictionary<Inventory, Container>();

	private readonly Queue<LedgerReport> ledgerQueue = new Queue<LedgerReport>();

	private readonly Dictionary<ZRpc, long> ledgerSequences = new Dictionary<ZRpc, long>();

	private readonly Dictionary<ZRpc, Tuple<float, int>> ledgerRates = new Dictionary<ZRpc, Tuple<float, int>>();

	private readonly Dictionary<string, double> ledgerSeen = new Dictionary<string, double>();

	private LedgerScope ledgerScope;

	private Piece lastLedgerPiece;

	private Vector3 lastLedgerPosition;

	private int lastLedgerFrame = -1;

	private float nextLedgerCleanup;

	private string craftingStorageLedger = "absent";

	private readonly ConditionalWeakTable<object, StoragePurpose> storagePurposes = new ConditionalWeakTable<object, StoragePurpose>();

	private void Awake()
	{
		//IL_0064: Unknown result type (might be due to invalid IL or missing references)
		//IL_006e: Expected O, but got Unknown
		//IL_00ea: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ff: Unknown result type (might be due to invalid IL or missing references)
		//IL_010c: Expected O, but got Unknown
		//IL_010c: Expected O, but got Unknown
		//IL_014c: Unknown result type (might be due to invalid IL or missing references)
		//IL_015a: Expected O, but got Unknown
		Instance = this;
		send = ((BaseUnityPlugin)this).Config.Bind<bool>("Reporting", "Send gameplay death reports", true, "Master opt-out for all gameplay reports, snapshots and stats negotiation. No chat contents, full inventory listing, credentials, or external service traffic.");
		collect = ((BaseUnityPlugin)this).Config.Bind<bool>("Server", "Collect gameplay death reports", true, "Master switch for accepting gameplay telemetry and serving each connected player's own stats. Writes local story files; grants no admin powers.");
		directory = Path.Combine(Paths.ConfigPath, "VikingStoryteller");
		harmony = new Harmony("JacobsValheim.VikingStoryteller");
		SetupRetention();
		SetupHistory();
		SetupSnapshots();
		SetupStatsUI();
		SetupLoot();
		SetupLedger();
		try
		{
			Type[] array = new Type[2]
			{
				typeof(Character),
				typeof(Player)
			};
			foreach (Type type in array)
			{
				harmony.Patch((MethodBase)AccessTools.DeclaredMethod(type, "OnDeath", (Type[])null, (Type[])null), new HarmonyMethod(typeof(Plugin), "BeforeDeath", (Type[])null), new HarmonyMethod(typeof(Plugin), "AfterDeath", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			}
			harmony.Patch((MethodBase)AccessTools.Method(typeof(ZDOMan), "HandleDestroyedZDO", (Type[])null, (Type[])null), new HarmonyMethod(typeof(Plugin), "BeforeDestroyed", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			SetupTelemetry();
			hooksReady = true;
		}
		catch (Exception ex)
		{
			harmony.UnpatchSelf();
			((BaseUnityPlugin)this).Logger.LogError((object)("Death observation disabled: " + ex.Message));
		}
		((BaseUnityPlugin)this).Logger.LogInfo((object)"Viking Storyteller loaded.");
	}

	private void OnDestroy()
	{
		CloseStats();
		if (statsUiHarmony != null)
		{
			statsUiHarmony.UnpatchSelf();
		}
		if ((Object)(object)statsBackground != (Object)null)
		{
			Object.Destroy((Object)(object)statsBackground);
		}
		if (model != null)
		{
			foreach (Connection value in connections.Values)
			{
				SessionEnded(value, "collector_stopping");
			}
		}
		if ((Object)(object)Instance == (Object)(object)this)
		{
			Instance = null;
		}
		if (harmony != null)
		{
			harmony.UnpatchSelf();
		}
		if (lootHarmony != null)
		{
			lootHarmony.UnpatchSelf();
		}
		if (ledgerHarmony != null)
		{
			ledgerHarmony.UnpatchSelf();
		}
		if (metricHarmony != null)
		{
			metricHarmony.UnpatchSelf();
		}
		if (model != null)
		{
			if (lootModel != null)
			{
				lootModel.Drain(0.0, force: true);
			}
			CloseAllWork("collector_stopping");
			SaveCheckpoint(wait: true);
			Flush();
		}
	}

	private static bool Server()
	{
		return (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer();
	}

	private static string CurrentWorld()
	{
		return (WorldGenerator.instance == null || WorldGenerator.instance.m_world == null) ? null : WorldGenerator.instance.m_world.m_uid.ToString();
	}

	private void Update()
	{
		try
		{
			UpdateStatsUI();
		}
		catch
		{
			CloseStats();
			failures++;
		}
		SampleActivity();
		if (Time.realtimeSinceStartup < nextTick)
		{
			return;
		}
		nextTick = Time.realtimeSinceStartup + 0.5f;
		PruneLedgerContainers();
		CompleteCheckpoint(wait: false);
		try
		{
			Tick();
		}
		catch (Exception ex)
		{
			failures++;
			if (failures <= 3)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("Story observation failed: " + ex.Message));
			}
		}
	}

	private void Tick()
	{
		//IL_0559: Unknown result type (might be due to invalid IL or missing references)
		//IL_055e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0580: Unknown result type (might be due to invalid IL or missing references)
		if ((Object)(object)ZNet.instance == (Object)null || ZDOMan.instance == null || (Object)(object)ZNetScene.instance == (Object)null)
		{
			return;
		}
		string text = CurrentWorld();
		if (text == null)
		{
			return;
		}
		Connection value;
		if (world != text)
		{
			if (model != null)
			{
				foreach (Connection value2 in connections.Values)
				{
					value = value2;
					SessionEnded(value, "world_changed");
				}
				if (lootModel != null)
				{
					lootModel.Drain(0.0, force: true);
				}
				CloseAllWork("world_changed");
				SaveCheckpoint(wait: true);
				Flush();
			}
			world = text;
			token = null;
			acceptedServer = null;
			connections.Clear();
			outgoing.Clear();
			cache.Clear();
			roster.Clear();
			emitted.Clear();
			model = null;
			savedCheckpointUtc = default(DateTime);
			ResetTelemetry();
		}
		bool flag = Server();
		float now = Time.realtimeSinceStartup;
		if (flag && collect.Value && model == null)
		{
			Directory.CreateDirectory(directory);
			StoryState storyState = null;
			string text2 = Path.Combine(directory, world + "-state.json");
			if (File.Exists(text2))
			{
				try
				{
					storyState = JsonConvert.DeserializeObject<StoryState>(File.ReadAllText(text2), JsonSettings);
					if (storyState == null || storyState.deaths == null || storyState.deathIds == null || storyState.links == null)
					{
						throw new Exception("Invalid saved story state");
					}
				}
				catch
				{
					File.Copy(text2, text2 + ".unreadable-" + Guid.NewGuid().ToString("N"));
					failures++;
					storyState = null;
				}
			}
			model = new StoryModel(world, storyState, WriteEvent);
			LoadTelemetry();
			LoadHistory();
			LoadWork();
			LoadLoot();
			WriteOutputGuide();
		}
		List<ZNetPeer> peers = ZNet.instance.GetPeers();
		foreach (ZNetPeer item in peers)
		{
			if (item.m_rpc == null)
			{
				continue;
			}
			if (!connections.TryGetValue(item.m_rpc, out value))
			{
				Connection connection = new Connection();
				connection.helloAt = now + 5f;
				value = connection;
				connections[item.m_rpc] = value;
				if (flag)
				{
					item.m_rpc.Register<int>("JVStory_Hello_v1", (Action<ZRpc, int>)ReceiveHello);
					item.m_rpc.Register<string>("JVStory_Death_v1", (Action<ZRpc, string>)ReceiveReport);
					item.m_rpc.Register<string>("VikingStory_Loot_v1", (Action<ZRpc, string>)ReceiveLoot);
					item.m_rpc.Register<string>("VikingStory_Ledger_v1", (Action<ZRpc, string>)ReceiveLedger);
					item.m_rpc.Register<string>("JVStory_Telemetry_v2", (Action<ZRpc, string>)ReceiveTelemetry);
					item.m_rpc.Register<string>("JVStory_PlayerMetrics_v3", (Action<ZRpc, string>)ReceiveMetrics);
					item.m_rpc.Register<string>("JVStory_PlayerSnapshot_v3", (Action<ZRpc, string>)ReceiveSnapshot);
					item.m_rpc.Register<string>("JVStory_StatsRequest_v3", (Action<ZRpc, string>)ReceiveStatsRequest);
				}
				else
				{
					item.m_rpc.Register<string>("JVStory_Accept_v1", (Action<ZRpc, string>)ReceiveAccept);
					item.m_rpc.Register<string>("JVStory_StatsResponse_v3", (Action<ZRpc, string>)ReceiveStatsResponse);
				}
			}
			if (!flag && send.Value && item.IsReady() && item == ZNet.instance.GetServerPeer() && acceptedServer != item.m_rpc && now >= value.helloAt)
			{
				if (value.received < 5)
				{
					item.m_rpc.Invoke("JVStory_Hello_v1", new object[1] { 5 - value.received });
					value.received++;
				}
				value.helloAt = now + 15f;
			}
			if (!flag || !item.IsReady() || !(item.m_characterID != ZDOID.None))
			{
				continue;
			}
			ZDO zDO = ZDOMan.instance.GetZDO(item.m_characterID);
			if (zDO != null)
			{
				Entity entity = FromZdo(zDO);
				if (entity != null)
				{
					entity.player = true;
					entity.account = item.m_socket.GetHostName();
					entity.name = Clean(item.m_playerName);
					roster[entity.id] = entity;
					SessionReady(value, entity);
				}
			}
		}
		ZRpc[] array = connections.Keys.Where((ZRpc rpc) => !peers.Any((ZNetPeer p) => p.m_rpc == rpc)).ToArray();
		foreach (ZRpc key in array)
		{
			if (flag)
			{
				SessionEnded(connections[key], "connection_removed");
			}
			connections.Remove(key);
			lootSequences.Remove(key);
			lootRates.Remove(key);
			ledgerSequences.Remove(key);
			ledgerRates.Remove(key);
		}
		if (!flag && (acceptedServer == null || !peers.Any((ZNetPeer p) => p.m_rpc == acceptedServer)))
		{
			if (acceptedServer != null)
			{
				ResetStatsUI();
				ResetSnapshots();
			}
			acceptedServer = null;
			token = null;
			outgoing.Clear();
			healthQueue.Clear();
			actionQueue.Clear();
			metricBuffer.Clear();
			workBuffer.Clear();
			serverWork = false;
			serverMetrics = false;
			serverTelemetry = false;
			serverLoot = false;
			lootQueue.Clear();
			ledgerQueue.Clear();
			ledgerSequence = 0L;
			activityClock.Reset();
		}
		if (flag)
		{
			TickWork();
		}
		SendTelemetry();
		SendMetrics();
		SendSnapshot();
		TickLoot();
		TickLedger();
		if (!flag && send.Value && acceptedServer != null && token != null)
		{
			for (int num2 = 0; num2 < 8; num2++)
			{
				if (outgoing.Count <= 0)
				{
					break;
				}
				if (healthQueue.Count != 0)
				{
					break;
				}
				DeathReport deathReport = outgoing.Dequeue();
				deathReport.token = token;
				if (StoryModel.Valid(deathReport, world, ZNet.instance.GetTimeSeconds()))
				{
					acceptedServer.Invoke("JVStory_Death_v1", new object[1] { JsonConvert.SerializeObject((object)deathReport) });
				}
				else
				{
					dropped++;
				}
			}
		}
		string[] array2 = (from p in cache
			where now - p.Value.time > 30f
			select p.Key).ToArray();
		foreach (string key2 in array2)
		{
			cache.Remove(key2);
		}
		if (roster.Count > 4096)
		{
			array2 = roster.Keys.Take(roster.Count - 4096).ToArray();
			foreach (string key2 in array2)
			{
				roster.Remove(key2);
			}
		}
		if (flag && model != null)
		{
			if (now >= nextFlush)
			{
				nextFlush = now + 2f;
				Flush();
			}
			if (now >= nextSave)
			{
				nextSave = now + 15f;
				SaveCheckpoint(wait: false);
			}
			TickRetention();
		}
	}

	private void ReceiveHello(ZRpc rpc, int version)
	{
		try
		{
			ZNetPeer val = Peer(rpc);
			if (Server() && collect.Value && model != null && version >= 1 && version <= 5 && val != null && connections.TryGetValue(rpc, out var value) && !(Time.realtimeSinceStartup - value.lastHello < 3f))
			{
				value.lastHello = Time.realtimeSinceStartup;
				if (value.token == null)
				{
					value.token = Guid.NewGuid().ToString("N");
					value.activityAt = Time.realtimeSinceStartup;
				}
				value.version = version;
				rpc.Invoke("JVStory_Accept_v1", new object[1] { world + "|" + value.token + ((version >= 2) ? ("|" + version) : "") });
			}
		}
		catch
		{
			failures++;
		}
	}

	private void ReceiveAccept(ZRpc rpc, string value)
	{
		try
		{
			if (!Server() && send.Value && value != null && value.Length <= 80 && ZNet.instance.GetServerPeer() != null && ZNet.instance.GetServerPeer().m_rpc == rpc)
			{
				string[] array = value.Split(new char[1] { '|' });
				if ((array.Length == 2 || array.Length == 3) && !(array[0] != world) && Guid.TryParse(array[1], out var _) && (array.Length != 3 || !(array[2] != "2") || !(array[2] != "3") || !(array[2] != "4") || !(array[2] != "5")))
				{
					serverLoot = array.Length == 3 && array[2] == "5";
					lootSequence = 0L;
					lootQueue.Clear();
					ledgerQueue.Clear();
					ledgerSequence = 0L;
					serverWork = array.Length == 3 && (array[2] == "4" || array[2] == "5");
					workBuffer.Clear();
					serverMetrics = array.Length == 3 && (array[2] == "3" || array[2] == "4" || array[2] == "5");
					metricsSequence = 0L;
					metricBuffer.Clear();
					ResetSnapshots();
					statsView = null;
					pendingStatsRequest = null;
					nextStatsRequest = 0f;
					serverTelemetry = array.Length == 3;
					activityClock.Reset();
					nextActivity = Time.realtimeSinceStartup + 15f;
					sequence = 0L;
					acceptedServer = rpc;
					token = array[1];
					((BaseUnityPlugin)this).Logger.LogInfo((object)"Gameplay reporting accepted by the connected server.");
				}
			}
		}
		catch
		{
			failures++;
		}
	}

	private static ZNetPeer Peer(ZRpc rpc)
	{
		return ((Object)(object)ZNet.instance == (Object)null) ? null : ((IEnumerable<ZNetPeer>)ZNet.instance.GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer p) => p.IsReady() && p.m_rpc == rpc));
	}

	private void ReceiveReport(ZRpc rpc, string json)
	{
		try
		{
			ZNetPeer val = Peer(rpc);
			if (!Server() || !collect.Value || model == null || val == null || !connections.TryGetValue(rpc, out var value) || value.token == null)
			{
				rejected++;
				return;
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (realtimeSinceStartup - value.window >= 10f)
			{
				value.window = realtimeSinceStartup;
				value.received = 0;
			}
			if (++value.received > 512 || json == null || json.Length > 4096)
			{
				rejected++;
				return;
			}
			DeathReport deathReport = JsonConvert.DeserializeObject<DeathReport>(json, JsonSettings);
			if (!StoryModel.Valid(deathReport, world, ZNet.instance.GetTimeSeconds()) || deathReport.token != value.token)
			{
				rejected++;
			}
			else
			{
				Record(deathReport, val.m_uid, val.m_socket.GetHostName(), "client_owner_report; native entity ownership checked; not anti-cheat proof");
			}
		}
		catch
		{
			rejected++;
		}
	}

	private void Record(DeathReport report, long sender, string reporter, string evidence)
	{
		Entity victim = Lookup(report.victim);
		Entity attacker = Lookup(report.attacker);
		if (model.Record(report, sender, reporter, victim, attacker, evidence, DateTime.UtcNow))
		{
			accepted++;
		}
		else
		{
			rejected++;
		}
	}

	public static string Clean(string text)
	{
		if (text == null)
		{
			return null;
		}
		text = text.Replace("\r", " ").Replace("\n", " ").Replace("<", "")
			.Replace(">", "");
		return (text.Length <= 128) ? text : text.Substring(0, 128);
	}

	private Entity FromZdo(ZDO z)
	{
		//IL_01aa: Unknown result type (might be due to invalid IL or missing references)
		//IL_01b6: Unknown result type (might be due to invalid IL or missing references)
		GameObject prefab = ZNetScene.instance.GetPrefab(z.GetPrefab());
		if ((Object)(object)prefab == (Object)null || (Object)(object)prefab.GetComponent<Character>() == (Object)null)
		{
			return null;
		}
		Entity entity = new Entity();
		entity.boss = (Object)(object)prefab.GetComponent<Character>() != (Object)null && prefab.GetComponent<Character>().IsBoss();
		entity.id = ((object)Unsafe.As<ZDOID, ZDOID>(ref z.m_uid)/*cast due to .constrained prefix*/).ToString();
		entity.prefab = ((Object)prefab).name;
		entity.owner = z.GetOwner();
		entity.player = (Object)(object)prefab.GetComponent<Player>() != (Object)null;
		entity.level = z.GetInt("level", 1);
		Entity entity2 = entity;
		if (entity2.player && roster.TryGetValue(entity2.id, out var value))
		{
			entity2.account = value.account;
			entity2.name = value.name;
		}
		if (entity2.player)
		{
			ZNetPeer val = ((IEnumerable<ZNetPeer>)ZNet.instance.GetPeers()).FirstOrDefault((Func<ZNetPeer, bool>)((ZNetPeer candidate) => candidate.m_characterID == z.m_uid));
			if (val != null)
			{
				entity2.account = val.m_socket.GetHostName();
				entity2.name = Clean(val.m_playerName);
			}
			else if ((Object)(object)Player.m_localPlayer != (Object)null && ((Character)Player.m_localPlayer).GetZDOID() == z.m_uid)
			{
				entity2.account = "local-host";
				entity2.name = Clean(Player.m_localPlayer.GetPlayerName());
			}
		}
		return entity2;
	}

	private Entity Lookup(string id)
	{
		//IL_0041: Unknown result type (might be due to invalid IL or missing references)
		if (!StoryModel.Id(id))
		{
			return null;
		}
		string[] array = id.Split(new char[1] { ':' });
		ZDO zDO = ZDOMan.instance.GetZDO(new ZDOID(long.Parse(array[0]), uint.Parse(array[1])));
		if (zDO != null)
		{
			return FromZdo(zDO);
		}
		if (cache.TryGetValue(id, out var value) && Time.realtimeSinceStartup - value.time <= 30f)
		{
			return value.entity;
		}
		Entity value2;
		return roster.TryGetValue(id, out value2) ? value2 : null;
	}

	private static void BeforeDestroyed(ZDOID __0)
	{
		//IL_0038: Unknown result type (might be due to invalid IL or missing references)
		Plugin instance = Instance;
		if ((Object)(object)instance == (Object)null || !Server() || instance.model == null)
		{
			return;
		}
		try
		{
			ZDO zDO = ZDOMan.instance.GetZDO(__0);
			if (zDO == null)
			{
				return;
			}
			instance.RememberLootOwner(zDO);
			Entity entity = instance.FromZdo(zDO);
			if (entity != null)
			{
				if (instance.cache.Count >= 4096)
				{
					instance.dropped++;
					return;
				}
				instance.cache[entity.id] = new Cached
				{
					entity = entity,
					time = Time.realtimeSinceStartup
				};
			}
		}
		catch
		{
			instance.failures++;
		}
	}

	private static void BeforeDeath(Character __instance, out Capture __state)
	{
		//IL_00e1: Unknown result type (might be due to invalid IL or missing references)
		//IL_00e6: Unknown result type (might be due to invalid IL or missing references)
		//IL_0154: Unknown result type (might be due to invalid IL or missing references)
		//IL_0159: Unknown result type (might be due to invalid IL or missing references)
		//IL_0185: Unknown result type (might be due to invalid IL or missing references)
		__state = null;
		Plugin instance = Instance;
		if ((Object)(object)instance == (Object)null || !instance.hooksReady || (Object)(object)ZNet.instance == (Object)null || instance.world == null || (!Server() && (!instance.send.Value || instance.token == null)) || (Server() && (!instance.collect.Value || instance.model == null)))
		{
			return;
		}
		try
		{
			ZNetView component = ((Component)__instance).GetComponent<ZNetView>();
			if ((Object)(object)component == (Object)null || !component.IsOwner() || component.GetZDO() == null || __instance.GetHealth() > 0f)
			{
				return;
			}
			ZDO zDO = component.GetZDO();
			object? value = LastHit.GetValue(__instance);
			HitData val = (HitData)((value is HitData) ? value : null);
			Vector3 position = ((Component)__instance).transform.position;
			__state = new Capture
			{
				zdo = zDO,
				owner = zDO.GetOwner(),
				report = new DeathReport
				{
					id = Guid.NewGuid().ToString(),
					world = instance.world,
					victim = ((object)Unsafe.As<ZDOID, ZDOID>(ref zDO.m_uid)/*cast due to .constrained prefix*/).ToString(),
					attacker = ((val == null || val.m_attacker == ZDOID.None) ? null : ((object)Unsafe.As<ZDOID, ZDOID>(ref val.m_attacker)/*cast due to .constrained prefix*/).ToString()),
					cause = ((val == null) ? "unknown" : ((object)val.m_hitType).ToString()),
					player = __instance.IsPlayer(),
					gameTime = ZNet.instance.GetTimeSeconds(),
					x = position.x,
					y = position.y,
					z = position.z
				}
			};
			instance.AttributeDotDeath(__instance, zDO, val, __state.report);
			if (Server())
			{
				Entity entity = instance.FromZdo(zDO);
				if (entity != null)
				{
					instance.cache[entity.id] = new Cached
					{
						entity = entity,
						time = Time.realtimeSinceStartup
					};
				}
			}
		}
		catch
		{
			instance.failures++;
			__state = null;
		}
	}

	private static void AfterDeath(Capture __state, bool __runOriginal)
	{
		Plugin instance = Instance;
		if ((Object)(object)instance == (Object)null || __state == null || !__runOriginal)
		{
			return;
		}
		try
		{
			DeathReport report = __state.report;
			if ((!report.player || __state.zdo.GetBool(ZDOVars.s_dead, false)) && instance.emitted.Add(report.victim))
			{
				if (instance.emitted.Count > 8192)
				{
					instance.emitted.Clear();
					instance.emitted.Add(report.victim);
				}
				if (Server())
				{
					instance.Record(report, __state.owner, "server", "server_owner_observation");
				}
				else if (instance.outgoing.Count < 256)
				{
					instance.outgoing.Enqueue(report);
				}
				else
				{
					instance.dropped++;
				}
			}
		}
		catch
		{
			instance.failures++;
		}
	}

	private void WriteEvent(StoryEvent e)
	{
		e.collectorRun = collectorRun;
		e.producerVersion = "0.4.9";
		if (telemetry != null && (e.kind == "player_death" || e.kind == "creature_death"))
		{
			e.damage = telemetry.Death(e);
		}
		ObserveHistory(e);
		ObserveStoryWork(e);
		Queue<string> queue = (Routine(e) ? detailLines : lines);
		if (queue.Count >= 2048)
		{
			dropped++;
		}
		else
		{
			queue.Enqueue(JsonConvert.SerializeObject((object)e, WriteSettings));
		}
	}

	private void Flush()
	{
		FlushQueue(lines, "events");
		FlushQueue(detailLines, "detail");
	}

	private void Atomic(string name, object data)
	{
		string text = Path.Combine(directory, name);
		string text2 = text + ".tmp";
		File.WriteAllText(text2, JsonConvert.SerializeObject(data, (Formatting)1, WriteSettings));
		if (File.Exists(text))
		{
			File.Replace(text2, text, null);
		}
		else
		{
			File.Move(text2, text);
		}
	}

	private void CompleteCheckpoint(bool wait)
	{
		if (checkpointWriter.Complete(wait, out var checkpoint, out var error))
		{
			if (error == null)
			{
				savedCheckpointUtc = checkpoint;
				return;
			}
			failures++;
			((BaseUnityPlugin)this).Logger.LogWarning((object)("Story checkpoint failed: " + error.Message));
		}
	}

	private void Save()
	{
		SaveCheckpoint(wait: true);
	}

	private void SaveCheckpoint(bool wait)
	{
		if (model == null)
		{
			return;
		}
		CompleteCheckpoint(wait);
		if (checkpointWriter.Pending)
		{
			return;
		}
		try
		{
			Stopwatch stopwatch = Stopwatch.StartNew();
			Dictionary<string, object> dictionary = new Dictionary<string, object>();
			dictionary.Add(world + "-state.json", CheckpointCopy.Capture(model.state));
			if (lootModel != null)
			{
				lootModel.Drain(ZNet.instance.GetTimeSeconds(), force: false);
				lootModel.Trim(ZNet.instance.GetTimeSeconds());
				dictionary.Add(world + "-loot.json", CheckpointCopy.Capture(lootModel.state));
			}
			if (workModel != null)
			{
				workModel.state.checkpointUtc = DateTime.UtcNow.ToString("o");
				dictionary.Add(world + "-work.json", CheckpointCopy.Capture(workModel.state));
			}
			if (telemetry != null)
			{
				if (telemetry.state.history != null)
				{
					History.Prune(telemetry.state.history, DateTime.UtcNow, historyDays.Value, historyMonths.Value);
				}
				dictionary.Add(world + "-stats.json", CheckpointCopy.Capture(telemetry.state));
			}
			DateTime utcNow = DateTime.UtcNow;
			checkpointCaptureMs = stopwatch.Elapsed.TotalMilliseconds;
			dictionary.Add("status.json", new
			{
				checkpointCaptureMs = checkpointCaptureMs,
				utc = DateTime.UtcNow.ToString("o"),
				world = world,
				version = "0.4.9",
				hooksReady = hooksReady,
				dotHooksReady = dotHooksReady,
				ledgerHooksReady = ledgerHooksReady,
				ledgerAccepted = ledgerAccepted,
				ledgerDropped = ledgerDropped,
				tidyLedger = tidyLedger,
				bestLedger = bestLedger,
				craftingStorageLedger = craftingStorageLedger,
				lootHooksReady = lootHooksReady,
				lootAccepted = lootAccepted,
				lootDropped = lootDropped,
				lootConnections = connections.Values.Count((Connection c) => c.token != null && c.version >= 5),
				accepted = accepted,
				rejected = rejected,
				dropped = dropped,
				failures = failures,
				healthAccepted = healthAccepted,
				activityAccepted = activityAccepted,
				metricsAccepted = metricsAccepted,
				snapshotsAccepted = snapshotsAccepted,
				statsUiReady = statsUiReady,
				retentionBlockedReason = ((telemetry == null) ? null : telemetry.state.retentionBlockedReason),
				worldName = ZNet.instance.GetWorldName(),
				nativeMetricHooksReady = nativeMetricHooksReady,
				collectorRun = collectorRun,
				queuedHealth = healthQueue.Count,
				queuedActions = actionQueue.Count,
				queuedMetrics = metricBuffer.Count,
				metricBufferDropped = metricBuffer.dropped,
				reportingConnections = connections.Values.Count((Connection c) => c.token != null),
				enhancedConnections = connections.Values.Count((Connection c) => c.token != null && c.version >= 2),
				metricsConnections = connections.Values.Count((Connection c) => c.token != null && c.version >= 3),
				queuedRecords = lines.Count,
				queuedDetails = detailLines.Count,
				workAccepted = workAccepted,
				workSummaries = workSummaries,
				workConnections = connections.Values.Count((Connection c) => c.token != null && c.version >= 4),
				workBufferDropped = workBuffer.dropped,
				limitations = "Owner-reported observations are not anti-cheat proof. Damage percentages cover recorded encounter damage only, not guaranteed full coverage. AFK is a client input heuristic; native counters are separate from verified combat. No automatic announcements or roasts."
			});
			checkpointWriter.Start(directory, dictionary, utcNow);
			if (wait)
			{
				CompleteCheckpoint(wait: true);
			}
		}
		catch (Exception ex)
		{
			failures++;
			((BaseUnityPlugin)this).Logger.LogWarning((object)("Story checkpoint capture failed: " + ex.Message));
		}
	}

	private void SetupTelemetry()
	{
		//IL_002e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0038: Expected O, but got Unknown
		//IL_0069: Unknown result type (might be due to invalid IL or missing references)
		//IL_0080: Unknown result type (might be due to invalid IL or missing references)
		//IL_008b: Expected O, but got Unknown
		//IL_008b: Expected O, but got Unknown
		//IL_00b8: Unknown result type (might be due to invalid IL or missing references)
		//IL_00cd: Unknown result type (might be due to invalid IL or missing references)
		//IL_00da: Expected O, but got Unknown
		//IL_00da: Expected O, but got Unknown
		//IL_0108: Unknown result type (might be due to invalid IL or missing references)
		//IL_0115: Expected O, but got Unknown
		//IL_0143: Unknown result type (might be due to invalid IL or missing references)
		//IL_0150: Expected O, but got Unknown
		//IL_017d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0192: Unknown result type (might be due to invalid IL or missing references)
		//IL_019f: Expected O, but got Unknown
		//IL_019f: Expected O, but got Unknown
		//IL_01cc: Unknown result type (might be due to invalid IL or missing references)
		//IL_01e3: Unknown result type (might be due to invalid IL or missing references)
		//IL_01ee: Expected O, but got Unknown
		//IL_01ee: Expected O, but got Unknown
		afkSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("Reporting", "AFK after seconds without input", 300, new ConfigDescription("Informational only. No kicks. Counts time after this threshold as AFK; previous idle grace time remains active. No key contents are recorded.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(60, 3600), new object[0]));
		harmony.Patch((MethodBase)AccessTools.Method(typeof(Character), "ApplyDamage", (Type[])null, (Type[])null), new HarmonyMethod(typeof(Plugin), "EnterDamage", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(Plugin), "LeaveDamage", (Type[])null), (HarmonyMethod)null);
		harmony.Patch((MethodBase)AccessTools.Method(typeof(Character), "SetHealth", (Type[])null, (Type[])null), new HarmonyMethod(typeof(Plugin), "BeforeHealth", (Type[])null), new HarmonyMethod(typeof(Plugin), "AfterHealth", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
		harmony.Patch((MethodBase)AccessTools.Method(typeof(Player), "StartEmote", (Type[])null, (Type[])null), (HarmonyMethod)null, new HarmonyMethod(typeof(Plugin), "AfterEmote", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
		harmony.Patch((MethodBase)AccessTools.Method(typeof(Player), "PlacePiece", (Type[])null, (Type[])null), (HarmonyMethod)null, new HarmonyMethod(typeof(Plugin), "AfterPlace", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
		harmony.Patch((MethodBase)AccessTools.Method(typeof(ZDOMan), "HandleDestroyedZDO", (Type[])null, (Type[])null), new HarmonyMethod(typeof(Plugin), "BeforeObjectRemoval", (Type[])null), new HarmonyMethod(typeof(Plugin), "AfterObjectRemoval", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
		harmony.Patch((MethodBase)AccessTools.DeclaredMethod(typeof(Character), "SetMaxHealth", (Type[])null, (Type[])null), new HarmonyMethod(typeof(Plugin), "EnterCapacity", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(Plugin), "LeaveCapacity", (Type[])null), (HarmonyMethod)null);
		SetupPlayerMetrics();
		SetupDotAttribution();
	}

	private void ResetTelemetry()
	{
		ResetLoot();
		ResetLedger();
		healthQueue.Clear();
		actionQueue.Clear();
		damageContext.Clear();
		capacityContext.Clear();
		dotHitContext.Clear();
		dotSources.Clear();
		dotTicks.Clear();
		lethalDots.Clear();
		activityClock.Reset();
		serverTelemetry = false;
		telemetry = null;
		sequence = 0L;
		metricBuffer.Clear();
		serverMetrics = false;
		metricsSequence = 0L;
		metricsCharacter = null;
		workBuffer.Clear();
		serverWork = false;
		workModel = null;
		ResetSnapshots();
		ResetStatsUI();
	}

	private void LoadTelemetry()
	{
		TelemetryState telemetryState = null;
		string text = Path.Combine(directory, world + "-stats.json");
		if (File.Exists(text))
		{
			try
			{
				telemetryState = JsonConvert.DeserializeObject<TelemetryState>(File.ReadAllText(text), JsonSettings);
			}
			catch
			{
				failures++;
				File.Copy(text, text + ".unreadable-" + Guid.NewGuid().ToString("N"));
			}
		}
		bool flag = Directory.GetFiles(directory, world + "-events*.jsonl").Length > 0 || Directory.GetFiles(directory, world + "-detail*.jsonl").Length > 0 || Directory.GetFiles(directory, world + "-*.jsonl.gz").Length > 0;
		bool flag2 = telemetryState != null && telemetryState.world == world && telemetryState.players != null;
		telemetry = new TelemetryModel(world, telemetryState);
		if (!flag2 && flag)
		{
			telemetry.state.retentionBlockedReason = "Previous records exist without a readable matching cumulative stats file. Retention paused pending history recovery; do not discard retained logs.";
		}
		if (telemetry.state.legacyImport == null)
		{
			try
			{
				TelemetryState telemetryState2 = LegacyBackfill.Apply(telemetry.state, Directory.GetFiles(directory, world + "-events*.jsonl"), DateTime.UtcNow);
				Atomic(world + "-stats.json", telemetryState2);
				telemetry.state = telemetryState2;
				LegacyImport legacyImport = telemetry.state.legacyImport;
				((BaseUnityPlugin)this).Logger.LogInfo((object)("Historical 0.1 import complete: " + legacyImport.events + " death events; " + legacyImport.duplicates + " duplicates skipped; " + legacyImport.unidentifiedPlayerRoles + " unidentified player roles. Other history remains unavailable."));
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("Historical 0.1 import deferred; existing totals preserved. " + ex.Message));
			}
		}
	}

	private bool Observing()
	{
		return hooksReady && world != null && (Object)(object)ZNet.instance != (Object)null && ((!Server()) ? (send.Value && token != null && serverTelemetry) : (collect.Value && telemetry != null));
	}

	private static void EnterDamage(Character __instance, HitData __0, out DamageContext __state)
	{
		__state = null;
		Plugin instance = Instance;
		if ((Object)(object)instance == (Object)null || !instance.Observing())
		{
			return;
		}
		try
		{
			instance.damageContext.TryGetValue(__instance, out var value);
			__state = new DamageContext
			{
				entered = true,
				previous = value
			};
			instance.damageContext[__instance] = __0;
		}
		catch
		{
			instance.failures++;
		}
	}

	private static void LeaveDamage(Character __instance, DamageContext __state)
	{
		Plugin instance = Instance;
		if (!((Object)(object)instance == (Object)null) && __state != null && __state.entered)
		{
			if (__state.previous == null)
			{
				instance.damageContext.Remove(__instance);
			}
			else
			{
				instance.damageContext[__instance] = __state.previous;
			}
		}
	}

	private static void BeforeHealth(Character __instance, out HealthCapture __state)
	{
		//IL_00fa: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ff: Unknown result type (might be due to invalid IL or missing references)
		//IL_012b: Unknown result type (might be due to invalid IL or missing references)
		__state = null;
		Plugin instance = Instance;
		if ((Object)(object)instance == (Object)null || !instance.Observing())
		{
			return;
		}
		try
		{
			ZNetView component = ((Component)__instance).GetComponent<ZNetView>();
			if (!((Object)(object)component == (Object)null) && component.IsOwner() && component.GetZDO() != null)
			{
				ZDO zDO = component.GetZDO();
				instance.damageContext.TryGetValue(__instance, out var value);
				__state = new HealthCapture
				{
					zdo = zDO,
					owner = zDO.GetOwner(),
					report = new HealthReport
					{
						victim = ((object)Unsafe.As<ZDOID, ZDOID>(ref zDO.m_uid)/*cast due to .constrained prefix*/).ToString(),
						player = __instance.IsPlayer(),
						before = Math.Max(0f, __instance.GetHealth()),
						maximum = __instance.GetMaxHealth(),
						gameTime = ZNet.instance.GetTimeSeconds(),
						attacker = ((value == null || value.m_attacker == ZDOID.None) ? null : ((object)Unsafe.As<ZDOID, ZDOID>(ref value.m_attacker)/*cast due to .constrained prefix*/).ToString()),
						cause = ((value != null) ? ((object)value.m_hitType).ToString() : (instance.capacityContext.ContainsKey(__instance) ? "maximum_health_clamp" : "unattributed_health_change"))
					}
				};
			}
		}
		catch
		{
			instance.failures++;
		}
	}

	private static void AfterHealth(Character __instance, HealthCapture __state)
	{
		Plugin instance = Instance;
		if ((Object)(object)instance == (Object)null || __state == null)
		{
			return;
		}
		try
		{
			HealthReport report = __state.report;
			instance.AttributeDot(__instance, report);
			report.after = Math.Max(0f, __instance.GetHealth());
			instance.CaptureLethalDot(__instance, __state);
			if (report.before != report.after && TelemetryModel.Valid(report, ZNet.instance.GetTimeSeconds()))
			{
				if (Server())
				{
					instance.RecordHealth(report, __state.owner, "server");
				}
				else if (instance.healthQueue.Count < 1024)
				{
					instance.healthQueue.Enqueue(report);
				}
				else
				{
					instance.dropped++;
				}
			}
		}
		catch
		{
			instance.failures++;
		}
	}

	private void RecordHealth(HealthReport r, long owner, string reporter)
	{
		Entity victim = Lookup(r.victim);
		Entity attacker = Lookup(r.attacker);
		if (telemetry == null || !TelemetryModel.Authorized(r, owner, victim) || model.state.deathIds.ContainsKey(r.victim))
		{
			rejected++;
			return;
		}
		telemetry.Health(r, victim, attacker);
		healthAccepted++;
		if (!HealthCapacity.IsClamp(r))
		{
			WriteEvent(new StoryEvent
			{
				id = Guid.NewGuid().ToString(),
				world = world,
				observedUtc = DateTime.UtcNow.ToString("o"),
				kind = "health_change",
				gameTime = r.gameTime,
				victim = victim,
				attacker = attacker,
				cause = r.cause,
				health = r,
				reporter = reporter,
				evidence = "owner-observed resulting HP; not anti-cheat proof"
			});
		}
	}

	private void SampleActivity()
	{
		//IL_001c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0021: Unknown result type (might be due to invalid IL or missing references)
		//IL_0072: Unknown result type (might be due to invalid IL or missing references)
		//IL_0077: Unknown result type (might be due to invalid IL or missing references)
		//IL_0086: Unknown result type (might be due to invalid IL or missing references)
		//IL_008b: Unknown result type (might be due to invalid IL or missing references)
		try
		{
			if (!Observing() || (Object)(object)Player.m_localPlayer == (Object)null || ((Character)Player.m_localPlayer).GetZDOID() == ZDOID.None || ZInput.instance == null)
			{
				activityClock.Reset();
				return;
			}
			DateTime dateTime = ZInput.instance.GetLastInputTimer();
			int num;
			if (Application.isFocused)
			{
				if (!(dateTime != lastInputTimer))
				{
					Vector2 val = ZInput.GetJoyLeftStick();
					if (!(((Vector2)(ref val)).sqrMagnitude > 0.04f))
					{
						val = ZInput.GetJoyRightStick();
						num = ((((Vector2)(ref val)).sqrMagnitude > 0.04f) ? 1 : 0);
						goto IL_00a1;
					}
				}
				num = 1;
			}
			else
			{
				num = 0;
			}
			goto IL_00a1;
			IL_00a1:
			bool input = (byte)num != 0;
			lastInputTimer = dateTime;
			activityClock.Sample(Time.realtimeSinceStartup, input, afkSeconds.Value);
		}
		catch
		{
			activityClock.Reset();
		}
	}

	private void SendTelemetry()
	{
		//IL_0056: Unknown result type (might be due to invalid IL or missing references)
		//IL_005b: Unknown result type (might be due to invalid IL or missing references)
		if (!Observing())
		{
			return;
		}
		ActivityReport activityReport = null;
		if (Time.realtimeSinceStartup >= nextActivity && (Object)(object)Player.m_localPlayer != (Object)null)
		{
			nextActivity = Time.realtimeSinceStartup + 15f;
			activityReport = activityClock.Drain(((object)((Character)Player.m_localPlayer).GetZDOID()/*cast due to .constrained prefix*/).ToString());
		}
		if (Server())
		{
			if (activityReport != null && TelemetryModel.Valid(activityReport))
			{
				RecordActivity(Lookup(activityReport.player), activityReport, "local-host", null);
			}
			return;
		}
		for (int i = 0; i < 4; i++)
		{
			if (healthQueue.Count <= 0 && actionQueue.Count <= 0 && activityReport == null)
			{
				break;
			}
			List<HealthReport> list = new List<HealthReport>();
			while (healthQueue.Count > 0 && list.Count < 32)
			{
				HealthReport healthReport = healthQueue.Dequeue();
				if (TelemetryModel.Valid(healthReport, ZNet.instance.GetTimeSeconds()))
				{
					list.Add(healthReport);
				}
				else
				{
					dropped++;
				}
			}
			if (activityReport != null && !TelemetryModel.Valid(activityReport))
			{
				activityReport = null;
				dropped++;
			}
			List<ActionReport> list2 = new List<ActionReport>();
			while (actionQueue.Count > 0 && list2.Count < 16)
			{
				list2.Add(actionQueue.Dequeue());
			}
			TelemetryBatch telemetryBatch = new TelemetryBatch();
			telemetryBatch.world = world;
			telemetryBatch.token = token;
			telemetryBatch.sequence = ++sequence;
			telemetryBatch.health = list.ToArray();
			telemetryBatch.activity = activityReport;
			telemetryBatch.actions = list2.ToArray();
			TelemetryBatch telemetryBatch2 = telemetryBatch;
			acceptedServer.Invoke("JVStory_Telemetry_v2", new object[1] { JsonConvert.SerializeObject((object)telemetryBatch2) });
			activityReport = null;
		}
	}

	private void ReceiveTelemetry(ZRpc rpc, string json)
	{
		try
		{
			ZNetPeer val = Peer(rpc);
			if (!Server() || !collect.Value || telemetry == null || val == null || !connections.TryGetValue(rpc, out var value) || value.token == null || value.version < 2 || json == null || json.Length > 24576)
			{
				rejected++;
				return;
			}
			TelemetryBatch telemetryBatch = JsonConvert.DeserializeObject<TelemetryBatch>(json, JsonSettings);
			if (telemetryBatch == null || telemetryBatch.protocol != 2 || telemetryBatch.world != world || telemetryBatch.token != value.token || telemetryBatch.sequence <= value.sequence || telemetryBatch.health == null || telemetryBatch.health.Length > 32 || (telemetryBatch.actions != null && telemetryBatch.actions.Length > 16))
			{
				rejected++;
				return;
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (realtimeSinceStartup - value.window >= 10f)
			{
				value.window = realtimeSinceStartup;
				value.received = 0;
			}
			value.received += Math.Max(1, telemetryBatch.health.Length + ((telemetryBatch.actions != null) ? telemetryBatch.actions.Length : 0));
			if (value.received > 512)
			{
				rejected++;
				return;
			}
			value.sequence = telemetryBatch.sequence;
			HealthReport[] health = telemetryBatch.health;
			foreach (HealthReport r in health)
			{
				if (TelemetryModel.Valid(r, ZNet.instance.GetTimeSeconds()))
				{
					RecordHealth(r, val.m_uid, val.m_socket.GetHostName());
				}
				else
				{
					rejected++;
				}
			}
			ActionReport[] array = telemetryBatch.actions ?? new ActionReport[0];
			foreach (ActionReport actionReport in array)
			{
				Entity entity = ((actionReport == null) ? null : Lookup(actionReport.player));
				if (!TelemetryModel.Valid(actionReport, ZNet.instance.GetTimeSeconds()) || actionReport.player != ((object)Unsafe.As<ZDOID, ZDOID>(ref val.m_characterID)/*cast due to .constrained prefix*/).ToString() || entity == null || !entity.player || entity.owner != val.m_uid || string.IsNullOrEmpty(entity.account))
				{
					rejected++;
				}
				else
				{
					RecordAction(entity, actionReport, val.m_socket.GetHostName());
				}
			}
			if (telemetryBatch.activity != null)
			{
				ActivityReport activity = telemetryBatch.activity;
				Entity entity = Lookup(activity.player);
				if (!TelemetryModel.Valid(activity) || activity.player != ((object)Unsafe.As<ZDOID, ZDOID>(ref val.m_characterID)/*cast due to .constrained prefix*/).ToString() || entity == null || !entity.player || entity.owner != val.m_uid || string.IsNullOrEmpty(entity.account) || activity.activeSeconds + activity.afkSeconds + activity.unobservedSeconds > (double)(Math.Max(0f, realtimeSinceStartup - value.activityAt) + 2f))
				{
					rejected++;
					return;
				}
				value.activityAt = realtimeSinceStartup;
				RecordActivity(entity, activity, val.m_socket.GetHostName(), value);
			}
		}
		catch
		{
			rejected++;
		}
	}

	private void RecordActivity(Entity player, ActivityReport activity, string reporter, Connection connection)
	{
		if (player == null || !player.player || string.IsNullOrEmpty(player.account))
		{
			return;
		}
		telemetry.Activity(player, activity);
		activityAccepted++;
		WriteEvent(new StoryEvent
		{
			id = Guid.NewGuid().ToString(),
			world = world,
			kind = "activity",
			observedUtc = DateTime.UtcNow.ToString("o"),
			gameTime = ZNet.instance.GetTimeSeconds(),
			victim = player,
			activity = activity,
			reporter = reporter,
			evidence = "client input-based AFK estimate; unreported time is unknown"
		});
		if (connection != null)
		{
			if (connection.afk != activity.afk && (connection.afk.HasValue || activity.afk))
			{
				WriteEvent(new StoryEvent
				{
					id = Guid.NewGuid().ToString(),
					world = world,
					kind = (activity.afk ? "afk_start" : "afk_end"),
					observedUtc = DateTime.UtcNow.ToString("o"),
					gameTime = ZNet.instance.GetTimeSeconds(),
					victim = player,
					evidence = "client input estimate; observed on 15-second reporting interval"
				});
			}
			connection.afk = activity.afk;
		}
	}

	private static void AfterEmote(Player __instance, string __0, bool __result)
	{
		//IL_005e: Unknown result type (might be due to invalid IL or missing references)
		Plugin instance = Instance;
		if ((Object)(object)instance == (Object)null || !__result || (Object)(object)__instance != (Object)(object)Player.m_localPlayer || !instance.Observing())
		{
			return;
		}
		try
		{
			instance.activityClock.Sample(Time.realtimeSinceStartup, input: true, instance.afkSeconds.Value);
			instance.LocalAction(__instance, "emote", __0, ((Component)__instance).transform.position);
		}
		catch
		{
			instance.failures++;
		}
	}

	private static void AfterPlace(Player __instance, Piece __0, Vector3 __1, bool __runOriginal)
	{
		//IL_004f: Unknown result type (might be due to invalid IL or missing references)
		Plugin instance = Instance;
		if ((Object)(object)instance == (Object)null || !__runOriginal || (Object)(object)__instance != (Object)(object)Player.m_localPlayer || !instance.Observing() || (Object)(object)__0 == (Object)null)
		{
			return;
		}
		try
		{
			instance.LocalAction(__instance, "piece_placed", ((Object)((Component)__0).gameObject).name, __1);
		}
		catch
		{
			instance.failures++;
		}
	}

	private void LocalAction(Player player, string kind, string name, Vector3 position)
	{
		//IL_0009: 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)
		ActionReport actionReport = new ActionReport();
		actionReport.player = ((object)((Character)player).GetZDOID()/*cast due to .constrained prefix*/).ToString();
		actionReport.kind = kind;
		actionReport.name = name;
		actionReport.gameTime = ZNet.instance.GetTimeSeconds();
		actionReport.x = position.x;
		actionReport.y = position.y;
		actionReport.z = position.z;
		ActionReport actionReport2 = actionReport;
		if (!TelemetryModel.Valid(actionReport2, actionReport2.gameTime))
		{
			dropped++;
		}
		else if (Server())
		{
			RecordAction(Lookup(actionReport2.player), actionReport2, "local-host");
		}
		else if (actionQueue.Count < 256)
		{
			actionQueue.Enqueue(actionReport2);
		}
		else
		{
			dropped++;
		}
	}

	private void RecordAction(Entity player, ActionReport r, string reporter)
	{
		if (player != null && player.player && !string.IsNullOrEmpty(player.account))
		{
			telemetry.Action(player, r);
			WriteEvent(new StoryEvent
			{
				id = Guid.NewGuid().ToString(),
				world = world,
				kind = r.kind,
				observedUtc = DateTime.UtcNow.ToString("o"),
				gameTime = r.gameTime,
				victim = player,
				action = r,
				x = r.x,
				y = r.y,
				z = r.z,
				reporter = reporter,
				evidence = "local-player report of completed game action; not anti-cheat proof"
			});
		}
	}

	private unsafe static void BeforeObjectRemoval(ZDOID __0, out RemovalCapture __state)
	{
		//IL_0035: Unknown result type (might be due to invalid IL or missing references)
		//IL_00c7: Unknown result type (might be due to invalid IL or missing references)
		//IL_00cc: Unknown result type (might be due to invalid IL or missing references)
		//IL_00d8: Unknown result type (might be due to invalid IL or missing references)
		//IL_00d9: Unknown result type (might be due to invalid IL or missing references)
		__state = null;
		Plugin instance = Instance;
		if ((Object)(object)instance == (Object)null || !Server() || !instance.Observing())
		{
			return;
		}
		try
		{
			ZDO zDO = ZDOMan.instance.GetZDO(__0);
			if (zDO == null)
			{
				return;
			}
			GameObject prefab = ZNetScene.instance.GetPrefab(zDO.GetPrefab());
			if (!((Object)(object)prefab == (Object)null) && !((Object)(object)prefab.GetComponent<Character>() != (Object)null))
			{
				bool flag = (Object)(object)prefab.GetComponentInChildren<Ship>() != (Object)null;
				if (flag || (!((Object)(object)prefab.GetComponent<Piece>() == (Object)null) && !((Object)(object)prefab.GetComponent<WearNTear>() == (Object)null)))
				{
					Vector3 position = zDO.GetPosition();
					__state = new RemovalCapture
					{
						id = __0,
						record = new StoryEvent
						{
							id = Guid.NewGuid().ToString(),
							world = instance.world,
							kind = (flag ? "boat_removed" : "structure_removed"),
							observedUtc = DateTime.UtcNow.ToString("o"),
							gameTime = ZNet.instance.GetTimeSeconds(),
							x = position.x,
							y = position.y,
							z = position.z,
							removedObject = new RemovedObject
							{
								id = ((object)(*(ZDOID*)(&__0))/*cast due to .constrained prefix*/).ToString(),
								prefab = ((Object)prefab).name,
								category = (flag ? "boat" : "structure")
							},
							evidence = "server observed permanent network-object removal; no attacker or reason inferred"
						}
					};
				}
			}
		}
		catch
		{
			instance.failures++;
		}
	}

	private static void AfterObjectRemoval(RemovalCapture __state)
	{
		//IL_0028: Unknown result type (might be due to invalid IL or missing references)
		Plugin instance = Instance;
		if ((Object)(object)instance == (Object)null || __state == null)
		{
			return;
		}
		try
		{
			if (ZDOMan.instance.GetZDO(__state.id) == null)
			{
				instance.WriteEvent(__state.record);
			}
		}
		catch
		{
			instance.failures++;
		}
	}

	private void SetupPlayerMetrics()
	{
		//IL_0007: Unknown result type (might be due to invalid IL or missing references)
		//IL_0011: Expected O, but got Unknown
		//IL_007d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0092: Unknown result type (might be due to invalid IL or missing references)
		//IL_009f: Expected O, but got Unknown
		//IL_009f: Expected O, but got Unknown
		//IL_00f6: Unknown result type (might be due to invalid IL or missing references)
		//IL_010b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0118: Expected O, but got Unknown
		//IL_0118: Expected O, but got Unknown
		metricHarmony = new Harmony("JacobsValheim.VikingStoryteller.NativeMetrics");
		try
		{
			if (RawProfileStats == null)
			{
				throw new Exception("Missing raw profile stats field");
			}
			string[] array = new string[2] { "IncrementStat", "SetStat" };
			foreach (string text in array)
			{
				metricHarmony.Patch((MethodBase)AccessTools.Method(typeof(PlayerProfile), text, (Type[])null, (Type[])null), new HarmonyMethod(typeof(Plugin), "BeforeNativeStat", (Type[])null), new HarmonyMethod(typeof(Plugin), "AfterNativeStat", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			}
			foreach (string key in NamedStatFields.Keys)
			{
				metricHarmony.Patch((MethodBase)AccessTools.Method(typeof(PlayerProfile), key, (Type[])null, (Type[])null), new HarmonyMethod(typeof(Plugin), "BeforeNamedStat", (Type[])null), new HarmonyMethod(typeof(Plugin), "AfterNamedStat", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			}
			nativeMetricHooksReady = true;
		}
		catch (Exception ex)
		{
			metricHarmony.UnpatchSelf();
			failures++;
			((BaseUnityPlugin)this).Logger.LogWarning((object)("Native statistic observation unavailable; combat reporting remains enabled: " + ex.Message));
		}
	}

	private object RawStats(PlayerProfile profile)
	{
		return (!(RawProfileStats.GetValue(profile) is Array { Length: not 0 } array)) ? null : array.GetValue(0);
	}

	private object RawField(object raw, string name)
	{
		if (!rawStatFields.TryGetValue(name, out var value))
		{
			value = AccessTools.Field(raw.GetType(), name);
			if (value == null)
			{
				throw new Exception("Missing raw statistic field: " + name);
			}
			rawStatFields[name] = value;
		}
		return value.GetValue(raw);
	}

	private bool LocalProfile(PlayerProfile profile)
	{
		return nativeMetricHooksReady && Observing() && (Server() || serverMetrics) && (Object)(object)Player.m_localPlayer != (Object)null && (Object)(object)Game.instance != (Object)null && object.ReferenceEquals(Game.instance.GetPlayerProfile(), profile);
	}

	private double NativeValue(PlayerProfile profile, PlayerStatType type)
	{
		//IL_0037: Unknown result type (might be due to invalid IL or missing references)
		object obj = RawStats(profile);
		if (obj == null)
		{
			throw new Exception("Raw stats unavailable");
		}
		float value;
		return (RawField(obj, "m_stats") is Dictionary<PlayerStatType, float> dictionary && dictionary.TryGetValue(type, out value)) ? value : 0f;
	}

	private double NamedValue(PlayerProfile profile, string method, string name)
	{
		object obj = RawStats(profile);
		if (obj == null)
		{
			throw new Exception("Raw stats unavailable");
		}
		object obj2 = RawField(obj, NamedStatFields[method]);
		if (method == "IncrementStatEnemy")
		{
			obj2 = ((Array)obj2).GetValue(0);
		}
		float value;
		return (obj2 is Dictionary<string, float> dictionary && dictionary.TryGetValue(name, out value)) ? value : 0f;
	}

	private static void BeforeNativeStat(PlayerProfile __instance, PlayerStatType __0, MethodBase __originalMethod, out MetricCapture __state)
	{
		//IL_002d: Unknown result type (might be due to invalid IL or missing references)
		//IL_005c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0076: Unknown result type (might be due to invalid IL or missing references)
		//IL_0090: Unknown result type (might be due to invalid IL or missing references)
		//IL_00a9: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ca: Unknown result type (might be due to invalid IL or missing references)
		//IL_00cf: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ee: Unknown result type (might be due to invalid IL or missing references)
		__state = null;
		Plugin instance = Instance;
		if ((Object)(object)instance == (Object)null)
		{
			return;
		}
		try
		{
			if (instance.LocalProfile(__instance) && instance.nativeStatNames.Contains(((object)__0).ToString()))
			{
				__state = new MetricCapture
				{
					x = ((Component)Player.m_localPlayer).transform.position.x,
					y = ((Component)Player.m_localPlayer).transform.position.y,
					z = ((Component)Player.m_localPlayer).transform.position.z,
					profile = __instance,
					before = instance.NativeValue(__instance, __0),
					time = ZNet.instance.GetTimeSeconds(),
					player = ((object)((Character)Player.m_localPlayer).GetZDOID()/*cast due to .constrained prefix*/).ToString(),
					group = "native",
					name = ((object)__0).ToString(),
					operation = ((__originalMethod.Name == "SetStat") ? "set" : "increment")
				};
			}
		}
		catch
		{
			instance.failures++;
		}
	}

	private static void AfterNativeStat(PlayerProfile __instance, PlayerStatType __0, MetricCapture __state, bool __runOriginal)
	{
		//IL_0023: Unknown result type (might be due to invalid IL or missing references)
		Plugin instance = Instance;
		if ((Object)(object)instance == (Object)null || __state == null || !__runOriginal)
		{
			return;
		}
		try
		{
			instance.BufferMetric(__state, instance.NativeValue(__instance, __0));
		}
		catch
		{
			instance.failures++;
		}
	}

	private static void BeforeNamedStat(PlayerProfile __instance, string __0, MethodBase __originalMethod, out MetricCapture __state)
	{
		//IL_0072: Unknown result type (might be due to invalid IL or missing references)
		//IL_0077: Unknown result type (might be due to invalid IL or missing references)
		__state = null;
		Plugin instance = Instance;
		if ((Object)(object)instance == (Object)null)
		{
			return;
		}
		try
		{
			if (instance.LocalProfile(__instance) && MetricBuffer.Name(__0))
			{
				__state = new MetricCapture
				{
					profile = __instance,
					before = instance.NamedValue(__instance, __originalMethod.Name, __0),
					time = ZNet.instance.GetTimeSeconds(),
					player = ((object)((Character)Player.m_localPlayer).GetZDOID()/*cast due to .constrained prefix*/).ToString(),
					group = NamedStatGroups[__originalMethod.Name],
					name = __0,
					operation = "increment",
					method = __originalMethod.Name
				};
			}
		}
		catch
		{
			instance.failures++;
		}
	}

	private static void AfterNamedStat(PlayerProfile __instance, MetricCapture __state, bool __runOriginal)
	{
		Plugin instance = Instance;
		if ((Object)(object)instance == (Object)null || __state == null || !__runOriginal)
		{
			return;
		}
		try
		{
			instance.BufferMetric(__state, instance.NamedValue(__instance, __state.method, __state.name));
		}
		catch
		{
			instance.failures++;
		}
	}

	private void BufferMetric(MetricCapture capture, double after)
	{
		metricBuffer.BindProfile(capture.profile);
		metricsCharacter = capture.player;
		metricBuffer.Observe(capture.group, capture.name, capture.operation, capture.before, after, capture.time);
		if ((Server() || serverWork) && capture.group == "native" && capture.operation == "increment" && after > capture.before && WorkBuffer.Category(capture.name) != null)
		{
			workBuffer.Bind(capture.profile);
			workBuffer.Observe(capture.name, after - capture.before, capture.time, capture.x, capture.y, capture.z);
		}
	}

	private void SendMetrics()
	{
		//IL_0077: Unknown result type (might be due to invalid IL or missing references)
		//IL_007c: Unknown result type (might be due to invalid IL or missing references)
		if (!Observing() || (!Server() && !serverMetrics) || Time.realtimeSinceStartup < nextMetrics)
		{
			return;
		}
		nextMetrics = Time.realtimeSinceStartup + 15f;
		if ((Object)(object)Player.m_localPlayer == (Object)null || (Object)(object)Game.instance == (Object)null)
		{
			return;
		}
		string text = ((object)((Character)Player.m_localPlayer).GetZDOID()/*cast due to .constrained prefix*/).ToString();
		metricBuffer.BindProfile(Game.instance.GetPlayerProfile());
		workBuffer.Bind(Game.instance.GetPlayerProfile());
		metricsCharacter = text;
		for (int i = 0; i < 4 && (metricBuffer.Count > 0 || workBuffer.Count > 0 || (i == 0 && (Server() || serverWork))); i++)
		{
			MetricDelta[] array = metricBuffer.DrainValid(32, ZNet.instance.GetTimeSeconds());
			WorkSample[] array2 = workBuffer.Drain(ZNet.instance.GetTimeSeconds());
			if (array.Length == 0 && array2.Length == 0 && (i != 0 || (!Server() && !serverWork)))
			{
				break;
			}
			MetricsEnvelope metricsEnvelope = new MetricsEnvelope();
			metricsEnvelope.world = world;
			metricsEnvelope.token = token;
			metricsEnvelope.sequence = ++metricsSequence;
			metricsEnvelope.workVersion = ((Server() || serverWork) ? 1 : 0);
			metricsEnvelope.work = array2;
			metricsEnvelope.report = new MetricReport
			{
				player = text,
				changes = array
			};
			MetricsEnvelope metricsEnvelope2 = metricsEnvelope;
			if (Server())
			{
				RecordMetrics(Lookup(text), metricsEnvelope2.report, "local-host");
				RecordWork(Lookup(text), array2, "local-host");
			}
			else
			{
				acceptedServer.Invoke("JVStory_PlayerMetrics_v3", new object[1] { JsonConvert.SerializeObject((object)metricsEnvelope2) });
			}
		}
	}

	private void ReceiveMetrics(ZRpc rpc, string json)
	{
		try
		{
			ZNetPeer val = Peer(rpc);
			if (!Server() || !collect.Value || telemetry == null || val == null || !connections.TryGetValue(rpc, out var value) || value.version < 3 || value.token == null || json == null || json.Length > 49152)
			{
				rejected++;
				return;
			}
			MetricsEnvelope metricsEnvelope = JsonConvert.DeserializeObject<MetricsEnvelope>(json, JsonSettings);
			if (metricsEnvelope == null || metricsEnvelope.protocol != 3 || metricsEnvelope.token != value.token || metricsEnvelope.world != world || metricsEnvelope.sequence <= value.metricsSequence || metricsEnvelope.report == null || metricsEnvelope.report.changes == null || metricsEnvelope.report.changes.Length > 32)
			{
				rejected++;
				return;
			}
			MetricReport report = metricsEnvelope.report;
			Entity entity = Lookup(report.player);
			if (report.player != ((object)Unsafe.As<ZDOID, ZDOID>(ref val.m_characterID)/*cast due to .constrained prefix*/).ToString() || entity == null || !entity.player || entity.owner != val.m_uid || string.IsNullOrEmpty(entity.account))
			{
				rejected++;
				return;
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (realtimeSinceStartup - value.metricsWindow >= 10f)
			{
				value.metricsWindow = realtimeSinceStartup;
				value.metricsReceived = 0;
			}
			if (metricsEnvelope.workVersion != 0 && (value.version < 4 || metricsEnvelope.workVersion != 1 || metricsEnvelope.work == null || metricsEnvelope.work.Length > 32))
			{
				rejected++;
				return;
			}
			value.metricsReceived += Math.Max(1, report.changes.Length + ((metricsEnvelope.work != null) ? metricsEnvelope.work.Length : 0));
			if (value.metricsReceived > 512)
			{
				rejected++;
				return;
			}
			double gameTime = ZNet.instance.GetTimeSeconds();
			if (report.changes.Any((MetricDelta d) => !MetricBuffer.Valid(d, gameTime) || (d.group == "native" && !nativeStatNames.Contains(d.name))))
			{
				rejected++;
				return;
			}
			if (metricsEnvelope.workVersion == 1 && metricsEnvelope.work.Any((WorkSample s) => !WorkBuffer.Valid(s, gameTime)))
			{
				rejected++;
				return;
			}
			if (report.changes.Length == 0 && metricsEnvelope.workVersion == 0)
			{
				rejected++;
				return;
			}
			value.metricsSequence = metricsEnvelope.sequence;
			RecordMetrics(entity, report, val.m_socket.GetHostName());
			if (metricsEnvelope.workVersion == 1)
			{
				RecordWork(entity, metricsEnvelope.work, val.m_socket.GetHostName());
			}
		}
		catch
		{
			rejected++;
		}
	}

	private void RecordMetrics(Entity player, MetricReport report, string reporter)
	{
		if (player == null || !player.player || string.IsNullOrEmpty(player.account) || report.changes.Length == 0)
		{
			return;
		}
		PlayerStats playerStats = telemetry.Player(player);
		if (playerStats != null)
		{
			if (playerStats.gameMetrics == null)
			{
				playerStats.gameMetrics = new PlayerMetricTotals();
			}
			MetricDelta[] changes = report.changes;
			foreach (MetricDelta d in changes)
			{
				MetricBuffer.Apply(playerStats.gameMetrics, d);
				metricsAccepted++;
			}
			WriteEvent(new StoryEvent
			{
				id = Guid.NewGuid().ToString(),
				world = world,
				kind = "player_metrics",
				observedUtc = DateTime.UtcNow.ToString("o"),
				gameTime = ZNet.instance.GetTimeSeconds(),
				victim = player,
				metrics = report,
				reporter = reporter,
				evidence = "client observed changes to native raw gameplay counters while connected; separate from verified deaths/damage; not anti-cheat proof"
			});
		}
	}

	private void SetupSnapshots()
	{
		//IL_002b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0035: Expected O, but got Unknown
		//IL_00a6: Unknown result type (might be due to invalid IL or missing references)
		//IL_00b0: Expected O, but got Unknown
		afkSnapshotSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("Reporting", "AFK snapshot interval seconds", 120, new ConfigDescription("Reduce routine snapshots during detected AFK. Activity resumes and biome changes trigger an earlier sample; combat reports continue normally.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(30, 300), new object[0]));
		epicLootReporting = ((BaseUnityPlugin)this).Config.Bind<bool>("Reporting", "Optional Epic Loot equipment", true, "Detect Epic Loot automatically and report equipped item rarity/enchantments on changes. No full inventory polling.");
		snapshotsEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Reporting", "Report player snapshots", true, "Send current skills, food, equipped weapon, vitals, armor, carry weight and biome to this game server. No inventory contents, chat text, or external services.");
		snapshotSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("Reporting", "Player snapshot interval seconds", 30, new ConfigDescription("Periodic current-state sample. Biome changes are checked every two seconds and can trigger an earlier sample. No per-frame network traffic.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(15, 300), new object[0]));
	}

	private void ResetSnapshots()
	{
		lastSnapshotAfk = false;
		equipmentFingerprint = null;
		snapshotSequence = 0L;
		nextSnapshot = (nextSnapshotCheck = 0f);
		lastSnapshotBiome = (lastSnapshotPlayer = null);
		localPreviousSnapshot = null;
		localSnapshotName = null;
	}

	private void SendSnapshot()
	{
		//IL_0065: 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_0087: Unknown result type (might be due to invalid IL or missing references)
		//IL_0098: Unknown result type (might be due to invalid IL or missing references)
		//IL_009d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0108: Unknown result type (might be due to invalid IL or missing references)
		//IL_010d: Unknown result type (might be due to invalid IL or missing references)
		if (!Observing() || (!Server() && !serverMetrics) || !snapshotsEnabled.Value || Time.realtimeSinceStartup < nextSnapshotCheck)
		{
			return;
		}
		nextSnapshotCheck = Time.realtimeSinceStartup + 2f;
		Player localPlayer = Player.m_localPlayer;
		if ((Object)(object)localPlayer == (Object)null || ((Character)localPlayer).GetZDOID() == ZDOID.None)
		{
			return;
		}
		string text = ((object)localPlayer.GetCurrentBiome()).ToString();
		string text2 = ((object)((Character)localPlayer).GetZDOID()/*cast due to .constrained prefix*/).ToString();
		if (text2 == lastSnapshotPlayer && text == lastSnapshotBiome && activityClock.afk == lastSnapshotAfk && Time.realtimeSinceStartup < nextSnapshot)
		{
			return;
		}
		ItemData currentWeapon = ((Humanoid)localPlayer).GetCurrentWeapon();
		Vector3 position = ((Component)localPlayer).transform.position;
		PlayerSnapshot playerSnapshot = new PlayerSnapshot();
		playerSnapshot.player = text2;
		playerSnapshot.gameTime = ZNet.instance.GetTimeSeconds();
		playerSnapshot.biome = text;
		playerSnapshot.x = position.x;
		playerSnapshot.y = position.y;
		playerSnapshot.z = position.z;
		playerSnapshot.health = Math.Max(0f, ((Character)localPlayer).GetHealth());
		playerSnapshot.maxHealth = ((Character)localPlayer).GetMaxHealth();
		playerSnapshot.stamina = Math.Max(0f, localPlayer.GetStamina());
		playerSnapshot.maxStamina = ((Character)localPlayer).GetMaxStamina();
		playerSnapshot.eitr = Math.Max(0f, localPlayer.GetEitr());
		playerSnapshot.maxEitr = ((Character)localPlayer).GetMaxEitr();
		playerSnapshot.armor = Math.Max(0f, ((Character)localPlayer).GetBodyArmor());
		playerSnapshot.carryWeight = Math.Max(0f, ((Humanoid)localPlayer).GetInventory().GetTotalWeight());
		playerSnapshot.maxCarryWeight = Math.Max(0f, localPlayer.GetMaxCarryWeight());
		playerSnapshot.comfort = localPlayer.GetComfortLevel();
		playerSnapshot.weapon = ((currentWeapon == null || (Object)(object)currentWeapon.m_dropPrefab == (Object)null) ? null : Clean(((Object)currentWeapon.m_dropPrefab).name));
		playerSnapshot.weaponQuality = currentWeapon?.m_quality ?? 0;
		playerSnapshot.dead = ((Character)localPlayer).IsDead();
		playerSnapshot.sleeping = localPlayer.IsSleeping();
		playerSnapshot.swimming = ((Character)localPlayer).IsSwimming();
		playerSnapshot.running = ((Character)localPlayer).IsRunning();
		playerSnapshot.aboard = ((Character)localPlayer).IsAttachedToShip();
		playerSnapshot.encumbered = ((Character)localPlayer).IsEncumbered();
		playerSnapshot.skills = (from k in (from k in ((Character)localPlayer).GetSkills().GetSkillList()
				where k.m_info != null
				select k).Take(128)
			select new SkillSnapshot
			{
				name = ((object)k.m_info.m_skill).ToString(),
				level = k.m_level
			}).ToArray();
		playerSnapshot.foods = (from f in localPlayer.GetFoods().Take(16)
			where f.m_item != null && (Object)(object)f.m_item.m_dropPrefab != (Object)null
			select new FoodSnapshot
			{
				prefab = ((Object)f.m_item.m_dropPrefab).name,
				remainingSeconds = Math.Max(0f, f.m_time)
			}).ToArray();
		PlayerSnapshot playerSnapshot2 = playerSnapshot;
		nextSnapshot = Time.realtimeSinceStartup + (float)(activityClock.afk ? Math.Max(snapshotSeconds.Value, afkSnapshotSeconds.Value) : snapshotSeconds.Value);
		lastSnapshotAfk = activityClock.afk;
		lastSnapshotBiome = text;
		lastSnapshotPlayer = text2;
		if (!SnapshotRules.Valid(playerSnapshot2, playerSnapshot2.gameTime))
		{
			dropped++;
			return;
		}
		EquipmentReport equipmentReport = epicAdapter.Read(((Humanoid)localPlayer).GetInventory().GetEquippedItems(), epicLootReporting.Value);
		string text3 = JsonConvert.SerializeObject((object)equipmentReport, WriteSettings);
		bool flag = text2 != localEquipmentPlayer || text3 != equipmentFingerprint;
		equipmentFingerprint = text3;
		localEquipmentPlayer = text2;
		if (Server())
		{
			RecordSnapshot(Lookup(text2), playerSnapshot2, "local-host", null, flag ? equipmentReport : null);
			return;
		}
		acceptedServer.Invoke("JVStory_PlayerSnapshot_v3", new object[1] { JsonConvert.SerializeObject((object)new SnapshotEnvelope
		{
			world = world,
			token = token,
			sequence = ++snapshotSequence,
			snapshot = playerSnapshot2,
			equipment = (flag ? equipmentReport : null)
		}) });
	}

	private void ReceiveSnapshot(ZRpc rpc, string json)
	{
		try
		{
			ZNetPeer val = Peer(rpc);
			if (!Server() || !collect.Value || telemetry == null || val == null || !connections.TryGetValue(rpc, out var value) || value.version < 3 || value.token == null || json == null || json.Length > 32768)
			{
				rejected++;
				return;
			}
			SnapshotEnvelope snapshotEnvelope = JsonConvert.DeserializeObject<SnapshotEnvelope>(json, JsonSettings);
			if (snapshotEnvelope == null || snapshotEnvelope.protocol != 3 || snapshotEnvelope.world != world || snapshotEnvelope.token != value.token || snapshotEnvelope.sequence <= value.snapshotSequence || !SnapshotRules.Valid(snapshotEnvelope.snapshot, ZNet.instance.GetTimeSeconds()) || (snapshotEnvelope.equipment != null && !EquipmentRules.Valid(snapshotEnvelope.equipment)))
			{
				rejected++;
				return;
			}
			PlayerSnapshot snapshot = snapshotEnvelope.snapshot;
			Entity entity = Lookup(snapshot.player);
			if (snapshot.player != ((object)Unsafe.As<ZDOID, ZDOID>(ref val.m_characterID)/*cast due to .constrained prefix*/).ToString() || entity == null || !entity.player || entity.owner != val.m_uid || string.IsNullOrEmpty(entity.account))
			{
				rejected++;
				return;
			}
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (realtimeSinceStartup - value.snapshotWindow >= 60f)
			{
				value.snapshotWindow = realtimeSinceStartup;
				value.snapshotsReceived = 0;
			}
			if (++value.snapshotsReceived > 40)
			{
				rejected++;
				return;
			}
			value.snapshotSequence = snapshotEnvelope.sequence;
			RecordSnapshot(entity, snapshot, val.m_socket.GetHostName(), value, snapshotEnvelope.equipment);
		}
		catch
		{
			rejected++;
		}
	}

	private void RecordSnapshot(Entity player, PlayerSnapshot s, string reporter, Connection c, EquipmentReport equipment = null)
	{
		if (player != null && player.player && !string.IsNullOrEmpty(player.account))
		{
			PlayerSnapshot playerSnapshot = ((c == null) ? localPreviousSnapshot : c.previousSnapshot);
			string text = ((c == null) ? localSnapshotName : c.snapshotName);
			SnapshotContext snapshotContext = SnapshotRules.Compare(playerSnapshot, s, text == player.name);
			PlayerStats playerStats = telemetry.Player(player);
			if (playerStats != null)
			{
				playerStats.latestSnapshot = s;
				playerStats.latestSnapshotUtc = DateTime.UtcNow.ToString("o");
			}
			if (playerStats != null && equipment != null)
			{
				playerStats.equipment = equipment;
				playerStats.equipmentUtc = DateTime.UtcNow.ToString("o");
				WriteEvent(new StoryEvent
				{
					id = Guid.NewGuid().ToString(),
					world = world,
					kind = "equipment_changed",
					observedUtc = playerStats.equipmentUtc,
					gameTime = s.gameTime,
					victim = player,
					equipment = equipment,
					evidence = "equipped metadata sampled with optional Epic Loot API; changes between samples may be missed"
				});
			}
			snapshotsAccepted++;
			WriteEvent(new StoryEvent
			{
				id = Guid.NewGuid().ToString(),
				world = world,
				kind = "player_snapshot",
				observedUtc = DateTime.UtcNow.ToString("o"),
				gameTime = s.gameTime,
				victim = player,
				snapshot = s,
				snapshotContext = snapshotContext,
				reporter = reporter,
				evidence = "authenticated client current-state sample; not anti-cheat proof",
				x = s.x,
				y = s.y,
				z = s.z
			});
			if (!snapshotContext.baseline && playerSnapshot.biome != s.biome && playerSnapshot.biome != "None" && s.biome != "None")
			{
				WriteEvent(new StoryEvent
				{
					id = Guid.NewGuid().ToString(),
					world = world,
					kind = "biome_crossing",
					observedUtc = DateTime.UtcNow.ToString("o"),
					gameTime = s.gameTime,
					victim = player,
					biome = new BiomeTransition
					{
						from = playerSnapshot.biome,
						to = s.biome,
						intervalSeconds = snapshotContext.gapSeconds
					},
					reporter = reporter,
					evidence = "client biome sampled every two seconds; brief crossings can be missed; travel method unknown",
					x = s.x,
					y = s.y,
					z = s.z
				});
			}
			if (c == null)
			{
				localPreviousSnapshot = s;
				localSnapshotName = player.name;
			}
			else
			{
				c.previousSnapshot = s;
				c.snapshotName = player.name;
			}
		}
	}

	private void SessionReady(Connection c, Entity player)
	{
		if (c.token != null && player != null && !string.IsNullOrEmpty(player.account) && model != null)
		{
			if (c.sessionPlayer == null)
			{
				c.sessionId = Guid.NewGuid().ToString("N");
				c.sessionAt = Time.realtimeSinceStartup;
				WriteEvent(new StoryEvent
				{
					id = Guid.NewGuid().ToString(),
					world = world,
					kind = "session_started",
					observedUtc = DateTime.UtcNow.ToString("o"),
					gameTime = ZNet.instance.GetTimeSeconds(),
					victim = player,
					session = new SessionInfo
					{
						id = c.sessionId,
						protocol = c.version,
						reason = "reporter_ready"
					},
					evidence = "server observed a ready reporting connection; not total login or active time"
				});
			}
			c.sessionPlayer = player;
			WorkReady(c, player);
		}
	}

	private void SessionEnded(Connection c, string reason)
	{
		if (c.sessionPlayer != null && model != null)
		{
			WriteEvent(new StoryEvent
			{
				id = Guid.NewGuid().ToString(),
				world = world,
				kind = "session_ended",
				observedUtc = DateTime.UtcNow.ToString("o"),
				gameTime = (((Object)(object)ZNet.instance == (Object)null) ? 0.0 : ZNet.instance.GetTimeSeconds()),
				victim = c.sessionPlayer,
				session = new SessionInfo
				{
					id = c.sessionId,
					protocol = c.version,
					reason = reason,
					observedSeconds = Math.Max(0f, Time.realtimeSinceStartup - c.sessionAt)
				},
				evidence = "server-observed reporting connection interval; includes AFK and excludes time before reporting became ready"
			});
			CloseWork(c.sessionPlayer.account, reason);
			c.sessionPlayer = null;
		}
	}

	private void SetupStatsUI()
	{
		//IL_003e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0058: Unknown result type (might be due to invalid IL or missing references)
		//IL_0062: Expected O, but got Unknown
		//IL_0090: Unknown result type (might be due to invalid IL or missing references)
		//IL_009d: Expected O, but got Unknown
		//IL_00cb: Unknown result type (might be due to invalid IL or missing references)
		//IL_00d8: Expected O, but got Unknown
		//IL_0105: Unknown result type (might be due to invalid IL or missing references)
		//IL_0113: Expected O, but got Unknown
		leaderboardEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Server", "Enable leaderboards", true, "Allow connected players to compare collected world totals in F8. Shows character names and ranked totals. Disable to hide the leaderboard for everyone.");
		statsKey = ((BaseUnityPlugin)this).Config.Bind<KeyboardShortcut>("Stats window", "Open stats hotkey", new KeyboardShortcut((KeyCode)289, (KeyCode[])(object)new KeyCode[0]), "Open your server statistics. Escape or this hotkey closes it. Requires a 0.3+ server collector. Change this key if another mod uses F8.");
		statsUiHarmony = new Harmony("JacobsValheim.VikingStoryteller.StatsUI");
		try
		{
			statsUiHarmony.Patch((MethodBase)AccessTools.Method(typeof(Player), "TakeInput", (Type[])null, (Type[])null), (HarmonyMethod)null, new HarmonyMethod(typeof(Plugin), "StatsWindowInput", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			statsUiHarmony.Patch((MethodBase)AccessTools.Method(typeof(PlayerController), "TakeInput", (Type[])null, (Type[])null), (HarmonyMethod)null, new HarmonyMethod(typeof(Plugin), "StatsControllerInput", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			statsUiHarmony.Patch((MethodBase)AccessTools.Method(typeof(GameCamera), "UpdateMouseCapture", (Type[])null, (Type[])null), new HarmonyMethod(typeof(Plugin), "StatsMouseCapture", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			statsUiReady = true;
		}
		catch (Exception ex)
		{
			statsUiHarmony.UnpatchSelf();
			((BaseUnityPlugin)this).Logger.LogWarning((object)("Stats window unavailable: " + ex.Message));
		}
	}

	private static void StatsWindowInput(Player __instance, ref bool __result)
	{
		if ((Object)(object)Instance != (Object)null && Instance.statsOpen && (Object)(object)__instance == (Object)(object)Player.m_localPlayer)
		{
			__result = false;
		}
	}

	private static void StatsControllerInput(ref bool __result)
	{
		if ((Object)(object)Instance != (Object)null && Instance.statsOpen)
		{
			__result = false;
		}
	}

	private static bool StatsMouseCapture()
	{
		if ((Object)(object)Instance == (Object)null || !Instance.statsOpen)
		{
			return true;
		}
		Cursor.lockState = (CursorLockMode)0;
		Cursor.visible = true;
		return false;
	}

	private void CloseStats()
	{
		//IL_0015: Unknown result type (might be due to invalid IL or missing references)
		if (statsOpen)
		{
			statsOpen = false;
			Cursor.lockState = previousCursorLock;
			Cursor.visible = previousCursorVisible;
		}
	}

	private void ResetStatsUI()
	{
		CloseStats();
		statsView = null;
		leaderboard = null;
		pendingStatsRequest = null;
		nextStatsRequest = 0f;
		statsNotice = "Waiting for the server...";
	}

	private void UpdateStatsUI()
	{
		//IL_0051: Unknown result type (might be due to invalid IL or missing references)
		//IL_0056: Unknown result type (might be due to invalid IL or missing references)
		//IL_0084: Unknown result type (might be due to invalid IL or missing references)
		//IL_0089: Unknown result type (might be due to invalid IL or missing references)
		//IL_00a1: Unknown result type (might be due to invalid IL or missing references)
		//IL_00a6: Unknown result type (might be due to invalid IL or missing references)
		//IL_00fc: Unknown result type (might be due to invalid IL or missing references)
		//IL_0101: Unknown result type (might be due to invalid IL or missing references)
		if (Application.isBatchMode || !statsUiReady)
		{
			return;
		}
		if ((Object)(object)Player.m_localPlayer == (Object)null || (Object)(object)ZNet.instance == (Object)null)
		{
			CloseStats();
			return;
		}
		KeyboardShortcut value = statsKey.Value;
		if (((KeyboardShortcut)(ref value)).IsDown())
		{
			if (statsOpen)
			{
				CloseStats();
			}
			else
			{
				previousCursorLock = Cursor.lockState;
				previousCursorVisible = Cursor.visible;
				statsOpen = true;
				statsScroll = Vector2.zero;
				statsRect = new Rect((float)Math.Max(16, (Screen.width - 860) / 2), (float)Math.Max(16, (Screen.height - 640) / 2), (float)Math.Min(860, Screen.width - 32), (float)Math.Min(640, Screen.height - 32));
				nextStatsRequest = 0f;
				RequestOwnStats();
			}
		}
		if (!statsOpen)
		{
			return;
		}
		if (Input.GetKeyDown((KeyCode)27))
		{
			CloseStats();
			return;
		}
		Cursor.lockState = (CursorLockMode)0;
		Cursor.visible = true;
		if (Time.realtimeSinceStartup >= nextStatsRequest)
		{
			RequestOwnStats();
		}
	}

	private void RequestOwnStats()
	{
		//IL_004e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0053: Unknown result type (might be due to invalid IL or missing references)
		if (Time.realtimeSinceStartup < nextStatsRequest)
		{
			return;
		}
		nextStatsRequest = Time.realtimeSinceStartup + 15f;
		if (Server())
		{
			Entity entity = (((Object)(object)Player.m_localPlayer == (Object)null) ? null : Lookup(((object)((Character)Player.m_localPlayer).GetZDOID()/*cast due to .constrained prefix*/).ToString()));
			PlayerStats value = null;
			if (entity != null && telemetry != null)
			{
				telemetry.state.players.TryGetValue(entity.account, out value);
			}
			leaderboard = BuildLeaderboard(entity?.account, Leaderboards.Keys[leaderboardMetric], PeriodKeys[leaderboardPeriod]);
			statsView = StatsView.From(value, (telemetry == null) ? null : telemetry.state.sinceUtc);
			statsNotice = ((statsView == null) ? "No observations collected for this player yet." : "Updated from this world's collector.");
		}
		else if (acceptedServer == null || token == null || !serverMetrics)
		{
			statsNotice = "This window needs a Storyteller 0.3+ server and reporting enabled. Waiting for negotiation...";
		}
		else
		{
			pendingStatsRequest = Guid.NewGuid().ToString("N");
			statsNotice = "Refreshing server statistics...";
			acceptedServer.Invoke("JVStory_StatsRequest_v3", new object[1] { JsonConvert.SerializeObject((object)new StatsRequest
			{
				world = world,
				token = token,
				id = pendingStatsRequest,
				leaderboardMetric = Leaderboards.Keys[leaderboardMetric],
				leaderboardPeriod = PeriodKeys[leaderboardPeriod]
			}) });
		}
	}

	private void ReceiveStatsRequest(ZRpc rpc, string json)
	{
		try
		{
			ZNetPeer val = Peer(rpc);
			if (!Server() || !collect.Value || telemetry == null || val == null || !connections.TryGetVa