The BepInEx console will not appear when launching like it does for other games on Thunderstore (you can turn it back on in your BepInEx.cfg file). If your PEAK crashes on startup, add -dx12 to your launch parameters.
Decompiled source of FollowTheWay v1.0.0
BepInEx/plugins/FollowTheWay.dll
Decompiled a day agousing System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Http; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using Cysharp.Threading.Tasks; using Cysharp.Threading.Tasks.CompilerServices; using FollowTheWay.Models; using FollowTheWay.Utilities; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: AssemblyTitle("FollowTheWay")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("FollowTheWay")] [assembly: AssemblyCopyright("Copyright © 2025")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("7aa796a6-4555-431c-9f0c-3f03bd9eca5e")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("1.0.0.0")] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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] [Microsoft.CodeAnalysis.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; } } } namespace FollowTheWay { [BepInPlugin("com.followtheway.peak.mod", "FollowTheWay", "1.0.0")] public class FollowTheWayPlugin : BaseUnityPlugin { internal static ManualLogSource Log; private PathVisualizer pathVisualizer; private PathSelectionUI pathSelectionUI; private PathRecorder pathRecorder; private float _nextNickProbeAt; private const float NickProbeInterval = 1f; private bool _nickLoggedOnce = false; public static FollowTheWayPlugin Instance { get; private set; } private void Awake() { //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Expected O, but got Unknown //IL_0085: Unknown result type (might be due to invalid IL or missing references) Instance = this; Log = ((BaseUnityPlugin)this).Logger; ConfigHandler.Init(((BaseUnityPlugin)this).Config); if (!ConfigHandler.EnableRecording.Value) { LogInfo("FollowTheWay disabled via config. Exiting…"); return; } GameObject val = new GameObject("FollowTheWay PathRecorder"); pathRecorder = val.AddComponent<PathRecorder>(); Object.DontDestroyOnLoad((Object)(object)val); PathRecorder.SetPlayerNameProvider(GetGameNicknameSafe); if (ConfigHandler.AllowUploads.Value) { new Harmony("com.followtheway.peak.mod").PatchAll(); LogInfo("Harmony patches applied successfully"); } JsonConvert.DefaultSettings = () => new JsonSerializerSettings { ReferenceLoopHandling = (ReferenceLoopHandling)1, NullValueHandling = (NullValueHandling)1 }; SceneManager.activeSceneChanged += OnActiveSceneChanged; LogInfo("FollowTheWay initialized successfully!"); } private void Update() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) if (Input.GetKeyDown(ConfigHandler.ToggleMenuKey.Value)) { pathSelectionUI?.ToggleMenu(); } if (Time.unscaledTime >= _nextNickProbeAt) { _nextNickProbeAt = Time.unscaledTime + 1f; string gameNicknameSafe = GetGameNicknameSafe(); if (!string.IsNullOrEmpty(gameNicknameSafe) && !_nickLoggedOnce) { LogInfo("Detected player nickname: " + gameNicknameSafe); _nickLoggedOnce = true; } } } private void OnActiveSceneChanged(Scene oldScene, Scene newScene) { string text = ((Scene)(ref newScene)).name.ToLowerInvariant(); if (text.Contains("menu") || text.Contains("title") || text.Contains("lobby") || ((Scene)(ref newScene)).name == "Pretitle" || ((Scene)(ref newScene)).name == "Airport") { PathRecorder.Instance?.StopIfRecording(PathRecorder.StopReason.LevelChange); } else if (IsGameLevel(((Scene)(ref newScene)).name)) { InitializeGameComponents(); } } private bool IsGameLevel(string sceneName) { if (string.IsNullOrEmpty(sceneName)) { return false; } string text = sceneName.ToLowerInvariant(); if (text.Contains("menu") || text.Contains("title") || text.Contains("lobby")) { return false; } if (sceneName == "Pretitle" || sceneName == "Airport") { return false; } return true; } private void InitializeGameComponents() { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Expected O, but got Unknown //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Expected O, but got Unknown try { GameObject val = new GameObject("FollowTheWay Path Manager"); pathVisualizer = val.AddComponent<PathVisualizer>(); Object.DontDestroyOnLoad((Object)(object)val); Color color = default(Color); ((Color)(ref color))..ctor(ConfigHandler.LineColorR.Value, ConfigHandler.LineColorG.Value, ConfigHandler.LineColorB.Value, 1f); pathVisualizer.UpdateStyleFromConfig(ConfigHandler.LineWidth.Value, color, ConfigHandler.ForceOnTop.Value, ConfigHandler.SmoothCorners.Value, ConfigHandler.DecimationStep.Value, ConfigHandler.MaxPoints.Value); if (ConfigHandler.ShowPathSelectionUI.Value) { GameObject val2 = new GameObject("FollowTheWay UI Manager"); pathSelectionUI = val2.AddComponent<PathSelectionUI>(); Object.DontDestroyOnLoad((Object)(object)val2); pathSelectionUI.Initialize(pathVisualizer); LogInfo("PathSelectionUI initialized"); } _nickLoggedOnce = false; LogInfo("Game components initialized successfully"); } catch (Exception arg) { LogError($"Failed to init game components: {arg}"); } } private string GetGameNicknameSafe() { try { Character localCharacter = Character.localCharacter; if ((Object)(object)localCharacter != (Object)null && localCharacter.IsLocal) { try { PropertyInfo property = ((object)localCharacter).GetType().GetProperty("characterName"); if (property != null) { string text = property.GetValue(localCharacter) as string; if (!string.IsNullOrWhiteSpace(text) && text != "Bot") { return text; } } } catch { } try { Component component = ((Component)localCharacter).GetComponent("PhotonView"); if ((Object)(object)component != (Object)null) { object obj2 = ((object)component).GetType().GetProperty("Owner")?.GetValue(component, null); if (obj2 != null) { string text2 = obj2.GetType().GetProperty("NickName")?.GetValue(obj2, null) as string; if (!string.IsNullOrWhiteSpace(text2) && text2 != "Bot") { return text2; } } } } catch { } } } catch (Exception ex) { LogDebug("GetGameNicknameSafe error: " + ex.Message); } return null; } public PathVisualizer GetPathVisualizer() { return pathVisualizer; } public PathSelectionUI GetPathSelectionUI() { return pathSelectionUI; } public static void LogDebug(string msg) { ManualLogSource log = Log; if (log != null) { log.Log((LogLevel)32, (object)msg); } } public static void LogInfo(string msg) { ManualLogSource log = Log; if (log != null) { log.Log((LogLevel)16, (object)msg); } } public static void LogWarning(string msg) { ManualLogSource log = Log; if (log != null) { log.Log((LogLevel)4, (object)msg); } } public static void LogError(string msg) { ManualLogSource log = Log; if (log != null) { log.Log((LogLevel)2, (object)msg); } } public static void LogError(Exception ex) { ManualLogSource log = Log; if (log != null) { log.Log((LogLevel)2, (object)(ex.Message + "\n" + ex.StackTrace)); } } private void OnDestroy() { SceneManager.activeSceneChanged -= OnActiveSceneChanged; } } [HarmonyPatch] public static class GameEventPatches { [HarmonyPatch(typeof(Character), "Update")] [HarmonyPostfix] public static void OnCharacterUpdate(Character __instance) { //IL_0022: 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_0028: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)__instance == (Object)(object)Character.localCharacter && (Object)(object)PathRecorder.Instance != (Object)null) { Vector3 center = __instance.Center; PathRecorder.RecordPosition(center); } } [HarmonyPatch(typeof(Character), "RPCA_Die")] [HarmonyPostfix] public static void OnCharacterDie(Character __instance) { PathRecorder instance = PathRecorder.Instance; if (!((Object)(object)__instance != (Object)(object)Character.localCharacter) && !((Object)(object)instance == (Object)null)) { instance.StopIfRecording(PathRecorder.StopReason.Death); } } [HarmonyPatch(typeof(Character), "RPCEndGame")] [HarmonyPostfix] public static void OnCharacterWin(Character __instance) { PathRecorder instance = PathRecorder.Instance; if (!((Object)(object)__instance != (Object)(object)Character.localCharacter) && !((Object)(object)instance == (Object)null)) { instance.ReportVictoryEvent(); } } } public static class PathDataManager { public class PathMetaLight { public string Id; public string Name; public string Author; public string CreatedAtLocal; public int PointCount; } private static string PathsDirectory => Path.Combine(Paths.ConfigPath, "FollowTheWay", "paths"); public static string SavePath(PathData pathData) { try { Directory.CreateDirectory(PathsDirectory); string text = $"path_{pathData.Timestamp}.json"; string path = Path.Combine(PathsDirectory, text); string contents = JsonConvert.SerializeObject((object)pathData, (Formatting)1); File.WriteAllText(path, contents); FollowTheWayPlugin.LogInfo("Path saved: " + text); return text; } catch (Exception ex) { FollowTheWayPlugin.LogError("Failed to save path: " + ex.Message); return null; } } public static void SavePathAs(string fileName, PathData pathData) { try { Directory.CreateDirectory(PathsDirectory); string path = Path.Combine(PathsDirectory, fileName); string contents = JsonConvert.SerializeObject((object)pathData, (Formatting)1); File.WriteAllText(path, contents); FollowTheWayPlugin.LogInfo("Path updated: " + fileName); } catch (Exception ex) { FollowTheWayPlugin.LogError("Failed to update path " + fileName + ": " + ex.Message); } } public static IEnumerable<string> GetAllPaths() { Directory.CreateDirectory(PathsDirectory); return Directory.EnumerateFiles(PathsDirectory, "*.json").Select(Path.GetFileName); } public static PathData LoadPath(string fileName) { try { string path = Path.Combine(PathsDirectory, fileName); if (File.Exists(path)) { string text = File.ReadAllText(path); return JsonConvert.DeserializeObject<PathData>(text); } } catch (Exception ex) { FollowTheWayPlugin.LogError("Failed to load path " + fileName + ": " + ex.Message); } return null; } public static void DeletePath(string fileName) { try { string path = Path.Combine(PathsDirectory, fileName); if (File.Exists(path)) { File.Delete(path); FollowTheWayPlugin.LogInfo("Deleted path: " + fileName); } } catch (Exception ex) { FollowTheWayPlugin.LogError("Failed to delete path " + fileName + ": " + ex.Message); } } public static void DeleteAll() { try { Directory.CreateDirectory(PathsDirectory); IEnumerable<string> enumerable = Directory.EnumerateFiles(PathsDirectory, "*.json"); int num = 0; foreach (string item in enumerable) { try { File.Delete(item); num++; } catch { } } FollowTheWayPlugin.LogInfo($"Deleted all paths: {num} files"); } catch (Exception ex) { FollowTheWayPlugin.LogError("Failed to delete all paths: " + ex.Message); } } public static PathMetaLight TryReadMeta(string fileName) { try { string path = Path.Combine(PathsDirectory, fileName); if (!File.Exists(path)) { return null; } PathData pathData = LoadPath(fileName); if (pathData == null) { return null; } string createdAtLocal = ""; try { createdAtLocal = DateTimeOffset.FromUnixTimeSeconds(pathData.Timestamp).ToLocalTime().DateTime.ToString("yyyy-MM-dd HH:mm:ss"); } catch { } int pointCount = pathData.Points?.Count ?? 0; string name = ((!string.IsNullOrWhiteSpace(pathData.Name)) ? pathData.Name : fileName); string author = pathData.Author ?? ""; return new PathMetaLight { Id = fileName, Name = name, Author = author, CreatedAtLocal = createdAtLocal, PointCount = pointCount }; } catch { return null; } } } public class PathRecorder : MonoBehaviour { public enum StopReason { Unknown, Manual, LevelChange, Victory, Death } public readonly List<Vector3> CurrentPath = new List<Vector3>(1024); private float lastRecordTime; private float startTime; private const float RECORD_INTERVAL = 0.1f; private const float VICTORY_GRACE = 3f; private StopReason? _finalReason; private bool _savingInProgress; private bool pendingVictory; private float pendingVictoryUntil; private string lastSavedFileName; private static Func<string> _playerNameProvider; public static PathRecorder Instance { get; private set; } public bool IsRecording { get; private set; } public StopReason? FinalReason => _finalReason; private static int Priority(StopReason r) { return r switch { StopReason.Death => 100, StopReason.Victory => 90, StopReason.LevelChange => 70, StopReason.Manual => 60, _ => 0, }; } public static void SetPlayerNameProvider(Func<string> provider) { _playerNameProvider = provider; } private void Awake() { if ((Object)(object)Instance == (Object)null) { Instance = this; Object.DontDestroyOnLoad((Object)(object)((Component)this).gameObject); FollowTheWayPlugin.LogInfo("PathRecorder initialized"); } else { Object.Destroy((Object)(object)((Component)this).gameObject); } } private void OnDestroy() { if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } } private void Update() { if (pendingVictory && Time.time >= pendingVictoryUntil) { pendingVictory = false; if (IsRecording) { FollowTheWayPlugin.LogDebug("[PathRecorder] Victory grace expired -> stopping as Victory"); StopRecording(StopReason.Victory); } } } public void StartRecording() { if (IsRecording) { FollowTheWayPlugin.LogDebug("Already recording path"); return; } CurrentPath.Clear(); IsRecording = true; lastRecordTime = Time.time; startTime = lastRecordTime; _finalReason = null; _savingInProgress = false; pendingVictory = false; pendingVictoryUntil = 0f; lastSavedFileName = null; FollowTheWayPlugin.LogInfo("Started recording path"); } public void ReportVictoryEvent() { FollowTheWayPlugin.LogDebug(string.Format("[PathRecorder] ReportVictoryEvent received. isRecording={0}, finalReason={1}, pendingVictory={2}", IsRecording, _finalReason?.ToString() ?? "null", pendingVictory)); if (IsRecording && _finalReason.GetValueOrDefault() != StopReason.Death) { _finalReason = StopReason.Victory; pendingVictory = true; pendingVictoryUntil = Time.time + 3f; FollowTheWayPlugin.LogDebug($"[PathRecorder] Victory scheduled with grace {3f}s (until {pendingVictoryUntil:0.00})"); } } public void StopRecording(StopReason reason) { FollowTheWayPlugin.LogDebug(string.Format("[PathRecorder] StopRecording called with {0}. isRecording={1}, finalReason={2}, pendingVictory={3}", reason, IsRecording, _finalReason?.ToString() ?? "null", pendingVictory)); pendingVictory = false; if (_finalReason.HasValue) { if (_finalReason.Value != StopReason.Death && Priority(reason) > Priority(_finalReason.Value)) { _finalReason = reason; } } else { _finalReason = reason; } if (IsRecording) { IsRecording = false; if (_savingInProgress) { FollowTheWayPlugin.LogDebug("[PathRecorder] Duplicate StopRecording ignored"); return; } _savingInProgress = true; string text = SafeGetPlayerName(); string text2 = ((!string.IsNullOrWhiteSpace(text)) ? text : "LocalPlayer"); DateTimeOffset now = DateTimeOffset.Now; string name = $"Run {now:yyyy-MM-dd HH:mm:ss}"; long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); float num = Mathf.Max(0f, Time.time - startTime); StopReason valueOrDefault = _finalReason.GetValueOrDefault(reason); PathData pathData = new PathData { Timestamp = timestamp, DurationSeconds = num, Reason = valueOrDefault, Points = new List<Vector3>(CurrentPath), PlayerName = text2, CompletionTime = num, PlayerDied = (valueOrDefault == StopReason.Death), Name = name, Author = text2 }; string text3 = (lastSavedFileName = PathDataManager.SavePath(pathData)); FollowTheWayPlugin.LogInfo(string.Format("Stopped recording path. Reason: {0}, Points: {1}, Duration: {2:F1}s, File: {3}", valueOrDefault, CurrentPath.Count, pathData.DurationSeconds, text3 ?? "n/a")); _savingInProgress = false; } } public void StopIfRecording(StopReason reason) { FollowTheWayPlugin.LogDebug(string.Format("[PathRecorder] StopIfRecording called with {0}. isRecording={1}, finalReason={2}, pendingVictory={3}", reason, IsRecording, _finalReason?.ToString() ?? "null", pendingVictory)); if (reason == StopReason.Victory) { ReportVictoryEvent(); return; } if (_finalReason.HasValue) { if (_finalReason.Value != StopReason.Death && Priority(reason) > Priority(_finalReason.Value)) { _finalReason = reason; } } else { _finalReason = reason; } if (IsRecording) { StopRecording(reason); } else if (reason == StopReason.Death) { TryFixLastSavedReasonToDeath(); } } public static void RecordPosition(Vector3 position) { //IL_0043: Unknown result type (might be due to invalid IL or missing references) PathRecorder instance = Instance; if (!((Object)(object)instance == (Object)null) && instance.IsRecording) { float time = Time.time; if (!(time - instance.lastRecordTime < 0.1f)) { instance.CurrentPath.Add(position); instance.lastRecordTime = time; } } } private void TryFixLastSavedReasonToDeath() { try { if (!string.IsNullOrEmpty(lastSavedFileName)) { PathData pathData = PathDataManager.LoadPath(lastSavedFileName); if (pathData != null && pathData.Reason != StopReason.Death) { pathData.Reason = StopReason.Death; pathData.PlayerDied = true; PathDataManager.SavePathAs(lastSavedFileName, pathData); FollowTheWayPlugin.LogDebug("[PathRecorder] Patched last saved file to Death: " + lastSavedFileName); } } } catch (Exception ex) { FollowTheWayPlugin.LogDebug("[PathRecorder] Failed to patch last saved file to Death: " + ex.Message); } } private string SafeGetPlayerName() { try { if (_playerNameProvider != null) { string text = _playerNameProvider(); if (!string.IsNullOrWhiteSpace(text)) { return text; } } } catch (Exception ex) { FollowTheWayPlugin.LogDebug("SafeGetPlayerName error: " + ex.Message); } return null; } } public class PathVisualizer : MonoBehaviour { private LineRenderer lineRenderer; private Material baseMaterial; private Vector3[] _tempBuffer; public int DecimationStep { get; set; } = 1; public int MaxPoints { get; set; } = 100000; public float LineWidth { get; set; } = 0.35f; public Color LineColor { get; set; } = Color.green; public bool ForceOnTop { get; set; } = false; public bool SmoothCorners { get; set; } = true; private void Awake() { //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Expected O, but got Unknown //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Expected O, but got Unknown lineRenderer = ((Component)this).gameObject.AddComponent<LineRenderer>(); Material val = null; string[] array = new string[4] { "Unlit/Color", "Sprites/Default", "Legacy Shaders/Unlit/Color", "Standard" }; string[] array2 = array; foreach (string text in array2) { Shader val2 = Shader.Find(text); if ((Object)(object)val2 != (Object)null) { val = new Material(val2); FollowTheWayPlugin.LogDebug("[FollowTheWay] Using shader: " + text); break; } } if ((Object)(object)val == (Object)null) { val = new Material(Shader.Find("Sprites/Default")); FollowTheWayPlugin.LogWarning("[FollowTheWay] Using fallback shader"); } baseMaterial = val; ((Renderer)lineRenderer).material = baseMaterial; ApplyStyle(); lineRenderer.positionCount = 0; ((Component)this).gameObject.SetActive(true); } private void ApplyStyle() { //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0082: 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_00b6: Unknown result type (might be due to invalid IL or missing references) lineRenderer.startWidth = LineWidth; lineRenderer.endWidth = LineWidth; if (SmoothCorners) { lineRenderer.numCapVertices = 5; lineRenderer.numCornerVertices = 5; } else { lineRenderer.numCapVertices = 0; lineRenderer.numCornerVertices = 0; } Color lineColor = LineColor; lineColor.a = 1f; lineRenderer.startColor = lineColor; lineRenderer.endColor = lineColor; if ((Object)(object)((Renderer)lineRenderer).material != (Object)null) { ((Renderer)lineRenderer).material.color = lineColor; } ((Renderer)lineRenderer).material.renderQueue = (ForceOnTop ? 5000 : 2000); lineRenderer.useWorldSpace = true; lineRenderer.alignment = (LineAlignment)0; ((Renderer)lineRenderer).shadowCastingMode = (ShadowCastingMode)0; ((Renderer)lineRenderer).receiveShadows = false; } public void UpdateStyleFromConfig(float width, Color color, bool forceOnTop, bool smooth, int decimation, int maxPoints) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) LineWidth = Mathf.Clamp(width, 0.02f, 1.5f); LineColor = color; ForceOnTop = forceOnTop; SmoothCorners = smooth; DecimationStep = Mathf.Max(1, decimation); MaxPoints = Mathf.Max(100, maxPoints); ApplyStyle(); } public void ShowPath(List<Vector3> points, Color? overrideColor = null) { //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_0058: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_00fb: Unknown result type (might be due to invalid IL or missing references) //IL_01c5: Unknown result type (might be due to invalid IL or missing references) //IL_0162: Unknown result type (might be due to invalid IL or missing references) //IL_0167: 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_013a: Unknown result type (might be due to invalid IL or missing references) if (points == null || points.Count < 2) { lineRenderer.positionCount = 0; FollowTheWayPlugin.LogDebug("[FollowTheWay] Path cleared - insufficient points"); return; } if (overrideColor.HasValue) { Color value = overrideColor.Value; value.a = 1f; LineColor = value; } ApplyStyle(); int count = points.Count; int num = Mathf.Min(count, MaxPoints); int num2 = Mathf.Max(1, DecimationStep); int num3 = (count + num2 - 1) / num2; if (num3 > num) { num2 = Mathf.CeilToInt((float)count / (float)num); num3 = (count + num2 - 1) / num2; } if (_tempBuffer == null || _tempBuffer.Length < num3) { _tempBuffer = (Vector3[])(object)new Vector3[Mathf.NextPowerOfTwo(num3)]; } int num4 = 0; for (int i = 0; i < count; i += num2) { if (num4 >= num) { break; } _tempBuffer[num4++] = points[i]; } if (num4 < num && count > 0 && (num4 == 0 || _tempBuffer[num4 - 1] != points[count - 1])) { _tempBuffer[num4++] = points[count - 1]; } lineRenderer.positionCount = num4; lineRenderer.SetPositions(_tempBuffer); FollowTheWayPlugin.LogDebug($"[FollowTheWay] Visualized path with {count} src -> {num4} rendered (step={num2}, max={MaxPoints}), color: {LineColor}"); } public void ClearPath() { lineRenderer.positionCount = 0; FollowTheWayPlugin.LogDebug("[FollowTheWay] Path cleared manually"); } } public class PathSelectionUI : MonoBehaviour { private struct CachedInfo { public long Timestamp; public float DurationSeconds; public PathRecorder.StopReason Reason; public int PointsCount; public string Name; public string Author; } private enum FilterType { All, Wins, Deaths, Manual, LevelChange } public static PathSelectionUI Instance; public static ManualLogSource Log; private bool _showMenu; private Vector2 _scroll; private Rect _windowRect = new Rect(40f, 40f, 900f, 820f); private int _windowId; private GUIStyle _valueLineStyle; private GUIStyle _titleStyle; private GUIStyle _headerStyle; private GUIStyle _statusStyle; private GUIStyle _greenButton; private GUIStyle _redButton; private GUIStyle _reasonStyle; private GUIStyle _boldLabel; private readonly List<string> _availablePaths = new List<string>(); private readonly Dictionary<string, CachedInfo> _cache = new Dictionary<string, CachedInfo>(); private string _selectedPath = ""; private FilterType _currentFilter = FilterType.All; private bool _sortByTime = false; private bool _sortByDate = true; private string _search = string.Empty; private string _pendingName = ""; private string _pendingAuthor = ""; public Action ApplyVisualizerStyle = delegate { }; public Action<string> ShowPath = delegate { }; public Action<string> HidePath = delegate { }; public Func<bool> IsPathShown = () => false; public Action<string> DeletePathById = delegate { }; public Action DeleteAllPaths = delegate { }; public Action<bool> ToggleRecording = delegate { }; public Func<bool> IsRecording = () => false; private void Awake() { Instance = this; _windowId = "FTW_PathSelectionUI".GetHashCode(); if (Log == null) { Log = Logger.CreateLogSource("FollowTheWay.UI"); } Log.LogInfo((object)"[UI] PathSelectionUI initialized"); } public void Initialize(PathVisualizer visualizer) { ApplyVisualizerStyle = delegate { //IL_004d: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)visualizer == (Object)null)) { Color color = default(Color); ((Color)(ref color))..ctor(ConfigHandler.LineColorR.Value, ConfigHandler.LineColorG.Value, ConfigHandler.LineColorB.Value, 1f); visualizer.UpdateStyleFromConfig(ConfigHandler.LineWidth.Value, color, ConfigHandler.ForceOnTop.Value, ConfigHandler.SmoothCorners.Value, Math.Max(1, ConfigHandler.DecimationStep.Value), Math.Max(100, ConfigHandler.MaxPoints.Value)); } }; ShowPath = delegate(string pathName) { PathData pathData = PathDataManager.LoadPath(pathName); if (pathData == null || pathData.Points == null || pathData.Points.Count < 2) { FollowTheWayPlugin.LogWarning("[FTW] ShowPath: path " + pathName + " is empty or not found"); visualizer.ClearPath(); } else { ApplyVisualizerStyle?.Invoke(); visualizer.ShowPath(pathData.Points); } }; HidePath = delegate { visualizer.ClearPath(); }; IsPathShown = delegate { LineRenderer component = ((Component)visualizer).GetComponent<LineRenderer>(); return (Object)(object)component != (Object)null && component.positionCount > 0; }; DeletePathById = delegate(string id) { PathDataManager.DeletePath(id); }; DeleteAllPaths = delegate { PathDataManager.DeleteAll(); }; ToggleRecording = delegate(bool wantOn) { if (wantOn) { PathRecorder.Instance?.StartRecording(); } else { PathRecorder.Instance?.StopRecording(PathRecorder.StopReason.Manual); } }; IsRecording = () => PathRecorder.Instance?.IsRecording ?? false; RefreshPathList(); } public void ToggleMenu() { _showMenu = !_showMenu; if (_showMenu) { RefreshPathList(); } } private void EnsureStyles() { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001f: 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_0030: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Expected O, but got Unknown //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Expected O, but got Unknown //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00d8: Expected O, but got Unknown //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_0100: Unknown result type (might be due to invalid IL or missing references) //IL_010d: Expected O, but got Unknown //IL_0129: 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_0134: Unknown result type (might be due to invalid IL or missing references) //IL_013f: 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) //IL_0154: Expected O, but got Unknown //IL_0170: Unknown result type (might be due to invalid IL or missing references) //IL_0175: Unknown result type (might be due to invalid IL or missing references) //IL_017b: Unknown result type (might be due to invalid IL or missing references) //IL_0186: Unknown result type (might be due to invalid IL or missing references) //IL_018e: Unknown result type (might be due to invalid IL or missing references) //IL_019b: Expected O, but got Unknown //IL_01b7: Unknown result type (might be due to invalid IL or missing references) //IL_01bc: Unknown result type (might be due to invalid IL or missing references) //IL_01c4: Unknown result type (might be due to invalid IL or missing references) //IL_01d1: Expected O, but got Unknown //IL_01ed: Unknown result type (might be due to invalid IL or missing references) //IL_01f2: Unknown result type (might be due to invalid IL or missing references) //IL_01fa: Unknown result type (might be due to invalid IL or missing references) //IL_0207: Expected O, but got Unknown if (_valueLineStyle == null) { _valueLineStyle = new GUIStyle(GUI.skin.label) { alignment = (TextAnchor)3, fontSize = 12, fontStyle = (FontStyle)0 }; _valueLineStyle.normal.textColor = new Color(0.92f, 0.95f, 1f, 1f); } if (_titleStyle == null) { _titleStyle = new GUIStyle(GUI.skin.label) { fontStyle = (FontStyle)1, alignment = (TextAnchor)3 }; } if (_headerStyle == null) { _headerStyle = new GUIStyle(GUI.skin.label) { fontSize = 16, fontStyle = (FontStyle)1, alignment = (TextAnchor)4 }; } if (_statusStyle == null) { _statusStyle = new GUIStyle(GUI.skin.label) { fontSize = 13, fontStyle = (FontStyle)1 }; } if (_greenButton == null) { GUIStyle val = new GUIStyle(GUI.skin.button); val.normal.textColor = Color.white; val.fontStyle = (FontStyle)1; val.alignment = (TextAnchor)4; _greenButton = val; } if (_redButton == null) { GUIStyle val2 = new GUIStyle(GUI.skin.button); val2.normal.textColor = Color.white; val2.fontStyle = (FontStyle)1; val2.alignment = (TextAnchor)4; _redButton = val2; } if (_reasonStyle == null) { _reasonStyle = new GUIStyle(GUI.skin.label) { fontStyle = (FontStyle)1, alignment = (TextAnchor)3 }; } if (_boldLabel == null) { _boldLabel = new GUIStyle(GUI.skin.label) { fontStyle = (FontStyle)1, alignment = (TextAnchor)3 }; } } private void OnGUI() { //IL_001e: 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_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Expected O, but got Unknown //IL_0052: 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) if (_showMenu) { EnsureStyles(); GUI.depth = 0; GUI.color = Color.white; GUI.backgroundColor = Color.white; _windowRect = GUI.Window(_windowId, _windowRect, new WindowFunction(DrawWindow), "FollowTheWay — Paths & Filters"); } } private void DrawWindow(int id) { //IL_008d: Unknown result type (might be due to invalid IL or missing references) GUILayout.BeginVertical(Array.Empty<GUILayoutOption>()); try { DrawHeaderAndRecordBar(); GUILayout.Space(6f); DrawMetaEditor(); GUILayout.Space(6f); DrawFilters(); GUILayout.Space(6f); DrawPathsList(); } catch (Exception arg) { ManualLogSource log = Log; if (log != null) { log.LogError((object)$"[UI] DrawWindow error: {arg}"); } } finally { GUILayout.EndVertical(); } GUI.DragWindow(new Rect(0f, 0f, 10000f, 24f)); } private void DrawHeaderAndRecordBar() { //IL_0049: 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_0074: 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_010f: Unknown result type (might be due to invalid IL or missing references) //IL_0125: Unknown result type (might be due to invalid IL or missing references) //IL_0093: 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_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_0179: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: Unknown result type (might be due to invalid IL or missing references) GUILayout.BeginVertical(GUI.skin.box, Array.Empty<GUILayoutOption>()); GUILayout.Label("FollowTheWay — Path Manager", _headerStyle, Array.Empty<GUILayoutOption>()); bool flag = IsRecording?.Invoke() ?? false; GUI.color = (flag ? Color.red : Color.green); GUILayout.Label(flag ? "Status: Recording" : "Status: Idle", _statusStyle, Array.Empty<GUILayoutOption>()); GUI.color = Color.white; GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); if (!flag) { Color backgroundColor = GUI.backgroundColor; GUI.backgroundColor = new Color(0.15f, 0.6f, 0.15f, 1f); if (GUILayout.Button("Start Recording", _greenButton, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Height(28f), GUILayout.Width(180f) })) { ToggleRecording?.Invoke(obj: true); } GUI.backgroundColor = backgroundColor; } else { Color backgroundColor2 = GUI.backgroundColor; GUI.backgroundColor = new Color(0.75f, 0.2f, 0.2f, 1f); if (GUILayout.Button("Stop Recording", _redButton, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Height(28f), GUILayout.Width(180f) })) { ToggleRecording?.Invoke(obj: false); } GUI.backgroundColor = backgroundColor2; } if (GUILayout.Button("Refresh List", (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Height(28f), GUILayout.Width(180f) })) { RefreshPathList(); } GUILayout.FlexibleSpace(); if (GUILayout.Button("Hide Visualization", (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Height(28f), GUILayout.Width(200f) })) { Func<bool> isPathShown = IsPathShown; if (isPathShown != null && isPathShown()) { HidePath?.Invoke(string.Empty); _selectedPath = ""; } } GUILayout.EndHorizontal(); GUILayout.EndVertical(); } private void DrawMetaEditor() { GUILayout.BeginVertical(GUI.skin.box, Array.Empty<GUILayoutOption>()); GUILayout.Label("Run Metadata", _titleStyle, Array.Empty<GUILayoutOption>()); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); GUILayout.Label("Name:", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(90f) }); _pendingName = GUILayout.TextField(_pendingName ?? "", (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.MinWidth(260f), GUILayout.ExpandWidth(true) }); GUILayout.Space(16f); GUILayout.Label("Author (nickname):", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(120f) }); _pendingAuthor = GUILayout.TextField(_pendingAuthor ?? "", (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.MinWidth(220f), GUILayout.ExpandWidth(true) }); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); GUILayout.FlexibleSpace(); if (GUILayout.Button("Apply to Last Saved", (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(220f), GUILayout.Height(26f) })) { ApplyMetaToLastSaved(_pendingName, _pendingAuthor); } GUILayout.EndHorizontal(); GUILayout.EndVertical(); } private void DrawFilters() { GUILayout.BeginVertical(GUI.skin.box, Array.Empty<GUILayoutOption>()); GUILayout.Label("Filters", _titleStyle, Array.Empty<GUILayoutOption>()); GUILayout.BeginHorizontal(GUI.skin.box, Array.Empty<GUILayoutOption>()); GUILayout.Label("Search:", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(60f) }); string text = GUILayout.TextField(_search, (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.MinWidth(260f), GUILayout.ExpandWidth(true) }); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(GUI.skin.box, Array.Empty<GUILayoutOption>()); string text2 = ((((Rect)(ref _windowRect)).width < 760f) ? "Date (↓ newer)" : "Date (newer → older)"); string text3 = ((((Rect)(ref _windowRect)).width < 760f) ? "Time (↑ faster)" : "Time (faster → slower)"); bool flag = GUILayout.Toggle(_sortByDate, text2, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(260f) }); bool flag2 = GUILayout.Toggle(_sortByTime, text3, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(260f) }); GUILayout.FlexibleSpace(); GUILayout.EndHorizontal(); if (text != _search || flag != _sortByDate || flag2 != _sortByTime) { _search = text; _sortByDate = flag; _sortByTime = flag2; if (_sortByDate) { _sortByTime = false; } if (_sortByTime) { _sortByDate = false; } } GUILayout.BeginHorizontal(GUI.skin.box, Array.Empty<GUILayoutOption>()); DrawFilterButton(FilterType.All, "All", 100f); DrawFilterButton(FilterType.Deaths, "Deaths", 100f); DrawFilterButton(FilterType.Wins, "Wins", 100f); DrawFilterButton(FilterType.Manual, "Manual", 100f); DrawFilterButton(FilterType.LevelChange, "Level Change", 100f); GUILayout.FlexibleSpace(); if (GUILayout.Button("Reset", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(120f) })) { _currentFilter = FilterType.All; _search = ""; _sortByDate = true; _sortByTime = false; } GUILayout.EndHorizontal(); GUILayout.EndVertical(); } private void DrawFilterButton(FilterType type, string label, float width) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) bool flag = _currentFilter == type; GUI.color = (flag ? Color.green : Color.white); if (GUILayout.Button(flag ? ("[" + label + "]") : label, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(width) })) { _currentFilter = type; } GUI.color = Color.white; } private void DrawPathsList() { //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: 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) //IL_011a: Unknown result type (might be due to invalid IL or missing references) //IL_019d: Unknown result type (might be due to invalid IL or missing references) //IL_0200: Unknown result type (might be due to invalid IL or missing references) //IL_0205: Unknown result type (might be due to invalid IL or missing references) //IL_020f: Unknown result type (might be due to invalid IL or missing references) //IL_0242: Unknown result type (might be due to invalid IL or missing references) //IL_02ee: Unknown result type (might be due to invalid IL or missing references) //IL_031f: Unknown result type (might be due to invalid IL or missing references) GUILayout.BeginVertical(GUI.skin.box, Array.Empty<GUILayoutOption>()); List<string> list = GetFilteredPaths().ToList(); GUILayout.Label($"Saved paths: {list.Count} of {_availablePaths.Count}", _titleStyle, Array.Empty<GUILayoutOption>()); if (list.Count == 0) { GUILayout.Label("Nothing found.", Array.Empty<GUILayoutOption>()); GUILayout.EndVertical(); return; } _scroll = GUILayout.BeginScrollView(_scroll, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(560f) }); string text = null; foreach (string item in list) { if (!_cache.TryGetValue(item, out var value)) { continue; } GUILayout.BeginHorizontal(GUI.skin.box, Array.Empty<GUILayoutOption>()); bool flag = _selectedPath == item && (IsPathShown?.Invoke() ?? false); GUI.color = (flag ? Color.green : Color.white); if (GUILayout.Button(flag ? "Hide" : "Show", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(110f) })) { if (flag) { HidePath?.Invoke(string.Empty); _selectedPath = ""; } else { _selectedPath = item; LoadAndVisualizePath(item); } } GUI.color = Color.white; GUILayout.Space(6f); GUILayout.BeginVertical((GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandWidth(true) }); string text2 = (string.IsNullOrWhiteSpace(value.Name) ? item : value.Name); GUILayout.Label(text2, _boldLabel, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandWidth(true) }); Color color = GUI.color; GUI.color = ReasonColor(value.Reason); GUILayout.Label(ReasonToText(value.Reason), _reasonStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandWidth(true) }); GUI.color = color; string text3 = (string.IsNullOrWhiteSpace(value.Author) ? "(no author)" : value.Author); string text4 = TimeSpan.FromSeconds(value.DurationSeconds).ToString("mm\\:ss"); string text5 = FormatLocalDate(value.Timestamp); GUILayout.Label($"Author: {text3} • Time: {text4} • Points: {value.PointsCount} • Date: {text5}", _valueLineStyle, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.ExpandWidth(true) }); GUILayout.EndVertical(); GUILayout.Space(6f); GUI.color = Color.red; if (GUILayout.Button("Delete", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(100f) })) { text = item; } GUI.color = Color.white; GUILayout.EndHorizontal(); GUILayout.Space(2f); } GUILayout.EndScrollView(); if (text != null) { DeletePath(text); } GUILayout.EndVertical(); } private Color ReasonColor(PathRecorder.StopReason r) { //IL_006b: 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_0087: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //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_0097: 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_0094: Unknown result type (might be due to invalid IL or missing references) return (Color)(r switch { PathRecorder.StopReason.Death => new Color(0.9f, 0.2f, 0.2f, 1f), PathRecorder.StopReason.Victory => new Color(0.2f, 0.8f, 0.2f, 1f), PathRecorder.StopReason.Manual => new Color(0.2f, 0.4f, 1f, 1f), PathRecorder.StopReason.LevelChange => new Color(0.65f, 0.65f, 0.65f, 1f), _ => Color.white, }); } private IEnumerable<string> GetFilteredPaths() { IEnumerable<string> source = _availablePaths.Where((string n) => _cache.ContainsKey(n)); if (!string.IsNullOrWhiteSpace(_search)) { string s = _search.Trim(); source = source.Where(delegate(string n) { CachedInfo cachedInfo2 = _cache[n]; if (!string.IsNullOrEmpty(cachedInfo2.Name) && cachedInfo2.Name.IndexOf(s, StringComparison.OrdinalIgnoreCase) >= 0) { return true; } return (!string.IsNullOrEmpty(cachedInfo2.Author) && cachedInfo2.Author.IndexOf(s, StringComparison.OrdinalIgnoreCase) >= 0) || n.IndexOf(s, StringComparison.OrdinalIgnoreCase) >= 0; }); } source = source.Where(delegate(string n) { CachedInfo cachedInfo = _cache[n]; return _currentFilter switch { FilterType.Deaths => cachedInfo.Reason == PathRecorder.StopReason.Death, FilterType.Wins => cachedInfo.Reason == PathRecorder.StopReason.Victory, FilterType.Manual => cachedInfo.Reason == PathRecorder.StopReason.Manual, FilterType.LevelChange => cachedInfo.Reason == PathRecorder.StopReason.LevelChange, _ => true, }; }); if (_sortByDate) { source = source.OrderByDescending((string n) => _cache[n].Timestamp); } else if (_sortByTime) { source = source.OrderBy((string n) => _cache[n].DurationSeconds); } return source; } private void LoadAndVisualizePath(string pathName) { PathData pathData = PathDataManager.LoadPath(pathName); if (pathData != null) { TryPullNameAuthor(pathName, pathData); ApplyVisualizerStyle?.Invoke(); ShowPath?.Invoke(pathName); FollowTheWayPlugin.LogDebug("Visualized path " + pathName); } } private void DeletePath(string pathName) { DeletePathById?.Invoke(pathName); _cache.Remove(pathName); RefreshPathList(); if (_selectedPath == pathName) { _selectedPath = ""; HidePath?.Invoke(string.Empty); } } private string ReasonToText(PathRecorder.StopReason reason) { return reason switch { PathRecorder.StopReason.Death => "Reason: Death", PathRecorder.StopReason.Victory => "Reason: Victory", PathRecorder.StopReason.Manual => "Reason: Manual stop", PathRecorder.StopReason.LevelChange => "Reason: Level change", _ => "Reason: Unknown", }; } private string FormatLocalDate(long timestampSeconds) { try { return DateTimeOffset.FromUnixTimeSeconds(timestampSeconds).ToLocalTime().DateTime.ToString("yyyy-MM-dd HH:mm:ss"); } catch { return ""; } } private void RefreshPathList() { _availablePaths.Clear(); _availablePaths.AddRange(PathDataManager.GetAllPaths()); _cache.Clear(); foreach (string availablePath in _availablePaths) { PathData pathData = PathDataManager.LoadPath(availablePath); if (pathData != null) { _cache[availablePath] = new CachedInfo { Timestamp = pathData.Timestamp, DurationSeconds = pathData.DurationSeconds, Reason = pathData.Reason, PointsCount = ((pathData.Points != null) ? pathData.Points.Count : 0), Name = GetField(pathData, "Name"), Author = GetField(pathData, "Author") }; } } FollowTheWayPlugin.LogDebug($"Refreshed path list: {_availablePaths.Count} paths (cached: {_cache.Count})"); } private string GetField(object data, string prop) { try { Type type = data.GetType(); PropertyInfo property = type.GetProperty(prop); if (property != null) { return (property.GetValue(data) as string) ?? ""; } FieldInfo field = type.GetField(prop); if (field != null) { return (field.GetValue(data) as string) ?? ""; } } catch { } return ""; } private void TryPullNameAuthor(string pathName, object pathData) { string field = GetField(pathData, "Name"); string field2 = GetField(pathData, "Author"); if (_cache.TryGetValue(pathName, out var value)) { bool flag = false; if (!string.IsNullOrEmpty(field) && field != value.Name) { value.Name = field; flag = true; } if (!string.IsNullOrEmpty(field2) && field2 != value.Author) { value.Author = field2; flag = true; } if (flag) { _cache[pathName] = value; } } } private void ApplyMetaToLastSaved(string name, string author) { if (_availablePaths.Count == 0) { return; } string text = null; long num = long.MinValue; foreach (string availablePath in _availablePaths) { if (_cache.TryGetValue(availablePath, out var value) && value.Timestamp > num) { num = value.Timestamp; text = availablePath; } } if (string.IsNullOrEmpty(text)) { return; } PathData pathData = PathDataManager.LoadPath(text); if (pathData == null) { return; } bool flag = false; flag |= TrySetField(pathData, "Name", name); if (flag | TrySetField(pathData, "Author", author)) { PathDataManager.SavePath(pathData); if (_cache.TryGetValue(text, out var value2)) { value2.Name = (string.IsNullOrEmpty(name) ? value2.Name : name); value2.Author = (string.IsNullOrEmpty(author) ? value2.Author : author); _cache[text] = value2; } FollowTheWayPlugin.LogInfo("Applied meta to last path: " + text + " (Name='" + name + "', Author='" + author + "')"); RefreshPathList(); } } private bool TrySetField(object data, string prop, string value) { try { if (value == null) { return false; } Type type = data.GetType(); PropertyInfo property = type.GetProperty(prop); if (property != null && property.PropertyType == typeof(string)) { property.SetValue(data, value); return true; } FieldInfo field = type.GetField(prop); if (field != null && field.FieldType == typeof(string)) { field.SetValue(data, value); return true; } } catch { } return false; } } } namespace FollowTheWay.UI { public class ToastController : MonoBehaviour { private static ToastController instance; private string currentMessage = ""; private float messageEndTime = 0f; private void Awake() { if ((Object)(object)instance == (Object)null) { instance = this; Object.DontDestroyOnLoad((Object)(object)((Component)this).gameObject); } else { Object.Destroy((Object)(object)((Component)this).gameObject); } } public static void ShowToast(string message, float duration = 2f) { if ((Object)(object)instance != (Object)null) { instance.DisplayToast(message, duration); } else { FollowTheWayPlugin.LogInfo($"[TOAST] {message} (for {duration}s)"); } } private void DisplayToast(string message, float duration) { currentMessage = message; messageEndTime = Time.time + duration; FollowTheWayPlugin.LogInfo("[ToastController] Showing toast: " + message); } private void OnGUI() { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0033: 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_0045: Expected O, but got Unknown //IL_0062: Unknown result type (might be due to invalid IL or missing references) if (Time.time < messageEndTime && !string.IsNullOrEmpty(currentMessage)) { GUIStyle val = new GUIStyle(GUI.skin.box) { fontSize = 16, alignment = (TextAnchor)4 }; GUI.Box(new Rect((float)(Screen.width / 2 - 150), 50f, 300f, 50f), currentMessage, val); } } } } namespace FollowTheWay.Models { public class PathData { public long Timestamp { get; set; } public float DurationSeconds { get; set; } public PathRecorder.StopReason Reason { get; set; } public List<Vector3> Points { get; set; } = new List<Vector3>(); public int MapId { get; set; } public string PlayerName { get; set; } public float CompletionTime { get; set; } public bool PlayerDied { get; set; } public string Name { get; set; } public string Author { get; set; } } public class PathPoint { public float X { get; set; } public float Y { get; set; } public float Z { get; set; } public float Timestamp { get; set; } public PathPoint() { } public PathPoint(Vector3 position, float time) { //IL_0009: 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_0023: Unknown result type (might be due to invalid IL or missing references) X = position.x; Y = position.y; Z = position.z; Timestamp = time; } } public class ServerResponse<T> { public bool Success { get; set; } public string Message { get; set; } public T Paths { get; set; } } } namespace FollowTheWay.Utilities { public static class ConfigHandler { public static ConfigEntry<bool> EnableRecording { get; private set; } public static ConfigEntry<bool> AllowUploads { get; private set; } public static ConfigEntry<bool> ShowToasts { get; private set; } public static ConfigEntry<bool> ShowPathSelectionUI { get; private set; } public static ConfigEntry<KeyCode> ToggleMenuKey { get; private set; } public static ConfigEntry<float> LineWidth { get; private set; } public static ConfigEntry<bool> ForceOnTop { get; private set; } public static ConfigEntry<bool> SmoothCorners { get; private set; } public static ConfigEntry<int> DecimationStep { get; private set; } public static ConfigEntry<int> MaxPoints { get; private set; } public static ConfigEntry<float> LineColorR { get; private set; } public static ConfigEntry<float> LineColorG { get; private set; } public static ConfigEntry<float> LineColorB { get; private set; } public static void Init(ConfigFile config) { EnableRecording = config.Bind<bool>("General", "EnableRecording", true, "Enable path recording"); AllowUploads = config.Bind<bool>("Network", "AllowUploads", true, "Apply Harmony patches (required for automatic recording)"); ShowToasts = config.Bind<bool>("UI", "ShowToasts", true, "Show toast notifications"); ShowPathSelectionUI = config.Bind<bool>("UI", "ShowPathSelectionUI", true, "Enable path selection UI"); ToggleMenuKey = config.Bind<KeyCode>("Hotkeys", "ToggleMenu", (KeyCode)287, "Open/close the mod menu"); LineWidth = config.Bind<float>("Visual", "LineWidth", 0.35f, "Line thickness (0.02..1.5)"); ForceOnTop = config.Bind<bool>("Visual", "ForceOnTop", false, "Render on top of geometry (higher renderQueue)"); SmoothCorners = config.Bind<bool>("Visual", "SmoothCorners", true, "Smooth corners/caps for the line"); LineColorR = config.Bind<float>("Visual", "LineColorR", 0f, "Line color R (0..1)"); LineColorG = config.Bind<float>("Visual", "LineColorG", 1f, "Line color G (0..1)"); LineColorB = config.Bind<float>("Visual", "LineColorB", 0f, "Line color B (0..1)"); DecimationStep = config.Bind<int>("Performance", "DecimationStep", 1, "Render each Nth point (min 1)"); MaxPoints = config.Bind<int>("Performance", "MaxPoints", 10000, "Max number of points in LineRenderer (min 100)"); } } public static class MapHelper { public static bool IsGameLevel(string sceneName) { if (string.IsNullOrEmpty(sceneName)) { return false; } string text = sceneName.ToLower(); return text.Contains("level") || text.Contains("stage") || text.Contains("map") || text.Contains("game"); } public static int GetCurrentMapId() { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) Scene activeScene = SceneManager.GetActiveScene(); switch (((Scene)(ref activeScene)).name.ToLower()) { case "level1": case "stage1": return 1; case "level2": case "stage2": return 2; default: return 1; } } public static string GetCurrentMapName() { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) Scene activeScene = SceneManager.GetActiveScene(); return ((Scene)(ref activeScene)).name; } } public class Vector3Converter : JsonConverter<Vector3> { public override void WriteJson(JsonWriter writer, Vector3 value, JsonSerializer serializer) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) writer.WriteStartObject(); writer.WritePropertyName("x"); writer.WriteValue(value.x); writer.WritePropertyName("y"); writer.WriteValue(value.y); writer.WritePropertyName("z"); writer.WriteValue(value.z); writer.WriteEndObject(); } public override Vector3 ReadJson(JsonReader reader, Type objectType, Vector3 existingValue, bool hasExistingValue, JsonSerializer serializer) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Invalid comparison between Unknown and I4 //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00c7: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Invalid comparison between Unknown and I4 //IL_00cb: Unknown result type (might be due to invalid IL or missing references) float num = 0f; float num2 = 0f; float num3 = 0f; while (reader.Read() && (int)reader.TokenType != 13) { if ((int)reader.TokenType == 4) { string text = reader.Value.ToString(); reader.Read(); switch (text) { case "x": num = Convert.ToSingle(reader.Value); break; case "y": num2 = Convert.ToSingle(reader.Value); break; case "z": num3 = Convert.ToSingle(reader.Value); break; } } } return new Vector3(num, num2, num3); } } } namespace FollowTheWay.Api { public static class PathClient { [CompilerGenerated] private sealed class <GetServerStatusAsync>d__3 : IAsyncStateMachine { public int <>1__state; public AsyncUniTaskMethodBuilder<ServerResponse<string>> <>t__builder; public string version; private HttpResponseMessage <response>5__1; private string <json>5__2; private HttpResponseMessage <>s__3; private string <>s__4; private Exception <ex>5__5; private TaskAwaiter<HttpResponseMessage> <>u__1; private TaskAwaiter<string> <>u__2; private void MoveNext() { int num = <>1__state; ServerResponse<string> result; try { if ((uint)num > 1u) { } try { TaskAwaiter<string> awaiter; TaskAwaiter<HttpResponseMessage> awaiter2; if (num != 0) { if (num == 1) { awaiter = <>u__2; <>u__2 = default(TaskAwaiter<string>); num = (<>1__state = -1); goto IL_0115; } awaiter2 = client.GetAsync("https://your-server.com/api/status?version=" + version).GetAwaiter(); if (!awaiter2.IsCompleted) { num = (<>1__state = 0); <>u__1 = awaiter2; <GetServerStatusAsync>d__3 <GetServerStatusAsync>d__ = this; <>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<HttpResponseMessage>, <GetServerStatusAsync>d__3>(ref awaiter2, ref <GetServerStatusAsync>d__); return; } } else { awaiter2 = <>u__1; <>u__1 = default(TaskAwaiter<HttpResponseMessage>); num = (<>1__state = -1); } <>s__3 = awaiter2.GetResult(); <response>5__1 = <>s__3; <>s__3 = null; awaiter = <response>5__1.Content.ReadAsStringAsync().GetAwaiter(); if (!awaiter.IsCompleted) { num = (<>1__state = 1); <>u__2 = awaiter; <GetServerStatusAsync>d__3 <GetServerStatusAsync>d__ = this; <>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, <GetServerStatusAsync>d__3>(ref awaiter, ref <GetServerStatusAsync>d__); return; } goto IL_0115; IL_0115: <>s__4 = awaiter.GetResult(); <json>5__2 = <>s__4; <>s__4 = null; result = JsonConvert.DeserializeObject<ServerResponse<string>>(<json>5__2); } catch (Exception ex) { <ex>5__5 = ex; FollowTheWayPlugin.LogError("[PathClient] Failed to get server status: " + <ex>5__5.Message); result = new ServerResponse<string> { Success = false, Message = <ex>5__5.Message }; } } catch (Exception ex) { <>1__state = -2; <>t__builder.SetException(ex); return; } <>1__state = -2; <>t__builder.SetResult(result); } void IAsyncStateMachine.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext this.MoveNext(); } [DebuggerHidden] private void SetStateMachine(IAsyncStateMachine stateMachine) { } void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { //ILSpy generated this explicit interface implementation from .override directive in SetStateMachine this.SetStateMachine(stateMachine); } } [CompilerGenerated] private sealed class <RetrievePathsAsync>d__2 : IAsyncStateMachine { public int <>1__state; public AsyncUniTaskMethodBuilder<ServerResponse<List<PathData>>> <>t__builder; public int mapId; private HttpResponseMessage <response>5__1; private string <json>5__2; private HttpResponseMessage <>s__3; private string <>s__4; private Exception <ex>5__5; private TaskAwaiter<HttpResponseMessage> <>u__1; private TaskAwaiter<string> <>u__2; private void MoveNext() { int num = <>1__state; ServerResponse<List<PathData>> result; try { if ((uint)num > 1u) { } try { TaskAwaiter<string> awaiter; TaskAwaiter<HttpResponseMessage> awaiter2; if (num != 0) { if (num == 1) { awaiter = <>u__2; <>u__2 = default(TaskAwaiter<string>); num = (<>1__state = -1); goto IL_011f; } awaiter2 = client.GetAsync(string.Format("{0}/paths?map={1}", "https://your-server.com/api", mapId)).GetAwaiter(); if (!awaiter2.IsCompleted) { num = (<>1__state = 0); <>u__1 = awaiter2; <RetrievePathsAsync>d__2 <RetrievePathsAsync>d__ = this; <>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<HttpResponseMessage>, <RetrievePathsAsync>d__2>(ref awaiter2, ref <RetrievePathsAsync>d__); return; } } else { awaiter2 = <>u__1; <>u__1 = default(TaskAwaiter<HttpResponseMessage>); num = (<>1__state = -1); } <>s__3 = awaiter2.GetResult(); <response>5__1 = <>s__3; <>s__3 = null; awaiter = <response>5__1.Content.ReadAsStringAsync().GetAwaiter(); if (!awaiter.IsCompleted) { num = (<>1__state = 1); <>u__2 = awaiter; <RetrievePathsAsync>d__2 <RetrievePathsAsync>d__ = this; <>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, <RetrievePathsAsync>d__2>(ref awaiter, ref <RetrievePathsAsync>d__); return; } goto IL_011f; IL_011f: <>s__4 = awaiter.GetResult(); <json>5__2 = <>s__4; <>s__4 = null; result = JsonConvert.DeserializeObject<ServerResponse<List<PathData>>>(<json>5__2); } catch (Exception ex) { <ex>5__5 = ex; FollowTheWayPlugin.LogError("[PathClient] Failed to retrieve paths: " + <ex>5__5.Message); result = new ServerResponse<List<PathData>> { Success = false, Message = <ex>5__5.Message, Paths = new List<PathData>() }; } } catch (Exception ex) { <>1__state = -2; <>t__builder.SetException(ex); return; } <>1__state = -2; <>t__builder.SetResult(result); } void IAsyncStateMachine.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext this.MoveNext(); } [DebuggerHidden] private void SetStateMachine(IAsyncStateMachine stateMachine) { } void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { //ILSpy generated this explicit interface implementation from .override directive in SetStateMachine this.SetStateMachine(stateMachine); } } [CompilerGenerated] private sealed class <SubmitPathAsync>d__4 : IAsyncStateMachine { public int <>1__state; public AsyncUniTaskMethodBuilder<ServerResponse<string>> <>t__builder; public PathData pathData; private string <json>5__1; private StringContent <content>5__2; private HttpResponseMessage <response>5__3; private string <responseJson>5__4; private HttpResponseMessage <>s__5; private string <>s__6; private Exception <ex>5__7; private TaskAwaiter<HttpResponseMessage> <>u__1; private TaskAwaiter<string> <>u__2; private void MoveNext() { //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Expected O, but got Unknown int num = <>1__state; ServerResponse<string> result; try { if ((uint)num > 1u) { } try { TaskAwaiter<string> awaiter; TaskAwaiter<HttpResponseMessage> awaiter2; if (num != 0) { if (num == 1) { awaiter = <>u__2; <>u__2 = default(TaskAwaiter<string>); num = (<>1__state = -1); goto IL_013c; } <json>5__1 = JsonConvert.SerializeObject((object)pathData); <content>5__2 = new StringContent(<json>5__1, Encoding.UTF8, "application/json"); awaiter2 = client.PostAsync("https://your-server.com/api/paths", (HttpContent)(object)<content>5__2).GetAwaiter(); if (!awaiter2.IsCompleted) { num = (<>1__state = 0); <>u__1 = awaiter2; <SubmitPathAsync>d__4 <SubmitPathAsync>d__ = this; <>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<HttpResponseMessage>, <SubmitPathAsync>d__4>(ref awaiter2, ref <SubmitPathAsync>d__); return; } } else { awaiter2 = <>u__1; <>u__1 = default(TaskAwaiter<HttpResponseMessage>); num = (<>1__state = -1); } <>s__5 = awaiter2.GetResult(); <response>5__3 = <>s__5; <>s__5 = null; awaiter = <response>5__3.Content.ReadAsStringAsync().GetAwaiter(); if (!awaiter.IsCompleted) { num = (<>1__state = 1); <>u__2 = awaiter; <SubmitPathAsync>d__4 <SubmitPathAsync>d__ = this; <>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, <SubmitPathAsync>d__4>(ref awaiter, ref <SubmitPathAsync>d__); return; } goto IL_013c; IL_013c: <>s__6 = awaiter.GetResult(); <responseJson>5__4 = <>s__6; <>s__6 = null; result = JsonConvert.DeserializeObject<ServerResponse<string>>(<responseJson>5__4); } catch (Exception ex) { <ex>5__7 = ex; FollowTheWayPlugin.LogError("[PathClient] Failed to submit path: " + <ex>5__7.Message); result = new ServerResponse<string> { Success = false, Message = <ex>5__7.Message }; } } catch (Exception ex) { <>1__state = -2; <>t__builder.SetException(ex); return; } <>1__state = -2; <>t__builder.SetResult(result); } void IAsyncStateMachine.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext this.MoveNext(); } [DebuggerHidden] private void SetStateMachine(IAsyncStateMachine stateMachine) { } void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { //ILSpy generated this explicit interface implementation from .override directive in SetStateMachine this.SetStateMachine(stateMachine); } } private static readonly HttpClient client = new HttpClient(); private const string BaseUrl = "https://your-server.com/api"; [AsyncStateMachine(typeof(<RetrievePathsAsync>d__2))] [DebuggerStepThrough] public static UniTask<ServerResponse<List<PathData>>> RetrievePathsAsync(int mapId) { //IL_0007: 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_0032: Unknown result type (might be due to invalid IL or missing references) <RetrievePathsAsync>d__2 <RetrievePathsAsync>d__ = new <RetrievePathsAsync>d__2(); <RetrievePathsAsync>d__.<>t__builder = AsyncUniTaskMethodBuilder<ServerResponse<List<PathData>>>.Create(); <RetrievePathsAsync>d__.mapId = mapId; <RetrievePathsAsync>d__.<>1__state = -1; <RetrievePathsAsync>d__.<>t__builder.Start<<RetrievePathsAsync>d__2>(ref <RetrievePathsAsync>d__); return <RetrievePathsAsync>d__.<>t__builder.Task; } [AsyncStateMachine(typeof(<GetServerStatusAsync>d__3))] [DebuggerStepThrough] public static UniTask<ServerResponse<string>> GetServerStatusAsync(string version) { //IL_0007: 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_0032: Unknown result type (might be due to invalid IL or missing references) <GetServerStatusAsync>d__3 <GetServerStatusAsync>d__ = new <GetServerStatusAsync>d__3(); <GetServerStatusAsync>d__.<>t__builder = AsyncUniTaskMethodBuilder<ServerResponse<string>>.Create(); <GetServerStatusAsync>d__.version = version; <GetServerStatusAsync>d__.<>1__state = -1; <GetServerStatusAsync>d__.<>t__builder.Start<<GetServerStatusAsync>d__3>(ref <GetServerStatusAsync>d__); return <GetServerStatusAsync>d__.<>t__builder.Task; } [AsyncStateMachine(typeof(<SubmitPathAsync>d__4))] [DebuggerStepThrough] public static UniTask<ServerResponse<string>> SubmitPathAsync(PathData pathData) { //IL_0007: 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_0032: Unknown result type (might be due to invalid IL or missing references) <SubmitPathAsync>d__4 <SubmitPathAsync>d__ = new <SubmitPathAsync>d__4(); <SubmitPathAsync>d__.<>t__builder = AsyncUniTaskMethodBuilder<ServerResponse<string>>.Create(); <SubmitPathAsync>d__.pathData = pathData; <SubmitPathAsync>d__.<>1__state = -1; <SubmitPathAsync>d__.<>t__builder.Start<<SubmitPathAsync>d__4>(ref <SubmitPathAsync>d__); return <SubmitPathAsync>d__.<>t__builder.Task; } } }