Decompiled source of LiveSplitHooks v1.2.4

BepInEx/plugins/LiveSplitHooks.dll

Decompiled 19 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.SceneManagement;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: IgnoresAccessChecksTo("Assembly-CSharp")]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("Sparroh")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.2.4.0")]
[assembly: AssemblyInformationalVersion("1.2.4")]
[assembly: AssemblyProduct("LiveSplitHooks")]
[assembly: AssemblyTitle("LiveSplitHooks")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.2.4.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
public static class ConfigManager
{
	private const float DebounceSeconds = 0.25f;

	private static ConfigFile _config;

	private static ManualLogSource _logger;

	private static FileSystemWatcher _configWatcher;

	private static volatile bool _reloadPending;

	private static float _lastReloadTime;

	public static ConfigEntry<bool> Enabled { get; private set; }

	public static ConfigEntry<bool> AutoReconnect { get; private set; }

	public static ConfigEntry<float> ReconnectInterval { get; private set; }

	public static ConfigEntry<bool> DebugLogging { get; private set; }

	public static ConfigEntry<bool> StartOnMissionBegin { get; private set; }

	public static ConfigEntry<bool> ResetBeforeStartOnMissionBegin { get; private set; }

	public static ConfigEntry<bool> UseStartOrSplit { get; private set; }

	public static ConfigEntry<bool> SplitOnMissionComplete { get; private set; }

	public static ConfigEntry<bool> SplitOnObjectiveComplete { get; private set; }

	public static ConfigEntry<bool> SplitOnExtractObjective { get; private set; }

	public static ConfigEntry<bool> ResetOnMissionFail { get; private set; }

	public static ConfigEntry<bool> PauseGameTimeDuringLoads { get; private set; }

	public static ConfigEntry<bool> SplitOnSubMissionTransition { get; private set; }

	public static ConfigEntry<string> FloorChangeSplitMissionIds { get; private set; }

	public static ConfigEntry<int> IncursionFloorSplitMax { get; private set; }

	public static ConfigEntry<bool> IncursionStopSplitsAtMaxFloor { get; private set; }

	public static ConfigEntry<bool> IncursionEndRunAtMaxFloor { get; private set; }

	public static ConfigEntry<bool> SplitProfilesEnabled { get; private set; }

	public static ConfigEntry<string> SplitProfilesDirectory { get; private set; }

	public static ConfigEntry<string> SplitProfilesDefaultSplits { get; private set; }

	public static ConfigEntry<string> SplitProfilesMappings { get; private set; }

	public static ConfigEntry<string> SplitProfilesMapFile { get; private set; }

	public static ConfigEntry<bool> SplitProfilesAllowSubstringMatch { get; private set; }

	public static void Initialize(ConfigFile configFile, ManualLogSource log)
	{
		_config = configFile;
		_logger = log;
		Enabled = _config.Bind<bool>("General", "Enabled", true, "Master switch for sending commands to LiveSplit.");
		AutoReconnect = _config.Bind<bool>("Connection", "AutoReconnect", true, "Automatically retry connecting to LiveSplit if the pipe is unavailable.");
		ReconnectInterval = _config.Bind<float>("Connection", "ReconnectIntervalSeconds", 3f, "Seconds between reconnect attempts.");
		DebugLogging = _config.Bind<bool>("General", "DebugLogging", true, "Log LiveSplit commands, mission identity, and connection status to the BepInEx console.");
		StartOnMissionBegin = _config.Bind<bool>("Triggers", "StartOnMissionBegin", true, "Send start when a mission begins on the local client.");
		ResetBeforeStartOnMissionBegin = _config.Bind<bool>("Triggers", "ResetBeforeStartOnMissionBegin", true, "Send reset before start when a mission begins, so each mission is a fresh LiveSplit run (IL-style). Disable for multi-mission full-game runs.");
		UseStartOrSplit = _config.Bind<bool>("Triggers", "UseStartOrSplit", false, "If true, mission begin sends 'startorsplit' instead of 'start'.");
		SplitOnMissionComplete = _config.Bind<bool>("Triggers", "SplitOnMissionComplete", true, "Send split when a mission is completed successfully.");
		SplitOnObjectiveComplete = _config.Bind<bool>("Triggers", "SplitOnObjectiveComplete", false, "Send split when each main (non-side) objective completes successfully.");
		SplitOnExtractObjective = _config.Bind<bool>("Triggers", "SplitOnExtractObjective", false, "If objective splits are enabled, also split when the extract objective completes. Usually leave off if SplitOnMissionComplete is on.");
		ResetOnMissionFail = _config.Bind<bool>("Triggers", "ResetOnMissionFail", true, "Send reset when a mission fails or is aborted.");
		PauseGameTimeDuringLoads = _config.Bind<bool>("Triggers", "PauseGameTimeDuringLoads", true, "Pause LiveSplit game time while the game is loading / waiting for players.");
		SplitOnSubMissionTransition = _config.Bind<bool>("Triggers", "SplitOnSubMissionTransition", true, "When a different mission starts while a run is active (e.g. Cranius after Moldy Tundra setup), split the current run, switch profiles, and start a new attempt.");
		FloorChangeSplitMissionIds = _config.Bind<string>("Triggers", "FloorChangeSplitMissionIds", "incursion", "Comma-separated mission ids that split when a floor is cleared (Incursion). Empty disables floor splits.");
		IncursionFloorSplitMax = _config.Bind<int>("Triggers", "IncursionFloorSplitMax", 30, "Only emit floor-clear splits up to this floor number (Incursion speedrun target).");
		IncursionStopSplitsAtMaxFloor = _config.Bind<bool>("Triggers", "IncursionStopSplitsAtMaxFloor", true, "Stop further floor-clear splits once IncursionFloorSplitMax is reached.");
		IncursionEndRunAtMaxFloor = _config.Bind<bool>("Triggers", "IncursionEndRunAtMaxFloor", true, "When the max floor is cleared, end the LiveSplit run (set run inactive). Needed because Incursion continues forever; leaves final time on screen without reset.");
		SplitProfilesEnabled = _config.Bind<bool>("SplitProfiles", "Enabled", false, "When true, switch LiveSplit .lss files based on mission identity (requires LiveSplit switchsplits support).");
		SplitProfilesDirectory = _config.Bind<string>("SplitProfiles", "Directory", "", "Folder containing your per-mission .lss files. Relative mapping paths are resolved from here.");
		SplitProfilesDefaultSplits = _config.Bind<string>("SplitProfiles", "DefaultSplits", "", "Fallback .lss path (absolute or relative to Directory) when no mapping matches. Leave empty to keep the current splits.");
		SplitProfilesMappings = _config.Bind<string>("SplitProfiles", "Mappings", "", "Inline mappings, one per line: MissionKey => file.lss  (also accepts | or =). Prefix key with ~ for substring match. Prefer MapFile for long lists.");
		SplitProfilesMapFile = _config.Bind<string>("SplitProfiles", "MapFile", "", "Optional path to an external mapping text file (see profiles.example.txt). Reloaded when the file changes.");
		SplitProfilesAllowSubstringMatch = _config.Bind<bool>("SplitProfiles", "AllowSubstringMatch", true, "If true, mapping keys may match when contained in the mission name/id (in addition to exact matches).");
		try
		{
			SetupFileWatcher();
		}
		catch (Exception ex)
		{
			_logger.LogError((object)("Error setting up config file watcher: " + ex.Message));
		}
	}

	public static void Tick()
	{
		if (!_reloadPending || Time.unscaledTime - _lastReloadTime < 0.25f)
		{
			return;
		}
		_reloadPending = false;
		_lastReloadTime = Time.unscaledTime;
		try
		{
			_config.Reload();
			_logger.LogInfo((object)"Config reloaded from disk.");
		}
		catch (Exception ex)
		{
			_logger.LogError((object)("Error reloading config: " + ex.Message));
		}
	}

	public static void Dispose()
	{
		if (_configWatcher != null)
		{
			_configWatcher.EnableRaisingEvents = false;
			_configWatcher.Changed -= OnConfigFileChanged;
			_configWatcher.Created -= OnConfigFileChanged;
			_configWatcher.Renamed -= OnConfigFileChanged;
			_configWatcher.Dispose();
			_configWatcher = null;
		}
	}

	private static void SetupFileWatcher()
	{
		_configWatcher = new FileSystemWatcher(Paths.ConfigPath, "sparroh.livesplithooks.cfg");
		_configWatcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite;
		_configWatcher.Changed += OnConfigFileChanged;
		_configWatcher.Created += OnConfigFileChanged;
		_configWatcher.Renamed += OnConfigFileChanged;
		_configWatcher.EnableRaisingEvents = true;
	}

	private static void OnConfigFileChanged(object sender, FileSystemEventArgs e)
	{
		_reloadPending = true;
	}
}
public sealed class GameLoadTracker
{
	private bool _gameTimePaused;

	private bool _loading;

	public void Update(LiveSplitClient client)
	{
		if (client == null)
		{
			return;
		}
		if (!ConfigManager.Enabled.Value || !ConfigManager.PauseGameTimeDuringLoads.Value)
		{
			if (_gameTimePaused)
			{
				client.UnpauseGameTime();
				_gameTimePaused = false;
			}
			return;
		}
		bool flag = IsGameLoading();
		if (flag != _loading)
		{
			_loading = flag;
			if (flag)
			{
				client.PauseGameTime();
				_gameTimePaused = true;
			}
			else
			{
				client.UnpauseGameTime();
				_gameTimePaused = false;
			}
		}
	}

	private static bool IsGameLoading()
	{
		//IL_000b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0010: Unknown result type (might be due to invalid IL or missing references)
		int sceneCount = SceneManager.sceneCount;
		for (int i = 0; i < sceneCount; i++)
		{
			Scene sceneAt = SceneManager.GetSceneAt(i);
			if (((Scene)(ref sceneAt)).IsValid() && !((Scene)(ref sceneAt)).isLoaded)
			{
				return true;
			}
		}
		GameManager instance = GameManager.Instance;
		if ((Object)(object)instance != (Object)null && (Object)(object)NetworkManager.Singleton != (Object)null && NetworkManager.Singleton.IsListening && !instance.AreAllPlayersLoaded)
		{
			return true;
		}
		return false;
	}
}
public sealed class IncursionFloorTracker
{
	private readonly HashSet<string> _floorSplitMissionIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

	private bool _active;

	private string _floorSplitIdsRaw;

	private int _lastFloor = -1;

	public void BeginIfNeeded(string missionKey)
	{
		ReloadFloorSplitIdsIfNeeded();
		_active = IsFloorSplitMission(missionKey);
		_lastFloor = -1;
		if (!_active)
		{
			return;
		}
		try
		{
			if ((Object)(object)IncursionObjective.Instance != (Object)null)
			{
				_lastFloor = IncursionObjective.Instance.CurrentFloor;
			}
		}
		catch
		{
		}
		if (ConfigManager.DebugLogging.Value)
		{
			LiveSplitHooksPlugin.Log.LogInfo((object)$"Incursion floor splits enabled for '{missionKey}' (seed floor={_lastFloor}).");
		}
	}

	public void Reset()
	{
		_active = false;
		_lastFloor = -1;
	}

	public bool OnFloorChanged(int currentFloor, bool runInProgress, string activeMissionKey, LiveSplitClient client)
	{
		if (!ConfigManager.Enabled.Value || !runInProgress || client == null)
		{
			return false;
		}
		if (!_active && !IsFloorSplitMission(activeMissionKey))
		{
			return false;
		}
		if (currentFloor < 0)
		{
			return false;
		}
		if (_lastFloor < 0)
		{
			_lastFloor = currentFloor;
			if (ConfigManager.DebugLogging.Value)
			{
				LiveSplitHooksPlugin.Log.LogInfo((object)$"Incursion floor tracking seeded at floor {currentFloor}.");
			}
			return false;
		}
		if (currentFloor <= _lastFloor)
		{
			return false;
		}
		int num = currentFloor - _lastFloor;
		int num2 = Math.Max(1, ConfigManager.IncursionFloorSplitMax.Value);
		bool flag = false;
		for (int i = 0; i < num; i++)
		{
			int num3 = _lastFloor + 1 + i;
			if (num3 > num2)
			{
				break;
			}
			if (ConfigManager.DebugLogging.Value)
			{
				LiveSplitHooksPlugin.Log.LogInfo((object)$"Incursion floor {num3} cleared → split.");
			}
			client.Split();
			if (num3 >= num2)
			{
				flag = true;
				break;
			}
		}
		_lastFloor = currentFloor;
		if (flag || (ConfigManager.IncursionStopSplitsAtMaxFloor.Value && currentFloor > num2))
		{
			if (ConfigManager.DebugLogging.Value)
			{
				LiveSplitHooksPlugin.Log.LogInfo((object)$"Incursion floor {num2} complete (now on {currentFloor}); ending LiveSplit run.");
			}
			_active = false;
			return ConfigManager.IncursionEndRunAtMaxFloor.Value;
		}
		return false;
	}

	private bool IsFloorSplitMission(string missionKey)
	{
		if (string.IsNullOrEmpty(missionKey))
		{
			return false;
		}
		ReloadFloorSplitIdsIfNeeded();
		if (_floorSplitMissionIds.Contains(missionKey))
		{
			return true;
		}
		foreach (string floorSplitMissionId in _floorSplitMissionIds)
		{
			if (missionKey.IndexOf(floorSplitMissionId, StringComparison.OrdinalIgnoreCase) >= 0 || floorSplitMissionId.IndexOf(missionKey, StringComparison.OrdinalIgnoreCase) >= 0)
			{
				return true;
			}
		}
		return false;
	}

	private void ReloadFloorSplitIdsIfNeeded()
	{
		string text = ConfigManager.FloorChangeSplitMissionIds.Value ?? string.Empty;
		if (!string.Equals(text, _floorSplitIdsRaw, StringComparison.Ordinal))
		{
			_floorSplitIdsRaw = text;
			_floorSplitMissionIds.Clear();
			string[] array = text.Split(new char[6] { ',', ';', ' ', '\n', '\r', '\t' }, StringSplitOptions.RemoveEmptyEntries);
			foreach (string text2 in array)
			{
				_floorSplitMissionIds.Add(text2.Trim());
			}
		}
	}
}
public sealed class LiveSplitClient : IDisposable
{
	private static readonly UTF8Encoding Utf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);

	private readonly Func<bool> _debugLogging;

	private readonly object _sync = new object();

	private bool _disposed;

	private NamedPipeClientStream _pipe;

	public bool IsConnected
	{
		get
		{
			lock (_sync)
			{
				return _pipe != null && _pipe.IsConnected;
			}
		}
	}

	public LiveSplitClient(Func<bool> debugLogging)
	{
		_debugLogging = debugLogging ?? ((Func<bool>)(() => false));
	}

	public void Dispose()
	{
		lock (_sync)
		{
			_disposed = true;
			DisposePipe_NoLock();
		}
	}

	public bool TryConnect()
	{
		lock (_sync)
		{
			if (_disposed)
			{
				return false;
			}
			if (_pipe != null && _pipe.IsConnected)
			{
				return true;
			}
			DisposePipe_NoLock();
			NamedPipeClientStream namedPipeClientStream = new NamedPipeClientStream(".", "LiveSplit", PipeDirection.InOut, PipeOptions.Asynchronous);
			try
			{
				namedPipeClientStream.Connect(250);
				_pipe = namedPipeClientStream;
				StartReadLoop(namedPipeClientStream);
				return true;
			}
			catch
			{
				try
				{
					namedPipeClientStream.Dispose();
				}
				catch
				{
				}
				_pipe = null;
				return false;
			}
		}
	}

	public void Send(string command)
	{
		if (string.IsNullOrEmpty(command))
		{
			return;
		}
		byte[] bytes = Utf8.GetBytes(command + "\n");
		lock (_sync)
		{
			if (_disposed || _pipe == null || !_pipe.IsConnected)
			{
				if (_debugLogging())
				{
					ManualLogSource log = LiveSplitHooksPlugin.Log;
					if (log != null)
					{
						log.LogDebug((object)("LiveSplit not connected; dropped command: " + command));
					}
				}
				return;
			}
			try
			{
				_pipe.Write(bytes, 0, bytes.Length);
				_pipe.Flush();
				if (_debugLogging())
				{
					ManualLogSource log2 = LiveSplitHooksPlugin.Log;
					if (log2 != null)
					{
						log2.LogInfo((object)("LiveSplit <= " + command));
					}
				}
			}
			catch (Exception ex)
			{
				if (_debugLogging())
				{
					ManualLogSource log3 = LiveSplitHooksPlugin.Log;
					if (log3 != null)
					{
						log3.LogWarning((object)("Failed to send '" + command + "': " + ex.Message));
					}
				}
				DisposePipe_NoLock();
			}
		}
	}

	public void Start()
	{
		Send("start");
	}

	public void StartOrSplit()
	{
		Send("startorsplit");
	}

	public void Split()
	{
		Send("split");
	}

	public void Reset()
	{
		Send("reset");
	}

	public void PauseGameTime()
	{
		Send("pausegametime");
	}

	public void UnpauseGameTime()
	{
		Send("unpausegametime");
	}

	public void SwitchSplits(string path)
	{
		if (!string.IsNullOrWhiteSpace(path))
		{
			Send("switchsplits " + path.Trim());
		}
	}

	public void SwitchLayout(string path)
	{
		if (!string.IsNullOrWhiteSpace(path))
		{
			Send("switchlayout " + path.Trim());
		}
	}

	private void StartReadLoop(NamedPipeClientStream pipe)
	{
		Task.Run(async delegate
		{
			byte[] buffer = new byte[256];
			try
			{
				while (pipe.IsConnected && await pipe.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(continueOnCapturedContext: false) > 0)
				{
				}
			}
			catch (Exception)
			{
			}
			finally
			{
				lock (_sync)
				{
					if (_pipe == pipe)
					{
						DisposePipe_NoLock();
					}
				}
			}
		});
	}

	private void DisposePipe_NoLock()
	{
		if (_pipe != null)
		{
			try
			{
				_pipe.Dispose();
			}
			catch
			{
			}
			_pipe = null;
		}
	}
}
internal static class LiveSplitPatches
{
	[HarmonyPatch(typeof(MissionManager), "OnMissionStarted_Client")]
	private static class MissionStartedPatch
	{
		private static void Postfix()
		{
			LiveSplitHooksPlugin.Splits?.OnMissionStarted();
		}
	}

