using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using DestroyedMoons.Config; using DestroyedMoons.Core; using DestroyedMoons.Integrations; using DestroyedMoons.Meltdown; using DestroyedMoons.Networking; using HarmonyLib; using Microsoft.CodeAnalysis; using Unity.Collections; using Unity.Netcode; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("DestroyedMoons")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.8.0")] [assembly: AssemblyInformationalVersion("1.0.8")] [assembly: AssemblyProduct("DestroyedMoons")] [assembly: AssemblyTitle("DestroyedMoons")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.8.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; } } } namespace DestroyedMoons { [BepInPlugin("soldierling.DestroyedMoons", "DestroyedMoons", "1.0.8")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class Plugin : BaseUnityPlugin { private Harmony _harmony; public static Plugin Instance { get; private set; } public static ManualLogSource Log { get; private set; } private void Awake() { //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; ModConfig.Init(((BaseUnityPlugin)this).Config); if (!ModConfig.Enabled.Value) { Log.LogWarning((object)"DestroyedMoons is disabled in config. Skipping all patches."); return; } _harmony = new Harmony("soldierling.DestroyedMoons"); _harmony.PatchAll(typeof(Plugin).Assembly); SafeInit("MeltdownDetector", delegate { MeltdownDetector.Initialize(_harmony); }); SafeInit("RandomMoonFX", delegate { RandomMoonFxIntegration.Initialize(_harmony); }); SafeInit("MeltdownChance", delegate { MeltdownChanceIntegration.Initialize(); }); SafeInit("SaveDeletion", delegate { SaveDeletionPatch.Initialize(_harmony); }); Log.LogInfo((object)("DestroyedMoons v1.0.8 loaded. " + $"FacilityMeltdown={SoftDeps.HasFacilityMeltdown}, " + $"RandomMoonFX={SoftDeps.HasRandomMoonFX}, " + $"MeltdownChance={SoftDeps.HasMeltdownChance}, " + $"LethalLevelLoader={SoftDeps.HasLethalLevelLoader}.")); } private static void SafeInit(string label, Action action) { try { action(); } catch (Exception arg) { Log.LogError((object)$"{label} init failed (mod continues): {arg}"); } } } internal static class PluginInfo { public const string GUID = "soldierling.DestroyedMoons"; public const string NAME = "DestroyedMoons"; public const string VERSION = "1.0.8"; public const string FACILITY_MELTDOWN_GUID = "me.loaforc.facilitymeltdown"; public const string FACILITY_MELTDOWN_GUID_LEGACY = "TeamXiaolan.FacilityMeltdown"; public const string RANDOM_MOON_FX_GUID = "zigzag.randommoonfx"; public const string MELTDOWN_CHANCE_GUID = "den.meltdownchance"; public const string LETHAL_LEVEL_LOADER_GUID = "imabatby.lethallevelloader"; } } namespace DestroyedMoons.Routing { [HarmonyPatch(typeof(StartOfRound), "ChangeLevelServerRpc")] internal static class ChangeLevelServerRpcPatch { [HarmonyPrefix] private static bool Prefix(int levelID) { try { string text = MoonKey.FromSelectableLevel(MoonResolver.LevelById(levelID)); if (DestroyedMoonsService.IsBlockedForRouting(text)) { Plugin.Log.LogWarning((object)$"Cancelled level change to destroyed moon '{text}' (levelID {levelID})."); HUDNotify.Show(ModConfig.BlockedMessage.Value); return false; } } catch { } return true; } } [HarmonyPatch(typeof(Terminal), "ParsePlayerSentence")] internal static class ParsePlayerSentencePatch { [HarmonyPostfix] private static void Postfix(Terminal __instance, ref TerminalNode __result) { try { Handle(__instance, ref __result); } catch { } } private static void Handle(Terminal __instance, ref TerminalNode __result) { if ((Object)(object)__instance == (Object)null || (Object)(object)__instance.screenText == (Object)null) { return; } string text = __instance.screenText.text; int textAdded = __instance.textAdded; if (textAdded <= 0 || textAdded > text.Length) { return; } string text2 = text.Substring(text.Length - textAdded).Trim().ToLowerInvariant(); if (text2.Length == 0) { return; } if (ModConfig.EnableTerminalCommands.Value && text2 == "moonstatus") { List list = DestroyedMoonsService.DescribeBlocked(); string text3 = ((list.Count == 0) ? "No moons are currently destroyed." : ("Destroyed / blocked moons:\n - " + string.Join("\n - ", list))); __result = TerminalNodeFactory.Message(text3); } else if (ModConfig.EnableMoonBuyback.Value) { if (text2 == "buyback") { __result = TerminalNodeFactory.Message(BuybackBody("MOON BUYBACK\n\n")); } else if (text2.StartsWith("buyback ")) { __result = TerminalNodeFactory.Message(DoBuyback(__instance, text2.Substring(8).Trim())); } else if ((text2 == "help" || text2 == "other") && DestroyedMoonsService.DescribeAutoBlocked().Count > 0) { __result = AppendBuybackSection(__result); } } } private static TerminalNode AppendBuybackSection(TerminalNode original) { string text = "\n\n>MOON BUYBACK\n____________________________\n\n" + BuybackBody(string.Empty); if ((Object)(object)original == (Object)null) { return TerminalNodeFactory.Message(text); } TerminalNode obj = Object.Instantiate(original); obj.displayText = (original.displayText ?? string.Empty) + text; return obj; } private static string BuybackBody(string header) { int value = ModConfig.MoonBuybackPrice.Value; StringBuilder stringBuilder = new StringBuilder(); if (!string.IsNullOrEmpty(header)) { stringBuilder.Append(header); } List list = DestroyedMoonsService.DescribeAutoBlocked(); if (list.Count == 0) { stringBuilder.Append("No moons are currently destroyed.\n"); return stringBuilder.ToString(); } stringBuilder.Append("These moons were lost to nuclear meltdowns.\n"); stringBuilder.Append("Restore one for ").Append(value).Append(" credits with:\n"); stringBuilder.Append(" >BUYBACK [moon name]\n\n"); foreach (string item in list) { stringBuilder.Append(" * ").Append(item).Append(" (") .Append(value) .Append("cr)\n"); } return stringBuilder.ToString(); } private static string DoBuyback(Terminal terminal, string arg) { if (string.IsNullOrEmpty(arg)) { return "Usage: BUYBACK [moon name]"; } string text = DestroyedMoonsService.ResolveAutoBlockedKey(arg); if (text == null) { return "'" + arg + "' is not a destroyed moon. Type BUYBACK to see the list."; } int value = ModConfig.MoonBuybackPrice.Value; if (terminal.groupCredits < value) { return $"Not enough credits. Restoring a moon costs {value}; you have {terminal.groupCredits}."; } string arg2 = text; foreach (SelectableLevel item in MoonResolver.AllLevels()) { if (MoonKey.FromSelectableLevel(item) == text) { arg2 = item.PlanetName; break; } } terminal.groupCredits -= value; try { terminal.SyncGroupCreditsServerRpc(terminal.groupCredits, terminal.numberOfItemsInDropship); } catch { } BlockedMoonsNetwork.RequestUnblock(text); return $"'{arg2}' has been restored for {value} credits.\nRemaining balance: {terminal.groupCredits}."; } } internal static class TerminalNodeFactory { public static TerminalNode Message(string text) { TerminalNode obj = ScriptableObject.CreateInstance(); ((Object)obj).name = "DestroyedMoonsMessage"; obj.displayText = "\n" + text + "\n\n"; obj.clearPreviousText = true; obj.maxCharactersToType = 999; obj.buyRerouteToMoon = -1; obj.buyItemIndex = -1; obj.itemCost = 0; obj.displayPlanetInfo = -1; obj.shipUnlockableID = -1; obj.playSyncedClip = -1; obj.acceptAnything = false; obj.overrideOptions = false; return obj; } } [HarmonyPatch(typeof(Terminal), "LoadNewNode")] internal static class LoadNewNodePatch { [HarmonyPrefix] private static void Prefix(ref TerminalNode node) { try { if ((Object)(object)node == (Object)null || node.buyRerouteToMoon < 0) { return; } string text = MoonKey.FromSelectableLevel(MoonResolver.LevelById(node.buyRerouteToMoon)); if (DestroyedMoonsService.IsBlockedForRouting(text)) { if (ModConfig.Verbose) { Plugin.Log.LogInfo((object)$"Blocked terminal reroute to '{text}' (levelID {node.buyRerouteToMoon})."); } node = TerminalNodeFactory.Message(ModConfig.BlockedMessage.Value); } } catch { } } } [HarmonyPatch(typeof(Terminal), "TextPostProcess")] [HarmonyAfter(new string[] { "imabatby.lethallevelloader" })] internal static class TextPostProcessStrikethroughPatch { [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(ref string __result) { if (!ModConfig.StrikethroughBlockedMoons.Value || string.IsNullOrEmpty(__result) || __result.IndexOf("", StringComparison.OrdinalIgnoreCase) >= 0) { return; } try { foreach (SelectableLevel item in MoonResolver.AllLevels()) { if (!DestroyedMoonsService.IsBlocked(MoonKey.FromSelectableLevel(item))) { continue; } string planetName = item.PlanetName; if (!string.IsNullOrEmpty(planetName) && __result.IndexOf(planetName, StringComparison.OrdinalIgnoreCase) >= 0) { __result = StrikeAll(__result, planetName); continue; } string text = MoonKey.NumberlessDisplay(planetName); if (!string.IsNullOrEmpty(text)) { __result = StrikeAll(__result, text); } } } catch { } } private static string StrikeAll(string text, string name) { int startIndex = 0; while (true) { int num = text.IndexOf(name, startIndex, StringComparison.OrdinalIgnoreCase); if (num < 0) { break; } if (num >= 3 && text.Substring(num - 3, 3).Equals("", StringComparison.OrdinalIgnoreCase)) { startIndex = num + name.Length; continue; } string text2 = text.Substring(num, name.Length); string text3 = "" + text2 + ""; text = text.Substring(0, num) + text3 + text.Substring(num + name.Length); startIndex = num + text3.Length; } return text; } } } namespace DestroyedMoons.Networking { internal static class BlockedMoonsNetwork { [CompilerGenerated] private static class <>O { public static HandleNamedMessageDelegate <0>__OnSyncReceived; public static HandleNamedMessageDelegate <1>__OnRequestReceived; public static HandleNamedMessageDelegate <2>__OnUnblockReceived; } private const string MSG_SYNC = "DestroyedMoons_Sync"; private const string MSG_REQUEST = "DestroyedMoons_Req"; private const string MSG_UNBLOCK = "DestroyedMoons_Unblock"; private static bool _registered; public static void Register() { //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Expected O, but got Unknown //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Expected O, but got Unknown //IL_0092: 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_009d: Expected O, but got Unknown NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || singleton.CustomMessagingManager == null || _registered) { return; } try { CustomMessagingManager customMessagingManager = singleton.CustomMessagingManager; object obj = <>O.<0>__OnSyncReceived; if (obj == null) { HandleNamedMessageDelegate val = OnSyncReceived; <>O.<0>__OnSyncReceived = val; obj = (object)val; } customMessagingManager.RegisterNamedMessageHandler("DestroyedMoons_Sync", (HandleNamedMessageDelegate)obj); CustomMessagingManager customMessagingManager2 = singleton.CustomMessagingManager; object obj2 = <>O.<1>__OnRequestReceived; if (obj2 == null) { HandleNamedMessageDelegate val2 = OnRequestReceived; <>O.<1>__OnRequestReceived = val2; obj2 = (object)val2; } customMessagingManager2.RegisterNamedMessageHandler("DestroyedMoons_Req", (HandleNamedMessageDelegate)obj2); CustomMessagingManager customMessagingManager3 = singleton.CustomMessagingManager; object obj3 = <>O.<2>__OnUnblockReceived; if (obj3 == null) { HandleNamedMessageDelegate val3 = OnUnblockReceived; <>O.<2>__OnUnblockReceived = val3; obj3 = (object)val3; } customMessagingManager3.RegisterNamedMessageHandler("DestroyedMoons_Unblock", (HandleNamedMessageDelegate)obj3); _registered = true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Failed to register net messages: " + ex.Message)); } } public unsafe static void BroadcastFullListToAll() { //IL_0051: Unknown result type (might be due to invalid IL or missing references) if (!NetworkUtils.IsHost) { return; } NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || singleton.CustomMessagingManager == null) { return; } string text = BlockedMoonsStore.SerializeForSync(); FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(text.Length * 2 + 256, (Allocator)2, -1); try { ((FastBufferWriter)(ref val)).WriteValueSafe(text, false); try { singleton.CustomMessagingManager.SendNamedMessageToAll("DestroyedMoons_Sync", val, (NetworkDelivery)4); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Broadcast failed: " + ex.Message)); } } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } private unsafe static void SendListToClient(ulong clientId) { //IL_004a: Unknown result type (might be due to invalid IL or missing references) NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || singleton.CustomMessagingManager == null) { return; } string text = BlockedMoonsStore.SerializeForSync(); FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(text.Length * 2 + 256, (Allocator)2, -1); try { ((FastBufferWriter)(ref val)).WriteValueSafe(text, false); try { singleton.CustomMessagingManager.SendNamedMessage("DestroyedMoons_Sync", clientId, val, (NetworkDelivery)4); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Targeted sync failed: " + ex.Message)); } } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } public unsafe static void RequestFullListFromHost() { //IL_0032: 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_004b: Unknown result type (might be due to invalid IL or missing references) if (NetworkUtils.IsHost) { return; } NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || singleton.CustomMessagingManager == null) { return; } FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(8, (Allocator)2, -1); try { int num = 0; ((FastBufferWriter)(ref val)).WriteValueSafe(ref num, default(ForPrimitives)); try { singleton.CustomMessagingManager.SendNamedMessage("DestroyedMoons_Req", 0uL, val, (NetworkDelivery)2); } catch (Exception ex) { Plugin.Log.LogWarning((object)("List request failed: " + ex.Message)); } } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } public unsafe static void RequestUnblock(string moonKey) { //IL_0058: Unknown result type (might be due to invalid IL or missing references) if (NetworkUtils.IsHost) { DestroyedMoonsService.UnblockMoon(moonKey); return; } NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || singleton.CustomMessagingManager == null || string.IsNullOrEmpty(moonKey)) { return; } FastBufferWriter val = default(FastBufferWriter); ((FastBufferWriter)(ref val))..ctor(moonKey.Length * 2 + 64, (Allocator)2, -1); try { ((FastBufferWriter)(ref val)).WriteValueSafe(moonKey, false); try { singleton.CustomMessagingManager.SendNamedMessage("DestroyedMoons_Unblock", 0uL, val, (NetworkDelivery)2); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Unblock request failed: " + ex.Message)); } } finally { ((IDisposable)(*(FastBufferWriter*)(&val))/*cast due to .constrained prefix*/).Dispose(); } } private static void OnSyncReceived(ulong senderClientId, FastBufferReader reader) { if (NetworkUtils.IsHost) { return; } try { string text = default(string); ((FastBufferReader)(ref reader)).ReadValueSafe(ref text, false); string[] array = (string.IsNullOrEmpty(text) ? Array.Empty() : text.Split('\n')); BlockedMoonsStore.ReplaceFromSync(array); Plugin.Log.LogInfo((object)$"Synced {array.Length} destroyed moon(s) from host."); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Failed to read sync: " + ex.Message)); } } private static void OnRequestReceived(ulong senderClientId, FastBufferReader reader) { if (NetworkUtils.IsHost) { SendListToClient(senderClientId); } } private static void OnUnblockReceived(ulong senderClientId, FastBufferReader reader) { if (!NetworkUtils.IsHost) { return; } try { string key = default(string); ((FastBufferReader)(ref reader)).ReadValueSafe(ref key, false); DestroyedMoonsService.UnblockMoon(key); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Failed to read unblock request: " + ex.Message)); } } } } namespace DestroyedMoons.Meltdown { internal static class MeltdownDetector { private const string HANDLER_TYPE = "FacilityMeltdown.MeltdownSequence.Behaviours.MeltdownHandler"; private const string START_METHOD = "StartMeltdownClientRpc"; private const string EXPLOSION_TYPE = "FacilityMeltdown.MeltdownSequence.Behaviours.FacilityExplosionHandler"; private const string EXPLOSION_METHOD = "Awake"; private const string API_TYPE = "FacilityMeltdown.API.MeltdownAPI"; private static Harmony _harmony; private static bool _bound; private static string _lastKey; private static DateTime _lastTime; private static readonly string[] MethodNameCandidates = new string[12] { "Detonate", "Explode", "Explosion", "SpawnExplosion", "MeltdownActually", "MeltdownEnd", "OnMeltdownFinished", "FinishMeltdown", "CauseMeltdown", "TriggerMeltdown", "StartMeltdown", "Meltdown" }; public static bool IsBound => _bound; public static void Initialize(Harmony harmony) { _harmony = harmony; if (!SoftDeps.HasFacilityMeltdown) { Plugin.Log.LogWarning((object)"FacilityMeltdown not detected. Auto-block on meltdown is disabled. Manual/terminal blocking and routing patches still work."); return; } string value = ModConfig.MeltdownPatchTarget.Value; if (string.IsNullOrWhiteSpace(value) || !TryPatchByTarget(value)) { int num = 0; if (ModConfig.BlockOnMeltdownStart.Value && TryPatchMethod("FacilityMeltdown.MeltdownSequence.Behaviours.MeltdownHandler", "StartMeltdownClientRpc", "meltdown start")) { num++; } if (TryPatchMethod("FacilityMeltdown.MeltdownSequence.Behaviours.FacilityExplosionHandler", "Awake", "real detonation")) { num++; } if (num == 0 && TrySubscribeApi()) { num++; } if (num == 0 && TryPatchByScan()) { num++; } if (num == 0) { Plugin.Log.LogError((object)"Could not locate any FacilityMeltdown detonation hook. Set 'Advanced/MeltdownPatchTarget' in the config to 'Namespace.Type:Method'. Routing block, persistence and the 'destroymoon' command still work."); } } } private static bool TryPatchMethod(string typeName, string methodName, string label) { try { Type type = AccessTools.TypeByName(typeName); if (type == null) { return false; } MethodInfo methodInfo = AccessTools.Method(type, methodName, (Type[])null, (Type[])null); if (methodInfo == null) { return false; } _harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, PostfixMethod(), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _bound = true; Plugin.Log.LogInfo((object)("Meltdown hook bound to " + typeName + ":" + methodName + " (" + label + ").")); return true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Hook " + typeName + ":" + methodName + " failed: " + ex.Message)); return false; } } private static bool TryPatchByTarget(string target) { try { string[] array = target.Split(':'); if (array.Length != 2) { Plugin.Log.LogError((object)("Invalid MeltdownPatchTarget '" + target + "'.")); return false; } Type type = AccessTools.TypeByName(array[0]); MethodInfo methodInfo = ((type != null) ? AccessTools.Method(type, array[1], (Type[])null, (Type[])null) : null); if (methodInfo == null) { Plugin.Log.LogError((object)("MeltdownPatchTarget '" + target + "' not found.")); return false; } _harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, PostfixMethod(), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _bound = true; Plugin.Log.LogInfo((object)("Meltdown hook bound to override target " + type.FullName + ":" + methodInfo.Name + ".")); return true; } catch (Exception arg) { Plugin.Log.LogError((object)$"Failed to patch override '{target}': {arg}"); return false; } } private static bool TrySubscribeApi() { try { Type type = AccessTools.TypeByName("FacilityMeltdown.API.MeltdownAPI"); if (type == null) { return false; } Action action = OnDetonationEvent; MethodInfo methodInfo = AccessTools.Method(type, "RegisterMeltdownListener", (Type[])null, (Type[])null); if (methodInfo != null) { ParameterInfo[] parameters = methodInfo.GetParameters(); if (parameters.Length == 1 && parameters[0].ParameterType.IsAssignableFrom(typeof(Action))) { methodInfo.Invoke(null, new object[1] { action }); _bound = true; Plugin.Log.LogInfo((object)"Meltdown hook subscribed via FacilityMeltdown.API.MeltdownAPI.RegisterMeltdownListener."); return true; } } FieldInfo field = type.GetField("OnMeltdownStart", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (field != null && typeof(Delegate).IsAssignableFrom(field.FieldType) && field.IsStatic) { Delegate a = field.GetValue(null) as Delegate; field.SetValue(null, Delegate.Combine(a, action)); _bound = true; Plugin.Log.LogInfo((object)"Meltdown hook attached to FacilityMeltdown.API.MeltdownAPI.OnMeltdownStart field."); return true; } } catch (Exception ex) { Plugin.Log.LogWarning((object)("MeltdownAPI hook failed: " + ex.Message)); } return false; } private static bool TryPatchByScan() { List<(int, MethodInfo)> list = new List<(int, MethodInfo)>(); foreach (Type item in MeltdownTypes()) { MethodInfo[] methods; try { methods = item.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); } catch { continue; } MethodInfo[] array = methods; foreach (MethodInfo m in array) { int num = Array.FindIndex(MethodNameCandidates, (string n) => string.Equals(n, m.Name, StringComparison.OrdinalIgnoreCase)); if (num < 0) { continue; } try { if (m.IsAbstract || m.IsGenericMethodDefinition || m.GetParameters().Length != 0) { continue; } goto IL_009d; } catch { } continue; IL_009d: list.Add((MethodNameCandidates.Length - num, m)); } } foreach (var item2 in list.OrderByDescending<(int, MethodInfo), int>(((int score, MethodInfo m) s) => s.score)) { try { _harmony.Patch((MethodBase)item2.Item2, (HarmonyMethod)null, PostfixMethod(), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _bound = true; Plugin.Log.LogInfo((object)("Meltdown hook bound to method " + item2.Item2.DeclaringType?.FullName + ":" + item2.Item2.Name + " (auto-scan).")); return true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Failed to patch " + item2.Item2.Name + ": " + ex.Message)); } } return false; } private static HarmonyMethod PostfixMethod() { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Expected O, but got Unknown return new HarmonyMethod(typeof(MeltdownDetector).GetMethod("DetonationPostfix", BindingFlags.Static | BindingFlags.NonPublic)); } private static IEnumerable MeltdownTypes() { Assembly assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault((Assembly a) => a.GetName().Name.IndexOf("FacilityMeltdown", StringComparison.OrdinalIgnoreCase) >= 0); if (assembly == null) { yield break; } Type[] array; try { array = assembly.GetTypes(); } catch (ReflectionTypeLoadException ex) { array = ex.Types.Where((Type t) => t != null).ToArray(); } catch { yield break; } Type[] array2 = array; for (int num = 0; num < array2.Length; num++) { yield return array2[num]; } } private static void DetonationPostfix() { HandleDetonation(); } private static void OnDetonationEvent() { HandleDetonation(); } private static void HandleDetonation() { if (!ModConfig.Enabled.Value) { return; } string currentMoonKey = MoonResolver.GetCurrentMoonKey(); bool isHost = NetworkUtils.IsHost; Plugin.Log.LogInfo((object)string.Format("Meltdown signal received. moon='{0}', IsHost={1}.", currentMoonKey ?? "?", isHost)); if (!isHost) { return; } if (string.IsNullOrEmpty(currentMoonKey)) { Plugin.Log.LogWarning((object)"Meltdown signal but current moon unresolved."); } else if (!(currentMoonKey == _lastKey) || !((DateTime.UtcNow - _lastTime).TotalSeconds < 10.0)) { _lastKey = currentMoonKey; _lastTime = DateTime.UtcNow; if (DestroyedMoonsService.Whitelist().Contains(currentMoonKey)) { Plugin.Log.LogInfo((object)("Moon '" + currentMoonKey + "' had a meltdown but is whitelisted — not blocking.")); return; } BlockedMoonsStore.EnsureLoaded(); Plugin.Log.LogInfo((object)("FacilityMeltdown on '" + currentMoonKey + "'. Blocking this moon permanently.")); DestroyedMoonsService.BlockMoon(currentMoonKey); } } } } namespace DestroyedMoons.Integrations { internal static class MeltdownChanceIntegration { public static bool Present { get; private set; } public static void Initialize() { Present = SoftDeps.HasMeltdownChance; if (Present) { Plugin.Log.LogInfo((object)"Meltdown_Chance detected. Blocking will only occur on real detonations."); } else if (ModConfig.Verbose) { Plugin.Log.LogInfo((object)"Meltdown_Chance not present."); } } } internal static class RandomMoonFxIntegration { private const string UTILS_TYPE = "RandomMoonFX.Utils"; private const string BLACKLIST_METHOD = "IsMoonBlacklisted"; private const string VALID_METHOD = "IsMoonValid"; private static bool _initialized; private static readonly Stopwatch _callClock = Stopwatch.StartNew(); private static long _lastCallMs = -1000L; private static int _burst; private const int BurstLimit = 2000; public static void Initialize(Harmony harmony) { //IL_010e: Unknown result type (might be due to invalid IL or missing references) //IL_011b: Expected O, but got Unknown //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Expected O, but got Unknown //IL_01d8: Unknown result type (might be due to invalid IL or missing references) //IL_01e3: Expected O, but got Unknown if (_initialized) { return; } _initialized = true; if (!SoftDeps.HasRandomMoonFX) { if (ModConfig.Verbose) { Plugin.Log.LogInfo((object)"RandomMoonFX not present — integration skipped."); } return; } if (!ModConfig.FilterRandomMoonFXPool.Value) { Plugin.Log.LogInfo((object)"RandomMoonFX pool filtering disabled by config. Destroyed moons are still blocked by the terminal/level-change guard."); return; } Type type = AccessTools.TypeByName("RandomMoonFX.Utils"); if (type == null) { Plugin.Log.LogWarning((object)"RandomMoonFX present but 'RandomMoonFX.Utils' not found. Destroyed moons still blocked by routing patches, but not filtered from the random pool."); return; } bool flag = false; MethodInfo methodInfo = AccessTools.Method(type, "IsMoonBlacklisted", (Type[])null, (Type[])null); if (methodInfo != null) { try { harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(RandomMoonFxIntegration).GetMethod("IsMoonBlacklistedPostfix", BindingFlags.Static | BindingFlags.NonPublic)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); flag = true; Plugin.Log.LogInfo((object)"RandomMoonFX filtered via postfix on RandomMoonFX.Utils.IsMoonBlacklisted."); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Failed to patch IsMoonBlacklisted: " + ex.Message)); } } MethodInfo methodInfo2 = AccessTools.Method(type, "IsMoonValid", (Type[])null, (Type[])null); if (methodInfo2 != null) { try { harmony.Patch((MethodBase)methodInfo2, (HarmonyMethod)null, new HarmonyMethod(typeof(RandomMoonFxIntegration).GetMethod("IsMoonValidPostfix", BindingFlags.Static | BindingFlags.NonPublic)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); flag = true; if (ModConfig.Verbose) { Plugin.Log.LogInfo((object)"RandomMoonFX also filtered via RandomMoonFX.Utils.IsMoonValid."); } } catch (Exception ex2) { Plugin.Log.LogWarning((object)("Failed to patch IsMoonValid: " + ex2.Message)); } } if (!flag) { Plugin.Log.LogWarning((object)"RandomMoonFX present but no filter method could be patched. Routing patches remain the backstop."); } Type type2 = AccessTools.TypeByName("RandomMoonFX.Plugin"); if (!(type2 != null)) { return; } string[] array = new string[2] { "RouteRandomPlanet", "StartRandomPlanet" }; foreach (string text in array) { MethodInfo methodInfo3 = AccessTools.Method(type2, text, (Type[])null, (Type[])null); if (!(methodInfo3 == null)) { try { harmony.Patch((MethodBase)methodInfo3, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, new HarmonyMethod(typeof(RandomMoonFxIntegration).GetMethod("RoutingFinalizer", BindingFlags.Static | BindingFlags.NonPublic)), (HarmonyMethod)null); } catch (Exception ex3) { Plugin.Log.LogWarning((object)("Failed to add finalizer to " + text + ": " + ex3.Message)); } } } } private static Exception RoutingFinalizer(Exception __exception) { if (__exception != null) { Plugin.Log.LogWarning((object)("Suppressed a RandomMoonFX routing exception to avoid a crash: " + __exception.Message)); DestroyedMoonsService.BeginRoutingBypass(3000); } return null; } public static void SyncBlacklist(IEnumerable blockedKeys) { } private static void IsMoonBlacklistedPostfix(object[] __args, ref bool __result) { try { if (!RerollStormGuard() && !__result && ArgIsBlockedLevel(__args)) { __result = true; } } catch { } } private static void IsMoonValidPostfix(object[] __args, ref bool __result) { try { if (!RerollStormGuard() && __result && ArgIsBlockedLevel(__args)) { __result = false; } } catch { } } private static bool RerollStormGuard() { long elapsedMilliseconds = _callClock.ElapsedMilliseconds; if (elapsedMilliseconds - _lastCallMs <= 3) { _burst++; } else { _burst = 0; } _lastCallMs = elapsedMilliseconds; if (_burst > 2000) { _burst = 0; DestroyedMoonsService.BeginRoutingBypass(3000); Plugin.Log.LogWarning((object)"RandomMoonFX reroll-storm detected — temporarily allowing routing to prevent a hang."); return true; } return false; } private static bool ArgIsBlockedLevel(object[] args) { if (args == null) { return false; } foreach (object obj in args) { SelectableLevel val = (SelectableLevel)((obj is SelectableLevel) ? obj : null); if (val != null) { return DestroyedMoonsService.IsBlockedForRouting(MoonKey.FromSelectableLevel(val)); } if (obj is string raw) { return DestroyedMoonsService.IsBlockedForRouting(MoonKey.Normalize(raw)); } } return false; } } } namespace DestroyedMoons.Core { internal static class BlockedMoonsStore { private const string ES3_KEY = "DestroyedMoons_BlockedList"; private static readonly HashSet _blocked = new HashSet(); private static string _loadedSaveFile; public static int Generation { get; private set; } private static string CurrentSaveFile { get { GameNetworkManager instance = GameNetworkManager.Instance; if (!((Object)(object)instance != (Object)null) || string.IsNullOrEmpty(instance.currentSaveFileName)) { return "LCSaveFile1"; } return instance.currentSaveFileName; } } private static string JsonMirrorPath => Path.Combine(Paths.ConfigPath, "DestroyedMoons", "blocked_" + CurrentSaveFile + ".json"); public static IReadOnlyCollection AutoBlocked => _blocked; public static void EnsureLoaded() { if (_loadedSaveFile == null || !string.Equals(_loadedSaveFile, CurrentSaveFile, StringComparison.Ordinal)) { Load(); } } public static void Load() { _blocked.Clear(); _loadedSaveFile = CurrentSaveFile; if (ModConfig.ResetOnNextLoad.Value) { Plugin.Log.LogWarning((object)("ResetOnNextLoad was set — clearing destroyed-moon list for '" + _loadedSaveFile + "'.")); DeleteJsonMirror(); Save(); ModConfig.ResetOnNextLoad.Value = false; Generation++; return; } try { string text = null; if (Es3Interop.Available) { if (Es3Interop.KeyExists("DestroyedMoons_BlockedList", _loadedSaveFile)) { text = Es3Interop.LoadString("DestroyedMoons_BlockedList", _loadedSaveFile, null); } else { DeleteJsonMirror(); } } else { text = ReadJsonMirror(); } if (!string.IsNullOrEmpty(text)) { string[] array = text.Split(new char[2] { '\n', ';' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < array.Length; i++) { string text2 = MoonKey.Normalize(array[i]); if (!string.IsNullOrEmpty(text2)) { _blocked.Add(text2); } } } } catch (Exception arg) { Plugin.Log.LogError((object)$"Failed to load destroyed-moon list: {arg}"); } Generation++; Plugin.Log.LogInfo((object)($"Loaded {_blocked.Count} destroyed moon(s) for save '{_loadedSaveFile}': " + "[" + string.Join(", ", _blocked) + "]")); } public static void Save() { string text = string.Join("\n", _blocked); try { Es3Interop.SaveString("DestroyedMoons_BlockedList", text, CurrentSaveFile); } catch (Exception ex) { Plugin.Log.LogWarning((object)("ES3 save failed: " + ex.Message)); } WriteJsonMirror(text); } public static bool Add(string key) { if (string.IsNullOrEmpty(key)) { return false; } if (_blocked.Add(key)) { Generation++; Save(); return true; } return false; } public static bool Remove(string key) { if (string.IsNullOrEmpty(key)) { return false; } if (_blocked.Remove(key)) { Generation++; Save(); return true; } return false; } public static void Clear() { _blocked.Clear(); Generation++; Save(); } public static void ReplaceFromSync(IEnumerable keys) { _blocked.Clear(); foreach (string key in keys) { string text = MoonKey.Normalize(key); if (!string.IsNullOrEmpty(text)) { _blocked.Add(text); } } Generation++; } public static string SerializeForSync() { return string.Join("\n", _blocked); } public static void OnSaveDeleted(string saveFilePath) { if (!string.IsNullOrEmpty(saveFilePath)) { string fileName = Path.GetFileName(saveFilePath); DeleteJsonMirrorFor(fileName); if (string.Equals(fileName, _loadedSaveFile, StringComparison.Ordinal)) { _blocked.Clear(); _loadedSaveFile = null; Generation++; } Plugin.Log.LogInfo((object)("Save '" + fileName + "' deleted — cleared DestroyedMoons data for that slot.")); } } private static void DeleteJsonMirror() { DeleteJsonMirrorFor(CurrentSaveFile); } private static void DeleteJsonMirrorFor(string saveFileName) { try { string path = Path.Combine(Paths.ConfigPath, "DestroyedMoons", "blocked_" + saveFileName + ".json"); if (File.Exists(path)) { File.Delete(path); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("JSON mirror delete failed: " + ex.Message)); } } private static void WriteJsonMirror(string joined) { try { string directoryName = Path.GetDirectoryName(JsonMirrorPath); if (!Directory.Exists(directoryName)) { Directory.CreateDirectory(directoryName); } IEnumerable values = from s in joined.Split(new char[1] { '\n' }, StringSplitOptions.RemoveEmptyEntries) select "\"" + s.Replace("\"", "\\\"") + "\""; File.WriteAllText(JsonMirrorPath, "{\n \"saveFile\": \"" + CurrentSaveFile + "\",\n \"blockedMoons\": [" + string.Join(", ", values) + "]\n}\n"); } catch (Exception ex) { Plugin.Log.LogWarning((object)("JSON mirror write failed: " + ex.Message)); } } private static string ReadJsonMirror() { try { if (!File.Exists(JsonMirrorPath)) { return null; } string text = File.ReadAllText(JsonMirrorPath); int num = text.IndexOf('['); int num2 = text.IndexOf(']'); if (num < 0 || num2 < 0 || num2 <= num) { return null; } string text2 = text.Substring(num + 1, num2 - num - 1); List list = new List(); string[] array = text2.Split(','); for (int i = 0; i < array.Length; i++) { string text3 = array[i].Trim().Trim('"', ' '); if (!string.IsNullOrEmpty(text3)) { list.Add(text3); } } return string.Join("\n", list); } catch { return null; } } } internal static class DestroyedMoonsService { private static HashSet _wlCache; private static string _wlRaw; private static HashSet _blCache; private static string _blRaw; private static bool _arbCache; private static int _arbGen = -1; private static string _arbWl; private static string _arbBl; private static int _arbLevelCount = -1; private static readonly Stopwatch _clock = Stopwatch.StartNew(); private static long _bypassUntilMs = -1L; private static bool RoutingBypassActive => _clock.ElapsedMilliseconds < _bypassUntilMs; private static HashSet ParseList(string csv) { HashSet hashSet = new HashSet(); if (string.IsNullOrWhiteSpace(csv)) { return hashSet; } string[] array = csv.Split(','); for (int i = 0; i < array.Length; i++) { string text = MoonKey.Normalize(array[i]); if (!string.IsNullOrEmpty(text)) { hashSet.Add(text); } } return hashSet; } public static HashSet Whitelist() { string text = ModConfig.Whitelist.Value ?? string.Empty; if (_wlCache == null || !string.Equals(text, _wlRaw, StringComparison.Ordinal)) { _wlRaw = text; _wlCache = ParseList(text); } return _wlCache; } public static HashSet ManualBlacklist() { string text = ModConfig.ManualBlacklist.Value ?? string.Empty; if (_blCache == null || !string.Equals(text, _blRaw, StringComparison.Ordinal)) { _blRaw = text; _blCache = ParseList(text); } return _blCache; } public static HashSet EffectiveBlocked() { HashSet hashSet = new HashSet(BlockedMoonsStore.AutoBlocked); hashSet.UnionWith(ManualBlacklist()); hashSet.ExceptWith(Whitelist()); return hashSet; } public static bool IsBlocked(string key) { if (string.IsNullOrEmpty(key)) { return false; } if (Whitelist().Contains(key)) { return false; } if (!BlockedMoonsStore.AutoBlocked.Contains(key)) { return ManualBlacklist().Contains(key); } return true; } public static bool IsBlocked(SelectableLevel level) { return IsBlocked(MoonKey.FromSelectableLevel(level)); } public static bool AllRoutableBlocked() { StartOfRound instance = StartOfRound.Instance; int num = (((Object)(object)instance != (Object)null && instance.levels != null) ? instance.levels.Length : 0); string text = ModConfig.Whitelist.Value ?? string.Empty; string text2 = ModConfig.ManualBlacklist.Value ?? string.Empty; if (_arbGen == BlockedMoonsStore.Generation && _arbLevelCount == num && string.Equals(text, _arbWl, StringComparison.Ordinal) && string.Equals(text2, _arbBl, StringComparison.Ordinal)) { return _arbCache; } int num2 = 0; int num3 = 0; foreach (SelectableLevel item in MoonResolver.AllLevels()) { if (!MoonResolver.IsCompanyMoon(item)) { num2++; if (!IsBlocked(MoonKey.FromSelectableLevel(item))) { num3++; } } } _arbCache = num2 > 0 && num3 == 0; _arbGen = BlockedMoonsStore.Generation; _arbLevelCount = num; _arbWl = text; _arbBl = text2; return _arbCache; } public static void BeginRoutingBypass(int milliseconds) { _bypassUntilMs = _clock.ElapsedMilliseconds + milliseconds; } public static bool IsBlockedForRouting(string key) { if (RoutingBypassActive) { return false; } if (IsBlocked(key)) { return !AllRoutableBlocked(); } return false; } public static bool IsBlockedForRouting(SelectableLevel level) { return IsBlockedForRouting(MoonKey.FromSelectableLevel(level)); } public static void BlockMoon(string key) { if (string.IsNullOrEmpty(key)) { return; } if (!NetworkUtils.IsHost) { if (ModConfig.Verbose) { Plugin.Log.LogInfo((object)("Ignoring BlockMoon('" + key + "') — not host.")); } return; } BlockedMoonsStore.EnsureLoaded(); bool num = BlockedMoonsStore.Add(key); if (num) { Plugin.Log.LogInfo((object)("Moon '" + key + "' permanently blocked for this save (nuclear meltdown).")); } RandomMoonFxIntegration.SyncBlacklist(EffectiveBlocked()); BlockedMoonsNetwork.BroadcastFullListToAll(); if (num) { MaybeResetIfExhausted(); } } public static void UnblockMoon(string key) { if (!string.IsNullOrEmpty(key) && NetworkUtils.IsHost) { BlockedMoonsStore.EnsureLoaded(); if (BlockedMoonsStore.Remove(key)) { Plugin.Log.LogInfo((object)("Moon '" + key + "' restored (buyback).")); } RandomMoonFxIntegration.SyncBlacklist(EffectiveBlocked()); BlockedMoonsNetwork.BroadcastFullListToAll(); } } private static void MaybeResetIfExhausted() { if (ModConfig.AutoResetWhenAllBlocked.Value && AllRoutableBlocked()) { Plugin.Log.LogInfo((object)"All routable moon(s) destroyed — auto-resetting the destroyed-moon list."); HUDNotify.Show("Every moon has been destroyed. The list resets — moons are available again."); ResetAll(); } } public static void ResetAll() { BlockedMoonsStore.Clear(); RandomMoonFxIntegration.SyncBlacklist(EffectiveBlocked()); BlockedMoonsNetwork.BroadcastFullListToAll(); Plugin.Log.LogInfo((object)"Destroyed-moon list cleared."); } private static Dictionary NiceNameMap() { Dictionary dictionary = new Dictionary(); foreach (SelectableLevel item in MoonResolver.AllLevels()) { string text = MoonKey.FromSelectableLevel(item); if (!string.IsNullOrEmpty(text) && !dictionary.ContainsKey(text)) { dictionary[text] = item.PlanetName; } } return dictionary; } public static List DescribeBlocked() { Dictionary map = NiceNameMap(); string value; return (from k in EffectiveBlocked() select (!map.TryGetValue(k, out value)) ? k : value into x orderby x select x).ToList(); } public static List DescribeAutoBlocked() { Dictionary map = NiceNameMap(); HashSet wl = Whitelist(); string value; return (from k in BlockedMoonsStore.AutoBlocked where !wl.Contains(k) select (!map.TryGetValue(k, out value)) ? k : value into x orderby x select x).ToList(); } public static string ResolveAutoBlockedKey(string typedName) { string text = MoonKey.Normalize(typedName); if (string.IsNullOrEmpty(text)) { return null; } foreach (string item in BlockedMoonsStore.AutoBlocked) { if (item == text) { return item; } } foreach (string item2 in BlockedMoonsStore.AutoBlocked) { if (item2.Contains(text) || text.Contains(item2)) { return item2; } } return null; } } internal static class Es3Interop { private static Type _es3; private static MethodInfo _keyExists; private static MethodInfo _saveObj; private static MethodInfo _loadDefault; private static bool _resolved; public static bool Available => Resolve(); private static bool Resolve() { if (_resolved) { return _es3 != null; } _resolved = true; try { _es3 = AccessTypeByName("ES3"); if (_es3 == null) { return false; } _keyExists = _es3.GetMethod("KeyExists", new Type[2] { typeof(string), typeof(string) }); _saveObj = _es3.GetMethod("Save", new Type[3] { typeof(string), typeof(object), typeof(string) }); _loadDefault = _es3.GetMethod("Load", new Type[3] { typeof(string), typeof(string), typeof(object) }); } catch (Exception ex) { Plugin.Log.LogWarning((object)("ES3 reflection failed: " + ex.Message)); } if (_es3 != null && _keyExists != null && _saveObj != null) { return _loadDefault != null; } return false; } private static Type AccessTypeByName(string name) { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); for (int i = 0; i < assemblies.Length; i++) { Type type = assemblies[i].GetType(name, throwOnError: false); if (type != null) { return type; } } return null; } public static bool KeyExists(string key, string saveFile) { if (!Resolve()) { return false; } try { return (bool)_keyExists.Invoke(null, new object[2] { key, saveFile }); } catch (Exception ex) { Plugin.Log.LogWarning((object)("ES3.KeyExists failed: " + ex.Message)); return false; } } public static void SaveString(string key, string value, string saveFile) { if (!Resolve()) { return; } try { _saveObj.Invoke(null, new object[3] { key, value, saveFile }); } catch (Exception ex) { Plugin.Log.LogWarning((object)("ES3.Save failed: " + ex.Message)); } } public static string LoadString(string key, string saveFile, string fallback) { if (!Resolve()) { return fallback; } try { return (string)_loadDefault.Invoke(null, new object[3] { key, saveFile, fallback }); } catch (Exception ex) { Plugin.Log.LogWarning((object)("ES3.Load failed: " + ex.Message)); return fallback; } } } internal static class HUDNotify { public static void Show(string body, string header = "Meltdown Aftermath") { HUDManager instance = HUDManager.Instance; if ((Object)(object)instance != (Object)null) { try { instance.DisplayTip(header, body, true, false, "LC_Tip1"); } catch { } } } } [HarmonyPatch(typeof(StartOfRound), "Start")] internal static class StartOfRoundStartPatch { [HarmonyPostfix] private static void Postfix() { bool isHost = NetworkUtils.IsHost; Plugin.Log.LogInfo((object)$"StartOfRound.Start — IsHost={isHost}. Wiring DestroyedMoons."); BlockedMoonsNetwork.Register(); if (isHost) { BlockedMoonsStore.Load(); RandomMoonFxIntegration.SyncBlacklist(DestroyedMoonsService.EffectiveBlocked()); BlockedMoonsNetwork.BroadcastFullListToAll(); } else { BlockedMoonsNetwork.RequestFullListFromHost(); } } } [HarmonyPatch(typeof(StartOfRound), "SetTimeAndPlanetToSavedSettings")] internal static class SetTimeAndPlanetToSavedSettingsPatch { [HarmonyPostfix] private static void Postfix() { if (NetworkUtils.IsHost) { BlockedMoonsStore.Load(); RandomMoonFxIntegration.SyncBlacklist(DestroyedMoonsService.EffectiveBlocked()); BlockedMoonsNetwork.BroadcastFullListToAll(); } } } internal static class MoonKey { public static string FromSelectableLevel(SelectableLevel level) { if ((Object)(object)level == (Object)null) { return null; } return Normalize(level.PlanetName); } public static string NumberlessDisplay(string raw) { if (string.IsNullOrWhiteSpace(raw)) { return raw; } raw = raw.Trim(); int i; for (i = 0; i < raw.Length && char.IsDigit(raw[i]); i++) { } if (i > 0 && i < raw.Length && (raw[i] == ' ' || raw[i] == '-' || raw[i] == '.')) { return raw.Substring(i + 1).Trim(); } return raw; } public static string Normalize(string raw) { if (string.IsNullOrWhiteSpace(raw)) { return null; } raw = raw.Trim(); int i; for (i = 0; i < raw.Length && char.IsDigit(raw[i]); i++) { } if (i > 0 && i < raw.Length && (raw[i] == ' ' || raw[i] == '-' || raw[i] == '.')) { raw = raw.Substring(i + 1).Trim(); } StringBuilder stringBuilder = new StringBuilder(raw.Length); string text = raw; foreach (char c in text) { if (!char.IsWhiteSpace(c)) { stringBuilder.Append(char.ToLowerInvariant(c)); } } if (stringBuilder.Length != 0) { return stringBuilder.ToString(); } return null; } } internal static class MoonResolver { public static string GetCurrentMoonKey() { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)instance.currentLevel == (Object)null) { return null; } return MoonKey.FromSelectableLevel(instance.currentLevel); } public static SelectableLevel CurrentLevel() { if (!((Object)(object)StartOfRound.Instance != (Object)null)) { return null; } return StartOfRound.Instance.currentLevel; } public static SelectableLevel LevelById(int levelID) { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || instance.levels == null) { return null; } SelectableLevel[] levels = instance.levels; foreach (SelectableLevel val in levels) { if ((Object)(object)val != (Object)null && val.levelID == levelID) { return val; } } return null; } public static IEnumerable AllLevels() { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || instance.levels == null) { yield break; } SelectableLevel[] levels = instance.levels; foreach (SelectableLevel val in levels) { if ((Object)(object)val != (Object)null) { yield return val; } } } public static string DisplayName(SelectableLevel level) { if (!((Object)(object)level != (Object)null) || string.IsNullOrEmpty(level.PlanetName)) { return "Unknown"; } return level.PlanetName; } public static bool IsCompanyMoon(SelectableLevel level) { if ((Object)(object)level == (Object)null) { return true; } string planetName = level.PlanetName; if (string.IsNullOrEmpty(planetName)) { return false; } planetName = planetName.ToLowerInvariant(); if (!planetName.Contains("gordion")) { return planetName.Contains("company"); } return true; } } internal static class NetworkUtils { public static bool IsHost { get { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton != (Object)null) { if (!singleton.IsHost) { return singleton.IsServer; } return true; } return false; } } public static bool IsClientConnected { get { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton != (Object)null) { return singleton.IsListening; } return false; } } } internal static class SaveDeletionPatch { public static void Initialize(Harmony harmony) { //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Expected O, but got Unknown try { Type type = AccessTools.TypeByName("ES3"); if (type == null) { Plugin.Log.LogWarning((object)"ES3 type not found — save-deletion cleanup disabled."); return; } MethodInfo methodInfo = AccessTools.Method(type, "DeleteFile", new Type[1] { typeof(string) }, (Type[])null); if (methodInfo == null) { Plugin.Log.LogWarning((object)"ES3.DeleteFile(string) not found — save-deletion cleanup disabled."); return; } harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(SaveDeletionPatch).GetMethod("OnDeleteFile", BindingFlags.Static | BindingFlags.NonPublic)), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Plugin.Log.LogInfo((object)"Save-deletion cleanup hooked (ES3.DeleteFile)."); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Failed to hook save deletion: " + ex.Message)); } } private static void OnDeleteFile(object[] __args) { try { if (__args != null && __args.Length != 0 && __args[0] is string saveFilePath) { BlockedMoonsStore.OnSaveDeleted(saveFilePath); } } catch { } } } internal static class SoftDeps { public static bool HasFacilityMeltdown => Has("me.loaforc.facilitymeltdown", "TeamXiaolan.FacilityMeltdown"); public static bool HasRandomMoonFX => Has("zigzag.randommoonfx"); public static bool HasMeltdownChance => Has("den.meltdownchance"); public static bool HasLethalLevelLoader => Has("imabatby.lethallevelloader"); private static bool Has(params string[] guids) { foreach (string key in Chainloader.PluginInfos.Keys) { foreach (string b in guids) { if (string.Equals(key, b, StringComparison.OrdinalIgnoreCase)) { return true; } } } return false; } } } namespace DestroyedMoons.Config { internal static class ModConfig { public static ConfigEntry Enabled; public static ConfigEntry ManualBlacklist; public static ConfigEntry Whitelist; public static ConfigEntry ResetOnNextLoad; public static ConfigEntry BlockedMessage; public static ConfigEntry EnableTerminalCommands; public static ConfigEntry VerboseLogging; public static ConfigEntry StrikethroughBlockedMoons; public static ConfigEntry BlockOnMeltdownStart; public static ConfigEntry AutoResetWhenAllBlocked; public static ConfigEntry EnableMoonBuyback; public static ConfigEntry MoonBuybackPrice; public static ConfigEntry FilterRandomMoonFXPool; public static ConfigEntry MeltdownPatchTarget; public static bool Verbose { get { if (VerboseLogging != null) { return VerboseLogging.Value; } return false; } } public static void Init(ConfigFile cfg) { Enabled = cfg.Bind("General", "Enabled", true, "Master switch. If false the mod applies no patches at all."); BlockedMessage = cfg.Bind("General", "BlockedMessage", "This moon was destroyed in a nuclear meltdown. Routing is impossible.", "Message shown when a player tries to travel to a destroyed moon."); ManualBlacklist = cfg.Bind("Lists", "ManualBlacklist", "", "Moons that are ALWAYS blocked (comma-separated planet names, e.g. \"Titan, March\"). Numbers and case are ignored, so \"8 Titan\" and \"titan\" both match Titan."); Whitelist = cfg.Bind("Lists", "Whitelist", "", "Moons that are NEVER blocked, even after a meltdown (comma-separated). Overrides everything else."); ResetOnNextLoad = cfg.Bind("Maintenance", "ResetBlockedListOnNextLoad", false, "If true, the stored destroyed-moon list for the loaded save is cleared on next load. This flag auto-resets itself back to false afterwards."); EnableTerminalCommands = cfg.Bind("Maintenance", "EnableTerminalCommands", true, "Adds terminal commands: 'moonstatus' (list blocked moons), 'destroymoon' (host: block current moon), 'restoremoons' (host: clear the list). Useful for testing the pipeline."); VerboseLogging = cfg.Bind("Debug", "VerboseLogging", false, "Extra logging for diagnosing detection and routing."); StrikethroughBlockedMoons = cfg.Bind("Visuals", "StrikethroughBlockedMoons", true, "Draw a line through destroyed moon names in the terminal (e.g. the >MOONS list) for clarity."); EnableMoonBuyback = cfg.Bind("Buyback", "EnableMoonBuyback", true, "Allow players to pay to restore a destroyed moon from the terminal. A 'MOON BUYBACK' section appears on the terminal HELP page once moons have been destroyed. Use the command: BUYBACK [moon name]."); MoonBuybackPrice = cfg.Bind("Buyback", "MoonBuybackPrice", 500, "Credit cost to buy back (restore) a single destroyed moon."); FilterRandomMoonFXPool = cfg.Bind("Compatibility", "FilterRandomMoonFXPool", true, "Remove destroyed moons from RandomMoonFX's random lever pool. Has a built-in anti-hang guard, but if the lever ever freezes the game you can set this to false — routing to destroyed moons is still blocked by the terminal/level-change guard."); AutoResetWhenAllBlocked = cfg.Bind("General", "AutoResetWhenAllBlocked", true, "When every routable (non-Company) moon has been destroyed, automatically clear the destroyed-moon list so the moons become available again — prevents a dead-end where nowhere is reachable."); BlockOnMeltdownStart = cfg.Bind("General", "BlockOnMeltdownStart", true, "Block the moon the moment a meltdown STARTS (reliable, catches meltdowns even if you leave before the blast). A started FacilityMeltdown always ends in an explosion, so the moon is doomed either way. If false, only the final detonation triggers the block."); MeltdownPatchTarget = cfg.Bind("Advanced", "MeltdownPatchTarget", "", "Optional override for the FacilityMeltdown explosion hook. Leave blank for auto-detection. Format 'Namespace.Type:Method'. Check the BepInEx log to see what auto-detection found."); } } }