using System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using GlobalEnums; using HarmonyLib; using HutongGames.PlayMaker; using HutongGames.PlayMaker.Actions; using InControl; using JetBrains.Annotations; using Microsoft.CodeAnalysis; using SpeedrunGym.Source.Moves; using SpeedrunGym.Source.WorldToasts; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("SpeedrunGym")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.3.0.0")] [assembly: AssemblyInformationalVersion("0.3.0+5703f9893ae30a0bd67bd0996095106d4d675a64")] [assembly: AssemblyProduct("SpeedrunGym")] [assembly: AssemblyTitle("SpeedrunGym")] [assembly: AssemblyMetadata("RepositoryUrl", "https://github.com/jakobhellermann/SpeedrunGym")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.3.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [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 BepInEx { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] [Conditional("CodeGeneration")] [Embedded] internal sealed class BepInAutoPluginAttribute : Attribute { public BepInAutoPluginAttribute(string? id = null, string? name = null, string? version = null) { } } } namespace BepInEx.Preloader.Core.Patching { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] [Conditional("CodeGeneration")] [Embedded] internal sealed class PatcherAutoPluginAttribute : Attribute { public PatcherAutoPluginAttribute(string? id = null, string? name = null, string? version = null) { } } } namespace Microsoft.CodeAnalysis { [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace SpeedrunGym.Source { [PublicAPI] internal static class Log { private static ManualLogSource? logSource; internal static void Init(ManualLogSource logSource) { Log.logSource = logSource; } internal static void Debug(object data) { ManualLogSource? obj = logSource; if (obj != null) { obj.LogDebug(data); } } internal static void Error(object data) { ManualLogSource? obj = logSource; if (obj != null) { obj.LogError(data); } } internal static void Fatal(object data) { ManualLogSource? obj = logSource; if (obj != null) { obj.LogFatal(data); } } internal static void Info(object data) { ManualLogSource? obj = logSource; if (obj != null) { obj.LogInfo(data); } } internal static void Message(object data) { ManualLogSource? obj = logSource; if (obj != null) { obj.LogMessage(data); } } internal static void Warning(object data) { ManualLogSource? obj = logSource; if (obj != null) { obj.LogWarning(data); } } } [BepInPlugin("io.github.jakobhellermann.speedrungym", "SpeedrunGym", "0.3.0")] public class SpeedrunGymPlugin : BaseUnityPlugin { private Harmony harmony; private WorldToastManager worldToasts; public const string Id = "io.github.jakobhellermann.speedrungym"; public static string Name => "SpeedrunGym"; public static string Version => "0.3.0"; private void Awake() { Log.Init(((BaseUnityPlugin)this).Logger); Log.Info("Plugin " + Name + " (io.github.jakobhellermann.speedrungym) has loaded!"); ForceCrawPogo.BindConfig(((BaseUnityPlugin)this).Config); PogoEndlagDetector.BindConfig(((BaseUnityPlugin)this).Config); JumpTimingDetector.BindConfig(((BaseUnityPlugin)this).Config); WorldToastManager.MaxAge = ((BaseUnityPlugin)this).Config.Bind("Toasts", "Lifetime seconds", 3f, "How long feedback popups stay on screen before fading out.").Value; worldToasts = WorldToastManager.Create(); SceneManager.sceneLoaded += OnSceneLoaded; try { harmony = Harmony.CreateAndPatchAll(((object)this).GetType().Assembly, (string)null); } catch (Exception arg) { Log.Info(string.Format("Plugin {0} ({1}) failed to initialize: {2}", Name, "io.github.jakobhellermann.speedrungym", arg)); } } private void LateUpdate() { try { PogoEndlagDetector.LateUpdate(); JumpTimingDetector.LateUpdate(); worldToasts.Update(); } catch (Exception arg) { Log.Error($"Error during LateUpdate: {arg}"); } } private void OnDestroy() { try { SceneManager.sceneLoaded -= OnSceneLoaded; harmony.UnpatchSelf(); ForceCrawPogo.Cleanup(); worldToasts.Destroy(); } catch (Exception arg) { Log.Info(string.Format("Plugin {0} ({1}) failed to clean up: {2}", Name, "io.github.jakobhellermann.speedrungym", arg)); } Log.Info("Plugin " + Name + " (io.github.jakobhellermann.speedrungym) has been unloaded!"); } private static void OnSceneLoaded(Scene scene, LoadSceneMode loadMode) { try { WorldToastManager.ClearAll(); ForceCrawPogo.OnSceneLoaded(); } catch (Exception arg) { Log.Error($"Error during OnSceneLoaded: {arg}"); } } } [HarmonyPatch] internal static class FsmStatePatches { [HarmonyPrefix] [HarmonyPatch(typeof(Fsm), "EnterState", new Type[] { typeof(FsmState) })] private static void EnterState(Fsm __instance, FsmState state) { try { ForceCrawPogo.OnEnterState(__instance, state); } catch (Exception data) { Log.Error(data); } } } } namespace SpeedrunGym.Source.WorldToasts { internal record struct WorldToastEntry { public GameObject Go { get; set; } public Text Text { get; set; } public Image Backdrop { get; set; } public float StartTime { get; set; } public Vector3 WorldPos { get; set; } public Color Color { get; set; } public bool Fade { get; set; } public bool MoveUp { get; set; } public bool Clamped; public WorldToastEntry(GameObject Go, Text Text, Image Backdrop, float StartTime, Vector3 WorldPos, Color Color, bool Fade, bool MoveUp) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) Clamped = false; this.Go = Go; this.Text = Text; this.Backdrop = Backdrop; this.StartTime = StartTime; this.WorldPos = WorldPos; this.Color = Color; this.Fade = Fade; this.MoveUp = MoveUp; } [CompilerGenerated] public readonly void Deconstruct(out GameObject Go, out Text Text, out Image Backdrop, out float StartTime, out Vector3 WorldPos, out Color Color, out bool Fade, out bool MoveUp) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) Go = this.Go; Text = this.Text; Backdrop = this.Backdrop; StartTime = this.StartTime; WorldPos = this.WorldPos; Color = this.Color; Fade = this.Fade; MoveUp = this.MoveUp; } } internal class WorldToastManager { internal static float MaxAge = 3f; private const float FadeFraction = 1f / 3f; private const float FloatSpeed = 0.5f; private const float BackdropAlpha = 0.5f; private const int DefaultFontSize = 10; private const float ScreenMargin = 40f; private static WorldToastManager? instance; private readonly Canvas canvas; private readonly List entries = new List(); private WorldToastManager(Canvas canvas) { this.canvas = canvas; } internal static WorldToastManager Create() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Expected O, but got Unknown GameObject val = new GameObject("SpeedrunGymToastCanvas"); Canvas val2 = val.AddComponent(); val2.renderMode = (RenderMode)0; val.AddComponent().uiScaleMode = (ScaleMode)1; Object.DontDestroyOnLoad((Object)val); return instance = new WorldToastManager(val2); } internal static void ClearAll() { instance?.ClearAllInner(); } private void ClearAllInner() { foreach (WorldToastEntry entry in entries) { if (Object.op_Implicit((Object)(object)entry.Go)) { Object.Destroy((Object)(object)entry.Go); } } entries.Clear(); } internal void Destroy() { if (Object.op_Implicit((Object)(object)canvas)) { Object.Destroy((Object)(object)((Component)canvas).gameObject); } if (instance == this) { instance = null; } } internal static void Show(string message, Vector3 worldPos, Color? color = null, int fontSize = 10, bool fade = true, bool moveUp = true) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) instance?.ShowInner(message, worldPos, color, fontSize, fade, moveUp); } private void ShowInner(string message, Vector3 worldPos, Color? color, int fontSize, bool fade, bool moveUp) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected O, but got Unknown //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Expected O, but got Unknown //IL_00a4: 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_00d1: Unknown result type (might be due to invalid IL or missing references) //IL_010a: 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) Color color2 = (Color)(((??)color) ?? Color.white); GameObject val = new GameObject("WorldToast"); val.transform.SetParent(((Component)canvas).transform, false); Image val2 = val.AddComponent(); ((Graphic)val2).color = new Color(0f, 0f, 0f, 0.5f); ((Graphic)val2).raycastTarget = false; HorizontalLayoutGroup obj = val.AddComponent(); ((LayoutGroup)obj).padding = new RectOffset(4, 4, 2, 2); ((HorizontalOrVerticalLayoutGroup)obj).childControlWidth = true; ((HorizontalOrVerticalLayoutGroup)obj).childControlHeight = true; ContentSizeFitter obj2 = val.AddComponent(); obj2.horizontalFit = (FitMode)2; obj2.verticalFit = (FitMode)2; GameObject val3 = new GameObject("Text"); val3.transform.SetParent(val.transform, false); Text val4 = val3.AddComponent(); val4.text = message; val4.fontSize = fontSize; ((Graphic)val4).color = color2; val4.alignment = (TextAnchor)4; val4.font = Resources.GetBuiltinResource("Arial.ttf"); ((Graphic)val4).raycastTarget = false; val4.horizontalOverflow = (HorizontalWrapMode)1; entries.Add(new WorldToastEntry(val, val4, val2, Time.time, worldPos, color2, fade, moveUp)); List list = entries; UpdatePosition(list[list.Count - 1]); } internal void Update() { //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) //IL_00e0: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Unknown result type (might be due to invalid IL or missing references) //IL_0121: Unknown result type (might be due to invalid IL or missing references) float time = Time.time; for (int num = entries.Count - 1; num >= 0; num--) { WorldToastEntry worldToastEntry = entries[num]; bool flag = UpdatePosition(worldToastEntry); float num2 = time - worldToastEntry.StartTime; float num3 = MaxAge * 0.6666666f; if (worldToastEntry.Clamped && !flag && num2 < num3) { worldToastEntry.StartTime = time; num2 = 0f; } worldToastEntry.Clamped = flag; if (num2 > MaxAge) { Object.Destroy((Object)(object)worldToastEntry.Go); entries.RemoveAt(num); } else { float num4 = (worldToastEntry.Fade ? Mathf.Clamp01((MaxAge - num2) / (MaxAge * (1f / 3f))) : 1f); ((Graphic)worldToastEntry.Text).color = new Color(worldToastEntry.Color.r, worldToastEntry.Color.g, worldToastEntry.Color.b, worldToastEntry.Color.a * num4); ((Graphic)worldToastEntry.Backdrop).color = new Color(0f, 0f, 0f, 0.5f * num4); entries[num] = worldToastEntry; } } } private bool UpdatePosition(WorldToastEntry entry) { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //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_0057: 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_0099: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_006c: 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_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Unknown result type (might be due to invalid IL or missing references) //IL_011a: Unknown result type (might be due to invalid IL or missing references) //IL_012a: Unknown result type (might be due to invalid IL or missing references) //IL_014c: Unknown result type (might be due to invalid IL or missing references) //IL_0151: Unknown result type (might be due to invalid IL or missing references) Camera main = Camera.main; if ((Object)(object)main == (Object)null) { return false; } float num = Time.time - entry.StartTime; Vector3 val = (entry.MoveUp ? (entry.WorldPos + Vector3.up * (num * 0.5f)) : entry.WorldPos); Vector3 val2 = main.WorldToScreenPoint(val); if (val2.z < 0f) { val2.x = (float)Screen.width - val2.x; val2.y = (float)Screen.height - val2.y; } RectTransform component = ((Component)canvas).GetComponent(); Vector2 val3 = default(Vector2); RectTransformUtility.ScreenPointToLocalPointInRectangle(component, Vector2.op_Implicit(val2), ((int)canvas.renderMode == 0) ? null : canvas.worldCamera, ref val3); Rect rect = component.rect; float num2 = Mathf.Clamp(val3.x, ((Rect)(ref rect)).xMin + 40f, ((Rect)(ref rect)).xMax - 40f); float num3 = Mathf.Clamp(val3.y, ((Rect)(ref rect)).yMin + 40f, ((Rect)(ref rect)).yMax - 40f); bool result = !Mathf.Approximately(num2, val3.x) || !Mathf.Approximately(num3, val3.y); ((Transform)entry.Go.GetComponent()).localPosition = Vector2.op_Implicit(new Vector2(num2, num3)); return result; } } } namespace SpeedrunGym.Source.Moves { internal static class ForceCrawPogo { private enum Mode { Normal, Early, Late1, Late2 } private static ConfigEntry mode = null; private static int decisionCount; private static SendRandomEventV4? patchedAction; private static float[] origWeights = Array.Empty(); private static int[] origEventMax = Array.Empty(); private static int[] origMissedMax = Array.Empty(); internal static void BindConfig(ConfigFile config) { mode = config.Bind("Normalization", "Force craw pogo", Mode.Normal, "Force how often the craw in Greymoor_15 flaps before diving."); mode.SettingChanged += delegate { decisionCount = 0; Restore(); }; } internal static void Cleanup() { Restore(); } internal static void OnSceneLoaded() { decisionCount = 0; patchedAction = null; } private static void Restore() { if (patchedAction != null) { for (int i = 0; i < origWeights.Length; i++) { patchedAction.weights[i].Value = origWeights[i]; } for (int j = 0; j < origEventMax.Length; j++) { patchedAction.eventMax[j].Value = origEventMax[j]; } for (int k = 0; k < origMissedMax.Length; k++) { patchedAction.missedMax[k].Value = origMissedMax[k]; } patchedAction = null; } } internal static void OnEnterState(Fsm fsm, FsmState state) { if (mode.Value == Mode.Normal || state.Name != "Flap Move" || fsm.Name != "Behaviour") { return; } GameObject gameObject = fsm.GameObject; if (!Object.op_Implicit((Object)(object)gameObject) || ((Object)gameObject).name != "Crow (3)") { return; } FsmStateAction? obj = Array.Find(state.Actions, (FsmStateAction a) => a is SendRandomEventV4); SendRandomEventV4 val = (SendRandomEventV4)(object)((obj is SendRandomEventV4) ? obj : null); if (val == null) { return; } int num = Array.FindIndex(val.events, (FsmEvent e) => e.Name == "ATTACK"); int num2 = Array.FindIndex(val.events, (FsmEvent e) => e.Name == "SHIFT"); if (num < 0 || num2 < 0) { return; } if (patchedAction != val) { patchedAction = val; origWeights = Array.ConvertAll(val.weights, (FsmFloat w) => w.Value); origEventMax = Array.ConvertAll(val.eventMax, (FsmInt m) => m.Value); origMissedMax = Array.ConvertAll(val.missedMax, (FsmInt m) => m.Value); } int num3 = mode.Value switch { Mode.Early => 1, Mode.Late1 => 2, Mode.Late2 => 3, _ => 0, }; decisionCount++; bool flag = decisionCount >= num3; Log.Info(string.Format("[craw] normalize flap-move decision #{0} (target {1}) -> {2}", decisionCount, num3, flag ? "ATTACK" : "SHIFT")); if (flag) { decisionCount = 0; } int num4 = (flag ? num : num2); for (int num5 = 0; num5 < val.weights.Length; num5++) { val.weights[num5].Value = ((num5 == num4) ? 1f : 0f); } FsmInt[] eventMax = val.eventMax; for (int num6 = 0; num6 < eventMax.Length; num6++) { eventMax[num6].Value = int.MaxValue; } eventMax = val.missedMax; for (int num6 = 0; num6 < eventMax.Length; num6++) { eventMax[num6].Value = int.MaxValue; } } } internal static class JumpTimingDetector { private const string Section = "Jump Timing"; private static ConfigEntry repress = null; private static ConfigEntry sprintjumpRelease = null; private static readonly Color Color = new Color(0.4f, 1f, 0.4f); private const float ResetMs = 250f; private static bool prevPressed; private static PogoEndlagDetector.FrameTime? released; private static bool shuttlecockActive; internal static void BindConfig(ConfigFile config) { repress = config.Bind("Jump Timing", "Repress", false, "Report how long each air jump repress took (release → re-press) in a popup next to Hornet."); sprintjumpRelease = config.Bind("Jump Timing", "Sprintjump Release", false, "Out of a dashjump (shuttlecock), report the vertical velocity at the moment jump was released."); } internal static void OnInputCleared() { released = null; prevPressed = false; } internal static void OnShuttleCockJump() { shuttlecockActive = true; } internal static void LateUpdate() { //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_0147: Unknown result type (might be due to invalid IL or missing references) if (!repress.Value && !sprintjumpRelease.Value) { return; } InputHandler silentInstance = ManagerSingleton.SilentInstance; if ((Object)(object)silentInstance == (Object)null) { return; } bool isPressed = ((OneAxisInputControl)silentInstance.inputActions.Jump).IsPressed; HeroController silentInstance2 = HeroController.SilentInstance; if (Object.op_Implicit((Object)(object)silentInstance2) && (silentInstance2.cState.onGround || silentInstance2.cState.dashing)) { released = null; shuttlecockActive = false; prevPressed = isPressed; return; } PogoEndlagDetector.FrameTime now = PogoEndlagDetector.FrameTime.Now; if (prevPressed && !isPressed) { released = now; if (shuttlecockActive) { if (sprintjumpRelease.Value) { HeroToast(silentInstance2, $"release yvel={0f - YVel(silentInstance2):+0.0;-0.0}", Color, 1.9f); } shuttlecockActive = false; } } else { PogoEndlagDetector.FrameTime? frameTime; if (!prevPressed && isPressed) { frameTime = released; if (frameTime.HasValue) { PogoEndlagDetector.FrameTime valueOrDefault = frameTime.GetValueOrDefault(); PogoEndlagDetector.FrameTimeDelta delta = now - valueOrDefault; released = null; if (repress.Value && delta.Ms <= 250f) { HeroToast(silentInstance2, $"repress +{Discount(delta)}", Color, 1.2f); } goto IL_0190; } } frameTime = released; if (frameTime.HasValue) { PogoEndlagDetector.FrameTime valueOrDefault2 = frameTime.GetValueOrDefault(); if ((now - valueOrDefault2).Ms > 250f) { released = null; } } } goto IL_0190; IL_0190: prevPressed = isPressed; } private static float YVel(HeroController? hero) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) if (!Object.op_Implicit((Object)(object)hero)) { return 0f; } Rigidbody2D component = ((Component)hero).GetComponent(); if (!((Object)(object)component != (Object)null)) { return 0f; } return component.linearVelocity.y; } private static PogoEndlagDetector.FrameTimeDelta Discount(PogoEndlagDetector.FrameTimeDelta delta) { int num = Math.Max(0, delta.Frames - 1); float ms = ((delta.Frames > 0) ? (delta.Ms * (float)num / (float)delta.Frames) : 0f); return new PogoEndlagDetector.FrameTimeDelta { Frames = num, Ms = ms }; } private static void HeroToast(HeroController? hero, string message, Color color, float heightOffset) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) if (Object.op_Implicit((Object)(object)hero)) { WorldToastManager.Show(message, hero.transform.position + Vector3.up * heightOffset + Vector3.right * (float)(hero.cState.facingRight ? 1 : (-1)), color, 10, fade: true, moveUp: false); } } } [HarmonyPatch] internal static class JumpTimingPatches { [HarmonyPostfix] [HarmonyPatch(typeof(HeroController), "ClearJumpInputState")] private static void ClearJumpInputState() { try { JumpTimingDetector.OnInputCleared(); } catch (Exception data) { Log.Error(data); } } [HarmonyPostfix] [HarmonyPatch(typeof(HeroController), "OnShuttleCockJump")] private static void OnShuttleCockJump() { try { JumpTimingDetector.OnShuttleCockJump(); } catch (Exception data) { Log.Error(data); } } } internal static class PogoEndlagDetector { internal record struct FrameTime { public static FrameTime Now => new FrameTime { frame = Time.frameCount, Time = Time.realtimeSinceStartup }; private int frame; private float Time; public static FrameTimeDelta operator -(FrameTime a, FrameTime b) { return new FrameTimeDelta { Frames = a.frame - b.frame, Ms = (a.Time - b.Time) * 1000f }; } [CompilerGenerated] private readonly bool PrintMembers(StringBuilder builder) { return false; } } internal record struct FrameTimeDelta { public int Frames; public float Ms; public override string ToString() { if (!showFrameCounts.Value) { return $"{Ms:0}ms"; } return $"{Frames}f ({Ms:0}ms)"; } } private record CurrentDownspike { public FrameTime? DirectionPress; public FrameTime? FirstCanJump; public FrameTime? JumpExecuted; public FrameTime LastInput; public FrameTime? NeutralDeadline; public bool ReleasedNeutral; public FrameTime? RelevantInput; public FrameTimeDelta? RepressDelta; public bool Success; public bool WasJumping; } private const string Section = "Pogo Endlag"; private static ConfigEntry enabled = null; private static ConfigEntry showGood = null; private static ConfigEntry slowRepressThresholdMs = null; private static ConfigEntry slowJumpThresholdMs = null; private static ConfigEntry showFrameCounts = null; private static readonly Color GoodColor = new Color(0.4f, 1f, 0.4f); private static readonly Color SlowColor = new Color(1f, 1f, 0.4f); private static readonly Color FailColor = new Color(1f, 0.3f, 0.4f); private static CurrentDownspike? startedDownspike; internal static void BindConfig(ConfigFile config) { enabled = config.Bind("Pogo Endlag", "Enabled", false, "Detect pogo (downspike) endlag cancels and show feedback popups next to Hornet."); showGood = config.Bind("Pogo Endlag", "Show good", true, "Show a popup on a successful endlag cancel."); slowRepressThresholdMs = config.Bind("Pogo Endlag", "Slow repress threshold", 60f, "Repress slower than this many ms turns the popup yellow."); slowJumpThresholdMs = config.Bind("Pogo Endlag", "Slow jump threshold", 60f, "Jump slower than this many ms turns the popup yellow."); showFrameCounts = config.Bind("Pogo Endlag", "Show frame counts", false, "Include the frame count in popups (e.g. '3f (50ms)') instead of just milliseconds."); } internal static void OnDownAttack(HeroController hero) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) if (enabled.Value && (int)hero.Config.DownSlashType == 0 && !hero.cState.shuttleCock) { startedDownspike = new CurrentDownspike(); } } internal static void OnUpdateState(HeroAnimationController anim, ActorStates newState) { //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Expected I4, but got Unknown //IL_00f6: Unknown result type (might be due to invalid IL or missing references) if ((object)startedDownspike == null) { return; } FrameTime? relevantInput = startedDownspike.RelevantInput; if (relevantInput.HasValue) { return; } HeroControllerStates cState = HeroController.SilentInstance.cState; if (!cState.downSpiking && !cState.downSpikeRecovery) { return; } FrameTime now = FrameTime.Now; switch (newState - 1) { case 0: startedDownspike = startedDownspike with { RelevantInput = now }; startedDownspike.Success = true; startedDownspike.WasJumping = cState.jumping; break; case 1: startedDownspike = startedDownspike with { RelevantInput = now, NeutralDeadline = startedDownspike.LastInput, Success = false }; relevantInput = startedDownspike.DirectionPress; if (relevantInput.HasValue) { FrameTime valueOrDefault = relevantInput.GetValueOrDefault(); HeroToast($"dir repress {now - valueOrDefault} early", FailColor); } break; case 2: break; } } private static void HeroToast(string message, Color color) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) HeroController silentInstance = HeroController.SilentInstance; if (Object.op_Implicit((Object)(object)silentInstance)) { WorldToastManager.Show(message, silentInstance.transform.position + Vector3.up * 1.5f + Vector3.right * (float)(silentInstance.cState.facingRight ? 1 : (-1)), color, 10, fade: true, moveUp: false); } } internal static void LateUpdate() { //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_01ea: Unknown result type (might be due to invalid IL or missing references) //IL_023e: Unknown result type (might be due to invalid IL or missing references) HeroController silentInstance = HeroController.SilentInstance; if (!Object.op_Implicit((Object)(object)silentInstance)) { return; } CurrentDownspike currentDownspike = startedDownspike; if ((object)currentDownspike == null) { return; } if (silentInstance.cState.dashing) { startedDownspike = null; return; } float x = ((TwoAxisInputControl)ManagerSingleton.SilentInstance.inputActions.MoveVector).Vector.x; FrameTime? directionPress; if (x == 0f) { currentDownspike.ReleasedNeutral = true; } else if ((object)currentDownspike != null && currentDownspike.ReleasedNeutral) { directionPress = currentDownspike.DirectionPress; if (!directionPress.HasValue) { currentDownspike.DirectionPress = FrameTime.Now; } } currentDownspike.LastInput = FrameTime.Now; if ((object)currentDownspike == null) { return; } directionPress = currentDownspike.RelevantInput; if (!directionPress.HasValue) { return; } FrameTime valueOrDefault = directionPress.GetValueOrDefault(); FrameTimeDelta value = FrameTime.Now - valueOrDefault; bool flag = value.Ms > 300f; if (currentDownspike.Success) { if (!showGood.Value) { startedDownspike = null; return; } FrameTimeDelta? repressDelta = currentDownspike.RepressDelta; if (!repressDelta.HasValue && x != 0f) { currentDownspike.RepressDelta = value; } FrameTime? firstCanJump; if ((object)currentDownspike != null) { directionPress = currentDownspike.JumpExecuted; if (!directionPress.HasValue) { firstCanJump = currentDownspike.FirstCanJump; if (!firstCanJump.HasValue && silentInstance.CanJump()) { currentDownspike.FirstCanJump = FrameTime.Now; } } } firstCanJump = currentDownspike.JumpExecuted; if (!firstCanJump.HasValue && silentInstance.cState.jumping && !currentDownspike.WasJumping) { currentDownspike.JumpExecuted = FrameTime.Now; } currentDownspike.WasJumping = silentInstance.cState.jumping; if ((object)currentDownspike != null) { repressDelta = currentDownspike.RepressDelta; if (repressDelta.HasValue) { firstCanJump = currentDownspike.JumpExecuted; if (firstCanJump.HasValue) { goto IL_01c3; } } } if (!flag) { return; } goto IL_01c3; } if (flag) { FrameTime? firstCanJump = currentDownspike.DirectionPress; if (!firstCanJump.HasValue) { HeroToast("no neutral", FailColor); } startedDownspike = null; } else if (x == 0f) { FrameTime? firstCanJump = currentDownspike.DirectionPress; if (!firstCanJump.HasValue) { FrameTime frameTime = currentDownspike.NeutralDeadline ?? valueOrDefault; HeroToast($"neutral {FrameTime.Now - frameTime} late", FailColor); } startedDownspike = null; } return; IL_01c3: ShowSuccessResult(currentDownspike); startedDownspike = null; } private static void ShowSuccessResult(CurrentDownspike d) { //IL_00e2: Unknown result type (might be due to invalid IL or missing references) //IL_00db: Unknown result type (might be due to invalid IL or missing references) bool flag = false; string text = null; FrameTimeDelta? repressDelta = d.RepressDelta; if (repressDelta.HasValue) { FrameTimeDelta valueOrDefault = repressDelta.GetValueOrDefault(); if (valueOrDefault.Ms >= slowRepressThresholdMs.Value) { flag = true; } text = $"repress +{valueOrDefault}"; } string text2 = null; FrameTime? jumpExecuted = d.JumpExecuted; if (jumpExecuted.HasValue) { FrameTime valueOrDefault2 = jumpExecuted.GetValueOrDefault(); jumpExecuted = d.FirstCanJump; FrameTimeDelta obj; if (jumpExecuted.HasValue) { FrameTime valueOrDefault3 = jumpExecuted.GetValueOrDefault(); obj = valueOrDefault2 - valueOrDefault3; } else { obj = default(FrameTimeDelta); } FrameTimeDelta frameTimeDelta = obj; if (frameTimeDelta.Ms >= slowJumpThresholdMs.Value) { flag = true; } text2 = $"jump +{frameTimeDelta}"; } string text3 = ((text == null) ? text2 : ((text2 == null) ? text : (text + "\n" + text2))); if (text3 != null) { HeroToast(text3, flag ? SlowColor : GoodColor); } } } [HarmonyPatch] internal static class PogoEndlagPatches { [HarmonyPrefix] [HarmonyPatch(typeof(HeroController), "DownAttack")] private static void DownAttack(HeroController __instance) { try { PogoEndlagDetector.OnDownAttack(__instance); } catch (Exception data) { Log.Error(data); } } [HarmonyPrefix] [HarmonyPatch(typeof(HeroAnimationController), "UpdateState")] private static void UpdateState(HeroAnimationController __instance, ActorStates newState) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) try { PogoEndlagDetector.OnUpdateState(__instance, newState); } catch (Exception data) { Log.Error(data); } } } }