	[HarmonyPatch(typeof(MissionManager), "OnMissionCompleted_ClientRpc")]
	private static class MissionCompletedPatch
	{
		private static void Postfix()
		{
			LiveSplitHooksPlugin.Splits?.OnMissionCompleted();
		}
	}

	[HarmonyPatch(typeof(MissionManager), "OnFailMission_Client")]
	private static class MissionFailedPatch
	{
		private static void Postfix(MissionState state)
		{
			//IL_000a: Unknown result type (might be due to invalid IL or missing references)
			LiveSplitHooksPlugin.Splits?.OnMissionFailed(state);
		}
	}

	[HarmonyPatch(typeof(ObjectiveBase), "Complete")]
	private static class ObjectiveCompletePatch
	{
		private static void Postfix(ObjectiveBase __instance, bool success)
		{
			if (!((Object)(object)__instance == (Object)null))
			{
				LiveSplitHooksPlugin.Splits?.OnObjectiveCompleted(__instance, success);
			}
		}
	}

	[HarmonyPatch(typeof(ObjectiveBase), "Complete_ClientRpc")]
	private static class ObjectiveCompleteClientRpcPatch
	{
		private static void Postfix(ObjectiveBase __instance, bool success)
		{
			if (!((Object)(object)__instance == (Object)null))
			{
				NetworkManager networkManager = ((NetworkBehaviour)__instance).NetworkManager;
				if (!((Object)(object)networkManager != (Object)null) || (!networkManager.IsServer && !networkManager.IsHost))
				{
					LiveSplitHooksPlugin.Splits?.OnObjectiveCompleted(__instance, success);
				}
			}
		}
	}

