Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of VikingStoryteller v0.4.9
plugins/VikingStoryteller.dll
Decompiled a month ago
The result has been truncated due to the large size, download it to view full contents!
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