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<CompletedSplit>(_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<SplitDefinition> Splits = new List<SplitDefinition>();
private ConfigEntry<string> _category;
private ConfigEntry<string> _timingMethod;
private ConfigEntry<bool> _hudEnabled;
private ConfigEntry<float> _scale;
private ConfigEntry<HudPosition> _position;
private ConfigEntry<float> _opacity;
private ConfigEntry<int> _visibleSplits;
private ConfigEntry<bool> _showCategory;
private ConfigEntry<bool> _showMilliseconds;
private ConfigEntry<KeyboardShortcut> _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<string>("Run", "Category", "Any%", new ConfigDescription("Category label recorded with the run.", (AcceptableValueBase)(object)new AcceptableValueList<string>(new string[2] { "Any%", "100%" }), new object[0]));
_timingMethod = plugin.Config.Bind<string>("Timer", "TimingMethod", "Loadless", new ConfigDescription("Timer shown by the HUD.", (AcceptableValueBase)(object)new AcceptableValueList<string>(new string[2] { "Loadless", "RTA" }), new object[0]));
_hudEnabled = plugin.Config.Bind<bool>("HUD", "Enabled", true, "Show the in-game speedrun HUD.");
_scale = plugin.Config.Bind<float>("HUD", "Scale", 1f, new ConfigDescription("HUD scale.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.6f, 1.8f), new object[0]));
_position = plugin.Config.Bind<HudPosition>("HUD", "Position", HudPosition.TopRight, "Screen anchor for the HUD.");
_opacity = plugin.Config.Bind<float>("HUD", "Opacity", 1f, new ConfigDescription("HUD text opacity.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.2f, 1f), new object[0]));
_visibleSplits = plugin.Config.Bind<int>("HUD", "VisibleSplits", 6, new ConfigDescription("Maximum visible split rows.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 12), new object[0]));
_showCategory = plugin.Config.Bind<bool>("HUD", "ShowCategory", true, "Show the category name.");
_showMilliseconds = plugin.Config.Bind<bool>("HUD", "ShowMilliseconds", true, "Show milliseconds in timer values.");
_toggleHud = plugin.Config.Bind<KeyboardShortcut>("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<ModConfig>(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<CompletedSplit> 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<SplitDefinition> _definitions;
private readonly List<CompletedSplit> _completed = new List<CompletedSplit>();
private TimeSpan _segmentStart;
public IList<CompletedSplit> 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<SplitDefinition> definitions)
{
_definitions = definitions ?? new List<SplitDefinition>();
}
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<string, string> 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<RectTransform>(val, "_creditsHolder");
GameObject val3 = ReadField<GameObject>(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<RectTransform>();
((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<TextMeshProUGUI>(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<RectTransform>();
((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<TextMeshProUGUI>();
((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<T>(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<TextMeshProUGUI> _names = new List<TextMeshProUGUI>();
private readonly List<TextMeshProUGUI> _times = new List<TextMeshProUGUI>();
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<Canvas>();
component.renderMode = (RenderMode)0;
component.sortingOrder = 32760;
CanvasScaler component2 = _root.GetComponent<CanvasScaler>();
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<CompletedSplit> 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<LocalizationManager>(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>();
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<TextMeshProUGUI> 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<RectTransform>();
((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<TextMeshProUGUI>();
((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);
}
}
}