	[HarmonyPatch(typeof(IncursionObjective), "SpawnRooms_ClientRpc")]
	private static class IncursionSpawnRoomsPatch
	{
		private static void Postfix(int currentFloor, int floorsReached)
		{
			LiveSplitHooksPlugin.Splits?.OnIncursionFloorChanged(currentFloor);
		}
	}

	[HarmonyPatch(typeof(IncursionHUD), "SetFloor")]
	private static class IncursionHudSetFloorPatch
	{
		private static void Postfix(int floor)
		{
			LiveSplitHooksPlugin.Splits?.OnIncursionFloorChanged(floor);
		}
	}
}
public static class MissionIdentity
{
	public readonly struct Info
	{
		public string PrimaryKey { get; }

		public string DisplayName { get; }

		public string Detail { get; }

		public bool HasKey => !string.IsNullOrWhiteSpace(PrimaryKey);

		public Info(string primaryKey, string displayName, string detail)
		{
			PrimaryKey = primaryKey ?? string.Empty;
			DisplayName = displayName ?? string.Empty;
			Detail = detail ?? string.Empty;
		}

		public IEnumerable<string> MatchCandidates()
		{
			HashSet<string> seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
			List<string> list = new List<string>();
			Add(PrimaryKey);
			Add(DisplayName);
			if (!string.IsNullOrWhiteSpace(Detail))
			{
				string[] array = Detail.Split(new char[4] { '|', ';', '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
				for (int i = 0; i < array.Length; i++)
				{
					Add(array[i]);
				}
			}
			return list;
			void Add(string value)
			{
				if (!string.IsNullOrWhiteSpace(value))
				{
					value = StripRichText(value.Trim());
					if (value.Length != 0 && !NoiseTokens.Contains(value) && seen.Add(value))
					{
						list.Add(value);
					}
				}
			}
		}
	}

	private static readonly Regex ColorTagRegex = new Regex("<color=[^>]*>|</color>|<b>|</b>|<i>|</i>", RegexOptions.IgnoreCase | RegexOptions.Compiled);

	private static readonly string[] MissionObjectMemberNames = new string[7] { "Mission", "_mission", "CurrentMission", "ActiveMission", "SelectedMission", "LoadedMission", "mission" };

	private static readonly string[] IdMemberNames = new string[5] { "ID", "Id", "id", "MissionId", "missionId" };

	private static readonly string[] NameMemberNames = new string[6] { "MissionName", "missionName", "DisplayName", "Title", "ColoredMissionName", "Name" };

	private static readonly string[] TypeMemberNames = new string[2] { "MissionTypeName", "missionTypeName" };

	private static readonly HashSet<string> NoiseTokens = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "MissionManager", "True", "False", "None", "null", "Untagged", "Empty Event", "DefaultMissionContainer", "Players" };

	private static readonly HashSet<string> PlaceholderIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "???", "?", "redacted", "unknown", "none", "null", "n/a", "na" };

