Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of Random Interiors v1.0.1
RandomInteriors.dll
Decompiled a day agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using DunGen.Graph; using HarmonyLib; using Unity.Netcode; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("RandomInteriors")] [assembly: AssemblyConfiguration("Debug")] [assembly: AssemblyFileVersion("1.0.6.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("RandomInteriors")] [assembly: AssemblyTitle("RandomInteriors")] [assembly: AssemblyVersion("1.0.6.0")] namespace Hibyhii.RandomInteriors; internal static class PatchHelpers { internal static bool IsServer() { NetworkManager singleton = NetworkManager.Singleton; if (singleton != null) { return singleton.IsServer || singleton.IsHost; } return true; } } [HarmonyPatch(typeof(RoundManager), "Start")] internal static class RoundManagerStartPatch { [HarmonyPostfix] [HarmonyPriority(-800)] private static void Postfix() { RuntimeRegistry.Scan(forceLog: true); } } [HarmonyPatch(typeof(StartOfRound), "Start")] internal static class StartOfRoundStartPatch { [HarmonyPostfix] [HarmonyPriority(-800)] private static void Postfix() { RuntimeRegistry.Scan(forceLog: true); } } [HarmonyPatch(typeof(StartOfRound), "ChooseNewRandomMapSeed")] internal static class ChooseNewRandomMapSeedPatch { [HarmonyPostfix] [HarmonyPriority(-800)] private static void Postfix(StartOfRound __instance) { try { if (!PatchHelpers.IsServer() || __instance == null || __instance.currentLevel == null) { return; } RuntimeRegistry.Scan(forceLog: false); int randomMapSeed = __instance.randomMapSeed; if (SelectionManager.IsEncodedSeed(randomMapSeed)) { return; } RoundManager instance = RoundManager.Instance; if (instance != null && instance.currentLevel != null && SelectionManager.TryComputeAndStoreHostSelection(instance, randomMapSeed, out var encodedSeed, out var globalInteriorId)) { __instance.randomMapSeed = encodedSeed; if (RuntimeRegistry.TryGetInterior(globalInteriorId, out var interior)) { Plugin.Log.LogDebug((object)("[Random Interiors] Host prepared interior '" + interior.Name + "' for the current landing and synchronized it through the level seed.")); } } } catch (Exception ex) { Plugin.Log.LogError((object)("[Random Interiors] ChooseNewRandomMapSeed patch failed safely: " + ex)); } } } [HarmonyPatch(typeof(RoundManager), "GenerateNewLevelClientRpc")] internal static class GenerateNewLevelClientRpcPatch { [HarmonyPrefix] [HarmonyPriority(-800)] private static void Prefix(RoundManager __instance, ref int randomSeed, int levelID) { try { RuntimeRegistry.Scan(forceLog: false); if (!SelectionManager.IsEncodedSeed(randomSeed)) { if (PatchHelpers.IsServer() && SelectionManager.TryComputeAndStoreHostSelection(__instance, randomSeed, out var encodedSeed, out var _)) { randomSeed = encodedSeed; if ((Object)(object)StartOfRound.Instance != (Object)null) { StartOfRound.Instance.randomMapSeed = encodedSeed; } } return; } int globalId = SelectionManager.DecodeInteriorId(randomSeed); if (!RuntimeRegistry.TryGetInterior(globalId, out var interior)) { Plugin.Log.LogError((object)("[Random Interiors] Client received synchronized interior ID " + globalId + " but it is not present in the local global DungeonFlow pool. This usually means the clients do not have identical interior registrations.")); return; } if (StartOfRound.Instance != null) { StartOfRound.Instance.randomMapSeed = randomSeed; } if (!PatchHelpers.IsServer()) { Plugin.Log.LogInfo((object)("[Random Interiors] Received host interior selection: '" + interior.Name + "' (global DungeonFlow ID " + globalId + ").")); } } catch (Exception ex) { Plugin.Log.LogError((object)("[Random Interiors] GenerateNewLevelClientRpc synchronization patch failed safely: " + ex)); } } } [HarmonyPatch(typeof(RoundManager), "GenerateNewFloor")] internal static class GenerateNewFloorPatch { [HarmonyPrefix] [HarmonyPriority(-800)] private static void Prefix(RoundManager __instance, out SelectionManager.GenerationState __state) { __state = null; try { RuntimeRegistry.Scan(forceLog: false); bool isServer = PatchHelpers.IsServer(); int explicitEncodedSeed = ((StartOfRound.Instance != null) ? StartOfRound.Instance.randomMapSeed : int.MinValue); SelectionManager.TryPrepareGeneration(__instance, out __state, out var _, isServer, explicitEncodedSeed); } catch (Exception ex) { __state = null; Plugin.Log.LogError((object)("[Random Interiors] GenerateNewFloor preparation failed safely: " + ex)); } } [HarmonyFinalizer] private static Exception Finalizer(RoundManager __instance, SelectionManager.GenerationState __state, Exception __exception) { try { SelectionManager.RestoreGeneration(__state, __exception == null); } catch (Exception ex) { Plugin.Log.LogError((object)("[Random Interiors] Failed to restore the moon's original DungeonFlow list: " + ex)); } if (__exception != null) { Plugin.Log.LogError((object)("[Random Interiors] The game's GenerateNewFloor threw an exception after Random Interiors preparation: " + __exception)); } return __exception; } } [HarmonyPatch(typeof(StartOfRound), "EndOfGame")] internal static class EndOfGamePatch { [HarmonyPrefix] [HarmonyPriority(-800)] private static void Prefix(StartOfRound __instance) { try { if (PatchHelpers.IsServer() && !((Object)(object)TimeOfDay.Instance == (Object)null) && TimeOfDay.Instance.daysUntilDeadline <= 0 && TimeOfDay.Instance.quotaFulfilled < TimeOfDay.Instance.profitQuota) { if (Plugin.DontRepeatInteriors.Value && Plugin.DontRepeatEvenWhenFired.Value) { Plugin.Log.LogInfo((object)"[Random Interiors] Crew was fired, but persistent no-repeat history is enabled; keeping the current interior cycle."); } else { SelectionManager.ResetHistory("the quota deadline was missed and the crew was fired"); } } } catch (Exception ex) { Plugin.Log.LogError((object)("[Random Interiors] EndOfGame reset check failed safely: " + ex)); } } } [HarmonyPatch(typeof(GameNetworkManager), "Disconnect")] internal static class DisconnectPatch { [HarmonyPostfix] [HarmonyPriority(-800)] private static void Postfix() { try { if (Plugin.DontRepeatInteriors.Value && Plugin.DontRepeatEvenWhenFired.Value) { Plugin.Log.LogInfo((object)"[Random Interiors] Multiplayer connection disconnected; persistent no-repeat history will be reloaded from the active save when needed."); } else { SelectionManager.ResetHistory("the multiplayer connection was disconnected"); } PersistentHistoryStore.ForgetLoadedSave(); RuntimeRegistry.MarkNotReady(); } catch (Exception ex) { Plugin.Log.LogError((object)("[Random Interiors] Disconnect reset failed safely: " + ex)); } } } internal static class PersistentHistoryStore { private const string SaveKey = "Hibyhii.RandomInteriors.NoRepeatHistory.v1"; private static readonly object SyncRoot = new object(); private static string _loadedSaveFileName; private static bool _loadAttempted; private static Type _es3Type; private static MethodInfo _saveMethod; private static MethodInfo _loadMethod; private static bool _methodsResolved; internal static bool IsAvailable { get { ResolveMethods(); return _saveMethod != null && _loadMethod != null; } } internal static void Load(HashSet<string> destination) { if (destination == null) { return; } lock (SyncRoot) { string currentSaveFileName = GetCurrentSaveFileName(); if (string.IsNullOrWhiteSpace(currentSaveFileName)) { _loadedSaveFileName = null; _loadAttempted = false; } else { if (_loadAttempted && string.Equals(_loadedSaveFileName, currentSaveFileName, StringComparison.Ordinal)) { return; } _loadedSaveFileName = currentSaveFileName; _loadAttempted = true; destination.Clear(); try { ResolveMethods(); if (_loadMethod == null) { Plugin.Log.LogWarning((object)"[Random Interiors] Could not locate Easy Save's load method. Persistent no-repeat history will remain in memory for this session only."); return; } object obj = InvokeLoad(currentSaveFileName); string text = obj as string; if (string.IsNullOrEmpty(text)) { Plugin.Log.LogInfo((object)("[Random Interiors] No persistent no-repeat history exists for save '" + currentSaveFileName + "'. Starting with a fresh history.")); return; } string[] array = text.Split(new char[1] { '\n' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < array.Length; i++) { try { string text2 = Encoding.UTF8.GetString(Convert.FromBase64String(array[i])); if (!string.IsNullOrEmpty(text2)) { destination.Add(text2); } } catch { } } Plugin.Log.LogInfo((object)("[Random Interiors] Loaded " + destination.Count + " persistent no-repeat interior(s) from save '" + currentSaveFileName + "'.")); return; } catch (Exception ex) { destination.Clear(); Plugin.Log.LogWarning((object)("[Random Interiors] Failed to load persistent no-repeat history safely: " + ex.Message)); return; } } } } internal static void Save(IEnumerable<string> history) { if (history == null) { return; } lock (SyncRoot) { string currentSaveFileName = GetCurrentSaveFileName(); if (string.IsNullOrWhiteSpace(currentSaveFileName)) { return; } try { ResolveMethods(); if (_saveMethod == null) { Plugin.Log.LogWarning((object)"[Random Interiors] Could not locate Easy Save's save method. Persistent no-repeat history could not be written."); return; } List<string> list = (from x in history.Where((string x) => !string.IsNullOrEmpty(x)).Distinct<string>(StringComparer.OrdinalIgnoreCase) select Convert.ToBase64String(Encoding.UTF8.GetBytes(x))).ToList(); string serialized = string.Join("\n", list.ToArray()); InvokeSave(currentSaveFileName, serialized); _loadedSaveFileName = currentSaveFileName; _loadAttempted = true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[Random Interiors] Failed to save persistent no-repeat history safely: " + ex.Message)); } } } internal static void ForgetLoadedSave() { lock (SyncRoot) { _loadedSaveFileName = null; _loadAttempted = false; } } private static string GetCurrentSaveFileName() { try { if ((Object)(object)GameNetworkManager.Instance == (Object)null) { return null; } return GameNetworkManager.Instance.currentSaveFileName; } catch { return null; } } private static void ResolveMethods() { if (_methodsResolved) { return; } lock (SyncRoot) { if (_methodsResolved) { return; } try { _es3Type = AppDomain.CurrentDomain.GetAssemblies().Select(delegate(Assembly a) { try { return a.GetType("ES3"); } catch { return (Type)null; } }).FirstOrDefault((Type t) => t != null); if (_es3Type == null) { _methodsResolved = true; return; } MethodInfo[] methods = _es3Type.GetMethods(BindingFlags.Static | BindingFlags.Public); foreach (MethodInfo methodInfo in methods) { ParameterInfo[] parameters = methodInfo.GetParameters(); if (_saveMethod == null && string.Equals(methodInfo.Name, "Save", StringComparison.Ordinal) && methodInfo.IsGenericMethodDefinition && parameters.Length == 3 && parameters[0].ParameterType == typeof(string) && parameters[1].ParameterType.IsGenericParameter && parameters[2].ParameterType == typeof(string)) { _saveMethod = methodInfo; } if (_loadMethod == null && string.Equals(methodInfo.Name, "Load", StringComparison.Ordinal) && methodInfo.IsGenericMethodDefinition && parameters.Length == 3 && parameters[0].ParameterType == typeof(string) && parameters[1].ParameterType.IsGenericParameter && parameters[2].ParameterType == typeof(string)) { _loadMethod = methodInfo; } } } catch { _saveMethod = null; _loadMethod = null; } _methodsResolved = true; } } private static object InvokeLoad(string saveFileName) { MethodInfo methodInfo = _loadMethod.MakeGenericMethod(typeof(string)); return methodInfo.Invoke(null, new object[3] { "Hibyhii.RandomInteriors.NoRepeatHistory.v1", string.Empty, saveFileName }); } private static void InvokeSave(string saveFileName, string serialized) { MethodInfo methodInfo = _saveMethod.MakeGenericMethod(typeof(string)); methodInfo.Invoke(null, new object[3] { "Hibyhii.RandomInteriors.NoRepeatHistory.v1", serialized, saveFileName }); } } [BepInPlugin("Hibyhii.RandomInteriors", "Random Interiors", "1.0.6")] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "Hibyhii.RandomInteriors"; public const string PluginName = "Random Interiors"; public const string PluginVersion = "1.0.6"; private Harmony _harmony; internal static Plugin Instance { get; private set; } internal static ManualLogSource Log { get; private set; } internal static ConfigEntry<bool> DontRepeatInteriors { get; private set; } internal static ConfigEntry<bool> DontRepeatEvenWhenFired { get; private set; } private void Awake() { //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Expected O, but got Unknown //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Expected O, but got Unknown //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; DontRepeatInteriors = ((BaseUnityPlugin)this).Config.Bind<bool>("Global", "Do Not Repeat Interiors", true, new ConfigDescription("When enabled, a randomly selected interior is not eligible to be selected again until every other currently included interior has been seen once. By default, the no-repeat history resets when the crew is fired or the lobby is disconnected.", (AcceptableValueBase)null, new object[1] { new ConfigurationManagerAttributes { Order = 1000 } })); DontRepeatEvenWhenFired = ((BaseUnityPlugin)this).Config.Bind<bool>("Global", "Do not repeat even when fired", false, new ConfigDescription("Only applies when Do Not Repeat Interiors is enabled. When true, the no-repeat history is saved with the current save and is preserved when the crew is fired, the game is restarted, or the lobby is disconnected. A newly created save starts with a fresh history. The history also resets naturally after every currently included interior has been selected once.", (AcceptableValueBase)null, new object[1] { new ConfigurationManagerAttributes { Order = 999 } })); _harmony = new Harmony("Hibyhii.RandomInteriors"); _harmony.PatchAll(); Log.LogInfo((object)"[Random Interiors] Hibyhii's Random Interiors 1.0.6 loaded."); Log.LogInfo((object)"[Random Interiors] No LethalLevelLoader, LethalLib, or other mod dependency is required."); ((MonoBehaviour)this).StartCoroutine(InitialScanRoutine()); } private IEnumerator InitialScanRoutine() { yield return null; RuntimeRegistry.Scan(forceLog: false); for (int i = 0; i < 6; i++) { yield return null; RuntimeRegistry.Scan(forceLog: false); } RuntimeRegistry.Scan(forceLog: true); } } internal sealed class ConfigurationManagerAttributes { public int? Order; } internal static class RuntimeRegistry { internal sealed class InteriorInfo { internal int GlobalId; internal DungeonFlow Flow; internal string Name; internal string ConfigKey; internal ConfigEntry<string> Config; } internal sealed class MoonInfo { internal SelectableLevel Level; internal string Name; internal string ConfigKey; internal ConfigEntry<string> Config; internal HashSet<int> NativeInteriorIds = new HashSet<int>(); } internal const string IncludeInterior = "Include Interior In Randomization"; internal const string ExcludeInterior = "Exclude Interior From Randomization"; internal const string RandomizeMoon = "Randomize"; internal const string DontRandomizeMoon = "Don't Randomize on This Moon"; internal const string NativeOnlyMoon = "Only Randomize Native Interiors"; private static readonly char[] InvalidConfigCharacters = new char[6] { '=', '\\', '"', '\'', '[', ']' }; private static readonly object SyncRoot = new object(); private static readonly Dictionary<int, InteriorInfo> InteriorsById = new Dictionary<int, InteriorInfo>(); private static readonly Dictionary<string, MoonInfo> MoonsByKey = new Dictionary<string, MoonInfo>(StringComparer.OrdinalIgnoreCase); internal static List<InteriorInfo> Interiors { get; private set; } = new List<InteriorInfo>(); internal static List<MoonInfo> Moons { get; private set; } = new List<MoonInfo>(); internal static bool IsReady { get; private set; } internal static int ScanGeneration { get; private set; } internal static void MarkNotReady() { IsReady = false; } internal static void Scan(bool forceLog) { lock (SyncRoot) { try { RoundManager instance = RoundManager.Instance; StartOfRound instance2 = StartOfRound.Instance; if (instance == null || instance2 == null) { if (forceLog) { Plugin.Log.LogWarning((object)"[Random Interiors] Scan skipped because RoundManager or StartOfRound is not ready yet."); } return; } IndoorMapType[] dungeonFlowTypes = instance.dungeonFlowTypes; SelectableLevel[] levels = instance2.levels; if (dungeonFlowTypes == null || dungeonFlowTypes.Length == 0) { if (forceLog) { Plugin.Log.LogWarning((object)"[Random Interiors] No global DungeonFlow entries are available yet. The mod will keep trying during startup."); } return; } List<InteriorInfo> list = new List<InteriorInfo>(); Dictionary<int, InteriorInfo> dictionary = new Dictionary<int, InteriorInfo>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < dungeonFlowTypes.Length; i++) { IndoorMapType val = dungeonFlowTypes[i]; if (val == null) { continue; } DungeonFlow dungeonFlow = val.dungeonFlow; if (dungeonFlow != null) { string text = (string.IsNullOrWhiteSpace(((Object)dungeonFlow).name) ? ("Unnamed DungeonFlow " + i) : ((Object)dungeonFlow).name); string proposedKey = MakeInteriorConfigKey(text, i); proposedKey = MakeUniqueConfigKey(proposedKey, hashSet); if (!TryBindInteriorConfig(proposedKey, out var config)) { Plugin.Log.LogError((object)("[Random Interiors] Could not create the config entry for DungeonFlow ID " + i + " (" + text + "). That interior will be skipped for randomization, but discovery will continue.")); } else { InteriorInfo interiorInfo = new InteriorInfo { GlobalId = i, Flow = dungeonFlow, Name = text, ConfigKey = proposedKey, Config = config }; hashSet.Add(proposedKey); list.Add(interiorInfo); dictionary[i] = interiorInfo; } } } List<MoonInfo> list2 = new List<MoonInfo>(); Dictionary<string, MoonInfo> dictionary2 = new Dictionary<string, MoonInfo>(StringComparer.OrdinalIgnoreCase); HashSet<string> hashSet2 = new HashSet<string>(StringComparer.OrdinalIgnoreCase); if (levels != null) { for (int j = 0; j < levels.Length; j++) { SelectableLevel val2 = levels[j]; if (val2 == null) { continue; } string text2 = (string.IsNullOrWhiteSpace(val2.PlanetName) ? ("Unnamed Moon " + j) : val2.PlanetName); string sceneName = (string.IsNullOrWhiteSpace(val2.sceneName) ? "no scene name" : val2.sceneName); string proposedKey2 = MakeMoonConfigKey(text2, sceneName, j); proposedKey2 = MakeUniqueConfigKey(proposedKey2, hashSet2); if (!TryBindMoonConfig(proposedKey2, out var config2)) { Plugin.Log.LogError((object)("[Random Interiors] Could not create the config entry for moon " + text2 + " (index " + j + "). That moon will be skipped, but discovery will continue.")); continue; } MoonInfo moonInfo = new MoonInfo { Level = val2, Name = text2, ConfigKey = proposedKey2, Config = config2 }; hashSet2.Add(proposedKey2); if (val2.dungeonFlowTypes != null) { for (int k = 0; k < val2.dungeonFlowTypes.Length; k++) { IntWithRarity val3 = val2.dungeonFlowTypes[k]; if (dictionary.ContainsKey(val3.id)) { moonInfo.NativeInteriorIds.Add(val3.id); } else if (val3.id >= 0) { Plugin.Log.LogDebug((object)("[Random Interiors] Moon '" + text2 + "' references global DungeonFlow ID " + val3.id + ", but that global entry is not currently valid.")); } } } list2.Add(moonInfo); dictionary2[proposedKey2] = moonInfo; } } Interiors = list; Moons = list2; InteriorsById.Clear(); foreach (KeyValuePair<int, InteriorInfo> item in dictionary) { InteriorsById[item.Key] = item.Value; } MoonsByKey.Clear(); foreach (KeyValuePair<string, MoonInfo> item2 in dictionary2) { MoonsByKey[item2.Key] = item2.Value; } IsReady = list.Count > 0; ScanGeneration++; try { ((BaseUnityPlugin)Plugin.Instance).Config.Save(); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[Random Interiors] Config save failed during scan: " + ex.Message)); } if (forceLog || !IsReady) { Plugin.Log.LogInfo((object)("[Random Interiors] Registry scan complete: " + list.Count + " usable global interiors and " + list2.Count + " moons discovered. Scan #" + ScanGeneration + ".")); } if (forceLog) { for (int l = 0; l < list.Count; l++) { InteriorInfo interiorInfo2 = list[l]; Plugin.Log.LogInfo((object)("[Random Interiors] Interior ID " + interiorInfo2.GlobalId + ": " + interiorInfo2.Name + " -> " + ((interiorInfo2.Config == null) ? "(no config)" : interiorInfo2.Config.Value) + " | Key: " + interiorInfo2.ConfigKey)); } Plugin.Log.LogInfo((object)("[Random Interiors] Moon configuration entries registered: " + list2.Count + ".")); } } catch (Exception ex2) { IsReady = false; Plugin.Log.LogError((object)("[Random Interiors] Registry scan failed safely: " + ex2)); } } } private static bool TryBindInteriorConfig(string configKey, out ConfigEntry<string> config) { //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Expected O, but got Unknown config = null; try { config = ((BaseUnityPlugin)Plugin.Instance).Config.Bind<string>("Interiors", configKey, "Include Interior In Randomization", new ConfigDescription("Controls whether this registered DungeonFlow is eligible for Random Interiors. The entry is discovered automatically from RoundManager's live global DungeonFlow pool. Include = eligible for global randomization; Exclude = never selected by this mod.", (AcceptableValueBase)null, new object[1] { new AcceptableValueList<string>(new string[2] { "Include Interior In Randomization", "Exclude Interior From Randomization" }) })); return true; } catch (Exception ex) { Plugin.Log.LogError((object)("[Random Interiors] Interior config bind failed for key '" + configKey + "': " + ex.Message)); return false; } } private static bool TryBindMoonConfig(string configKey, out ConfigEntry<string> config) { //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Expected O, but got Unknown config = null; try { config = ((BaseUnityPlugin)Plugin.Instance).Config.Bind<string>("Moons", configKey, "Randomize", new ConfigDescription("Controls dungeon randomization for this moon. Randomize = choose from every included interior. Don't Randomize on This Moon = leave the moon's native dungeon generation untouched. Only Randomize Native Interiors = choose only among DungeonFlows already listed by this moon. The host's selection is synchronized to clients through the existing level-generation seed.", (AcceptableValueBase)null, new object[1] { new AcceptableValueList<string>(new string[3] { "Randomize", "Don't Randomize on This Moon", "Only Randomize Native Interiors" }) })); return true; } catch (Exception ex) { Plugin.Log.LogError((object)("[Random Interiors] Moon config bind failed for key '" + configKey + "': " + ex.Message)); return false; } } internal static bool TryGetInterior(int globalId, out InteriorInfo interior) { return InteriorsById.TryGetValue(globalId, out interior); } internal static MoonInfo FindMoon(SelectableLevel level) { if (level == null) { return null; } for (int i = 0; i < Moons.Count; i++) { if (Moons[i].Level == level) { return Moons[i]; } } return null; } internal static bool IsInteriorIncluded(int globalId) { if (!InteriorsById.TryGetValue(globalId, out var value) || value.Config == null) { return false; } return string.Equals(value.Config.Value, "Include Interior In Randomization", StringComparison.OrdinalIgnoreCase); } private static string MakeInteriorConfigKey(string name, int index) { return SanitizeConfigKey(name) + " - Global ID " + index; } private static string MakeMoonConfigKey(string name, string sceneName, int index) { return SanitizeConfigKey(name) + " - " + SanitizeConfigKey(sceneName) + " - Moon Index " + index; } private static string MakeUniqueConfigKey(string proposedKey, HashSet<string> usedKeys) { string text = SanitizeConfigKey(proposedKey); if (text.Length == 0) { text = "Unnamed"; } string text2 = text; int num = 2; while (usedKeys.Contains(text2)) { text2 = text + " - " + num; num++; } return text2; } private static string SanitizeConfigKey(string value) { if (string.IsNullOrWhiteSpace(value)) { return "Unnamed"; } string text = value.Trim(); for (int i = 0; i < InvalidConfigCharacters.Length; i++) { text = text.Replace(InvalidConfigCharacters[i].ToString(), " "); } text = text.Replace("\r", " ").Replace("\n", " ").Replace("\t", " "); StringBuilder stringBuilder = new StringBuilder(text.Length); foreach (char c in text) { if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == ' ' || c == '-' || c == '_' || c == '.') { stringBuilder.Append(c); } else { stringBuilder.Append('_'); } } string text2 = stringBuilder.ToString(); while (text2.Contains(" ")) { text2 = text2.Replace(" ", " "); } text2 = text2.Trim(' ', '.', '_', '-'); if (text2.Length == 0) { return "Unnamed"; } return (text2.Length > 180) ? text2.Substring(0, 180).TrimEnd(' ', '.', '_', '-') : text2; } } internal static class SelectionManager { internal sealed class PendingHostSelection { internal int SourceSeed; internal int GlobalInteriorId; internal int MoonInstanceId; } internal sealed class GenerationState { internal SelectableLevel Level; internal IntWithRarity[] OriginalChoices; internal bool ReplacedChoices; internal bool IsHostGeneration; internal string SelectedInteriorKey; } private const int MarkerBit = 31; private const int SeedBits = 19; private const int IndexBits = 12; private const int SeedMask = 524287; private const int IndexMask = 4095; private const int MaxInteriorId = 4095; private static readonly HashSet<string> SeenThisCycle = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private static PendingHostSelection _pendingHostSelection; private static int _lastProcessedSeed = int.MinValue; internal static void ResetHistory(string reason) { SeenThisCycle.Clear(); _pendingHostSelection = null; _lastProcessedSeed = int.MinValue; if (ShouldPersistHistory()) { PersistentHistoryStore.Save(SeenThisCycle); } Plugin.Log.LogInfo((object)("[Random Interiors] Interior no-repeat history reset: " + reason)); } internal static bool IsEncodedSeed(int seed) { return (seed & int.MinValue) != 0; } internal static int EncodeSeed(int sourceSeed, int globalInteriorId) { return int.MinValue | ((globalInteriorId & 0xFFF) << 19) | (sourceSeed & 0x7FFFF); } internal static int DecodeInteriorId(int encodedSeed) { return (encodedSeed >>> 19) & 0xFFF; } internal static bool TryGetPendingHostSelection(int sourceSeed, int moonInstanceId, out int globalInteriorId) { globalInteriorId = -1; if (_pendingHostSelection == null) { return false; } if (_pendingHostSelection.SourceSeed != sourceSeed || _pendingHostSelection.MoonInstanceId != moonInstanceId) { return false; } globalInteriorId = _pendingHostSelection.GlobalInteriorId; return true; } internal static bool TryComputeAndStoreHostSelection(RoundManager roundManager, int sourceSeed, out int encodedSeed, out int globalInteriorId) { encodedSeed = sourceSeed; globalInteriorId = -1; if (roundManager == null || roundManager.currentLevel == null) { return false; } EnsurePersistenceLoaded(); RuntimeRegistry.Scan(forceLog: false); RuntimeRegistry.MoonInfo moonInfo = RuntimeRegistry.FindMoon(roundManager.currentLevel); if (moonInfo == null) { Plugin.Log.LogWarning((object)"[Random Interiors] Current moon was not present in the scanned moon registry. Native generation will be preserved."); return false; } string text = ((moonInfo.Config == null) ? "Randomize" : moonInfo.Config.Value); if (string.Equals(text, "Don't Randomize on This Moon", StringComparison.OrdinalIgnoreCase)) { Plugin.Log.LogInfo((object)("[Random Interiors] Moon '" + moonInfo.Name + "' is configured as 'Don't Randomize on This Moon'; native interior generation will be used.")); return false; } List<RuntimeRegistry.InteriorInfo> list = BuildCandidates(text, moonInfo); if (list.Count == 0) { if (string.Equals(text, "Only Randomize Native Interiors", StringComparison.OrdinalIgnoreCase)) { Plugin.Log.LogWarning((object)("[Random Interiors] Moon '" + moonInfo.Name + "' is set to native-only, but none of its native interiors are currently included. Native generation will be preserved for safety.")); } else { Plugin.Log.LogWarning((object)"[Random Interiors] No interiors are currently included in global randomization. Native generation will be preserved for safety."); } return false; } if (list.Count > 4096) { Plugin.Log.LogError((object)("[Random Interiors] The global interior registry contains more than " + 4096 + " entries. The seed synchronization format cannot safely encode that many entries; native generation will be preserved.")); return false; } List<RuntimeRegistry.InteriorInfo> list2 = list; if (Plugin.DontRepeatInteriors.Value) { PruneSeen(list); List<RuntimeRegistry.InteriorInfo> list3 = list.Where((RuntimeRegistry.InteriorInfo info) => !SeenThisCycle.Contains(GetInteriorKey(info))).ToList(); if (list3.Count == 0) { SeenThisCycle.Clear(); list3 = list; Plugin.Log.LogInfo((object)"[Random Interiors] Every currently included interior has been seen. Starting a new no-repeat cycle."); } list2 = list3; } Random random = new Random(sourceSeed ^ StableStringHash(moonInfo.Name)); RuntimeRegistry.InteriorInfo interiorInfo = list2[random.Next(list2.Count)]; Plugin.Log.LogInfo((object)("[Random Interiors] Moon '" + moonInfo.Name + "' has " + list.Count + " eligible interiors (" + list2.Count + " currently selectable after no-repeat filtering). Chosen: '" + interiorInfo.Name + "' [ID " + interiorInfo.GlobalId + "]. Source seed: " + sourceSeed + ".")); globalInteriorId = interiorInfo.GlobalId; encodedSeed = EncodeSeed(sourceSeed, globalInteriorId); _pendingHostSelection = new PendingHostSelection { SourceSeed = sourceSeed, GlobalInteriorId = globalInteriorId, MoonInstanceId = ((Object)roundManager.currentLevel).GetInstanceID() }; return true; } internal static bool TryPrepareGeneration(RoundManager roundManager, out GenerationState state, out int globalInteriorId, bool isServer, int explicitEncodedSeed = int.MinValue) { //IL_0293: Unknown result type (might be due to invalid IL or missing references) //IL_0299: Expected O, but got Unknown state = null; globalInteriorId = -1; if (roundManager == null || roundManager.currentLevel == null) { return false; } EnsurePersistenceLoaded(); int instanceID = ((Object)roundManager.currentLevel).GetInstanceID(); int num = ((explicitEncodedSeed == int.MinValue) ? (((Object)(object)StartOfRound.Instance != (Object)null) ? StartOfRound.Instance.randomMapSeed : 0) : explicitEncodedSeed); bool flag = IsEncodedSeed(num); int globalInteriorId2 = -1; if (flag) { globalInteriorId2 = DecodeInteriorId(num); } else if (isServer && TryGetPendingHostSelection(num, instanceID, out globalInteriorId2)) { flag = true; int num2 = EncodeSeed(num, globalInteriorId2); if ((Object)(object)StartOfRound.Instance != (Object)null) { StartOfRound.Instance.randomMapSeed = num2; } num = num2; } else { if (!isServer) { Plugin.Log.LogError((object)"[Random Interiors] Client reached dungeon generation without a synchronized host interior selection. Native generation is being preserved to avoid guessing and making the desync worse."); return false; } if (!TryComputeAndStoreHostSelection(roundManager, num, out var encodedSeed, out globalInteriorId2)) { return false; } flag = true; num = encodedSeed; if ((Object)(object)StartOfRound.Instance != (Object)null) { StartOfRound.Instance.randomMapSeed = encodedSeed; } } if (!RuntimeRegistry.TryGetInterior(globalInteriorId2, out var interior) || interior.Flow == null) { Plugin.Log.LogError((object)("[Random Interiors] Synchronized interior ID " + globalInteriorId2 + " could not be resolved in this client's global DungeonFlow pool. Native generation will be preserved.")); return false; } RuntimeRegistry.MoonInfo moonInfo = RuntimeRegistry.FindMoon(roundManager.currentLevel); if (moonInfo == null) { Plugin.Log.LogWarning((object)"[Random Interiors] Current moon was not found during floor preparation. Native generation will be preserved."); return false; } if (moonInfo.Config == null || string.Equals(moonInfo.Config.Value, "Don't Randomize on This Moon", StringComparison.OrdinalIgnoreCase)) { Plugin.Log.LogInfo((object)("[Random Interiors] Moon '" + moonInfo.Name + "' is not randomized; its native interior selection will be used.")); return false; } IntWithRarity[] dungeonFlowTypes = roundManager.currentLevel.dungeonFlowTypes; if (dungeonFlowTypes == null || dungeonFlowTypes.Length == 0) { Plugin.Log.LogWarning((object)("[Random Interiors] Current moon '" + moonInfo.Name + "' has no DungeonFlow choices for the base generator to consume. Native generation will be preserved.")); return false; } state = new GenerationState { Level = roundManager.currentLevel, OriginalChoices = dungeonFlowTypes, IsHostGeneration = isServer, ReplacedChoices = true, SelectedInteriorKey = GetInteriorKey(interior) }; roundManager.currentLevel.dungeonFlowTypes = (IntWithRarity[])(object)new IntWithRarity[1] { new IntWithRarity(globalInteriorId2, 100, (LevelAmbienceLibrary)null) }; if (_lastProcessedSeed != num || isServer) { Plugin.Log.LogInfo((object)("[Random Interiors] Landing on '" + moonInfo.Name + "': forcing interior '" + interior.Name + "' (global DungeonFlow ID " + globalInteriorId2 + ").")); _lastProcessedSeed = num; } return true; } internal static void RestoreGeneration(GenerationState state, bool generationSucceeded) { if (state == null || !state.ReplacedChoices || (Object)(object)state.Level == (Object)null) { return; } state.Level.dungeonFlowTypes = state.OriginalChoices; if (generationSucceeded && state.IsHostGeneration && Plugin.DontRepeatInteriors.Value && !string.IsNullOrEmpty(state.SelectedInteriorKey)) { SeenThisCycle.Add(state.SelectedInteriorKey); if (Plugin.DontRepeatEvenWhenFired.Value) { PersistentHistoryStore.Save(SeenThisCycle); } } } private static void EnsurePersistenceLoaded() { if (Plugin.DontRepeatInteriors.Value && Plugin.DontRepeatEvenWhenFired.Value) { PersistentHistoryStore.Load(SeenThisCycle); } } private static bool ShouldPersistHistory() { return Plugin.DontRepeatInteriors.Value && Plugin.DontRepeatEvenWhenFired.Value; } private static List<RuntimeRegistry.InteriorInfo> BuildCandidates(string moonMode, RuntimeRegistry.MoonInfo moon) { List<RuntimeRegistry.InteriorInfo> list = new List<RuntimeRegistry.InteriorInfo>(); for (int i = 0; i < RuntimeRegistry.Interiors.Count; i++) { RuntimeRegistry.InteriorInfo interiorInfo = RuntimeRegistry.Interiors[i]; if (RuntimeRegistry.IsInteriorIncluded(interiorInfo.GlobalId) && (!string.Equals(moonMode, "Only Randomize Native Interiors", StringComparison.OrdinalIgnoreCase) || moon.NativeInteriorIds.Contains(interiorInfo.GlobalId))) { list.Add(interiorInfo); } } return list; } private static void PruneSeen(List<RuntimeRegistry.InteriorInfo> activeCandidates) { HashSet<string> activeKeys = new HashSet<string>(activeCandidates.Select(GetInteriorKey), StringComparer.OrdinalIgnoreCase); SeenThisCycle.RemoveWhere((string key) => !activeKeys.Contains(key)); } private static string GetInteriorKey(RuntimeRegistry.InteriorInfo interior) { return interior.GlobalId + "|" + interior.Name; } private static int StableStringHash(string text) { int num = 23; if (text == null) { return num; } for (int i = 0; i < text.Length; i++) { num = num * 31 + text[i]; } return num; } }