using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Security.Cryptography; using BepInEx; using BepInEx.Configuration; using HarmonyLib; using HowToFish.SpeedrunMod.Configuration; using HowToFish.SpeedrunMod.Core; using HowToFish.SpeedrunMod.Game; using HowToFish.SpeedrunMod.UI; using Newtonsoft.Json; using TMPro; using UnityEngine; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: AssemblyVersion("0.0.0.0")] namespace HowToFish.SpeedrunMod { [BepInPlugin("community.howtofish.speedrunmod", "How to Fish Speedrun Mod", "0.2.6")] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "community.howtofish.speedrunmod"; public const string PluginName = "How to Fish Speedrun Mod"; public const string PluginVersion = "0.2.6"; private ModConfig _config; private RunClock _clock; private SplitStateMachine _splits; private GameTelemetry _telemetry; private SpeedrunHud _hud; private FinalResultUI _finalUI; private Harmony _harmony; private RunState _state; private FinishedRunResult _finished; private bool _manualHudVisible = true; private bool _lastMenu; private bool _lastLoading; private TimeSpan Elapsed { get { if (!UseLoadless) { return _clock.Rta; } return _clock.Loadless; } } private bool UseLoadless => string.Equals(_config.TimingMethod, "Loadless", StringComparison.OrdinalIgnoreCase); private void Awake() { string text = Path.Combine(Paths.ConfigPath, "HowToFish.Speedrun"); Directory.CreateDirectory(text); string path = Path.Combine(text, "any-percent.json"); _config = ModConfig.Load(path); _config.Bind((BaseUnityPlugin)(object)this); _clock = new RunClock(new StopwatchTickSource()); _splits = new SplitStateMachine(_config.Splits); _telemetry = new GameTelemetry(); _hud = new SpeedrunHud(_config); _hud.Create(); _finalUI = new FinalResultUI(); GameSignals.FinalAcceptedImmediate = (Action)Delegate.Combine(GameSignals.FinalAcceptedImmediate, new Action(FinishRun)); InstallReadOnlyHooks(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"READ-ONLY GAME TELEMETRY — NO GAMEPLAY STATE MODIFICATION"); ((BaseUnityPlugin)this).Logger.LogInfo((object)("Game version " + Application.version + ", executable SHA256 " + ExecutableHash())); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Progression detector: DISCOVERED/INSTRUMENTED (5 production islands; level6 is dev island)."); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Final flow: INSTRUMENTED — requires user validation before leaderboard release."); } private void Update() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) KeyboardShortcut toggleHud = _config.ToggleHud; if (((KeyboardShortcut)(ref toggleHud)).IsDown()) { _manualHudVisible = !_manualHudVisible; } if (UnityInput.Current.GetKeyDown((KeyCode)290)) { ResetRun("manual-emergency"); } SignalBatch signalBatch = GameSignals.Consume(); GameSnapshot gameSnapshot = _telemetry.Read(); LogLoadingEdge(gameSnapshot); if (signalBatch.NewGameRequested) { ResetRun("new-game-created"); _state = RunState.PendingStart; ((BaseUnityPlugin)this).Logger.LogInfo((object)"NEW_GAME_PENDING first playable frame"); } if (signalBatch.SaveSelected) { ResetRun("different-save-loaded"); _state = RunState.Inactive; } if (_state == RunState.PendingStart && gameSnapshot.PlayerReady && !gameSnapshot.InMainMenu && !gameSnapshot.Loading) { _splits.Reset(); _clock.Start(); _telemetry.ResetBaseline(gameSnapshot); _state = RunState.Running; ((BaseUnityPlugin)this).Logger.LogInfo((object)"RUN_START first playable frame"); } if (_state == RunState.Running) { _clock.SetLoading(gameSnapshot.Loading); if (!string.IsNullOrEmpty(signalBatch.BossKilled)) { LogEvent("BossKilled", signalBatch.BossKilled); } if (signalBatch.BoatUnlocked) { OnEvent("BoatUnlocked", null); } if (signalBatch.IslandUnlocked >= 0) { OnEvent("IslandUnlocked", signalBatch.IslandUnlocked.ToString()); } if (signalBatch.FinalKeysAcquired) { OnEvent("FinalKeysAcquired", null); } if (signalBatch.FinalInteractionDispatched) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"FINAL_INTERACTION_DISPATCHED (instrumentation; not timing finish)"); } if (signalBatch.FinalAccepted) { FinishRun(); } _telemetry.EmitEdges(gameSnapshot, LogEvent); if (!_lastMenu && gameSnapshot.InMainMenu) { ResetRun("returned-to-main-menu"); } } else { _telemetry.EmitEdges(gameSnapshot, LogEvent); } if (_state == RunState.Finished && gameSnapshot.InMainMenu) { _finalUI.Clear(); _finished = null; _state = RunState.Inactive; _clock.Reset(); _splits.Reset(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"FINAL_RESULT_CLEARED main-menu"); } _lastMenu = gameSnapshot.InMainMenu; _finalUI.Tick(gameSnapshot.InMainMenu); bool flag = _state == RunState.Running && _config.HudEnabled && _manualHudVisible && !gameSnapshot.InMainMenu && !gameSnapshot.Loading && !EndGameUI.IsShowingEndGame; _hud.Visible = flag; if (flag) { _hud.Render(_config.Category, Elapsed, _splits); } } private void OnEvent(string eventName, string value) { CompletedSplit completedSplit = _splits.Handle(eventName, value, Elapsed); if (completedSplit != null) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("SPLIT " + completedSplit.Name + " segment=" + RunClock.Format(completedSplit.Segment) + " cumulative=" + RunClock.Format(completedSplit.Cumulative))); } else { LogEvent(eventName, value); } } private void LogEvent(string eventName, string value) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("EVENT " + eventName + (string.IsNullOrEmpty(value) ? string.Empty : (" value=" + value)))); } private void FinishRun() { if (_state == RunState.Running) { _clock.Stop(); TimeSpan elapsed = Elapsed; CompletedSplit completedSplit = _splits.Handle("RunCompleted", null, elapsed); if (completedSplit != null) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("SPLIT " + completedSplit.Name + " segment=" + RunClock.Format(completedSplit.Segment) + " cumulative=" + RunClock.Format(completedSplit.Cumulative))); } _finished = new FinishedRunResult { Category = _config.Category, TimingMethod = _config.TimingMethod, TotalTime = elapsed, FinalSegmentTime = (completedSplit?.Segment ?? TimeSpan.Zero), CompletionTimestampUtc = DateTime.UtcNow, GameVersion = Application.version, Splits = new List(_splits.Completed) }; _state = RunState.Finished; _hud.Visible = false; _finalUI.SetResult(_finished); ((BaseUnityPlugin)this).Logger.LogInfo((object)("RUN_COMPLETE_ACCEPTED frozen=" + RunClock.Format(elapsed) + " detector=INSTRUMENTED")); SaveRun(_finished); } } private void ResetRun(string reason) { if (_clock != null && (_clock.Active || _clock.Finished)) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("RUN_RESET " + reason + " at " + RunClock.Format(_clock.Rta))); } if (_clock != null) { _clock.Reset(); } if (_splits != null) { _splits.Reset(); } if (_finalUI != null) { _finalUI.Clear(); } _finished = null; _state = RunState.Inactive; } private void InstallReadOnlyHooks() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected O, but got Unknown _harmony = new Harmony("community.howtofish.speedrunmod"); PatchPostfix(typeof(SaveManager), "CreateServer", "NewGamePostfix"); PatchPostfix(typeof(SaveManager), "SelectServer", "SelectSavePostfix"); PatchPrefix(typeof(BossManager), "OnBossDeath", "BossDeathPrefix"); PatchPostfix(typeof(Boat), "UnlockBoat", "BoatUnlockedPostfix"); PatchPrefix(typeof(OnlineIslandManager), "UnlockIsland", "UnlockIslandPrefix"); PatchPostfix(typeof(NPCManager), "SetFinalBossKilled", "FinalKeysPostfix"); PatchPostfix(typeof(EndGameInteractable), "Interact", "FinalInteractionPostfix"); PatchPrefix(typeof(EndGameManager), "FinishGame", "FinalAcceptedPrefix"); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Read-only observer hooks installed; Tutorial split/hook removed."); } private void PatchPostfix(Type targetType, string targetName, string hookName) { //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(targetType, targetName, (Type[])null, (Type[])null); MethodInfo methodInfo2 = AccessTools.Method(typeof(GameHooks), hookName, (Type[])null, (Type[])null); if (methodInfo == null || methodInfo2 == null) { throw new MissingMethodException(targetType.FullName, targetName); } _harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } private void PatchPrefix(Type targetType, string targetName, string hookName) { //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Expected O, but got Unknown MethodInfo methodInfo = AccessTools.Method(targetType, targetName, (Type[])null, (Type[])null); MethodInfo methodInfo2 = AccessTools.Method(typeof(GameHooks), hookName, (Type[])null, (Type[])null); if (methodInfo == null || methodInfo2 == null) { throw new MissingMethodException(targetType.FullName, targetName); } _harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } private void LogLoadingEdge(GameSnapshot snapshot) { if (snapshot.Loading != _lastLoading) { ((BaseUnityPlugin)this).Logger.LogInfo((object)(snapshot.Loading ? "LOADING_STARTED" : "LOADING_ENDED")); } _lastLoading = snapshot.Loading; } private void SaveRun(FinishedRunResult result) { try { string text = Path.Combine(Paths.ConfigPath, "HowToFish.Speedrun", "runs"); Directory.CreateDirectory(text); string path = Path.Combine(text, result.CompletionTimestampUtc.ToString("yyyyMMdd-HHmmss") + ".json"); File.WriteAllText(path, JsonConvert.SerializeObject((object)new { category = result.Category, timingMethod = result.TimingMethod, duration = result.TotalTime.TotalMilliseconds, finalSegment = result.FinalSegmentTime.TotalMilliseconds, completedAtUtc = result.CompletionTimestampUtc, gameVersion = result.GameVersion, toolVersion = "0.2.6", executableSha256 = ExecutableHash(), splits = result.Splits, detectorStatus = "INSTRUMENTED" }, (Formatting)1)); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Unable to save run record: " + ex.Message)); } } private static string ExecutableHash() { try { string path = Path.Combine(Paths.GameRootPath, "How to Fish.exe"); using SHA256 sHA = SHA256.Create(); using FileStream inputStream = File.OpenRead(path); return BitConverter.ToString(sHA.ComputeHash(inputStream)).Replace("-", string.Empty); } catch { return "unavailable"; } } private void OnDestroy() { GameSignals.FinalAcceptedImmediate = (Action)Delegate.Remove(GameSignals.FinalAcceptedImmediate, new Action(FinishRun)); if (_harmony != null) { _harmony.UnpatchSelf(); } } } } namespace HowToFish.SpeedrunMod.Configuration { public enum HudPosition { TopRight, TopLeft, BottomRight, BottomLeft } public sealed class ModConfig { public int SchemaVersion = 5; public List Splits = new List(); private ConfigEntry _category; private ConfigEntry _timingMethod; private ConfigEntry _hudEnabled; private ConfigEntry _scale; private ConfigEntry _position; private ConfigEntry _opacity; private ConfigEntry _visibleSplits; private ConfigEntry _showCategory; private ConfigEntry _showMilliseconds; private ConfigEntry _toggleHud; public string Category => _category.Value; public string TimingMethod => _timingMethod.Value; public bool HudEnabled => _hudEnabled.Value; public float Scale => _scale.Value; public HudPosition Position => _position.Value; public float Opacity => _opacity.Value; public int MaxVisibleRows => _visibleSplits.Value; public bool ShowCategory => _showCategory.Value; public bool ShowMilliseconds => _showMilliseconds.Value; public KeyboardShortcut ToggleHud => _toggleHud.Value; public float RightMargin => 32f; public float TopMargin => 28f; public float Width => 370f; public void Bind(BaseUnityPlugin plugin) { //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Expected O, but got Unknown //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Expected O, but got Unknown //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Expected O, but got Unknown //IL_014b: Unknown result type (might be due to invalid IL or missing references) //IL_0155: Expected O, but got Unknown //IL_017f: Unknown result type (might be due to invalid IL or missing references) //IL_0189: Expected O, but got Unknown //IL_01ec: Unknown result type (might be due to invalid IL or missing references) _category = plugin.Config.Bind("Run", "Category", "Any%", new ConfigDescription("Category label recorded with the run.", (AcceptableValueBase)(object)new AcceptableValueList(new string[2] { "Any%", "100%" }), new object[0])); _timingMethod = plugin.Config.Bind("Timer", "TimingMethod", "Loadless", new ConfigDescription("Timer shown by the HUD.", (AcceptableValueBase)(object)new AcceptableValueList(new string[2] { "Loadless", "RTA" }), new object[0])); _hudEnabled = plugin.Config.Bind("HUD", "Enabled", true, "Show the in-game speedrun HUD."); _scale = plugin.Config.Bind("HUD", "Scale", 1f, new ConfigDescription("HUD scale.", (AcceptableValueBase)(object)new AcceptableValueRange(0.6f, 1.8f), new object[0])); _position = plugin.Config.Bind("HUD", "Position", HudPosition.TopRight, "Screen anchor for the HUD."); _opacity = plugin.Config.Bind("HUD", "Opacity", 1f, new ConfigDescription("HUD text opacity.", (AcceptableValueBase)(object)new AcceptableValueRange(0.2f, 1f), new object[0])); _visibleSplits = plugin.Config.Bind("HUD", "VisibleSplits", 6, new ConfigDescription("Maximum visible split rows.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 12), new object[0])); _showCategory = plugin.Config.Bind("HUD", "ShowCategory", true, "Show the category name."); _showMilliseconds = plugin.Config.Bind("HUD", "ShowMilliseconds", true, "Show milliseconds in timer values."); _toggleHud = plugin.Config.Bind("Hotkeys", "ToggleHUD", new KeyboardShortcut((KeyCode)289, (KeyCode[])(object)new KeyCode[0]), "Toggle the gameplay HUD. Settings remain available through Mod Menu when installed."); } public static ModConfig Load(string path) { ModConfig modConfig = null; if (File.Exists(path)) { modConfig = JsonConvert.DeserializeObject(File.ReadAllText(path)); } if (modConfig == null || modConfig.SchemaVersion < 5 || modConfig.Splits == null || modConfig.Splits.Exists((SplitDefinition s) => string.Equals(s.Event, "TutorialComplete", StringComparison.OrdinalIgnoreCase))) { modConfig = CreateDefault(); Directory.CreateDirectory(Path.GetDirectoryName(path)); File.WriteAllText(path, JsonConvert.SerializeObject((object)modConfig, (Formatting)1)); } return modConfig; } public static ModConfig CreateDefault() { ModConfig modConfig = new ModConfig(); modConfig.Splits.Add(new SplitDefinition { Name = "Island 1", Event = "BoatUnlocked" }); modConfig.Splits.Add(new SplitDefinition { Name = "Island 2", Event = "IslandUnlocked", Value = "3" }); modConfig.Splits.Add(new SplitDefinition { Name = "Island 3", Event = "IslandUnlocked", Value = "4" }); modConfig.Splits.Add(new SplitDefinition { Name = "Island 4", Event = "IslandUnlocked", Value = "5" }); modConfig.Splits.Add(new SplitDefinition { Name = "Final Island", Event = "FinalKeysAcquired" }); modConfig.Splits.Add(new SplitDefinition { Name = "Game Completion", Event = "RunCompleted" }); return modConfig; } } } namespace HowToFish.SpeedrunMod.Core { internal enum RunState { Inactive, PendingStart, Running, Finished } internal sealed class FinishedRunResult { internal string Category; internal string TimingMethod; internal TimeSpan TotalTime; internal TimeSpan FinalSegmentTime; internal DateTime CompletionTimestampUtc; internal string GameVersion; internal List Splits; } public interface ITickSource { long Frequency { get; } long Timestamp { get; } } public sealed class StopwatchTickSource : ITickSource { public long Frequency => Stopwatch.Frequency; public long Timestamp => Stopwatch.GetTimestamp(); } public sealed class RunClock { private readonly ITickSource _ticks; private long _startTick; private long _excludedTicks; private long _loadingStartTick; private long _stopTick; public bool Active { get; private set; } public bool Finished { get; private set; } public bool Loading { get; private set; } public TimeSpan Rta { get { if (!Active && !Finished) { return TimeSpan.Zero; } return ToTimeSpan((Finished ? _stopTick : _ticks.Timestamp) - _startTick); } } public TimeSpan Loadless { get { if (!Active && !Finished) { return TimeSpan.Zero; } long num = (Finished ? _stopTick : _ticks.Timestamp); long num2 = (Loading ? (num - _loadingStartTick) : 0); return ToTimeSpan(num - _startTick - _excludedTicks - num2); } } public RunClock(ITickSource ticks) { _ticks = ticks; } public void Start() { _startTick = _ticks.Timestamp; _excludedTicks = 0L; _loadingStartTick = 0L; Loading = false; Active = true; Finished = false; _stopTick = 0L; } public void Stop() { if (Active) { _stopTick = _ticks.Timestamp; if (Loading && _loadingStartTick != 0) { _excludedTicks += _stopTick - _loadingStartTick; } Loading = false; _loadingStartTick = 0L; Active = false; Finished = true; } } public void Reset() { Active = false; Loading = false; _startTick = 0L; _excludedTicks = 0L; _loadingStartTick = 0L; _stopTick = 0L; Finished = false; } public void SetLoading(bool loading) { if (Active && loading != Loading) { long timestamp = _ticks.Timestamp; if (loading) { _loadingStartTick = timestamp; } else if (_loadingStartTick != 0) { _excludedTicks += timestamp - _loadingStartTick; } Loading = loading; if (!loading) { _loadingStartTick = 0L; } } } private TimeSpan ToTimeSpan(long ticks) { return TimeSpan.FromSeconds((double)Math.Max(0L, ticks) / (double)_ticks.Frequency); } public static string Format(TimeSpan value) { if (value.TotalHours >= 1.0) { return string.Format(CultureInfo.InvariantCulture, "{0}:{1:00}:{2:00}.{3:000}", (int)value.TotalHours, value.Minutes, value.Seconds, value.Milliseconds); } return string.Format(CultureInfo.InvariantCulture, "{0:00}:{1:00}.{2:000}", (int)value.TotalMinutes, value.Seconds, value.Milliseconds); } } public sealed class SplitDefinition { public string Name; public string Event; public string Value; } public sealed class CompletedSplit { public string Name; public TimeSpan Segment; public TimeSpan Cumulative; } public sealed class SplitStateMachine { private readonly IList _definitions; private readonly List _completed = new List(); private TimeSpan _segmentStart; public IList Completed => _completed.AsReadOnly(); public int CurrentIndex { get; private set; } public bool Finished => CurrentIndex >= _definitions.Count; public SplitDefinition Current { get { if (!Finished) { return _definitions[CurrentIndex]; } return null; } } public SplitStateMachine(IList definitions) { _definitions = definitions ?? new List(); } public void Reset() { _completed.Clear(); CurrentIndex = 0; _segmentStart = TimeSpan.Zero; } public CompletedSplit Handle(string eventName, string value, TimeSpan elapsed) { SplitDefinition current = Current; if (current == null || !Matches(current, eventName, value)) { return null; } CompletedSplit completedSplit = new CompletedSplit(); completedSplit.Name = current.Name; completedSplit.Cumulative = elapsed; completedSplit.Segment = elapsed - _segmentStart; _segmentStart = elapsed; _completed.Add(completedSplit); CurrentIndex++; return completedSplit; } public TimeSpan CurrentSegment(TimeSpan elapsed) { return elapsed - _segmentStart; } private static bool Matches(SplitDefinition definition, string eventName, string value) { if (!string.Equals(definition.Event, eventName, StringComparison.OrdinalIgnoreCase)) { return false; } if (!string.IsNullOrEmpty(definition.Value)) { return string.Equals(definition.Value, value, StringComparison.OrdinalIgnoreCase); } return true; } } } namespace HowToFish.SpeedrunMod.Game { internal sealed class SignalBatch { internal bool NewGameRequested; internal bool SaveSelected; internal string BossKilled; internal bool BoatUnlocked; internal int IslandUnlocked = -1; internal bool FinalKeysAcquired; internal bool FinalInteractionDispatched; internal bool FinalAccepted; } internal static class GameSignals { internal static bool NewGameRequested; internal static bool SaveSelected; internal static string BossKilled; internal static bool BoatUnlocked; internal static int IslandUnlocked = -1; internal static bool FinalKeysAcquired; internal static bool FinalInteractionDispatched; internal static bool FinalAccepted; internal static Action FinalAcceptedImmediate; internal static SignalBatch Consume() { SignalBatch signalBatch = new SignalBatch(); signalBatch.NewGameRequested = NewGameRequested; signalBatch.SaveSelected = SaveSelected; signalBatch.BossKilled = BossKilled; signalBatch.BoatUnlocked = BoatUnlocked; signalBatch.IslandUnlocked = IslandUnlocked; signalBatch.FinalKeysAcquired = FinalKeysAcquired; signalBatch.FinalInteractionDispatched = FinalInteractionDispatched; signalBatch.FinalAccepted = FinalAccepted; SignalBatch result = signalBatch; NewGameRequested = false; SaveSelected = false; BossKilled = null; BoatUnlocked = false; IslandUnlocked = -1; FinalKeysAcquired = false; FinalInteractionDispatched = false; FinalAccepted = false; return result; } } internal static class GameHooks { internal static void NewGamePostfix() { GameSignals.NewGameRequested = true; } internal static void SelectSavePostfix() { GameSignals.SaveSelected = true; } internal static void BossDeathPrefix() { try { Creature boss = BossManager.Boss; GameSignals.BossKilled = (((Object)(object)boss == (Object)null) ? "UnknownBoss" : ((object)boss).GetType().Name); } catch { GameSignals.BossKilled = "UnknownBoss"; } } internal static void UnlockIslandPrefix(byte islandIndex) { try { if (islandIndex > OnlineIslandManager.MaxIslandUnlocked) { GameSignals.IslandUnlocked = islandIndex; } } catch { GameSignals.IslandUnlocked = islandIndex; } } internal static void BoatUnlockedPostfix(Boat __instance) { if ((Object)(object)__instance != (Object)null && __instance.BoatUnlocked) { GameSignals.BoatUnlocked = true; } } internal static void FinalKeysPostfix() { GameSignals.FinalKeysAcquired = true; } internal static void FinalInteractionPostfix() { GameSignals.FinalInteractionDispatched = true; } internal static void FinalAcceptedPrefix() { GameSignals.FinalAccepted = true; GameSignals.FinalAcceptedImmediate?.Invoke(); } } internal sealed class GameSnapshot { internal bool PlayerReady; internal bool InMainMenu; internal bool Loading; internal byte Island; internal bool Finished; } internal sealed class GameTelemetry { private bool _initialized; private GameSnapshot _last; internal GameSnapshot Read() { GameSnapshot gameSnapshot = new GameSnapshot(); Player localPlayer = Player.LocalPlayer; gameSnapshot.PlayerReady = (Object)(object)localPlayer != (Object)null && Player.LocalPlayerEnabled; gameSnapshot.InMainMenu = MainMenuManager.IsInMenu; gameSnapshot.Loading = IslandManager.IsLoading || ReadPrivateStaticBool(typeof(LoadingManager), "_isShowingLoading"); gameSnapshot.Island = (byte)((!((Object)(object)OnlineIslandManager.Instance == (Object)null)) ? OnlineIslandManager.CurIsland : 0); gameSnapshot.Finished = (Object)(object)EndGameManager.Instance != (Object)null && EndGameManager.Instance.HasFinishedGame; return gameSnapshot; } internal void EmitEdges(GameSnapshot current, Action emit) { if (!_initialized) { _initialized = true; _last = current; return; } if (_last.Island != current.Island) { emit("IslandEntered", current.Island.ToString()); } if (!_last.Finished && current.Finished) { emit("CompletionStateObserved", null); } _last = current; } internal void ResetBaseline(GameSnapshot current) { _initialized = true; _last = current; } private static bool ReadPrivateStaticBool(Type type, string fieldName) { try { FieldInfo field = type.GetField(fieldName, BindingFlags.Static | BindingFlags.NonPublic); return field != null && (bool)field.GetValue(null); } catch { return false; } } } } namespace HowToFish.SpeedrunMod.UI { internal sealed class FinalResultUI { private GameObject _root; private FinishedRunResult _result; internal void SetResult(FinishedRunResult result) { _result = result; } internal void Tick(bool inMainMenu) { if (inMainMenu) { Clear(); return; } if (_result == null || !EndGameUI.IsShowingEndGame) { Hide(); return; } EndGameUI val = FindEndGameUI(); if ((Object)(object)val == (Object)null) { Hide(); return; } RectTransform val2 = ReadField(val, "_creditsHolder"); GameObject val3 = ReadField(val, "_endGameCanvas"); if ((Object)(object)val3 == (Object)null || !val3.activeInHierarchy || (Object)(object)val2 == (Object)null) { Hide(); return; } if ((Object)(object)_root == (Object)null) { Create(val3.transform, val2); } _root.transform.SetAsLastSibling(); _root.SetActive(true); } internal void Clear() { _result = null; if ((Object)(object)_root != (Object)null) { Object.Destroy((Object)(object)_root); } _root = null; } private void Hide() { if ((Object)(object)_root != (Object)null) { _root.SetActive(false); } } private void Create(Transform parent, RectTransform credits) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Expected O, but got Unknown //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) _root = new GameObject("SpeedrunFinalResult", new Type[1] { typeof(RectTransform) }); RectTransform component = _root.GetComponent(); ((Transform)component).SetParent(parent, false); component.anchorMin = new Vector2(0.5f, 0.7f); component.anchorMax = new Vector2(0.5f, 0.7f); component.pivot = new Vector2(0.5f, 0.5f); component.sizeDelta = new Vector2(720f, 180f); TextMeshProUGUI native = null; TextMeshProUGUI[] componentsInChildren = ((Component)credits).GetComponentsInChildren(true); if (componentsInChildren.Length > 0) { native = componentsInChildren[0]; } CreateText("Label", (Transform)(object)component, "TIME", 34f, new Vector2(0f, 42f), native); CreateText("Value", (Transform)(object)component, RunClock.Format(_result.TotalTime), 58f, new Vector2(0f, -28f), native); } private static void CreateText(string name, Transform parent, string value, float size, Vector2 position, TextMeshProUGUI native) { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Expected O, but got Unknown //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_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject(name, new Type[3] { typeof(RectTransform), typeof(CanvasRenderer), typeof(TextMeshProUGUI) }); RectTransform component = val.GetComponent(); ((Transform)component).SetParent(parent, false); Vector2 anchorMin = (component.anchorMax = new Vector2(0.5f, 0.5f)); component.anchorMin = anchorMin; component.pivot = new Vector2(0.5f, 0.5f); component.anchoredPosition = position; component.sizeDelta = new Vector2(700f, 68f); TextMeshProUGUI component2 = val.GetComponent(); ((TMP_Text)component2).text = value; ((TMP_Text)component2).fontSize = size; ((TMP_Text)component2).alignment = (TextAlignmentOptions)514; ((Graphic)component2).raycastTarget = false; if ((Object)(object)native != (Object)null) { ((TMP_Text)component2).font = ((TMP_Text)native).font; ((TMP_Text)component2).fontSharedMaterial = ((TMP_Text)native).fontSharedMaterial; ((Graphic)component2).color = ((Graphic)native).color; ((TMP_Text)component2).outlineColor = ((TMP_Text)native).outlineColor; ((TMP_Text)component2).outlineWidth = ((TMP_Text)native).outlineWidth; } } private static EndGameUI FindEndGameUI() { FieldInfo field = typeof(EndGameUI).GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic); if (!(field == null)) { object? value = field.GetValue(null); return (EndGameUI)((value is EndGameUI) ? value : null); } return null; } private static T ReadField(object instance, string name) where T : class { FieldInfo field = instance.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); if (!(field == null)) { return field.GetValue(instance) as T; } return null; } } internal sealed class SpeedrunHud { private readonly ModConfig _config; private GameObject _root; private TextMeshProUGUI _category; private TextMeshProUGUI _timer; private RectTransform _rowsRoot; private RectTransform _panel; private readonly List _names = new List(); private readonly List _times = new List(); private TMP_FontAsset _font; private TMP_FontAsset _backdropFont; private Material _sharedMaterial; internal bool Visible { get { if ((Object)(object)_root != (Object)null) { return _root.activeSelf; } return false; } set { if ((Object)(object)_root != (Object)null) { _root.SetActive(value); } } } internal SpeedrunHud(ModConfig config) { _config = config; } internal void Create() { //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Expected O, but got Unknown //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: 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_0106: Unknown result type (might be due to invalid IL or missing references) //IL_012e: Unknown result type (might be due to invalid IL or missing references) //IL_014e: Unknown result type (might be due to invalid IL or missing references) _root = new GameObject("SpeedrunHUD", new Type[3] { typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler) }); Object.DontDestroyOnLoad((Object)(object)_root); Canvas component = _root.GetComponent(); component.renderMode = (RenderMode)0; component.sortingOrder = 32760; CanvasScaler component2 = _root.GetComponent(); component2.uiScaleMode = (ScaleMode)1; component2.referenceResolution = new Vector2(1920f, 1080f); component2.screenMatchMode = (ScreenMatchMode)0; component2.matchWidthOrHeight = 0.5f; _panel = CreateRect("Anchor", _root.transform); _panel.anchorMin = new Vector2(1f, 1f); _panel.anchorMax = new Vector2(1f, 1f); _panel.pivot = new Vector2(1f, 1f); _panel.anchoredPosition = new Vector2(0f - _config.RightMargin, 0f - _config.TopMargin); _panel.sizeDelta = new Vector2(_config.Width, 480f); _category = CreateText("Category", (Transform)(object)_panel, 25f, (FontStyles)32, (TextAlignmentOptions)260); SetRect(((TMP_Text)_category).rectTransform, 0f, -2f, _config.Width, 34f); _timer = CreateText("Timer", (Transform)(object)_panel, 43f, (FontStyles)0, (TextAlignmentOptions)260); SetRect(((TMP_Text)_timer).rectTransform, 0f, -36f, _config.Width, 58f); _rowsRoot = CreateRect("Segments", (Transform)(object)_panel); SetRect(_rowsRoot, 0f, -104f, _config.Width, 370f); for (int i = 0; i < 12; i++) { TextMeshProUGUI val = CreateText("SegmentName" + i, (Transform)(object)_rowsRoot, 22f, (FontStyles)0, (TextAlignmentOptions)4097); TextMeshProUGUI val2 = CreateText("SegmentTime" + i, (Transform)(object)_rowsRoot, 22f, (FontStyles)0, (TextAlignmentOptions)4100); SetRect(((TMP_Text)val).rectTransform, -155f, (float)(-i) * 31f, _config.Width - 155f, 30f); SetRect(((TMP_Text)val2).rectTransform, 0f, (float)(-i) * 31f, 150f, 30f); _names.Add(val); _times.Add(val2); } ApplyNativeStyleWhenAvailable(); } internal void Render(string category, TimeSpan elapsed, SplitStateMachine splits) { if ((Object)(object)_root == (Object)null) { return; } if ((Object)(object)_font == (Object)null) { ApplyNativeStyleWhenAvailable(); } ApplyLiveSettings(); ((Component)_category).gameObject.SetActive(_config.ShowCategory); ((TMP_Text)_category).text = (category ?? "Any%").ToUpperInvariant(); ((TMP_Text)_timer).text = Format(elapsed); int num = Math.Max(1, Math.Min(_names.Count, _config.MaxVisibleRows)); int currentIndex = splits.CurrentIndex; int num2 = Math.Max(0, currentIndex - Math.Max(0, num - 2)); IList completed = splits.Completed; for (int i = 0; i < num; i++) { int num3 = num2 + i; if (num3 < completed.Count) { ((TMP_Text)_names[i]).text = completed[num3].Name; ((TMP_Text)_times[i]).text = Format(completed[num3].Segment); SetActiveStyle(i, active: false); } else if (num3 == currentIndex && splits.Current != null) { ((TMP_Text)_names[i]).text = splits.Current.Name; ((TMP_Text)_times[i]).text = Format(splits.CurrentSegment(elapsed)); SetActiveStyle(i, active: true); } else { ((TMP_Text)_names[i]).text = string.Empty; ((TMP_Text)_times[i]).text = string.Empty; SetActiveStyle(i, active: false); } } for (int j = num; j < _names.Count; j++) { ((TMP_Text)_names[j]).text = string.Empty; ((TMP_Text)_times[j]).text = string.Empty; ((Component)_names[j]).gameObject.SetActive(false); ((Component)_times[j]).gameObject.SetActive(false); } } private string Format(TimeSpan value) { string text = RunClock.Format(value); if (!_config.ShowMilliseconds) { return text.Substring(0, text.Length - 4); } return text; } private void ApplyLiveSettings() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: 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_012c: Unknown result type (might be due to invalid IL or missing references) //IL_0131: Unknown result type (might be due to invalid IL or missing references) //IL_0145: Unknown result type (might be due to invalid IL or missing references) ((Transform)_panel).localScale = Vector3.one * _config.Scale; bool flag = _config.Position == HudPosition.TopLeft || _config.Position == HudPosition.BottomLeft; bool flag2 = _config.Position == HudPosition.TopLeft || _config.Position == HudPosition.TopRight; RectTransform panel = _panel; Vector2 anchorMin = (_panel.anchorMax = new Vector2(flag ? 0f : 1f, flag2 ? 1f : 0f)); panel.anchorMin = anchorMin; _panel.pivot = new Vector2(flag ? 0f : 1f, flag2 ? 1f : 0f); _panel.anchoredPosition = new Vector2(flag ? _config.RightMargin : (0f - _config.RightMargin), flag2 ? (0f - _config.TopMargin) : _config.TopMargin); foreach (TextMeshProUGUI item in AllTexts()) { Color color = ((Graphic)item).color; color.a = _config.Opacity; ((Graphic)item).color = color; } } private void SetActiveStyle(int row, bool active) { //IL_0072: 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_0077: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) ((Component)_names[row]).gameObject.SetActive(true); ((Component)_times[row]).gameObject.SetActive(true); Color color = (active ? new Color(1f, 1f, 1f, _config.Opacity) : new Color(1f, 1f, 1f, 0.82f * _config.Opacity)); ((Graphic)_names[row]).color = color; ((Graphic)_times[row]).color = color; ((TMP_Text)_names[row]).fontSize = (active ? 23f : 22f); ((TMP_Text)_times[row]).fontSize = (active ? 23f : 22f); } private void ApplyNativeStyleWhenAvailable() { //IL_00fb: Unknown result type (might be due to invalid IL or missing references) //IL_0112: Unknown result type (might be due to invalid IL or missing references) TextMeshProUGUI val = FindVersionText(); if ((Object)(object)val != (Object)null) { _font = ((TMP_Text)val).font; _sharedMaterial = ((TMP_Text)val).fontSharedMaterial; } LocalizationManager val2 = Object.FindObjectOfType(true); if ((Object)(object)val2 != (Object)null) { FieldInfo field = typeof(LocalizationManager).GetField("_defaultFontAsset", BindingFlags.Instance | BindingFlags.NonPublic); FieldInfo field2 = typeof(LocalizationManager).GetField("_backdropFontAsset", BindingFlags.Instance | BindingFlags.NonPublic); if ((Object)(object)_font == (Object)null && field != null) { ref TMP_FontAsset font = ref _font; object? value = field.GetValue(val2); font = (TMP_FontAsset)((value is TMP_FontAsset) ? value : null); } if (field2 != null) { ref TMP_FontAsset backdropFont = ref _backdropFont; object? value2 = field2.GetValue(val2); backdropFont = (TMP_FontAsset)((value2 is TMP_FontAsset) ? value2 : null); } } if ((Object)(object)_font == (Object)null) { return; } foreach (TextMeshProUGUI item in AllTexts()) { ((TMP_Text)item).font = _font; if ((Object)(object)_sharedMaterial != (Object)null) { ((TMP_Text)item).fontSharedMaterial = _sharedMaterial; } ((Graphic)item).color = Color.white; ((TMP_Text)item).outlineColor = new Color32((byte)20, (byte)18, (byte)18, byte.MaxValue); ((TMP_Text)item).outlineWidth = 0.18f; ((TMP_Text)item).characterSpacing = 0.5f; } } private TextMeshProUGUI FindVersionText() { TextMeshProUGUI[] array = Resources.FindObjectsOfTypeAll(); TextMeshProUGUI[] array2 = array; foreach (TextMeshProUGUI val in array2) { if ((Object)(object)val != (Object)null && ((TMP_Text)val).text != null && ((TMP_Text)val).text.IndexOf("Version", StringComparison.OrdinalIgnoreCase) >= 0) { return val; } } return null; } private IEnumerable AllTexts() { yield return _category; yield return _timer; foreach (TextMeshProUGUI name in _names) { yield return name; } foreach (TextMeshProUGUI time in _times) { yield return time; } } private static RectTransform CreateRect(string name, Transform parent) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Expected O, but got Unknown //IL_0036: 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_0060: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject(name, new Type[1] { typeof(RectTransform) }); RectTransform component = val.GetComponent(); ((Transform)component).SetParent(parent, false); component.anchorMin = new Vector2(1f, 1f); component.anchorMax = new Vector2(1f, 1f); component.pivot = new Vector2(1f, 1f); return component; } private static TextMeshProUGUI CreateText(string name, Transform parent, float size, FontStyles style, TextAlignmentOptions alignment) { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Expected O, but got Unknown //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject(name, new Type[3] { typeof(RectTransform), typeof(CanvasRenderer), typeof(TextMeshProUGUI) }); val.transform.SetParent(parent, false); TextMeshProUGUI component = val.GetComponent(); ((TMP_Text)component).fontSize = size; ((TMP_Text)component).fontStyle = style; ((TMP_Text)component).alignment = alignment; ((TMP_Text)component).enableWordWrapping = false; ((Graphic)component).raycastTarget = false; ((TMP_Text)component).overflowMode = (TextOverflowModes)0; return component; } private static void SetRect(RectTransform rect, float x, float y, float width, float height) { //IL_000b: 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_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0042: 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) rect.anchorMin = new Vector2(1f, 1f); rect.anchorMax = new Vector2(1f, 1f); rect.pivot = new Vector2(1f, 1f); rect.anchoredPosition = new Vector2(x, y); rect.sizeDelta = new Vector2(width, height); } } }