	public static Info Capture()
	{
		try
		{
			MissionManager instance = MissionManager.Instance;
			if ((Object)(object)instance == (Object)null)
			{
				return new Info(string.Empty, string.Empty, "MissionManager.Instance is null");
			}
			return CaptureFrom(instance);
		}
		catch (Exception ex)
		{
			return new Info(string.Empty, string.Empty, "Capture failed: " + ex.Message);
		}
	}

	public static Info CaptureFrom(object missionManager)
	{
		if (missionManager == null)
		{
			return new Info(string.Empty, string.Empty, "null");
		}
		object obj = null;
		string[] missionObjectMemberNames = MissionObjectMemberNames;
		foreach (string name in missionObjectMemberNames)
		{
			if (TryGetMemberValue(missionManager, name, out var value) && value != null && !(value is string))
			{
				obj = value;
				break;
			}
		}
		string text = null;
		string displayName = null;
		string value2 = null;
		string value3 = null;
		string value4 = null;
		if (obj != null)
		{
			text = FirstStringMember(obj, IdMemberNames);
			displayName = FirstStringMember(obj, NameMemberNames);
			value3 = FirstStringMember(obj, TypeMemberNames);
			value4 = obj.GetType().Name;
			Object val = (Object)((obj is Object) ? obj : null);
			value2 = ((val == null || !(val != (Object)null)) ? FirstStringMember(obj, new string[1] { "name" }) : val.name);
		}
		if (string.IsNullOrWhiteSpace(text))
		{
			text = FirstStringMember(missionManager, IdMemberNames);
		}
		if (string.IsNullOrWhiteSpace(displayName))
		{
			displayName = FirstStringMember(missionManager, NameMemberNames);
		}
		string text2 = (string.IsNullOrWhiteSpace(text) ? null : text.Trim());
		text = Clean(text);
		displayName = Clean(displayName);
		value2 = Clean(value2);
		value3 = Clean(value3);
		value4 = Clean(value4);
		if (string.Equals(displayName, "MissionManager", StringComparison.OrdinalIgnoreCase))
		{
			displayName = null;
		}
		if (IsPlaceholderId(displayName) || string.IsNullOrWhiteSpace(displayName))
		{
			displayName = value2;
		}
		string text3 = null;
		if (!string.IsNullOrEmpty(value4) && (value4.IndexOf("FlatTundra", StringComparison.OrdinalIgnoreCase) >= 0 || value4.IndexOf("Cranius", StringComparison.OrdinalIgnoreCase) >= 0))
		{
			text3 = "cranius";
		}
		if (text3 == null && !string.IsNullOrEmpty(value2) && (value2.IndexOf("FlatTundra", StringComparison.OrdinalIgnoreCase) >= 0 || value2.IndexOf("Cranius", StringComparison.OrdinalIgnoreCase) >= 0))
		{
			text3 = "cranius";
		}
		string primary;
		if (!string.IsNullOrWhiteSpace(text) && !IsPlaceholderId(text))
		{
			primary = text;
		}
		else if (!string.IsNullOrWhiteSpace(text3))
		{
			primary = text3;
		}
		else if (!string.IsNullOrWhiteSpace(value2))
		{
			primary = value2;
		}
		else if (!string.IsNullOrWhiteSpace(displayName) && !IsPlaceholderId(displayName))
		{
			primary = displayName;
		}
		else if (!string.IsNullOrWhiteSpace(text2))
		{
			primary = text2;
		}
		else
		{
			primary = string.Empty;
		}
		List<string> aliases = new List<string>();
		Alias(value2);
		Alias(text3);
		Alias(value4);
		Alias(value3);
		Alias(text);
		Alias(text2);
		Alias(displayName);
		if (!string.IsNullOrEmpty(text3))
		{
			Alias("FlatTundra Mission");
			Alias("FlatTundraMission");
		}
		string detail = string.Join(" | ", aliases);
		return new Info(primary ?? string.Empty, displayName ?? string.Empty, detail);
		void Alias(string text4)
		{
			if (!string.IsNullOrWhiteSpace(text4))
			{
				if (!IsPlaceholderId(text4))
				{
					text4 = Clean(text4);
					if (string.IsNullOrWhiteSpace(text4) || NoiseTokens.Contains(text4))
					{
						return;
					}
				}
				else
				{
					text4 = text4.Trim();
				}
				if (!string.Equals(text4, primary, StringComparison.OrdinalIgnoreCase) && !string.Equals(text4, displayName, StringComparison.OrdinalIgnoreCase))
				{
					foreach (string item in aliases)
					{
						if (string.Equals(item, text4, StringComparison.OrdinalIgnoreCase))
						{
							return;
						}
					}
					aliases.Add(text4);
				}
			}
		}
	}

