Decompiled source of StateKeeper v0.1.0
StateKeeper.dll
Decompiled a week 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.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using Peak.Afflictions; using Photon.Pun; using Photon.Realtime; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Events; using UnityEngine.UI; using Zorro.Core; using Zorro.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("StateKeeper")] [assembly: AssemblyDescription("PEAK expedition run telemetry collector")] [assembly: AssemblyCompany("Local MOD")] [assembly: AssemblyProduct("StateKeeper")] [assembly: AssemblyFileVersion("0.1.0.0")] [assembly: ComVisible(false)] [assembly: InternalsVisibleTo("StateKeeper.Tests")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("0.1.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace StateKeeper { [Serializable] internal sealed class AnalysisResult { public const int CurrentVersion = 5; public List<LifecycleObservation> lifecycle = new List<LifecycleObservation>(); public List<LifecycleInterval> lifecycleIntervals = new List<LifecycleInterval>(); public List<ReportEpisode> episodes = new List<ReportEpisode>(); public List<QualityObservation> diagnostics = new List<QualityObservation>(); public List<ReportEvent> timeline = new List<ReportEvent>(); public int processedInventoryCount; public int analysisVersion = 5; public string[] collectionCapabilities = new string[0]; public List<AnalysisAssistanceEvent> assistance = new List<AnalysisAssistanceEvent>(); public List<AnalysisEffectChange> effects = new List<AnalysisEffectChange>(); public string sourceFingerprint; public string[] statusTypeOrder = new string[0]; public string[] afflictionTypeOrder = new string[0]; public List<AnalysisDistancePoint> teamSpan = new List<AnalysisDistancePoint>(); public int schemaVersion; public string runId; public string sourceLastSavedUtc; public int sourceChunkCount; public int sourceSampleCount; public int sourceInventorySnapshotCount; public int sourceEventCount; public bool hasAscentLevel; public int ascentLevel; public bool hasCustomRun; public bool isCustomRun; public string gameVersion; public string recordingUserId; public double analysisMilliseconds; public long observedManagedBytes; public AnalysisOverview overview = new AnalysisOverview(); public AnalysisQuality quality = new AnalysisQuality(); public List<AnalysisPlayer> players = new List<AnalysisPlayer>(); public List<AnalysisDistancePair> distancePairs = new List<AnalysisDistancePair>(); public List<AnalysisItemObservation> items = new List<AnalysisItemObservation>(); public List<ItemDefinition> definitions = new List<ItemDefinition>(); public List<MountainSegmentDefinition> mountainSegments = new List<MountainSegmentDefinition>(); public List<AnalysisSegment> segments = new List<AnalysisSegment>(); } [Serializable] internal sealed class AnalysisOverview { public string outcome; public float durationSeconds; public int playerCount; public int jumpCount; public int deathCount; public int passedOutCount; public int lifeSavedCount; public int rescuePullCount; public float friendHealingAmount; public int itemObservationCount; public int validDistanceCount; public int mountainSegmentCount; } [Serializable] internal sealed class AnalysisAssistanceEvent { public float time; public int epoch; public string kind; public int actorPlayerIndex = -1; public int targetPlayerIndex = -1; public string itemName; public string itemGuid; public float amount; public string resourceKey; } [Serializable] internal sealed class AnalysisQuality { public int missingChunkCount; public int corruptChunkCount; public int timeBackwardsCount; public int missingPlayerSampleCount; public int invalidPositionCount; public int nonFiniteValueCount; public int excludedDistanceCount; public int gapCount; public int countMismatchCount; public int outOfRangeStaminaCount; public List<string> warnings = new List<string>(); } [Serializable] internal sealed class AnalysisPlayer { public int playerIndex; public string displayName; public string stableUserId; public int rawDeathCount; public int duplicateDeathCount; public int unconfirmedDeathCount; public int rawPassedOutCount; public int consumedCount; public int possibleHelpCount; public float lowStaminaSeconds; public float lowCapacitySeconds; public float isolatedSeconds; public Dictionary<string, float> movementSeconds = new Dictionary<string, float>(); public Dictionary<string, float> statusIntegrals = new Dictionary<string, float>(); public int jumpCount; public List<float> jumpTimes = new List<float>(); public List<EvidenceReference> jumpEvidence = new List<EvidenceReference>(); public int deathCount; public int passedOutCount; public float staminaMin = 1f; public float staminaMax; public float staminaAverage; public float observedSeconds; public float aliveSeconds; public List<float> deathTimes = new List<float>(); public List<float> passedOutTimes = new List<float>(); public List<AnalysisDistancePoint> nearestSeries = new List<AnalysisDistancePoint>(); public List<AnalysisSeriesPoint> staminaSeries = new List<AnalysisSeriesPoint>(); public List<AnalysisStaminaEpisode> staminaEpisodes = new List<AnalysisStaminaEpisode>(); public List<AnalysisIsolationEpisode> isolationEpisodes = new List<AnalysisIsolationEpisode>(); public int lifeSavedCount; public int rescuePullCount; public float friendHealingAmount; } [Serializable] internal sealed class AnalysisSeriesPoint { public EvidenceReference evidence; public float time; public float endTime; public int epoch; public bool breakBefore; public bool dead; public float maxStamina; public int? petrifyAmount; public float[] statuses; public List<int> afflictions; public float minimum; public float maximum; public float validSeconds; public double totalIntegral; public float regularStamina; public float extraStamina; public float regularMinimum; public float regularMaximum; public float extraMinimum; public float extraMaximum; public float totalStamina; public float normalizedStamina; public int sampleCount; public float observedSeconds; public float[] movementSeconds; public float[] statusIntegrals; } [Serializable] internal sealed class AnalysisDistancePair { public int playerA; public int playerB; public int sampleCount; public float median; public float p90; public float p99; public float max; public float nearestObserved; public float over25Ratio; public float over50Ratio; public float over100Ratio; public float validSeconds; public List<AnalysisDistancePoint> series = new List<AnalysisDistancePoint>(); public List<float> intervals = new List<float>(); public List<int> intervalEpochs = new List<int>(); } [Serializable] internal sealed class AnalysisItemObservation { public EvidenceReference evidence; public int observationId; public int attributionGroupId; public int? cookedAmount; public float endTime; public List<string> attributionReasons = new List<string>(); public List<AnalysisEffectRule> rules = new List<AnalysisEffectRule>(); public bool possibleHelp; public bool possibleRescue; public float time; public int playerIndex = -1; public int actorPlayerIndex = -1; public bool actorInferred; public int targetPlayerIndex = -1; public ushort itemId; public int epoch; public int evidenceCount = 1; public string attribution = "Ambiguous"; public string attributionReason; public List<AnalysisEffectChange> observedEffects = new List<AnalysisEffectChange>(); public string itemGuid; public string itemName; public string prefabName; public string slot; public string kind; public string resourceKey; public float previousValue; public float value; public string confidence; public string detail; public List<string> effectHints = new List<string>(); } [Serializable] internal sealed class AnalysisSegment { public int index; public string titleKey; public string displayName; public bool hasStartTime; public bool hasEndTime; public float startTime; public float endTime; public bool derivedFromBoundary; } [Serializable] internal sealed class AnalysisStaminaEpisode { public float startTime; public float endTime; public float startValue; public float endValue; public string direction; public bool burst; } [Serializable] internal sealed class AnalysisIsolationEpisode { public EvidenceReference evidence; public float startTime; public float endTime; public float peakNearestDistance; public string endReason; } [Serializable] internal sealed class AnalysisDistancePoint { public EvidenceReference evidence; public float time; public float endTime; public float value; public float maximum; public float minimum; public int count; public bool breakBefore; public int epoch; } [Serializable] internal sealed class AnalysisEffectChange { public int effectId; public int epoch; public string attribution = "Ambiguous"; public List<string> reasons = new List<string>(); public List<int> candidateGroupIds = new List<int>(); public float cumulativeDelta; public string channel; public int playerIndex; public float previousValue; public float value; public float beforeTime; public float afterTime; public bool matchesTheory; } [Serializable] internal sealed class AnalysisEffectRule { public string channel; public float? amount; public int direction; public float delay; public float duration; public bool continuous; public string scope = "Recipient"; public float? radius; public bool ignoreCaster; public string trigger; public string source; public bool uncertain; public string requiredAffliction; public string afterAfflictionEnds; public string budgetGroup; public float? budget; public int priority; } internal static class AssistanceEvidence { internal static float HealedAmount(float before, float after) { if (float.IsNaN(before) || float.IsInfinity(before) || float.IsNaN(after) || float.IsInfinity(after)) { return 0f; } return Math.Max(0f, before - after); } internal static bool RestoredRecoveryEligibility(bool wasPassedOut, bool dead, float beforeSum, float afterSum, int petrify) { if (wasPassedOut && !dead && petrify < 100 && beforeSum > 0.99f && afterSum >= 0f && afterSum <= 0.99f) { return !float.IsInfinity(beforeSum); } return false; } } [HarmonyPatch(typeof(CharacterSyncer), "OnDataReceived")] internal static class CharacterObservationSyncPatch { private static void Postfix(CharacterSyncer __instance) { if ((Object)(object)RunCollector.Instance != (Object)null && (Object)(object)__instance != (Object)null) { RunCollector.Instance.ObserveSync(((Component)__instance).GetComponent<Character>()); } } } [HarmonyPatch(typeof(Character), "OnJump")] internal static class CharacterJumpPatch { private static void Postfix(Character __instance) { if ((Object)(object)RunCollector.Instance != (Object)null) { RunCollector.Instance.RecordJump(__instance); } } } [HarmonyPatch(typeof(GlobalEvents), "TriggerSomeoneWonRun")] internal static class GlobalEventsVictoryPatch { private static void Postfix() { if ((Object)(object)RunCollector.Instance != (Object)null) { RunCollector.Instance.MarkVictory(); } } } [HarmonyPatch(typeof(GlobalEvents), "TriggerRunEnded")] internal static class GlobalEventsRunEndedPatch { private static void Prefix() { if ((Object)(object)RunCollector.Instance != (Object)null) { RunCollector.Instance.EndRun(); } } } [HarmonyPatch(typeof(GlobalEvents), "TriggerCharacterDied")] internal static class CharacterDiedPatch { private static void Postfix(Character character) { if ((Object)(object)RunCollector.Instance != (Object)null) { RunCollector.Instance.RecordSimpleEvent("PlayerDied", character, (!((Object)(object)character != (Object)null) || !character.IsLocal) ? EventSource.RemoteObserved : EventSource.LocalAuthoritative, "GlobalEvents"); } } } [HarmonyPatch(typeof(GlobalEvents), "TriggerCharacterPassedOut")] internal static class CharacterPassedOutPatch { private static void Postfix(Character character) { if ((Object)(object)RunCollector.Instance != (Object)null) { RunCollector.Instance.RecordSimpleEvent("PlayerPassedOut", character, (!((Object)(object)character != (Object)null) || !character.IsLocal) ? EventSource.RemoteObserved : EventSource.LocalAuthoritative, "GlobalEvents"); } } } [HarmonyPatch(typeof(Item), "Interact")] internal static class ItemInteractPatch { private static void Postfix(Item __instance, Character interactor) { if ((Object)(object)RunCollector.Instance != (Object)null) { RunCollector.Instance.RecordItemEvent("ItemPickupRequested", __instance, interactor, null, EventSource.LocalAuthoritative, "Item.Interact"); } } } [HarmonyPatch(typeof(CharacterItems), "OnPickupAccepted")] internal static class PickupAcceptedPatch { private static void Postfix(CharacterItems __instance, byte slotID) { if (!((Object)(object)RunCollector.Instance == (Object)null)) { Character val = (((Object)(object)__instance == (Object)null) ? null : ((Component)__instance).GetComponent<Character>()); ItemSlot itemSlot = null; if ((Object)(object)val != (Object)null && (Object)(object)val.player != (Object)null) { itemSlot = val.player.GetItemSlot(slotID); } RunCollector.Instance.RecordItemSlotEvent("ItemPickedUp", itemSlot, val, EventSource.LocalAuthoritative, "OnPickupAccepted:" + slotID); } } } [HarmonyPatch(typeof(Item), "StartUsePrimary")] internal static class ItemPrimaryStartedPatch { private static void Postfix(Item __instance) { RecordItem("ItemUseStarted", __instance, "StartUsePrimary"); } private static void RecordItem(string type, Item item, string detail) { if (!((Object)(object)RunCollector.Instance == (Object)null) && !((Object)(object)item == (Object)null)) { Character holderCharacter = item.holderCharacter; RunCollector.Instance.RecordItemEvent(type, item, holderCharacter, null, (!((Object)(object)holderCharacter != (Object)null) || !holderCharacter.IsLocal) ? EventSource.RemoteObserved : EventSource.LocalAuthoritative, detail); } } } [HarmonyPatch(typeof(Item), "FinishCastPrimary")] internal static class ItemPrimaryFinishedPatch { private static void Postfix(Item __instance) { if (!((Object)(object)RunCollector.Instance == (Object)null) && !((Object)(object)__instance == (Object)null)) { Character holderCharacter = __instance.holderCharacter; RunCollector.Instance.RecordItemEvent("ItemPrimaryCastFinished", __instance, holderCharacter, null, (!((Object)(object)holderCharacter != (Object)null) || !holderCharacter.IsLocal) ? EventSource.RemoteObserved : EventSource.LocalAuthoritative, "FinishCastPrimary"); } } } [HarmonyPatch(typeof(Item), "FinishCastSecondary")] internal static class ItemSecondaryFinishedPatch { private static void Postfix(Item __instance) { if (!((Object)(object)RunCollector.Instance == (Object)null) && !((Object)(object)__instance == (Object)null)) { Character holderCharacter = __instance.holderCharacter; RunCollector.Instance.RecordItemEvent("ItemSecondaryCastFinished", __instance, holderCharacter, null, (!((Object)(object)holderCharacter != (Object)null) || !holderCharacter.IsLocal) ? EventSource.RemoteObserved : EventSource.LocalAuthoritative, "FinishCastSecondary"); } } } [HarmonyPatch(typeof(Character), "GetFedItemRPC")] internal static class FedItemObservedPatch { private static void Prefix(Character __instance, int itemPhotonID, out RunCollector.ItemEventCapture __state) { __state = null; if (!((Object)(object)RunCollector.Instance == (Object)null) && !((Object)(object)__instance == (Object)null) && ((MonoBehaviourPun)__instance).photonView.IsMine) { PhotonView val = PhotonView.Find(itemPhotonID); Item val2 = (((Object)(object)val == (Object)null) ? null : ((Component)val).GetComponent<Item>()); if ((Object)(object)val2 != (Object)null && val2.OnPrimaryFinishedCast != null) { __state = RunCollector.Instance.CaptureItemEvent(val2, __instance, __instance); } } } private static void Postfix(RunCollector.ItemEventCapture __state) { if ((Object)(object)RunCollector.Instance != (Object)null) { RunCollector.Instance.RecordCapturedItemEvent("ItemFedToPlayer", __state); } } } internal sealed class ModifyStatusObservation { internal Item item; internal Character target; internal Character actor; internal float before; internal float statusSumBefore; internal bool wasPassedOut; internal STATUSTYPE status; } [HarmonyPatch(typeof(Action_ModifyStatus), "RunAction")] internal static class ModifyStatusOutcomePatch { private static void Prefix(Action_ModifyStatus __instance, out ModifyStatusObservation __state) { //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: 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) __state = null; if (!((Object)(object)RunCollector.Instance == (Object)null) && RunCollector.Instance.CollectionEnabled && !((Object)(object)__instance == (Object)null) && !(__instance.changeAmount >= 0f)) { Item component = ((Component)__instance).GetComponent<Item>(); Character val = (((Object)(object)component == (Object)null) ? null : component.holderCharacter); Character val2 = default(Character); if (!((Object)(object)component == (Object)null) && !((Object)(object)val == (Object)null) && val.IsLocal && !((Object)(object)val.refs.afflictions == (Object)null) && component.TryGetFeeder(ref val2) && !((Object)(object)val2 == (Object)null) && !((Object)(object)val2 == (Object)(object)val)) { __state = new ModifyStatusObservation { item = component, target = val, before = ReadStatus(val, __instance.statusType), statusSumBefore = val.refs.afflictions.statusSum, wasPassedOut = (val.data.passedOut || val.data.fullyPassedOut), status = __instance.statusType, actor = val2 }; } } } private static void Postfix(ModifyStatusObservation __state) { //IL_0030: Unknown result type (might be due to invalid IL or missing references) if (__state == null || (Object)(object)RunCollector.Instance == (Object)null || (Object)(object)__state.target.refs.afflictions == (Object)null) { return; } float after = ReadStatus(__state.target, __state.status); float num = AssistanceEvidence.HealedAmount(__state.before, after); if (!(num <= 0f)) { RunCollector.Instance.RecordItemOutcome("PlayerFriendHealed", __state.item, __state.actor, __state.target, __state.actor, num, "Observed status reduction", ((object)Unsafe.As<STATUSTYPE, STATUSTYPE>(ref __state.status)/*cast due to .constrained prefix*/).ToString(), __state.before); if (AssistanceEvidence.RestoredRecoveryEligibility(__state.wasPassedOut, __state.target.data.dead, __state.statusSumBefore, __state.target.refs.afflictions.statusSum, __state.target.data.petrifyAmount)) { RunCollector.Instance.RecordItemOutcome("PlayerLifeSaved", __state.item, __state.actor, __state.target, __state.actor, num, "Treatment crossed unconsciousness threshold; not a resurrection"); } } } private static float ReadStatus(Character target, STATUSTYPE status) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Invalid comparison between Unknown and I4 //IL_0010: Unknown result type (might be due to invalid IL or missing references) if ((int)status != 13) { return target.refs.afflictions.GetCurrentStatus(status); } return (float)target.data.petrifyAmount / 100f; } } [HarmonyPatch(typeof(RescueHook), "RPCA_RescueCharacter")] internal static class RescueHookOutcomePatch { private static void Postfix(RescueHook __instance, PhotonView characterView) { if ((Object)(object)RunCollector.Instance == (Object)null || (Object)(object)__instance == (Object)null || (Object)(object)characterView == (Object)null) { return; } Character component = ((Component)characterView).GetComponent<Character>(); Item component2 = ((Component)__instance).GetComponent<Item>(); if (!((Object)(object)component2 == (Object)null)) { Character trueHolderCharacter = component2.trueHolderCharacter; if ((Object)(object)component != (Object)null && (Object)(object)trueHolderCharacter != (Object)null && (Object)(object)trueHolderCharacter != (Object)(object)component) { RunCollector.Instance.RecordItemOutcome("PlayerRescuePulled", component2, trueHolderCharacter, component, trueHolderCharacter, 1f, "RescueHook target acquired"); } } } } [HarmonyPatch(typeof(Character), "RPCA_ReviveAtPosition")] internal static class ReviveObservedPatch { private static void Prefix(Character __instance, out bool __state) { __state = (Object)(object)__instance != (Object)null && (__instance.data.dead || __instance.data.passedOut || __instance.data.fullyPassedOut); } private static void Postfix(Character __instance, bool __state) { if (__state && (Object)(object)__instance != (Object)null && !__instance.data.dead && (Object)(object)RunCollector.Instance != (Object)null) { RunCollector.Instance.RecordSimpleEvent("PlayerRevivedObserved", __instance, EventSource.RemoteObserved, "RPCA_ReviveAtPosition; rescuer unknown"); } } } [HarmonyPatch(typeof(Item), "Consume")] internal static class ItemConsumedPatch { private static void Prefix(Item __instance, int consumerID, out RunCollector.ItemEventCapture __state) { __state = null; if (!((Object)(object)RunCollector.Instance == (Object)null) && !((Object)(object)__instance == (Object)null)) { Character val = null; PhotonView val2 = PhotonView.Find(consumerID); if ((Object)(object)val2 != (Object)null) { val = ((Component)val2).GetComponent<Character>(); } __state = RunCollector.Instance.CaptureItemEvent(__instance, val ?? __instance.holderCharacter, val); } } private static void Postfix(RunCollector.ItemEventCapture __state) { if ((Object)(object)RunCollector.Instance != (Object)null) { RunCollector.Instance.RecordCapturedItemEvent("ItemConsumed", __state); } } } [HarmonyPatch(typeof(Action_ReduceUses), "ReduceUsesRPC")] internal static class ItemUsesReducedPatch { private static void Postfix(Action_ReduceUses __instance) { if (!((Object)(object)RunCollector.Instance == (Object)null) && !((Object)(object)__instance == (Object)null)) { Item component = ((Component)__instance).GetComponent<Item>(); Character val = (((Object)(object)component == (Object)null) ? null : component.holderCharacter); RunCollector.Instance.RecordItemEvent("ItemUsesReduced", component, val, null, (!((Object)(object)val != (Object)null) || !val.IsLocal) ? EventSource.RemoteObserved : EventSource.LocalAuthoritative, "ReduceUsesRPC"); } } } [HarmonyPatch(typeof(Character), "UseStamina")] internal static class StaminaUsePatch { private static void Prefix(Character __instance, out float __state) { __state = (((Object)(object)__instance == (Object)null || (Object)(object)__instance.data == (Object)null) ? 0f : __instance.GetTotalStamina()); } private static void Postfix(Character __instance, float __state) { if (!((Object)(object)RunCollector.Instance == (Object)null) && !((Object)(object)__instance == (Object)null) && !((Object)(object)__instance.data == (Object)null)) { float totalStamina = __instance.GetTotalStamina(); float num = Mathf.Abs(totalStamina - __state); if (num > 0f && num + 1E-06f >= StateKeeperPlugin.StaminaEventThreshold) { RunCollector.Instance.RecordSimpleEvent("StaminaChanged", __instance, (!__instance.IsLocal) ? EventSource.RemoteObserved : EventSource.LocalAuthoritative, "UseStamina:" + (__state - totalStamina).ToString("R")); } } } } internal sealed class ItemAttributionEngine { private sealed class Frame { internal float time; internal PlayerTelemetry value; } private sealed class Change { internal string channel; internal float before; internal float after; internal float start; internal float time; internal int player; internal int epoch; internal PlayerTelemetry beforePlayer; internal PlayerTelemetry afterPlayer; internal Dictionary<int, PlayerTelemetry> players; } private sealed class Candidate { internal AnalysisItemObservation row; internal List<AnalysisItemObservation> rows = new List<AnalysisItemObservation>(); internal List<AnalysisEffectRule> rules; internal List<string> reasons = new List<string>(); internal List<AnalysisEffectChange> effects = new List<AnalysisEffectChange>(); internal HashSet<int> recipients = new HashSet<int>(); internal Dictionary<string, float> spent = new Dictionary<string, float>(); internal List<StatsEvent> proofs = new List<StatsEvent>(); internal Dictionary<string, float> endedAt = new Dictionary<string, float>(); internal float lateEnd; internal int actor; internal int target; internal float time; internal float lastEvidence; internal float end = float.PositiveInfinity; internal bool weak; internal bool explicitTarget; internal bool passive; } private sealed class Match { internal Candidate candidate; internal AnalysisEffectRule rule; internal List<string> reasons = new List<string>(); } internal const float Lead = 0.75f; internal const float Lag = 1.5f; private const float Settle = 2f; private readonly AnalysisResult _result; private readonly RunRecord _record; private readonly RecordedItemRules _rules; private readonly List<Candidate> _candidates = new List<Candidate>(); private readonly List<Change> _changes = new List<Change>(); private readonly Dictionary<int, Frame> _previous = new Dictionary<int, Frame>(); private readonly Dictionary<int, float> _barriers = new Dictionary<int, float>(); private readonly HashSet<int> _transformed = new HashSet<int>(); private readonly Dictionary<string, Candidate> _passive = new Dictionary<string, Candidate>(); private readonly Dictionary<string, AnalysisEffectChange> _lastEffects = new Dictionary<string, AnalysisEffectChange>(); private float _lastTime = float.NaN; private float _environmentTime = float.NegativeInfinity; private int _epoch; private bool _trackingLimited; internal int PeakPendingChanges { get; private set; } internal ItemAttributionEngine(RunRecord record, AnalysisResult result) { _record = record; _result = result; _rules = new RecordedItemRules(record); } internal void Observe(AnalysisItemObservation row) { bool flag = row.kind == "ItemRemovedObserved"; bool flag2 = row.kind == "ItemConsumed" || row.kind == "ItemFedToPlayer" || row.kind == "ItemPrimaryCastFinished" || row.kind == "ItemSecondaryCastFinished"; bool flag3 = row.kind == "ResourceChanged" && ((new string[4] { "uses", "fuel", "useRemaining", "petterItemUses" }.Contains(row.resourceKey) && row.value < row.previousValue) || (row.resourceKey == "used" && row.previousValue == 0f && row.value == 1f)); if (row.kind == "ItemMoved" || row.kind == "ItemTransferObserved" || row.kind == "ItemAppeared") { foreach (Candidate item in _candidates.Where((Candidate c) => c.weak && c.row.itemGuid == row.itemGuid && row.time >= c.time && row.time - c.time <= 2f)) { item.reasons.Add("MovedOrTransferred"); } } if (!flag2 && !flag3 && !flag) { return; } if ((flag3 || flag) && row.actorPlayerIndex < 0) { row.actorPlayerIndex = row.playerIndex; row.actorInferred = true; } Candidate candidate = (string.IsNullOrEmpty(row.itemGuid) ? null : _candidates.LastOrDefault((Candidate c) => !c.passive && c.row.itemGuid == row.itemGuid && c.row.epoch == row.epoch && Math.Abs(c.time - row.time) <= 0.75f && !c.rows.Any((AnalysisItemObservation r) => r.kind == row.kind && r.resourceKey == row.resourceKey) && (c.target < 0 || row.targetPlayerIndex < 0 || c.target == row.targetPlayerIndex) && (c.actor < 0 || row.actorPlayerIndex < 0 || c.actor == row.actorPlayerIndex))); if (candidate != null) { candidate.rows.Add(row); row.attributionGroupId = candidate.row.attributionGroupId; if (row.targetPlayerIndex >= 0) { candidate.target = row.targetPlayerIndex; } if (row.actorPlayerIndex >= 0) { candidate.actor = row.actorPlayerIndex; } candidate.weak &= flag; candidate.lastEvidence = Math.Max(candidate.lastEvidence, row.time); if (row.detail == "ConflictingGuid") { candidate.reasons.Add("ConflictingGuid"); } if (row.cookedAmount.HasValue && !candidate.row.cookedAmount.HasValue) { candidate.row.cookedAmount = row.cookedAmount; List<AnalysisEffectRule> collection = candidate.rules.Where((AnalysisEffectRule r) => r.source == "DirectStatusObservation").ToList(); candidate.rules = _rules.Compile(candidate.row, candidate.reasons); candidate.rules.AddRange(collection); } if (row.kind == "ItemConsumed" || row.kind == "ItemFedToPlayer") { candidate.explicitTarget = true; } return; } Candidate candidate2 = new Candidate { row = row, actor = row.actorPlayerIndex, target = row.targetPlayerIndex, time = row.time, lastEvidence = row.time, weak = flag, explicitTarget = (row.kind == "ItemConsumed" || row.kind == "ItemFedToPlayer") }; candidate2.rows.Add(row); row.attributionGroupId = row.observationId; candidate2.rules = _rules.Compile(row, candidate2.reasons); if (string.IsNullOrEmpty(row.itemGuid)) { candidate2.reasons.Add("InstanceUnknown"); } if (row.detail == "ConflictingGuid") { candidate2.reasons.Add("ConflictingGuid"); } _candidates.Add(candidate2); if (_candidates.Count > 1024) { _trackingLimited = true; FinishCandidate(_candidates[0], "TrackingLimitReached"); if (!_result.quality.warnings.Contains("Attribution candidate limit reached")) { _result.quality.warnings.Add("Attribution candidate limit reached"); } } } internal void Inventory(InventorySnapshot snapshot, int epoch, EvidenceReference evidence = null) { _epoch = epoch; List<ItemSnapshot> source = (snapshot.slots ?? new List<ItemSnapshot>()).Where((ItemSnapshot i) => i != null && !string.IsNullOrEmpty(i.guid)).ToList(); KeyValuePair<string, Candidate>[] array = _passive.Where((KeyValuePair<string, Candidate> p) => p.Value.actor == snapshot.playerIndex).ToArray(); for (int num = 0; num < array.Length; num++) { KeyValuePair<string, Candidate> entry = array[num]; if (!source.Any((ItemSnapshot i) => i.guid == entry.Value.row.itemGuid && PocketEnabled(i))) { entry.Value.end = snapshot.time; _passive.Remove(entry.Key); } } foreach (ItemSnapshot item in source.Where(PocketEnabled)) { string key = snapshot.playerIndex + ":" + item.guid; if (!_passive.ContainsKey(key)) { ItemDefinition definition = RunAnalysisEngine.Definition(_record.definitions ?? new List<ItemDefinition>(), item.itemId, item.prefabName); List<AnalysisEffectRule> list = _rules.Passive(definition); if (list.Count != 0) { AnalysisItemObservation analysisItemObservation = new AnalysisItemObservation { evidence = evidence, observationId = _result.items.Count + 1, time = snapshot.time, epoch = epoch, playerIndex = snapshot.playerIndex, actorPlayerIndex = snapshot.playerIndex, actorInferred = true, targetPlayerIndex = snapshot.playerIndex, itemId = item.itemId, itemGuid = item.guid, itemName = item.itemName, prefabName = item.prefabName, kind = "PassiveEffectObserved", confidence = "Likely" }; analysisItemObservation.attributionGroupId = analysisItemObservation.observationId; _result.items.Add(analysisItemObservation); Candidate candidate = new Candidate { row = analysisItemObservation, time = snapshot.time, actor = snapshot.playerIndex, target = snapshot.playerIndex, passive = true, rules = list }; candidate.rows.Add(analysisItemObservation); _passive[key] = candidate; _candidates.Add(candidate); } } } } private static bool PocketEnabled(ItemSnapshot item) { if (item.hasPowerEnabled && item.powerEnabled && item.slot != null) { if (!item.slot.StartsWith("main:", StringComparison.Ordinal)) { return item.slot == "temporary"; } return true; } return false; } internal void Event(StatsEvent e, int epoch) { if (e == null || !Finite(e.time)) { return; } _epoch = epoch; if (e.type == "MountainSegmentReached" || e.type == "CampfireLit" || e.type == "CampfireRested") { _environmentTime = e.time; } if (e.type == "ObservationContextChanged") { if (((int)e.value & 6) != 0) { _transformed.Add(e.subjectPlayerIndex); } else { _transformed.Remove(e.subjectPlayerIndex); } Interrupt(e.subjectPlayerIndex, e.time); } if (e.type == "PlayerDied" || e.type == "PlayerRevivedObserved" || e.type == "PlayerTeleported") { Interrupt(e.subjectPlayerIndex, e.time); } } internal void DirectTreatment(StatsEvent e, EvidenceReference evidence = null) { if (e.actorPlayerIndex.HasValue && e.actorPlayerIndex.Value != e.targetPlayerIndex && e.targetPlayerIndex >= 0 && !string.IsNullOrEmpty(e.resourceKey) && Finite(e.value) && Finite(e.previousValue) && !(e.value <= 0f) && !(e.previousValue < e.value)) { AnalysisItemObservation analysisItemObservation = new AnalysisItemObservation { evidence = evidence, observationId = _result.items.Count + 1, time = e.time, epoch = _epoch, itemId = e.itemId, itemName = e.itemName, itemGuid = e.itemGuid, prefabName = e.definitionKey, kind = "PlayerFriendHealed", confidence = "Certain", playerIndex = e.actorPlayerIndex.Value, actorPlayerIndex = e.actorPlayerIndex.Value, targetPlayerIndex = e.targetPlayerIndex }; Candidate candidate = _candidates.LastOrDefault((Candidate p) => !string.IsNullOrEmpty(e.itemGuid) && p.row.itemGuid == e.itemGuid && Math.Abs(p.time - e.time) <= 0.75f && (p.actor < 0 || p.actor == e.actorPlayerIndex) && (p.target < 0 || p.target == e.targetPlayerIndex)); if (candidate == null) { candidate = new Candidate { row = analysisItemObservation, actor = analysisItemObservation.actorPlayerIndex, target = analysisItemObservation.targetPlayerIndex, time = analysisItemObservation.time, lastEvidence = analysisItemObservation.time, rules = new List<AnalysisEffectRule>(), explicitTarget = true }; analysisItemObservation.attributionGroupId = analysisItemObservation.observationId; _candidates.Add(candidate); } analysisItemObservation.attributionGroupId = candidate.row.attributionGroupId; _result.items.Add(analysisItemObservation); candidate.rows.Add(analysisItemObservation); candidate.actor = analysisItemObservation.actorPlayerIndex; candidate.target = analysisItemObservation.targetPlayerIndex; candidate.explicitTarget = true; candidate.weak = false; candidate.proofs.Add(e); candidate.rules.Add(new AnalysisEffectRule { channel = e.resourceKey, amount = 0f - e.value, direction = -1, source = "DirectStatusObservation" }); AnalysisEffectChange effect = new AnalysisEffectChange { epoch = _epoch, channel = e.resourceKey, playerIndex = e.targetPlayerIndex, previousValue = e.previousValue, value = e.previousValue - e.value, cumulativeDelta = 0f - e.value, beforeTime = e.time, afterTime = e.time, matchesTheory = true, attribution = "Certain", reasons = new List<string> { "DirectStatusObservation" }, candidateGroupIds = new List<int> { analysisItemObservation.attributionGroupId } }; candidate.effects.Add(StoreEffect(effect, candidate)); } } internal void Sample(PlayerSample sample, int epoch) { _epoch = epoch; if (!Finite(sample.time)) { return; } if (Finite(_lastTime) && (sample.time <= _lastTime || sample.time - _lastTime > 1f)) { Reset(); } Dictionary<int, PlayerTelemetry> current = (from playerTelemetry in sample.players ?? new List<PlayerTelemetry>() where playerTelemetry != null group playerTelemetry by playerTelemetry.playerIndex).ToDictionary((IGrouping<int, PlayerTelemetry> g) => g.Key, (IGrouping<int, PlayerTelemetry> g) => g.Last()); int[] array = _previous.Keys.Where((int id) => !current.ContainsKey(id)).ToArray(); foreach (int player in array) { Interrupt(player, sample.time); } foreach (KeyValuePair<int, PlayerTelemetry> item in current) { PlayerTelemetry p = item.Value; if (!_previous.TryGetValue(item.Key, out var value)) { continue; } float num2 = sample.time - value.time; if (num2 <= 0f || num2 > 1f || p.dead || value.value.dead) { Interrupt(item.Key, sample.time); continue; } if (!RunAnalysisEngine.ValidStamina(p) || !RunAnalysisEngine.ValidStamina(value.value)) { Interrupt(item.Key, sample.time); continue; } if (RunAnalysisEngine.ValidPosition(p) && RunAnalysisEngine.ValidPosition(value.value) && WorldDistance(p, value.value) > Math.Max(20f, 60f * num2)) { Interrupt(item.Key, sample.time); continue; } PlayerTelemetry value2 = value.value; foreach (Candidate item2 in _candidates.Where((Candidate candidate) => candidate.target < 0 || candidate.target == p.playerIndex)) { foreach (string name in (from r in item2.rules select r.afterAfflictionEnds into n where n != null select n).Distinct()) { int num3 = Array.IndexOf(_result.afflictionTypeOrder, name); bool num4 = num3 >= 0 && (value2.activeAfflictionTypes?.Contains(num3) ?? false); bool flag = num3 >= 0 && (p.activeAfflictionTypes?.Contains(num3) ?? false); if (flag) { item2.lateEnd = Math.Max(item2.lateEnd, sample.time + 1.5f + 2f); } if (num4 && !flag) { item2.endedAt[p.playerIndex + ":" + name] = sample.time; item2.lateEnd = Math.Max(item2.lateEnd, sample.time + item2.rules.Where((AnalysisEffectRule r) => r.afterAfflictionEnds == name).Max((AnalysisEffectRule r) => r.delay + r.duration) + 1.5f); } } } ChangeValue("RegularStamina", value2.regularStamina, p.regularStamina, value, p, sample.time, current); ChangeValue("ExtraStamina", value2.extraStamina, p.extraStamina, value, p, sample.time, current); if (value2.petrifyAmount.HasValue && p.petrifyAmount.HasValue) { ChangeValue("Petrify", (float)value2.petrifyAmount.Value / 100f, (float)p.petrifyAmount.Value / 100f, value, p, sample.time, current); } if (value2.statuses != null && p.statuses != null) { for (int num5 = 0; num5 < Math.Min(_result.statusTypeOrder.Length, Math.Min(value2.statuses.Length, p.statuses.Length)); num5++) { if (_result.statusTypeOrder[num5] != "Petrify") { ChangeValue(_result.statusTypeOrder[num5], value2.statuses[num5], p.statuses[num5], value, p, sample.time, current); } } } if (value2.activeAfflictionTypes != null && p.activeAfflictionTypes != null) { foreach (int item3 in value2.activeAfflictionTypes.Union(p.activeAfflictionTypes).Distinct()) { if (item3 >= 0 && item3 < _result.afflictionTypeOrder.Length) { ChangeValue("Affliction:" + _result.afflictionTypeOrder[item3], value2.activeAfflictionTypes.Contains(item3) ? 1 : 0, p.activeAfflictionTypes.Contains(item3) ? 1 : 0, value, p, sample.time, current); } } } ChangeValue("Unconscious", (value2.passedOut || value2.fullyPassedOut) ? 1 : 0, (p.passedOut || p.fullyPassedOut) ? 1 : 0, value, p, sample.time, current); } _previous.Clear(); foreach (KeyValuePair<int, PlayerTelemetry> item4 in current) { _previous[item4.Key] = new Frame { time = sample.time, value = item4.Value }; } _lastTime = sample.time; PeakPendingChanges = Math.Max(PeakPendingChanges, _changes.Count); SettleChanges(sample.time - 2f, interrupted: false); Candidate[] array2 = _candidates.Where((Candidate c2) => sample.time > End(c2) + 1.5f + 2f).ToArray(); foreach (Candidate c in array2) { FinishCandidate(c, null); } } private void ChangeValue(string channel, float before, float after, Frame frame, PlayerTelemetry player, float time, Dictionary<int, PlayerTelemetry> players) { if (Finite(before) && Finite(after) && !(Math.Abs(after - before) < 0.002f)) { _changes.Add(new Change { channel = channel, before = before, after = after, start = frame.time, time = time, player = player.playerIndex, beforePlayer = frame.value, afterPlayer = player, players = players, epoch = _epoch }); } } private void SettleChanges(float deadline, bool interrupted) { Change[] array = _changes.Where((Change c) => c.time <= deadline).ToArray(); foreach (Change change in array) { List<Match> list = new List<Match>(); List<Candidate> list2 = new List<Candidate>(); foreach (Candidate candidate in _candidates) { if (change.epoch != candidate.row.epoch || change.time < candidate.time - 0.75f || change.start > End(candidate) + 1.5f) { continue; } if (candidate.explicitTarget && candidate.target == change.player && change.time <= candidate.time + 1.5f) { list2.Add(candidate); } if (change.channel == "Unconscious" && change.after < change.before && (candidate.target == change.player || candidate.recipients.Contains(change.player))) { list2.Add(candidate); } foreach (AnalysisEffectRule rule in candidate.rules) { if (rule.channel != change.channel || rule.direction == 0 || Math.Sign(change.after - change.before) != rule.direction || !TriggerMatches(candidate, rule)) { continue; } float num2 = candidate.time + rule.delay; float num3 = ((!rule.continuous) ? num2 : (candidate.passive ? candidate.end : (num2 + rule.duration))); if (rule.afterAfflictionEnds != null) { if (!candidate.endedAt.TryGetValue(change.player + ":" + rule.afterAfflictionEnds, out var value)) { continue; } num2 = value + rule.delay; num3 = num2 + rule.duration; } if (change.time < num2 - 0.75f || change.time > num3 + 1.5f) { continue; } Match match = new Match { candidate = candidate, rule = rule }; if (!RecipientMatches(match, change)) { continue; } if (rule.uncertain) { match.reasons.Add("IncompleteRuleParameters"); } if (!string.IsNullOrEmpty(rule.requiredAffliction)) { bool num4 = rule.requiredAffliction.StartsWith("Ended:", StringComparison.Ordinal); int num5 = Array.IndexOf(value: num4 ? rule.requiredAffliction.Substring(6) : rule.requiredAffliction, array: _result.afflictionTypeOrder); bool flag = num5 >= 0 && (change.beforePlayer.activeAfflictionTypes?.Contains(num5) ?? false); bool flag2 = num5 >= 0 && (change.afterPlayer.activeAfflictionTypes?.Contains(num5) ?? false); bool num6; if (!num4) { if (flag) { goto IL_03df; } num6 = !flag2; } else { num6 = !flag || flag2; } if (num6) { match.reasons.Add("RequiredAfflictionNotObserved"); } } goto IL_03df; IL_03df: CheckAmount(match, change); if (_transformed.Contains(change.player) || _transformed.Contains(candidate.actor)) { match.reasons.Add("TransformedCharacter"); } if (_barriers.TryGetValue(change.player, out var value2) && value2 >= change.start - 1.5f && value2 <= change.time + 2f) { match.reasons.Add("DeathRevivalOrDiscontinuity"); } if (candidate.weak) { match.reasons.Add("DisappearanceOnly"); } match.reasons.AddRange(candidate.reasons.Where(delegate(string r) { switch (r) { default: return r == "InstanceUnknown"; case "MovedOrTransferred": case "ConflictingGuid": case "ObservationInterrupted": return true; } })); if (Natural(change)) { match.reasons.Add("NaturalRecoveryPossible"); } if (change.time >= _environmentTime && change.time - _environmentTime <= 5f && change.after < change.before) { match.reasons.Add("CheckpointRecoveryPossible"); } if (change.after < change.before && _changes.Any((Change other) => other.player != change.player && other.channel == change.channel && other.after < other.before && Math.Abs(other.time - change.time) <= 0.5f)) { match.reasons.Add("SharedEnvironmentRecoveryPossible"); } if (interrupted) { match.reasons.Add("IncompleteWindow"); } if (_trackingLimited) { match.reasons.Add("TrackingLimitReached"); } list.Add(match); } } List<Match> list3 = list.Where((Match m) => m.candidate.proofs.Any((StatsEvent p) => p.resourceKey == change.channel && p.targetPlayerIndex == change.player && Math.Abs(p.time - change.time) <= 1.5f && Math.Abs(p.previousValue - change.before) <= 0.002f && Math.Abs(p.previousValue - p.value - change.after) <= 0.002f)).ToList(); if (list3.Count > 0) { list = list3; foreach (Match item2 in list) { item2.reasons.Clear(); } } List<Candidate> candidates = list.Select((Match m) => m.candidate).Distinct().ToList(); List<string> list4 = list.SelectMany((Match m) => m.reasons).Distinct().ToList(); if (list3.Count == 0) { List<Candidate> list5 = list2.Where((Candidate c) => !candidates.Contains(c) && !c.weak && (c.rules.Count == 0 || c.reasons.Any((string r) => r.StartsWith("UnsupportedAction:", StringComparison.Ordinal)))).Distinct().ToList(); if (list5.Count > 0 && candidates.Count > 0) { list4.Add("UnknownItemCompetition"); candidates.AddRange(list5); } } if (candidates.Count > 1) { list4.Add("CompetingItems"); } bool flag3 = candidates.Count == 1 && list4.Count == 0; AnalysisEffectChange analysisEffectChange = new AnalysisEffectChange { channel = change.channel, playerIndex = change.player, previousValue = change.before, value = change.after, beforeTime = change.start, afterTime = change.time, epoch = change.epoch, cumulativeDelta = change.after - change.before, matchesTheory = (list.Count > 0), attribution = ((!flag3) ? "Ambiguous" : ((list3.Count > 0) ? "Certain" : "Likely")), reasons = list4, candidateGroupIds = candidates.Select((Candidate c) => c.row.attributionGroupId).ToList() }; if (analysisEffectChange.reasons.Count == 0 && !flag3) { analysisEffectChange.reasons.Add((list2.Count > 0) ? "ObservedWithoutMatchingRule" : "NoItemCandidate"); } foreach (Candidate item3 in candidates.Union(list2)) { if (candidates.Contains(item3)) { item3.recipients.Add(change.player); } AnalysisEffectChange item = StoreEffect(analysisEffectChange, item3); if (!item3.effects.Contains(item)) { item3.effects.Add(item); } } foreach (Match item4 in from m in list where !m.rule.continuous group m by m.candidate.row.attributionGroupId + ":" + m.rule.source + ":" + m.rule.channel into g select g.First()) { string key = change.player + ":" + item4.rule.source + ":" + item4.rule.channel; item4.candidate.spent.TryGetValue(key, out var value3); item4.candidate.spent[key] = value3 + Math.Abs(change.after - change.before); } if (candidates.Count == 0 && list2.Count == 0 && change.channel != "RegularStamina") { StoreEffect(analysisEffectChange, null); } } _changes.RemoveAll((Change c) => c.time <= deadline); } private AnalysisEffectChange StoreEffect(AnalysisEffectChange effect, Candidate candidate) { string key = (candidate?.row.attributionGroupId ?? 0) + ":" + effect.playerIndex + ":" + effect.channel + ":" + effect.attribution + ":" + string.Join(",", effect.reasons) + ":" + string.Join(",", effect.candidateGroupIds); if (_lastEffects.TryGetValue(key, out var value) && value.epoch == effect.epoch && effect.beforeTime >= value.afterTime && effect.beforeTime - value.afterTime <= 0.6f && Math.Sign(value.cumulativeDelta) == Math.Sign(effect.cumulativeDelta) && effect.afterTime - value.beforeTime <= 10f && !effect.channel.StartsWith("Affliction:", StringComparison.Ordinal)) { value.afterTime = effect.afterTime; value.value = effect.value; value.cumulativeDelta += effect.cumulativeDelta; return value; } effect.effectId = _result.effects.Count + 1; AnalysisEffectChange analysisEffectChange = new AnalysisEffectChange { effectId = effect.effectId, epoch = effect.epoch, channel = effect.channel, playerIndex = effect.playerIndex, previousValue = effect.previousValue, value = effect.value, beforeTime = effect.beforeTime, afterTime = effect.afterTime, cumulativeDelta = effect.cumulativeDelta, matchesTheory = effect.matchesTheory, attribution = effect.attribution, reasons = new List<string>(effect.reasons), candidateGroupIds = new List<int>(effect.candidateGroupIds) }; _result.effects.Add(analysisEffectChange); _lastEffects[key] = analysisEffectChange; if (_lastEffects.Count > 4096) { _lastEffects.Clear(); } return analysisEffectChange; } private bool RecipientMatches(Match match, Change change) { Candidate candidate = match.candidate; AnalysisEffectRule rule = match.rule; bool flag = rule.scope == "Area" || rule.scope == "PlayerArea"; if (rule.scope == "Actor") { if (candidate.actor >= 0) { return candidate.actor == change.player; } return false; } if (!flag && candidate.target >= 0) { return candidate.target == change.player; } if (!flag && candidate.actor == change.player) { return true; } if (candidate.actor < 0 || !change.players.TryGetValue(candidate.actor, out var value) || !RunAnalysisEngine.ValidPosition(value) || !RunAnalysisEngine.ValidPosition(change.afterPlayer)) { match.reasons.Add("SpatialEvidenceUnavailable"); return true; } float num = WorldDistance(value, change.afterPlayer); if (flag) { if (rule.ignoreCaster && candidate.actor == change.player) { return false; } if (rule.radius.HasValue && num > rule.radius.Value + 1f) { return false; } if (rule.scope == "Area") { match.reasons.Add("ItemOriginNotRecorded"); } if (rule.radius.HasValue && num > rule.radius.Value) { match.reasons.Add("SpatialBoundaryUncertain"); } return true; } return num <= 3f; } private void CheckAmount(Match match, Change change) { AnalysisEffectRule rule = match.rule; if (!rule.amount.HasValue) { match.reasons.Add("AmountNotRecorded"); return; } float num = Math.Abs(change.after - change.before); float num2 = Math.Abs(rule.amount.Value); if (rule.continuous) { num2 *= Math.Max(0.5f, change.time - change.start); } else { match.candidate.spent.TryGetValue(change.player + ":" + rule.source + ":" + rule.channel, out var value); num2 = Math.Max(0f, num2 - value); } if (rule.direction < 0) { num2 = Math.Min(num2, Math.Max(0f, change.before)); } else if (rule.channel == "ExtraStamina") { float num3 = 1f - (float)change.beforePlayer.petrifyAmount.GetValueOrDefault() / 100f; num2 = Math.Min(num2, Math.Max(0f, num3 - change.before)); } if (rule.budget.HasValue && !rule.continuous) { float num4 = match.candidate.rules.Where((AnalysisEffectRule r) => r.budgetGroup == rule.budgetGroup && r.priority < rule.priority).Sum((AnalysisEffectRule r) => Status(change.beforePlayer, r.channel)); num2 = Math.Min(num2, Math.Max(0f, rule.budget.Value - num4)); } if (rule.budget.HasValue && rule.continuous && match.candidate.rules.Any((AnalysisEffectRule r) => r.budgetGroup == rule.budgetGroup && r.priority < rule.priority && Status(change.beforePlayer, r.channel) >= 0.025f)) { match.reasons.Add("HealingPriorityMismatch"); } if (rule.channel.StartsWith("Affliction:", StringComparison.Ordinal)) { num2 = 1f; } if (num > num2 + 0.026f) { match.reasons.Add("AmountOrBudgetMismatch"); } } private float Status(PlayerTelemetry player, string channel) { int num = Array.IndexOf(_result.statusTypeOrder, channel); if (num < 0 || player.statuses == null || num >= player.statuses.Length || !Finite(player.statuses[num])) { return 0f; } return Math.Max(0f, player.statuses[num]); } private static bool TriggerMatches(Candidate candidate, AnalysisEffectRule rule) { if (candidate.passive || string.IsNullOrEmpty(rule.trigger)) { return true; } string[] triggers = rule.trigger.Split(new char[1] { ',' }); return candidate.rows.Any((AnalysisItemObservation row) => (row.kind == "ItemConsumed" && (triggers.Contains("OnConsumed") || triggers.Contains("OnCastFinished"))) || (row.kind == "ItemFedToPlayer" && (triggers.Contains("OnConsumed") || triggers.Contains("OnCastFinished"))) || (row.kind == "ItemPrimaryCastFinished" && triggers.Contains("OnCastFinished")) || (row.kind == "ItemSecondaryCastFinished" && triggers.Contains("OnSecondaryCastFinished")) || ((row.kind == "ResourceChanged" || row.kind == "ItemRemovedObserved") && triggers.Any((string t) => t == "OnConsumed" || t == "OnCastFinished" || t == "OnSecondaryCastFinished"))); } private static bool Natural(Change change) { if (!(change.channel == "RegularStamina")) { if (change.after < change.before) { return new string[6] { "Poison", "Drowsy", "Hot", "Cold", "Spores", "Thorns" }.Contains(change.channel); } return false; } return true; } private static float End(Candidate c) { if (!c.passive) { return Math.Max(c.lateEnd, c.time + Math.Max(4f, c.rules.Select((AnalysisEffectRule r) => r.delay + r.duration).DefaultIfEmpty(0f).Max())); } return c.end; } internal void Interrupt(int player, float time) { _barriers[player] = time; _previous.Remove(player); foreach (Candidate item in _candidates.Where((Candidate c) => c.actor == player || c.target == player)) { item.reasons.Add("ObservationInterrupted"); if (item.passive) { item.end = time; } } string[] array = (from p in _passive where p.Value.actor == player select p.Key).ToArray(); foreach (string key in array) { _passive.Remove(key); } } private void FinishCandidate(Candidate c, string reason) { if (reason != null) { c.reasons.Add(reason); } bool flag = c.target < 0 && c.recipients.Count > 1 && !c.rules.Any((AnalysisEffectRule r) => r.scope == "Area" || r.scope == "PlayerArea"); bool flag2 = c.reasons.Contains("ObservationInterrupted") || c.reasons.Contains("MovedOrTransferred") || c.reasons.Contains("IncompleteWindow") || c.reasons.Contains("ConflictingGuid") || c.reasons.Contains("TrackingLimitReached"); if (flag || flag2) { foreach (AnalysisEffectChange item in c.effects.Where((AnalysisEffectChange e) => e.attribution != "Certain")) { item.attribution = "Ambiguous"; item.reasons.Add(flag ? "CompetingRecipients" : "ObservationInterrupted"); } } int num = ((c.target >= 0) ? c.target : ((c.recipients.Count == 1) ? c.recipients.First() : (-1))); foreach (AnalysisItemObservation row in c.rows) { row.rules = c.rules; row.endTime = ((!c.passive) ? c.lastEvidence : (Finite(c.end) ? c.end : _lastTime)); row.observedEffects = c.effects; row.attributionReasons = c.reasons.Concat(c.effects.SelectMany((AnalysisEffectChange e) => e.reasons)).Distinct().ToList(); row.attribution = (c.effects.Any((AnalysisEffectChange e) => e.attribution == "Certain" && e.candidateGroupIds.Contains(c.row.attributionGroupId)) ? "Certain" : (c.effects.Any((AnalysisEffectChange e) => e.attribution == "Likely" && e.candidateGroupIds.Contains(c.row.attributionGroupId)) ? "Likely" : "Ambiguous")); if (row.attributionReasons.Count == 0) { row.attributionReasons.Add((c.rules.Count == 0) ? "NoObservableEffectRule" : ((c.effects.Count == 0) ? "NoObservedChange" : "CompatibleObservedChange")); } row.attributionReason = row.attributionReasons[0]; if (row.targetPlayerIndex < 0 && num >= 0) { row.targetPlayerIndex = num; } row.possibleHelp = c.actor >= 0 && c.effects.Any((AnalysisEffectChange e) => e.playerIndex != c.actor && e.attribution != "Ambiguous" && Helpful(e)); row.possibleRescue = row.possibleHelp && c.effects.Any((AnalysisEffectChange recovery) => recovery.playerIndex != c.actor && recovery.channel == "Unconscious" && recovery.cumulativeDelta < 0f && !recovery.reasons.Contains("DeathRevivalOrDiscontinuity") && c.effects.Any((AnalysisEffectChange healing) => healing.playerIndex == recovery.playerIndex && healing.attribution != "Ambiguous" && healing.cumulativeDelta < 0f && Helpful(healing) && healing.afterTime <= recovery.afterTime && recovery.afterTime - healing.afterTime <= 2f)); } _candidates.Remove(c); string[] array = (from p in _passive where p.Value == c select p.Key).ToArray(); foreach (string key in array) { _passive.Remove(key); } } internal void Reset() { SettleChanges(float.MaxValue, interrupted: true); Candidate[] array = _candidates.ToArray(); foreach (Candidate c in array) { FinishCandidate(c, "ObservationInterrupted"); } _changes.Clear(); _previous.Clear(); _passive.Clear(); _lastEffects.Clear(); _barriers.Clear(); _trackingLimited = false; _environmentTime = float.NegativeInfinity; _lastTime = float.NaN; } internal void Finish() { SettleChanges(float.MaxValue, interrupted: true); Candidate[] array = _candidates.ToArray(); foreach (Candidate candidate in array) { FinishCandidate(candidate, (!candidate.passive && _lastTime < End(candidate) + 1.5f) ? "IncompleteWindow" : null); } } private static bool Finite(float value) { return RunAnalysisEngine.Finite(value); } private static bool Helpful(AnalysisEffectChange effect) { if (effect.channel == "ExtraStamina" && effect.cumulativeDelta > 0f) { return true; } if (effect.cumulativeDelta < 0f) { return new string[12] { "Injury", "Hunger", "Poison", "Cold", "Hot", "Drowsy", "Spores", "Petrify", "Curse", "Web", "Thorns", "Arrow" }.Contains(effect.channel); } if (effect.channel.StartsWith("Affliction:", StringComparison.Ordinal)) { return new string[10] { "InfiniteStamina", "FasterBoi", "Invincibility", "LowGravity", "NoHunger", "Sunscreen", "ClimbingChalk", "HealAll", "DoubleJumpAmulet", "BingBongShield" }.Contains(effect.channel.Substring(11)); } return false; } private static float WorldDistance(PlayerTelemetry a, PlayerTelemetry b) { double num = a.positionX - b.positionX; double num2 = a.positionY - b.positionY; double num3 = a.positionZ - b.positionZ; return (float)Math.Sqrt(num * num + num2 * num2 + num3 * num3); } } internal static class ItemDefinitionCatalog { internal static List<ItemDefinition> Capture() { List<ItemDefinition> list = new List<ItemDefinition>(); try { if ((Object)(object)SingletonAsset<ItemDatabase>.Instance == (Object)null || SingletonAsset<ItemDatabase>.Instance.itemLookup == null) { return list; } foreach (KeyValuePair<ushort, Item> item in SingletonAsset<ItemDatabase>.Instance.itemLookup) { if ((Object)(object)item.Value == (Object)null) { continue; } try { list.Add(Capture(item.Key, item.Value)); } catch (Exception ex) { if (StateKeeperPlugin.IsDebugLogging) { StateKeeperPlugin.LogException("capture item definition " + item.Key, ex); } } } } catch (Exception ex2) { if (StateKeeperPlugin.IsDebugLogging) { StateKeeperPlugin.LogException("capture item definitions", ex2); } } return list.OrderBy((ItemDefinition definition) => definition.itemId).ToList(); } private unsafe static ItemDefinition Capture(ushort id, Item item) { ItemDefinition itemDefinition = new ItemDefinition { itemId = id, itemName = ((item.UIData == null) ? ((Object)item).name : item.UIData.itemName), prefabName = (((Object)(object)((Component)item).gameObject == (Object)null) ? ((Object)item).name : ((Object)((Component)item).gameObject).name), totalUses = item.totalUses, usingTimePrimary = item.usingTimePrimary, itemTags = (from ItemTags tag in Enum.GetValues(typeof(ItemTags)) where (int)tag != 0 && ((Enum)item.itemTags).HasFlag((Enum)(object)tag) select ((object)(*(ItemTags*)(&tag))/*cast due to .constrained prefix*/).ToString()).ToList() }; ItemActionBase[] componentsInChildren = ((Component)item).GetComponentsInChildren<ItemActionBase>(false); foreach (ItemActionBase val in componentsInChildren) { if (!((Object)(object)val == (Object)null)) { List<ItemDefinitionParameter> parameters = DescribePublicParameters(val); itemDefinition.actions.Add(new ItemDefinitionAction { typeName = ((object)val).GetType().FullName, trigger = DescribeTriggers(val), parameterSummary = DescribeParameters(parameters), parameters = parameters }); AddEffectHints(itemDefinition.effectHints, val); } } ItemComponent[] componentsInChildren2 = ((Component)item).GetComponentsInChildren<ItemComponent>(false); foreach (ItemComponent val2 in componentsInChildren2) { if (!((Object)(object)val2 == (Object)null)) { List<ItemDefinitionParameter> parameters2 = DescribePublicParameters(val2); ItemCooking val3 = (ItemCooking)(object)((val2 is ItemCooking) ? val2 : null); if (val3 != null) { itemDefinition.cookingRules.Add(new ItemDefinitionCooking { typeName = ((object)val3).GetType().FullName, parameterSummary = DescribeParameters(parameters2), parameters = parameters2 }); } else { itemDefinition.components.Add(new ItemDefinitionComponent { typeName = ((object)val2).GetType().FullName, parameterSummary = DescribeParameters(parameters2), parameters = parameters2 }); } } } return itemDefinition; } private static string DescribeTriggers(ItemActionBase action) { if (!(action is ItemAction)) { return string.Empty; } List<string> list = new List<string>(); FieldInfo[] fields = typeof(ItemAction).GetFields(BindingFlags.Instance | BindingFlags.Public); foreach (FieldInfo fieldInfo in fields) { if (fieldInfo.FieldType == typeof(bool) && (bool)fieldInfo.GetValue(action)) { list.Add(fieldInfo.Name); } } return string.Join(",", list); } private static List<ItemDefinitionParameter> DescribePublicParameters(object value, int depth = 0) { List<ItemDefinitionParameter> list = new List<ItemDefinitionParameter>(); FieldInfo[] fields = value.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public); foreach (FieldInfo fieldInfo in fields) { try { object value2 = fieldInfo.GetValue(value); if (TryDescribeValue(value2, fieldInfo.FieldType, out var text)) { list.Add(new ItemDefinitionParameter { name = fieldInfo.Name, type = fieldInfo.FieldType.FullName, value = text, children = DescribeChildren(value2, depth + 1), truncated = (depth >= 4) }); } } catch { } } return list; } private static List<ItemDefinitionParameter> DescribeChildren(object value, int depth) { if (value == null || depth > 4 || value is Object || value is string) { return null; } if (value is IEnumerable enumerable) { List<ItemDefinitionParameter> list = new List<ItemDefinitionParameter>(); foreach (object item in enumerable) { if (list.Count >= 64) { list.Add(new ItemDefinitionParameter { name = "truncated", truncated = true }); break; } list.Add(new ItemDefinitionParameter { name = list.Count.ToString(), type = item?.GetType().FullName, value = ((item == null) ? null : (TryDescribeValue(item, item.GetType(), out var text) ? text : item.GetType().Name)), children = DescribeChildren(item, depth + 1) }); } return list; } if (value is Affliction || value is AdditionalCookingBehavior) { return DescribePublicParameters(value, depth); } return null; } private static string DescribeParameters(IEnumerable<ItemDefinitionParameter> parameters) { return string.Join(",", parameters.Select((ItemDefinitionParameter parameter) => parameter.name + "=" + parameter.value)); } private static bool TryDescribeValue(object value, Type declaredType, out string text) { text = null; if (value == null) { return false; } Type type = value.GetType(); if (type.IsEnum || type == typeof(bool) || type == typeof(byte) || type == typeof(short) || type == typeof(ushort) || type == typeof(int) || type == typeof(uint) || type == typeof(long) || type == typeof(float) || type == typeof(double) || type == typeof(decimal) || type == typeof(string)) { text = Convert.ToString(value, CultureInfo.InvariantCulture); return true; } Object val = (Object)((value is Object) ? value : null); if (val != null) { text = ((val == (Object)null) ? declaredType.Name : (((object)val).GetType().Name + ":" + val.name)); return true; } if (value is IEnumerable enumerable && !(value is string)) { List<string> list = new List<string>(); foreach (object item in enumerable) { if (list.Count >= 16) { list.Add("..."); break; } string text2; if (item == null) { list.Add("null"); } else if (TryDescribeValue(item, item.GetType(), out text2)) { list.Add(text2); } else { list.Add(item.GetType().Name); } } text = "[" + string.Join("|", list) + "]"; return true; } if (type.Namespace != null && type.Namespace.StartsWith("Peak.Afflictions", StringComparison.Ordinal)) { text = type.Name; return true; } if (value is AdditionalCookingBehavior) { text = type.Name; return true; } return false; } private static void AddEffectHints(List<ItemEffectHint> result, ItemActionBase action) { string trigger = DescribeTriggers(action); Action_RestoreHunger val = (Action_RestoreHunger)(object)((action is Action_RestoreHunger) ? action : null); if (val != null) { result.Add(new ItemEffectHint { type = "RestoreStatus", target = "Hunger", hasAmount = true, amount = 0f - Mathf.Abs(val.restorationAmount), trigger = trigger }); return; } Action_GiveExtraStamina val2 = (Action_GiveExtraStamina)(object)((action is Action_GiveExtraStamina) ? action : null); if (val2 != null) { result.Add(new ItemEffectHint { type = "GiveExtraStamina", target = "ExtraStamina", hasAmount = true, amount = val2.amount, trigger = trigger }); return; } Action_ModifyStatus val3 = (Action_ModifyStatus)(object)((action is Action_ModifyStatus) ? action : null); if (val3 != null) { result.Add(new ItemEffectHint { type = "ModifyStatus", target = ((object)Unsafe.As<STATUSTYPE, STATUSTYPE>(ref val3.statusType)/*cast due to .constrained prefix*/).ToString(), hasAmount = true, amount = val3.changeAmount, trigger = trigger }); return; } Action_ApplyAffliction val4 = (Action_ApplyAffliction)(object)((action is Action_ApplyAffliction) ? action : null); if (val4 != null) { result.Add(new ItemEffectHint { type = "ApplyAffliction", target = ((val4.affliction == null) ? null : ((object)val4.affliction).GetType().Name), trigger = trigger }); } else if (action is Action_ClearAllStatus) { result.Add(new ItemEffectHint { type = "ClearStatuses", target = "Multiple", trigger = trigger }); } else if (action is Action_ReduceUses) { result.Add(new ItemEffectHint { type = "ReduceUses", target = "ItemUses", trigger = trigger }); } else if (((object)action).GetType().Name == "Action_Consume") { result.Add(new ItemEffectHint { type = "ConsumeItem", target = "Item", trigger = trigger }); } } } internal sealed class LifecycleAnalysis { private sealed class State { internal EvidenceReference evidence; internal int player; internal int epoch; internal int count; internal float start; internal float end; internal float second = float.NaN; internal string kind; internal bool entered; } private readonly AnalysisResult _result; private readonly Dictionary<int, State> _current = new Dictionary<int, State>(); private readonly List<State> _states = new List<State>(); private readonly Dictionary<string, LifecycleObservation> _events = new Dictionary<string, LifecycleObservation>(); private readonly List<Tuple<int, int, float>> _revivals = new List<Tuple<int, int, float>>(); internal LifecycleAnalysis(AnalysisResult result) { _result = result; } internal void Interrupt(int player = -1) { if (player < 0) { _current.Clear(); } else { _current.Remove(player); } } internal void Sample(float time, PlayerTelemetry p, EvidenceReference evidence) { string text = (p.dead ? "Death" : ((p.passedOut || p.fullyPassedOut) ? "PassedOut" : "Alive")); State value; bool flag = _current.TryGetValue(p.playerIndex, out value) && value.epoch == evidence.epoch && time > value.end && time - value.end <= 1f; if (!flag || value.kind != text) { State state = new State { evidence = evidence, player = p.playerIndex, epoch = evidence.epoch, start = time, end = time, count = 1, kind = text, entered = (flag && (value.kind == "Alive" || (text == "Death" && value.kind == "PassedOut"))) }; _current[p.playerIndex] = state; _states.Add(state); if (flag && value.kind != "Alive" && text == "Alive") { _result.timeline.Add(new ReportEvent { evidence = evidence, playerIndex = p.playerIndex, time = time, endTime = time, kind = "RecoveryObserved", confidence = "Likely" }); } } else { value.end = time; value.count++; if (value.count == 2) { value.second = time; } } } internal void Event(StatsEvent e, EvidenceReference evidence) { if (e.type == "PlayerRevivedObserved" && evidence.epoch >= 0) { _revivals.Add(Tuple.Create(e.subjectPlayerIndex, evidence.epoch, e.time)); Interrupt(e.subjectPlayerIndex); } if (!(e.type != "PlayerDied") || !(e.type != "PlayerPassedOut")) { string key = evidence.epoch + ":" + e.subjectPlayerIndex + ":" + e.type + ":" + e.time.ToString("R", CultureInfo.InvariantCulture); if (evidence.epoch >= 0 && _events.TryGetValue(key, out var value)) { value.duplicateCount++; return; } LifecycleObservation lifecycleObservation = new LifecycleObservation { evidence = evidence, playerIndex = e.subjectPlayerIndex, time = e.time, kind = ((e.type == "PlayerDied") ? "Death" : "PassedOut") }; _result.lifecycle.Add(lifecycleObservation); _events[key] = lifecycleObservation; } } internal void Finish() { foreach (AnalysisPlayer player in _result.players) { State state = null; State state2 = null; IOrderedEnumerable<LifecycleObservation> orderedEnumerable = from lifecycleObservation in _result.lifecycle where lifecycleObservation.playerIndex == player.playerIndex orderby lifecycleObservation.evidence.epoch, lifecycleObservation.time select lifecycleObservation; List<State> source = _states.Where((State s) => s.player == player.playerIndex).ToList(); foreach (LifecycleObservation e in orderedEnumerable) { State state3 = ((e.evidence.epoch < 0) ? null : (from s in source where s.epoch == e.evidence.epoch && s.kind == e.kind && ((s.entered && Math.Abs(s.start - e.time) <= 2f) || (s.count >= 2 && s.start >= e.time && s.second <= e.time + 2f)) orderby (!(e.time >= s.start) || !(e.time <= s.end)) ? Math.Min(Math.Abs(s.start - e.time), Math.Abs(s.end - e.time)) : 0f, Math.Abs(s.start - e.time) select s).FirstOrDefault()); if (e.kind == "Death") { player.rawDeathCount += 1 + e.duplicateCount; } else { player.rawPassedOutCount += 1 + e.duplicateCount; } State credited = ((e.kind == "Death") ? state : state2); bool flag = credited == null || credited.epoch != e.evidence.epoch || source.Any((State s) => s.epoch == e.evidence.epoch && s.kind == "Alive" && s.count >= 2 && s.start > credited.end && s.second <= e.time) || _revivals.Any((Tuple<int, int, float> r) => r.Item1 == e.playerIndex && r.Item2 == e.evidence.epoch && r.Item3 > credited.end && r.Item3 <= e.time); if (state3 != null && state3 != credited && flag) { e.verdict = "Confirmed"; e.reason = "StateCorroborated"; if (e.kind == "Death") { player.deathCount++; player.deathTimes.Add(e.time); state = state3; } else { player.passedOutCount++; player.passedOutTimes.Add(e.time); state2 = state3; } _result.timeline.Add(new ReportEvent { evidence = e.evidence, playerIndex = e.playerIndex, time = e.time, endTime = e.time, kind = e.kind, confidence = "Certain" }); } else if (credited != null && credited.epoch == e.evidence.epoch && (state3 == credited || !flag)) { e.verdict = "Duplicate"; e.reason = "AlreadyInState"; } else { e.reason = ((e.evidence.epoch < 0) ? "ClockAmbiguous" : "StateNotCorroborated"); } if (e.kind == "Death") { player.duplicateDeathCount += e.duplicateCount + ((e.verdict == "Duplicate") ? 1 : 0); if (e.verdict == "Unconfirmed") { player.unconfirmedDeathCount++; } } } } _result.overview.deathCount = _result.players.Sum((AnalysisPlayer p) => p.deathCount); _result.overview.passedOutCount = _result.players.Sum((AnalysisPlayer p) => p.passedOutCount); foreach (State state4 in _states) { _result.lifecycleIntervals.Add(new LifecycleInterval { evidence = state4.evidence, playerIndex = state4.player, start = state4.start, end = state4.end, state = state4.kind, entered = state4.entered, sampleCount = state4.count }); if (state4.entered && state4.kind != "Alive" && !_result.lifecycle.Any((LifecycleObservation lifecycleObservation) => lifecycleObservation.playerIndex == state4.player && lifecycleObservation.kind == state4.kind && lifecycleObservation.evidence.epoch == state4.epoch && lifecycleObservation.verdict == "Confirmed" && Math.Abs(lifecycleObservation.time - state4.start) <= 2f)) { _result.timeline.Add(new ReportEvent { evidence = state4.evidence, playerIndex = state4.player, time = state4.start, endTime = state4.start, kind = state4.kind + "StateObserved", confidence = "Likely" }); } } } } [HarmonyPatch(typeof(PauseMenuMainPage), "Start")] internal static class PauseMenuMainPagePatches { private static void Postfix(PauseMenuMainPage __instance) { StateKeeperUi.BuildPauseMenu(__instance); } } [HarmonyPatch(typeof(PauseMenuMainPage), "OnEnable")] internal static class PauseMenuMainPageEnablePatches { private static void Postfix(PauseMenuMainPage __instance) { StateKeeperUi.BuildPauseMenu(__instance); } } internal sealed class PerformanceWindow { private long _count; private double _total; private double _maximum; internal void Record(long start) { double num = (double)(Stopwatch.GetTimestamp() - start) * 1000.0 / (double)Stopwatch.Frequency; _count++; _total += num; if (num > _maximum) { _maximum = num; } } internal string Take(string name) { string result = string.Format(CultureInfo.InvariantCulture, "{0}: calls={1}, mean={2:0.000}ms, max={3:0.000}ms", name, _count, (_count > 0) ? (_total / (double)_count) : 0.0, _maximum); _count = 0L; _total = (_maximum = 0.0); return result; } } internal sealed class RecordedItemRules { internal sealed class Parameters { private readonly List<ItemDefinitionParameter> _entries; private readonly string _legacy; internal Parameters(List<ItemDefinitionParameter> entries, string legacy) { _entries = entries ?? new List<ItemDefinitionParameter>(); _legacy = legacy; } internal ItemDefinitionParameter Entry(string name) { return _entries.FirstOrDefault((ItemDefinitionParameter e) => e != null && e.name == name); } internal string String(string name) { ItemDefinitionParameter itemDefinitionParameter = Entry(name); if (itemDefinitionParameter != null) { if (!itemDefinitionParameter.truncated) { return itemDefinitionParameter.value; } return null; } if (string.IsNullOrEmpty(_legacy)) { return null; } MatchCollection matchCollection = Regex.Matches(_legacy, "(?:^|,)" + Regex.Escape(name) + "=([^,\\[\\]]+)(?=,|$)"); if (matchCollection.Count != 1) { return null; } return matchCollection[0].Groups[1].Value; } internal float? Number(string name) { if (!float.TryParse(String(name), NumberStyles.Float, CultureInfo.InvariantCulture, out var result) || !RunAnalysisEngine.Finite(result)) { return null; } return result; } internal bool? Boolean(string name) { if (!bool.TryParse(String(name), out var result)) { return null; } return result; } } private readonly RunRecord _record; internal static readonly string[] HealingOrder = new string[6] { "Injury", "Spores", "Poison", "Cold", "Hot", "Drowsy" }; internal RecordedItemRules(RunRecord record) { _record = record; } internal List<AnalysisEffectRule> Compile(AnalysisItemObservation row, List<string> reasons) { ItemDefinition itemDefinition = RunAnalysisEngine.Definition(_record.definitions ?? new List<ItemDefinition>(), row.itemId, row.prefabName); List<AnalysisEffectRule> list = new List<AnalysisEffectRule>(); if (itemDefinition == null) { reasons.Add("DefinitionNotRecorded"); return list; } int num = 0; foreach (ItemDefinitionAction item in itemDefinition.actions ?? new List<ItemDefinitionAction>()) { if (item == null) { continue; } Parameters parameters = new Parameters(item.parameters, item.parameterSummary); string source = item.typeName + "#" + num++; int count = list.Count; switch (item.typeName) { case "Action_RestoreHunger": Add(list, "Hunger", Negative(parameters.Number("restorationAmount")), -1, source); break; case "Action_GiveExtraStamina": Add(list, "ExtraStamina", parameters.Number("amount"), 1, source); break; case "Action_ApplyInfiniteStamina": Add(list, "Affliction:InfiniteStamina", 1f, 1, source); break; case "Action_ModifyStatus": { string text = parameters.String("statusType"); float? amount = parameters.Number("changeAmount"); if (!string.IsNullOrEmpty(text) && amount.HasValue) { Add(list, text, amount, Math.Sign(amount.Value), source); } else { reasons.Add("StatusTypeNotRecorded"); } if (parameters.Boolean("ifSkeleton") != true && parameters.Boolean("ifSkeleton").HasValue) { break; } foreach (AnalysisEffectRule item2 in list.Skip(count)) { item2.uncertain = true; } break; } case "Action_ClearAllStatus": { string[] healingOrder = _record.statusTypeOrder ?? new string[0]; foreach (string channel2 in healingOrder) { if (!new string[4] { "Weight", "Petrify", "Arrow", "Thorns" }.Contains(channel2) && (channel2 != "Curse" || parameters.Boolean("excludeCurse") == false) && !(parameters.Entry("otherExclusions")?.children ?? new List<ItemDefinitionParameter>()).Any((ItemDefinitionParameter v) => v.value == channel2)) { Add(list, channel2, -5f, -1, source).uncertain = parameters.Entry("otherExclusions") == null; } } break; } case "Action_InflictPoison": { Add(list, "Affliction:PoisonOverTime", 1f, 1, source); AnalysisEffectRule analysisEffectRule2 = Add(list, "Poison", parameters.Number("poisonPerSecond"), 1, source); analysisEffectRule2.delay = parameters.Number("delay").GetValueOrDefault(); analysisEffectRule2.duration = parameters.Number("inflictionTime").GetValueOrDefault(); analysisEffectRule2.continuous = true; analysisEffectRule2.requiredAffliction = "PoisonOverTime"; analysisEffectRule2.uncertain = !parameters.Number("delay").HasValue || analysisEffectRule2.duration <= 0f; break; } case "Action_ApplyAffliction": case "Action_ApplyMassAffliction": CompileAffliction(parameters.Entry("affliction"), list, source, 0f, reasons); foreach (ItemDefinitionParameter item3 in parameters.Entry("extraAfflictions")?.children ?? new List<ItemDefinitionParameter>()) { CompileAffliction(item3, list, source, 0f, reasons); } if (!(item.typeName == "Action_ApplyMassAffliction")) { break; } foreach (AnalysisEffectRule item4 in list.Skip(count)) { item4.scope = "Area"; item4.radius = parameters.Number("radius"); item4.ignoreCaster = parameters.Boolean("ignoreCaster") == true; item4.uncertain |= !item4.radius.HasValue; } break; case "Peak.Action_HealingGem": { CompileAffliction(parameters.Entry("healingAffliction"), list, source, 0f, reasons); CompileAffliction(parameters.Entry("invincibilityAffliction"), list, source, 0f, reasons); if (list.Count == count) { string[] healingOrder = HealingOrder; foreach (string channel in healingOrder) { Add(list, channel, null, -1, source).uncertain = true; } } AnalysisEffectRule analysisEffectRule = Add(list, "Petrify", null, 1, source); analysisEffectRule.scope = "Actor"; analysisEffectRule.uncertain = true; break; } case "Action_RandomMushroomEffect": Mushroom(parameters, list, row.time, source, reasons); break; default: if (!NonEffectAction(item.typeName)) { reasons.Add("UnsupportedAction:" + item.typeName); } break; } foreach (AnalysisEffectRule item5 in list.Skip(count)) { item5.trigger = item.trigger; if (!RunAnalysisEngine.Finite(item5.delay) || !RunAnalysisEngine.Finite(item5.duration) || item5.delay < 0f || item5.duration < 0f) { item5.uncertain = true; } } } Cook(itemDefinition, row.cookedAmount, list, reasons); return list.GroupBy(delegate(AnalysisEffectRule r) { string[] obj = new string[25] { r.channel, "|", r.delay.ToString(), "|", r.duration.ToString(), "|", r.scope, "|", r.trigger, "|", r.budgetGroup, "|", r.continuous.ToString(), "|", r.requiredAffliction, "|", r.afterAfflictionEnds, "|", r.direction.ToString(), "|", null, null, null, null, null }; float? radius = r.radius; obj[20] = radius.ToString(); obj[21] = "|"; obj[22] = r.ignoreCaster.ToString(); obj[23] = "|"; obj[24] = r.priority.ToString(); return string.Concat(obj); }).Select(delegate(IGrouping<string, AnalysisEffectRule> group) { AnalysisEffectRule analysisEffectRule3 = group.First(); if (group.Count() > 1 && group.All((AnalysisEffectRule r) => r.amount.HasValue)) { analysisEffectRule3.amount = group.Sum((AnalysisEffectRule r) => r.amount.Value); } analysisEffectRule3.uncertain |= group.Any((AnalysisEffectRule r) => r.uncertain); return analysisEffectRule3; }).ToList(); } internal List<AnalysisEffectRule> Passive(ItemDefinition definition) { List<AnalysisEffectRule> list = new List<AnalysisEffectRule>(); foreach (ItemDefinitionComponent item in definition?.components ?? new List<ItemDefinitionComponent>()) { Parameters parameters = new Parameters(item.parameters, item.parameterSummary); if (item.typeName == "Peak.InfiniteStamAmulet") { AnalysisEffectRule analysisEffectRule = Add(list, "Affliction:InfiniteStamina", 1f, 1, "PEAK2.4.b:InfiniteStamAmulet"); analysisEffectRule.scope = "PlayerArea"; analysisEffectRule.radius = parameters.Number("radius"); analysisEffectRule.continuous = true; analysisEffectRule.uncertain = !analysisEffectRule.radius.HasValue; AnalysisEffectRule analysisEffectRule2 = Add(list, "Petrify", parameters.Number("petrifyPerSecond") / 100f, 1, "PEAK2.4.b:InfiniteStamAmulet"); analysisEffectRule2.continuous = true; analysisEffectRule2.scope = "Actor"; } if (item.typeName == "Peak.DoubleJumpAmulet") { Add(list, "Affliction:DoubleJumpAmulet", 1f, 1, "PEAK2.4.b:DoubleJumpAmulet").continuous = true; AnalysisEffectRule analysisEffectRule3 = Add(list, "Petrify", parameters.Number("petrifyPerJump") / 100f, 1, "PEAK2.4.b:DoubleJumpAmulet"); analysisEffectRule3.continuous = true; analysisEffectRule3.uncertain = true; } } if (!(definition?.components ?? new List<ItemDefinitionComponent>()).Any((ItemDefinitionComponent c) => c.typeName == "Peak.HealingAmulet")) { return list; } string[] array = new string[6] { "Injury", "Poison", "Spores", "Cold", "Drowsy", "Hot" }; for (int num = 0; num < array.Length; num++) { AnalysisEffectRule analysisEffectRule4 = Add(list, array[num], -0.05f, -1, "PEAK2.4.b:HealingAmulet"); analysisEffectRule4.continuous = true; analysisEffectRule4.budgetGroup = "HealingAmuletTick"; analysisEffectRule4.priority = num; analysisEffectRule4.budget = 0.05f; } Add(list, "Petrify", 0.02f, 1, "PEAK2.4.b:HealingAmulet").continuous = true; return list; } private static AnalysisEffectRule Add(List<AnalysisEffectRule> rules, string channel, float? amount, int direction, string source) { AnalysisEffectRule analysisEffectRule = new AnalysisEffectRule { channel = channel, amount = amount, direction = (amount.HasValue ? Math.Sign(amount.Value) : direction), source = source }; rules.Add(analysisEffectRule); return analysisEffectRule; } private static float? Negative(float? number) { if (!number.HasValue) { return null; } return 0f - Math.Abs(number.Value); } private static void CompileAffliction(ItemDefinitionParameter entry, List<AnalysisEffectRule> rules, string source, float delay, List<string> reasons, int depth = 0) { if (entry == null) { reasons.Add("NestedParametersNotRecorded"); return; } if (depth > 4 || entry.truncated) { reasons.Add("TruncatedParameters"); return; } string type = entry.type; string text = ((type != null && type.StartsWith("Peak.Afflictions.Affliction_", StringComparison.Ordinal)) ? entry.type.Substring("Peak.Afflictions.Affliction_".Length) : null); if (text == null) { string value = entry.value; if (value != null && value.StartsWith("Affliction_", StringComparison.Ordinal)) { text = entry.value.Substring("Affliction_".Length); } } if (text == null) { reasons.Add("UnknownAfflictionType"); return; } Parameters parameters = new Parameters(entry.children, null); AnalysisEffectRule analysisEffectRule = Add(rules, "Affliction:" + text switch { "Exhaustion" => "Exhausted", "AdjustColdOverTime" => "ColdOverTime", "AdjustDrowsyOverTime" => "DrowsyOverTime", _ => text, }, 1f, 1, source); analysisEffectRule.delay = delay; int count = rules.Count; switch (text) { case "HealAll": { for (int i = 0; i < HealingOrder.Length; i++) { AnalysisEffectRule analysisEffectRule2 = Add(rules, HealingOrder[i], Negative(parameters.Number("maxHealing")), -1, source); analysisEffectRule2.budgetGroup = source + ":HealAll"; analysisEffectRule2.budget = parameters.Number("maxHealing"); analysisEffectRule2.priority = i; analysisEffectRule2.uncertain = !analysisEffectRule2.budget.HasValue; } break; } case "ClearAllStatus": foreach (string item in HealingOrder.Concat(new string[1] { "Hunger" })) { Add(rules, item, -2f, -1, source); } break; case "AdjustStatus": if (parameters.String("statusType") != null && parameters.Number("statusAmount").HasValue) { Add(rules, parameters.String("statusType"), parameters.Number("statusAmount"), 0, source); } else { reasons.Add("NestedParametersNotRecorded"); } break; case "AddBonusStamina": Add(rules, "ExtraStamina", parameters.Number("staminaAmount"), 1, source); break; case "PoisonOverTime": case "AdjustDrowsyOverTime": case "AdjustColdOverTime": { AnalysisEffectRule analysisEffectRule4 = Add(rules, (text == "AdjustDrowsyOverTime") ? "Drowsy" : ((text == "AdjustColdOverTime") ? "Cold" : "Poison"), parameters.Number("statusPerSecond"), 0, source); analysisEffectRule4.delay = parameters.Number("delayBeforeEffect").GetValueOrDefault(); analysisEffectRule4.duration = parameters.Number("totalTime").GetValueOrDefault(); analysisEffectRule4.continuous = true; analysisEffectRule4.requiredAffliction = analysisEffectRule.channel.Substring(11); analysisEffectRule4.uncertain = !analysisEffectRule4.amount.HasValue || analysisEffectRule4.duration <= 0f; break; } case "FasterBoi": { Add(rules, "Drowsy", -0.5f, -1, source); AnalysisEffectRule analysisEffectRule3 = Add(rules, "Drowsy", parameters.Number("drowsyOnEnd"), 1, source); analysisEffectRule3.afterAfflictionEnds = "FasterBoi"; analysisEffectRule3.uncertain = !analysisEffectRule3.amount.HasValue; break; } case "InfiniteStamina": { if (parameters.Entry("drowsyAffliction") == null) { break; } int count2 = rules.Count; CompileAffliction(parameters.Entry("drowsyAffliction"), rules, source, 0f, reasons, depth + 1); foreach (AnalysisEffectRule item2 in rules.Skip(count2)) { item2.afterAfflictionEnds = "InfiniteStamina"; } break; } } foreach (AnalysisEffectRule item3 in rules.Skip(count)) { item3.delay += delay; } } private void Mushroom(Parameters p, List<AnalysisEffectRule> rules, float time, string source, List<string> reasons) { int num = -1; if (p.Boolean("useDebugEffect") == true && p.Number("debugEffect").HasValue) { num = (int)p.Number("debugEffect").Value; } else { RunEffectContext runEffectContext = (from c in _record.effectContexts ?? new List<RunEffectContext>() where c != null && c.observedTime <= time orderby c.observedTime select c).LastOrDefault(); int num2 = (int)(p.Number("mushroomTypeIndex") ?? (-1f)); if (runEffectContext?.mushroomEffects == null || runEffectContext.mushroomEffects.Length == 0 || num2 < 0) { reasons.Add("RandomContextNotRecorded"); return; } num2 %= runEffectContext.mushroomEffects.Length; num = runEffectContext.mushroomEffects[num2]; if (runEffectContext.mushroomStaminaAmounts != null && num2 < runEffectContext.mushroomStaminaAmounts.Length) { Add(rules, "ExtraStamina", (float)runEffectContext.mushroomStaminaAmounts[num2] * 0.05f, 1, source); } } string[] array = new string[4] { "InfiniteStamina", "FasterBoi", "LowGravity", "Invincibility" }; if (num >= 0 && num <= 3) { Add(rules, "Affliction:" + array[num], 1f, 1, source).delay = ((num != 3) ? 3 : 0); return; } switch (num) { case 4: { string[] array2 = new string[3] { "Hunger", "Injury", "Poison" }; foreach (string channel in array2) { Add(rules, channel, -0.15f, -1, source); } break; } case 6: case 9: Add(rules, "Affliction:" + ((num == 6) ? "Blind" : "Numb"), 1f, 1, source).delay = 3f; break; case 8: Add(rules, "Spores", 0.25f, 1, source); break; default: reasons.Add("EffectNotObservableInTelemetry"); break; } } private static void Cook(ItemDefinition definition, int? cooked, List<AnalysisEffectRule> rules, List<string> reasons) { if (definition.cookingRules == null || definition.cookingRules.Count == 0) { return; } if (!cooked.HasValue) { reasons.Add("CookingStateUnknown"); { foreach (AnalysisEffectRule rule in rules) { rule.uncertain = true; } return; } } if (cooked <= 0) { return; } foreach (ItemDefinitionCooking cookingRule in definition.cookingRules) { Parameters parameters = new Parameters(cookingRule.parameters, cookingRule.parameterSummary); if (!(cookingRule.typeName != "ItemCooking")) { ItemDefinitionParameter itemDefinitionParameter = parameters.Entry("additionalCookingBehaviors"); if (itemDefinitionParameter == null || !(itemDefinitionParameter.children?.Count > 0)) { if (parameters.Boolean("wreckWhenCooked") == true) { rules.Clear(); reasons.Add("WreckedItem"); break; } if (parameters.Boolean("ignoreDefaultCookBehavior") == true) { continue; } if (!parameters.Boolean("ignoreDefaultCookBehavior").HasValue) { foreach (AnalysisEffectRule rule2 in rules) { rule2.uncertain = true; } reasons.Add("CookingRulesNotRecorded"); continue; } AnalysisEffectRule analysisEffectRule = rules.FirstOrDefault((AnalysisEffectRule r) => r.source.StartsWith("Action_RestoreHunger#", StringComparison.Ordinal)); if (analysisEffectRule != null && analysisEffectRule.amount.HasValue) { analysisEffectRule.amount = 0f - Math.Max(0f, Math.Abs(analysisEffectRule.amount.Value) * 2f - (float)Math.Max(0, cooked.Value - 2) * 0.05f); } AnalysisEffectRule analysisEffectRule2 = rules.FirstOrDefault((AnalysisEffectRule r) => r.source.StartsWith("Action_GiveExtraStamina#", StringComparison.Ordinal)); if (analysisEffectRule2 == null) { analysisEffectRule2 = Add(rules, "ExtraStamina", 0f, 1, "ItemCooking"); analysisEffectRule2.trigger = "OnConsumed"; } analysisEffectRule2.amount = ((cooked <= 2) ? Math.Max(0.1f, analysisEffectRule2.amount.GetValueOrDefault() * 1.5f) : 0f); analysisEffectRule2.direction = ((analysisEffectRule2.amount > 0f) ? 1 : 0); if (cooked >= 4 && parameters.Boolean("ignoreDefaultPoisonBehavior") == false) { Add(rules, "Poison", 0.1f * (float)(cooked.Value - 3), 1, "ItemCooking").trigger = "OnConsumed"; } continue; } } reasons.Add("CustomCookingRules"); foreach (AnalysisEffectRule rule3 in rules) { rule3.uncertain = true; } } } private static bool NonEffectAction(string type) { return new string[10] { "Action_Consume", "Action_ReduceUses", "Action_PlayAnimation", "Action_PlaySound", "Action_PlaySFX", "Action_PlayParticles", "Action_StrangeGem", "Action_RopeShooter", "Action_Shoot", "Action_ApplySuperJump" }.Contains(type); } } internal sealed class RecordingClock { private readonly Func<double> _seconds; private double _origin; private double _offset; internal float Time => (float)(_offset + Math.Max(0.0, _seconds() - _origin)); internal RecordingClock(Func<double> seconds = null) { _seconds = seconds ?? ((Func<double>)(() => (double)Stopwatch.GetTimestamp() / (double)Stopwatch.Frequency)); Reset(0.0); } internal void Reset(double offset) { _origin = _seconds(); _offset = Math.Max(0.0, offset); } } internal sealed class ReportAnalysis { private sealed class Frame { internal float time; internal PlayerTelemetry value; } private sealed class Pending { internal ReportEpisode episode; internal float last; internal bool wasActive; internal float minimum; } internal static readonly string[] MovementTypes = new string[8] { "Unconscious", "Rope", "Vine", "Climbing", "Gliding", "Sprinting", "Grounded", "Airborne" }; private readonly AnalysisResult _result; private readonly Dictionary<int, AnalysisPlayer> _players; private readonly Dictionary<int, Frame> _previous = new Dictionary<int, Frame>(); private readonly Dictionary<int, Queue<Frame>> _statusWindow = new Dictionary<int, Queue<Frame>>(); private readonly Dictionary<string, Pending> _pending = new Dictionary<string, Pending>(); private readonly Dictionary<string, bool> _together = new Dictionary<string, bool>(); internal static string Movement(PlayerTelemetry p) { if (!p.passedOut && !p.fullyPassedOut) { if (!p.ropeClimbing) { if (!p.vineClimbing) { if (!p.climbing) { if (!p.gliding) { if (!p.sprinting) { if (!p.grounded) { return "Airborne"; } return "Grounded"; } return "Sprinting"; } return "Gliding"; } return "Climbing"; } return "Vine"; } return "Rope"; } return "Unconscious"; } internal ReportAnalysis(AnalysisResult result) { _result = result; _players = result.players.ToDictionary((AnalysisPlayer p) => p.playerIndex); } internal void Interrupt(int player = -1) { string[] array = (from p in _pending where player < 0 || p.Value.episode.playerIndex == player || p.Value.episode.otherPlayerIndex == player select p.Key).ToArray(); foreach (string key in array) { Close(key); } if (player < 0) { _previous.Clear(); _statusWindow.Clear(); _together.Clear(); return; } _previous.Remove(player); _statusWindow.Remove(player); array = _together.Keys.Where((string k) => k.StartsWith(player + ":", StringComparison.Ordinal) || k.EndsWith(":" + player, StringComparison.Ordinal)).ToArray(); foreach (string key2 in array) { _together.Remove(key2); } } internal void Sample(float time, Dictionary<int, PlayerTelemetry> current, EvidenceReference evidence) { int[] array = _previous.Keys.Where((int p) => !current.ContainsKey(p)).ToArray(); foreach (int player in array) { Interrupt(player); } foreach (PlayerTele