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)
{
}
}
}