	private static bool IsPlaceholderId(string value)
	{
		if (string.IsNullOrWhiteSpace(value))
		{
			return true;
		}
		value = value.Trim();
		if (PlaceholderIds.Contains(value))
		{
			return true;
		}
		bool flag = true;
		string text = value;
		foreach (char c in text)
		{
			if (c != '?' && c != '*' && c != '.' && c != '-' && c != '_' && !char.IsWhiteSpace(c))
			{
				flag = false;
				break;
			}
		}
		if (flag)
		{
			return value.Length > 0;
		}
		return false;
	}

	private static string FirstStringMember(object obj, string[] names)
	{
		if (obj == null)
		{
			return null;
		}
		foreach (string name in names)
		{
			if (TryGetMemberValue(obj, name, out var value) && value != null)
			{
				string text = Stringify(value);
				if (!string.IsNullOrWhiteSpace(text))
				{
					return text;
				}
			}
		}
		return null;
	}

	private static bool TryGetMemberValue(object obj, string name, out object value)
	{
		value = null;
		if (obj == null || string.IsNullOrEmpty(name))
		{
			return false;
		}
		Type type = obj.GetType();
		try
		{
			PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			if (property != null && property.CanRead && property.GetIndexParameters().Length == 0)
			{
				value = property.GetValue(obj, null);
				return true;
			}
		}
		catch
		{
		}
		try
		{
			FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			if (field != null)
			{
				value = field.GetValue(obj);
				return true;
			}
		}
		catch
		{
		}
		try
		{
			PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			foreach (PropertyInfo propertyInfo in properties)
			{
				if (string.Equals(propertyInfo.Name, name, StringComparison.OrdinalIgnoreCase) && propertyInfo.CanRead && propertyInfo.GetIndexParameters().Length == 0)
				{
					value = propertyInfo.GetValue(obj, null);
					return true;
				}
			}
			FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			foreach (FieldInfo fieldInfo in fields)
			{
				if (string.Equals(fieldInfo.Name, name, StringComparison.OrdinalIgnoreCase))
				{
					value = fieldInfo.GetValue(obj);
					return true;
				}
			}
		}
		catch
		{
		}
		return false;
	}

	private static string Stringify(object value)
	{
		if (value == null)
		{
			return null;
		}
		if (!(value is string result))
		{
			Object val = (Object)((value is Object) ? value : null);
			if (val == null)
			{
				if (value is Enum obj)
				{
					return obj.ToString();
				}
			}
			else if (val != (Object)null)
			{
				return val.name;
			}
			return null;
		}
		return result;
	}

	private static string Clean(string value)
	{
		if (string.IsNullOrWhiteSpace(value))
		{
			return null;
		}
		value = StripRichText(value.Trim());
		if (value.Length == 0 || NoiseTokens.Contains(value))
		{
			return null;
		}
		if (bool.TryParse(value, out var _))
		{
			return null;
		}
		if (double.TryParse(value, out var _))
		{
			return null;
		}
		return value;
	}

	private static string StripRichText(string value)
	{
		if (string.IsNullOrEmpty(value))
		{
			return value;
		}
		return ColorTagRegex.Replace(value, string.Empty).Trim();
	}

	public static string FormatForLog(Info info)
	{
		StringBuilder stringBuilder = new StringBuilder();
		stringBuilder.Append("key='").Append(info.PrimaryKey).Append('\'');
		if (!string.IsNullOrEmpty(info.DisplayName) && !string.Equals(info.DisplayName, info.PrimaryKey, StringComparison.OrdinalIgnoreCase))
		{
			stringBuilder.Append(" name='").Append(info.DisplayName).Append('\'');
		}
		if (!string.IsNullOrEmpty(info.Detail))
		{
			stringBuilder.Append(" detail=[").Append(info.Detail).Append(']');
		}
		return stringBuilder.ToString();
	}
}
[BepInPlugin("sparroh.livesplithooks", "LiveSplitHooks", "1.2.4")]
[MycoMod(/*Could not decode attribute arguments.*/)]
public class LiveSplitHooksPlugin : BaseUnityPlugin
{
	public const string PluginGuid = "sparroh.livesplithooks";

	public const string PluginName = "LiveSplitHooks";

	public const string PluginVersion = "1.2.4";

	private LiveSplitClient _client;

	private Harmony _harmony;

	private SplitProfileSwitcher _profiles;

	private float _reconnectTimer;

	private SplitController _splits;

	internal static LiveSplitHooksPlugin Instance { get; private set; }

	internal static ManualLogSource Log { get; private set; }

	internal static SplitController Splits => Instance?._splits;

	internal static LiveSplitClient Client => Instance?._client;

	internal static SplitProfileSwitcher Profiles => Instance?._profiles;

	private void Awake()
	{
		//IL_0079: Unknown result type (might be due to invalid IL or missing references)
		//IL_0083: Expected O, but got Unknown
		Instance = this;
		Log = ((BaseUnityPlugin)this).Logger;
		ConfigManager.Initialize(((BaseUnityPlugin)this).Config, Log);
		_client = new LiveSplitClient(() => ConfigManager.DebugLogging.Value);
		_profiles = new SplitProfileSwitcher(_client);
		_splits = new SplitController(_client, _profiles);
		_harmony = new Harmony("sparroh.livesplithooks");
		_harmony.PatchAll(typeof(LiveSplitHooksPlugin).Assembly);
		if (ConfigManager.Enabled.Value)
		{
			TryConnect();
		}
		Log.LogInfo((object)"LiveSplitHooks v1.2.4 loaded.");
	}

	private void Update()
	{
		ConfigManager.Tick();
		if (!ConfigManager.Enabled.Value)
		{
			return;
		}
		if (!_client.IsConnected)
		{
			if (ConfigManager.AutoReconnect.Value)
			{
				_reconnectTimer -= Time.unscaledDeltaTime;
				if (_reconnectTimer <= 0f)
				{
					_reconnectTimer = Mathf.Max(0.5f, ConfigManager.ReconnectInterval.Value);
					TryConnect();
				}
			}
		}
		else
		{
			_splits.UpdateLoadingState();
		}
	}

	private void OnDestroy()
	{
		try
		{
			Harmony harmony = _harmony;
			if (harmony != null)
			{
				harmony.UnpatchSelf();
			}
		}
		catch
		{
		}
		ConfigManager.Dispose();
		_client?.Dispose();
		Instance = null;
	}

	private void TryConnect()
	{
		try
		{
			if (_client.TryConnect())
			{
				if (ConfigManager.DebugLogging.Value)
				{
					Log.LogInfo((object)"Connected to LiveSplit named pipe.");
				}
				if (ConfigManager.PauseGameTimeDuringLoads.Value)
				{
					_client.Send("initgametime");
				}
			}
		}
		catch (Exception ex)
		{
			if (ConfigManager.DebugLogging.Value)
			{
				Log.LogDebug((object)("LiveSplit connect failed: " + ex.Message));
			}
		}
	}
}
public sealed class SplitController
{
	private readonly LiveSplitClient _client;

	private readonly IncursionFloorTracker _incursionFloors = new IncursionFloorTracker();

	private readonly GameLoadTracker _loadTracker = new GameLoadTracker();

	private readonly SplitProfileSwitcher _profiles;

	private string _activeMissionKey = string.Empty;

	public bool RunInProgress { get; private set; }

	public SplitController(LiveSplitClient client, SplitProfileSwitcher profiles = null)
	{
		_client = client ?? throw new ArgumentNullException("client");
		_profiles = profiles;
	}

	public void OnMissionStarted()
	{
		if (!ConfigManager.Enabled.Value)
		{
			return;
		}
		MissionIdentity.Info info = MissionIdentity.Capture();
		if (ConfigManager.DebugLogging.Value)
		{
			LiveSplitHooksPlugin.Log.LogInfo((object)("Mission identity " + MissionIdentity.FormatForLog(info)));
		}
		string text = info.PrimaryKey ?? string.Empty;
		bool value = ConfigManager.StartOnMissionBegin.Value;
		bool value2 = ConfigManager.ResetBeforeStartOnMissionBegin.Value;
		bool flag = ConfigManager.SplitProfilesEnabled.Value && _profiles != null;
		if (RunInProgress && !string.IsNullOrEmpty(_activeMissionKey) && !string.IsNullOrEmpty(text) && !string.Equals(_activeMissionKey, text, StringComparison.OrdinalIgnoreCase) && ConfigManager.SplitOnSubMissionTransition.Value)
		{
			if (ConfigManager.DebugLogging.Value)
			{
				LiveSplitHooksPlugin.Log.LogInfo((object)("Sub-mission transition '" + _activeMissionKey + "' → '" + text + "': split, reset, swap profile, start."));
			}
			_client.Split();
			RunInProgress = false;
			_incursionFloors.Reset();
			_client.Reset();
			if (flag)
			{
				_profiles.TrySwitchForMission(info);
			}
			_activeMissionKey = text;
			RunInProgress = true;
			_incursionFloors.BeginIfNeeded(text);
			_client.Start();
			return;
		}
		if (!value && !flag)
		{
			_incursionFloors.BeginIfNeeded(text);
			return;
		}
		if (value)
		{
			if (value2 || flag)
			{
				_client.Reset();
				RunInProgress = false;
				_incursionFloors.Reset();
			}
			else if (RunInProgress)
			{
				if (ConfigManager.DebugLogging.Value)
				{
					LiveSplitHooksPlugin.Log.LogDebug((object)"Mission started ignored (run already in progress).");
				}
				return;
			}
		}
		else if (flag && RunInProgress)
		{
			if (ConfigManager.DebugLogging.Value)
			{
				LiveSplitHooksPlugin.Log.LogDebug((object)"Split profile switch skipped (run already in progress).");
			}
			return;
		}
		if (flag)
		{
			if (!value)
			{
				_client.Reset();
				RunInProgress = false;
				_incursionFloors.Reset();
			}
			_profiles.TrySwitchForMission(info);
		}
		if (!value)
		{
			_activeMissionKey = text;
			_incursionFloors.BeginIfNeeded(text);
			return;
		}
		_activeMissionKey = text;
		RunInProgress = true;
		_incursionFloors.BeginIfNeeded(text);
		if (ConfigManager.UseStartOrSplit.Value)
		{
			_client.StartOrSplit();
		}
		else
		{
			_client.Start();
		}
	}

	public void OnMissionCompleted()
	{
		if (!ConfigManager.Enabled.Value)
		{
			return;
		}
		if (!RunInProgress)
		{
			if (ConfigManager.DebugLogging.Value)
			{
				LiveSplitHooksPlugin.Log.LogDebug((object)"Mission complete ignored (no run in progress / already handled).");
			}
			return;
		}
		RunInProgress = false;
		_incursionFloors.Reset();
		if (ConfigManager.SplitOnMissionComplete.Value)
		{
			_client.Split();
		}
	}

	public void OnMissionFailed(MissionState state)
	{
		//IL_002b: Unknown result type (might be due to invalid IL or missing references)
		if (!ConfigManager.Enabled.Value)
		{
			return;
		}
		if (!RunInProgress)
		{
			if (ConfigManager.DebugLogging.Value)
			{
				LiveSplitHooksPlugin.Log.LogDebug((object)$"Mission {state} ignored (no run in progress / already handled).");
			}
			return;
		}
		RunInProgress = false;
		_incursionFloors.Reset();
		if (ConfigManager.ResetOnMissionFail.Value)
		{
			_client.Reset();
		}
	}

	public void OnObjectiveCompleted(ObjectiveBase objective, bool success)
	{
		if (ConfigManager.Enabled.Value && ConfigManager.SplitOnObjectiveComplete.Value && success && !((Object)(object)objective == (Object)null) && !objective.IsSideObjective && !objective.IsOptional && (!(objective is ExtractObjective) || ConfigManager.SplitOnExtractObjective.Value) && RunInProgress)
		{
			_client.Split();
		}
	}

	public void OnIncursionFloorChanged(int currentFloor)
	{
		if (_incursionFloors.OnFloorChanged(currentFloor, RunInProgress, _activeMissionKey, _client))
		{
			RunInProgress = false;
		}
	}

	public void UpdateLoadingState()
	{
		_loadTracker.Update(_client);
	}
}
public sealed class SplitProfileSwitcher
{
	private readonly struct Mapping
	{
		public string Key { get; }

		public string Path { get; }

		public bool Substring { get; }

		public Mapping(string key, string path, bool substring)
		{
			Key = key;
			Path = path;
			Substring = substring;
		}
	}

	private readonly LiveSplitClient _client;

	private string _lastLoadedPath;

	private string _mapFilePath;

	private DateTime _mapFileWriteTimeUtc = DateTime.MinValue;

	private List<Mapping> _mappings = new List<Mapping>();

	private string _mappingsRaw;

	public SplitProfileSwitcher(LiveSplitClient client)
	{
		_client = client ?? throw new ArgumentNullException("client");
	}

	public string TrySwitchForMission(MissionIdentity.Info mission)
	{
		if (!ConfigManager.SplitProfilesEnabled.Value)
		{
			return null;
		}
		ReloadMappingsIfNeeded();
		string text = ResolvePath(mission);
		if (string.IsNullOrWhiteSpace(text))
		{
			if (ConfigManager.DebugLogging.Value)
			{
				LiveSplitHooksPlugin.Log.LogInfo((object)("Split profile: no mapping/default for mission " + MissionIdentity.FormatForLog(mission)));
			}
			return null;
		}
		text = NormalizePath(text);
		if (!File.Exists(text))
		{
			ManualLogSource log = LiveSplitHooksPlugin.Log;
			if (log != null)
			{
				log.LogWarning((object)("Split profile file not found: " + text + " (mission " + mission.PrimaryKey + ")"));
			}
			return text;
		}
		if (string.Equals(_lastLoadedPath, text, StringComparison.OrdinalIgnoreCase))
		{
			if (ConfigManager.DebugLogging.Value)
			{
				LiveSplitHooksPlugin.Log.LogDebug((object)("Split profile already loaded: " + text));
			}
			return text;
		}
		_client.SwitchSplits(text);
		_lastLoadedPath = text;
		if (ConfigManager.DebugLogging.Value)
		{
			LiveSplitHooksPlugin.Log.LogInfo((object)("Split profile switched → " + text + " for " + MissionIdentity.FormatForLog(mission)));
		}
		return text;
	}

	private string ResolvePath(MissionIdentity.Info mission)
	{
		string directory = ConfigManager.SplitProfilesDirectory.Value?.Trim() ?? string.Empty;
		foreach (string item in mission.MatchCandidates())
		{
			foreach (Mapping mapping in _mappings)
			{
				if (!mapping.Substring && string.Equals(item, mapping.Key, StringComparison.OrdinalIgnoreCase))
				{
					return MakeAbsolute(mapping.Path, directory);
				}
			}
		}
		foreach (string item2 in mission.MatchCandidates())
		{
			foreach (Mapping mapping2 in _mappings)
			{
				if (((!mapping2.Substring && ConfigManager.SplitProfilesAllowSubstringMatch.Value) || mapping2.Substring) && (item2.IndexOf(mapping2.Key, StringComparison.OrdinalIgnoreCase) >= 0 || mapping2.Key.IndexOf(item2, StringComparison.OrdinalIgnoreCase) >= 0))
				{
					return MakeAbsolute(mapping2.Path, directory);
				}
			}
		}
		if (ConfigManager.SplitProfilesAllowSubstringMatch.Value)
		{
			foreach (string item3 in mission.MatchCandidates())
			{
				foreach (Mapping mapping3 in _mappings)
				{
					if (item3.IndexOf(mapping3.Key, StringComparison.OrdinalIgnoreCase) >= 0)
					{
						return MakeAbsolute(mapping3.Path, directory);
					}
				}
			}
		}
		string text = ConfigManager.SplitProfilesDefaultSplits.Value?.Trim();
		if (!string.IsNullOrEmpty(text))
		{
			return MakeAbsolute(text, directory);
		}
		return null;
	}

	private void ReloadMappingsIfNeeded()
	{
		string text = ConfigManager.SplitProfilesMappings.Value ?? string.Empty;
		string text2 = ConfigManager.SplitProfilesMapFile.Value?.Trim() ?? string.Empty;
		bool flag = false;
		DateTime dateTime = DateTime.MinValue;
		if (!string.IsNullOrEmpty(text2) && File.Exists(text2))
		{
			dateTime = File.GetLastWriteTimeUtc(text2);
			flag = !string.Equals(text2, _mapFilePath, StringComparison.OrdinalIgnoreCase) || dateTime != _mapFileWriteTimeUtc;
		}
		else if (!string.Equals(text2, _mapFilePath, StringComparison.OrdinalIgnoreCase))
		{
			flag = true;
		}
		if (flag || !string.Equals(text, _mappingsRaw, StringComparison.Ordinal))
		{
			_mappingsRaw = text;
			_mapFilePath = text2;
			_mapFileWriteTimeUtc = dateTime;
			_mappings = ParseAll(text, text2);
			if (ConfigManager.DebugLogging.Value)
			{
				LiveSplitHooksPlugin.Log.LogInfo((object)string.Format("Split profile mappings loaded: {0} entr{1}.", _mappings.Count, (_mappings.Count == 1) ? "y" : "ies"));
			}
		}
	}

	private static List<Mapping> ParseAll(string rawConfig, string mapFile)
	{
		List<Mapping> list = new List<Mapping>();
		ParseInto(list, rawConfig);
		if (!string.IsNullOrEmpty(mapFile) && File.Exists(mapFile))
		{
			try
			{
				ParseInto(list, File.ReadAllText(mapFile, Encoding.UTF8));
			}
			catch (Exception ex)
			{
				ManualLogSource log = LiveSplitHooksPlugin.Log;
				if (log != null)
				{
					log.LogWarning((object)("Failed to read split profile map file: " + ex.Message));
				}
			}
		}
		return list;
	}

	private static void ParseInto(List<Mapping> list, string text)
	{
		if (string.IsNullOrWhiteSpace(text))
		{
			return;
		}
		using StringReader stringReader = new StringReader(text);
		string text2;
		while ((text2 = stringReader.ReadLine()) != null)
		{
			text2 = text2.Trim();
			if (text2.Length == 0 || text2.StartsWith("#") || text2.StartsWith("//"))
			{
				continue;
			}
			bool substring = false;
			if (text2.StartsWith("~", StringComparison.Ordinal))
			{
				substring = true;
				text2 = text2.Substring(1).TrimStart();
			}
			string text3 = null;
			string text4 = null;
			int num = text2.IndexOf("=>", StringComparison.Ordinal);
			if (num >= 0)
			{
				text3 = text2.Substring(0, num).Trim();
				text4 = text2.Substring(num + 2).Trim();
			}
			else
			{
				int num2 = text2.IndexOf('|');
				int num3 = text2.IndexOf('=');
				int num4 = -1;
				if (num2 >= 0 && (num3 < 0 || num2 < num3))
				{
					num4 = num2;
				}
				else if (num3 >= 0)
				{
					num4 = num3;
				}
				if (num4 >= 0)
				{
					text3 = text2.Substring(0, num4).Trim();
					text4 = text2.Substring(num4 + 1).Trim();
				}
			}
			if (!string.IsNullOrEmpty(text3) && !string.IsNullOrEmpty(text4))
			{
				text4 = Unquote(text4);
				text3 = Unquote(text3);
				list.Add(new Mapping(text3, text4, substring));
			}
		}
	}

	private static string Unquote(string s)
	{
		if (s.Length >= 2 && ((s[0] == '"' && s[s.Length - 1] == '"') || (s[0] == '\'' && s[s.Length - 1] == '\'')))
		{
			return s.Substring(1, s.Length - 2);
		}
		return s;
	}

	private static string MakeAbsolute(string path, string directory)
	{
		path = path.Trim();
		if (Path.IsPathRooted(path))
		{
			return path;
		}
		if (!string.IsNullOrEmpty(directory))
		{
			return Path.GetFullPath(Path.Combine(directory, path));
		}
		return Path.GetFullPath(path);
	}

	private static string NormalizePath(string path)
	{
		try
		{
			return Path.GetFullPath(path);
		}
		catch
		{
			return path;
		}
	}
}
namespace LiveSplitHooks
{
	public static class MyPluginInfo
	{
		public const string PLUGIN_GUID = "LiveSplitHooks";

		public const string PLUGIN_NAME = "LiveSplitHooks";

		public const string PLUGIN_VERSION = "1.2.4";
	}
}
namespace System.Runtime.CompilerServices
{
	[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
	internal sealed class IgnoresAccessChecksToAttribute : Attribute
	{
		public IgnoresAccessChecksToAttribute(string assemblyName)
		{
		}
	}
}