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 WorldStageDirector v0.5.3
WorldStageDirector.dll
Decompiled 2 weeks ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Runtime.Serialization.Json; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using JG224.ModCore.API; using Jotunn.Utils; using Microsoft.CodeAnalysis; using TMPro; using UnityEngine; using WorldStageDirector.Bosses; using WorldStageDirector.Commands; using WorldStageDirector.Config; using WorldStageDirector.Core; using WorldStageDirector.Networking; using WorldStageDirector.Progression; using WorldStageDirector.Spawning; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("jg224")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Server-side progression control. Protects players from content beyond their progress and provides expanded boss-stone progression management.")] [assembly: AssemblyFileVersion("0.5.3.0")] [assembly: AssemblyInformationalVersion("0.5.3+021621d422ae9d27eccba527722890746c0a2a66")] [assembly: AssemblyProduct("WorldStageDirector")] [assembly: AssemblyTitle("WorldStageDirector")] [assembly: AssemblyVersion("0.5.3.0")] [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 WorldStageDirector { [BepInPlugin("jg224.worldstagedirector", "WorldStageDirector", "0.5.3")] [BepInDependency("com.jg224.modcore", "0.5.0")] [BepInDependency("com.jotunn.jotunn", "2.30.0")] [BepInIncompatibility("ZenDragon.ZenBossStone")] [BepInIncompatibility("jg224.progressguard")] [NetworkCompatibility(/*Could not decode attribute arguments.*/)] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "jg224.worldstagedirector"; internal const string LegacyPluginGuid = "jg224.progressguard"; public const string PluginName = "WorldStageDirector"; public const string PluginVersion = "0.5.3"; public const string ModCoreGuid = "com.jg224.modcore"; public const string JotunnGuid = "com.jotunn.jotunn"; public static readonly ModuleId ModuleId = new ModuleId("worldstagedirector"); private Harmony _harmony; private readonly List<IDisposable> _registrations = new List<IDisposable>(); private bool _shutDown; internal static ManualLogSource Log { get; private set; } internal static ICoreServices Core { get; private set; } internal static bool IsActive { get; private set; } private void Awake() { //IL_005d: 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_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Expected O, but got Unknown //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00e0: Unknown result type (might be due to invalid IL or missing references) //IL_0157: Unknown result type (might be due to invalid IL or missing references) //IL_018f: Unknown result type (might be due to invalid IL or missing references) //IL_0199: Expected O, but got Unknown //IL_01c8: Unknown result type (might be due to invalid IL or missing references) //IL_021c: Unknown result type (might be due to invalid IL or missing references) IsActive = false; _shutDown = false; Log = ((BaseUnityPlugin)this).Logger; try { if (!ModCoreApi.IsAvailable) { throw new InvalidOperationException("ModCore did not initialize before WorldStageDirector."); } Core = ModCoreApi.Services; SemanticVersion val = default(SemanticVersion); if (!SemanticVersion.TryParse("0.5.3", ref val)) { throw new InvalidOperationException("WorldStageDirector has invalid release version metadata."); } _registrations.Add(Core.Modules.Register(new ModuleDescriptor(ModuleId, "jg224.worldstagedirector", "WorldStageDirector", val, 9, (ModuleSide)3, (ModuleRequirement)4, 0uL, 1, 1))); _registrations.Add(Core.Namespaces.Register(ModuleId, (NamespaceKind)1, "jg224.worldstagedirector.", 1, Array.Empty<string>())); _registrations.Add(Core.Namespaces.Register(ModuleId, (NamespaceKind)6, "jg224.worldstagedirector.", 1, Array.Empty<string>())); _registrations.Add(RoutedRpcIngress.Register(ModuleId, new string[10] { "jg224.worldstagedirector.SyncState", "jg224.worldstagedirector.ImportLegacyProgression", "jg224.worldstagedirector.ImportLegacyProgressionAck", "jg224.worldstagedirector.BossDeath", "jg224.worldstagedirector.BossDamage", "jg224.worldstagedirector.BossPlayerNotice", "jg224.worldstagedirector.BossSacrificeRequest", "jg224.worldstagedirector.BossSacrificeResult", "jg224.worldstagedirector.BossSacrificeEvent", "jg224.worldstagedirector.BossOfferingReconcile" })); _registrations.Add(Core.Namespaces.Register(ModuleId, (NamespaceKind)4, "WorldStageDirector", 1, Array.Empty<string>())); LegacyIdentityMigration.TryMigrateConfig(((BaseUnityPlugin)this).Config); ModConfig.Bind(((BaseUnityPlugin)this).Config); _harmony = new Harmony("jg224.worldstagedirector"); _harmony.PatchAll(typeof(Plugin).Assembly); AdminCommands.Register(); IsActive = true; Core.Modules.SetState(ModuleId, (ModuleRuntimeState)2, "Required server-authoritative progression protocol ready."); Log.LogInfo((object)"WorldStageDirector v0.5.3 loaded. Required on server and every client. Boss deaths now advance the world only after an eligible trophy sacrifice."); } catch (Exception ex) { Log.LogError((object)("WorldStageDirector failed to initialize safely: " + ex)); if (Core != null) { Core.Modules.SetState(ModuleId, (ModuleRuntimeState)5, ex.Message); } Shutdown(); throw; } } private void OnDestroy() { Shutdown(); } private void Update() { if (IsActive) { LocalOfferingRecovery.Tick(); } } private void Shutdown() { //IL_00c8: Unknown result type (might be due to invalid IL or missing references) if (_shutDown) { return; } _shutDown = true; IsActive = false; try { ProgressionRegistry.Save(); } catch (Exception ex) { ManualLogSource log = Log; if (log != null) { log.LogError((object)("Shutdown save failed; retained durable progression: " + ex.Message)); } } ProgressionSync.Shutdown(); Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } _harmony = null; for (int num = _registrations.Count - 1; num >= 0; num--) { try { _registrations[num].Dispose(); } catch (Exception ex2) { ManualLogSource log2 = Log; if (log2 != null) { log2.LogWarning((object)("Registration cleanup failed: " + ex2.Message)); } } } _registrations.Clear(); if (Core != null) { Core.Metrics.RemoveOwner(ModuleId); } Core = null; } internal static void Debug(string message) { ConfigEntry<bool> verboseLogging = ModConfig.VerboseLogging; if (verboseLogging != null && verboseLogging.Value) { ManualLogSource log = Log; if (log != null) { log.LogInfo((object)("[debug] " + message)); } } } } } namespace WorldStageDirector.Spawning { [HarmonyPatch(typeof(MonsterAI), "Start")] internal static class ExistingCreatureSafetyPatch { [HarmonyPostfix] private static void Postfix(MonsterAI __instance) { TryRemove(__instance); } internal static void SweepLoaded() { BaseAI[] array = BaseAI.GetAllInstances().ToArray(); foreach (BaseAI obj in array) { MonsterAI val = (MonsterAI)(object)((obj is MonsterAI) ? obj : null); if (val != null) { TryRemove(val); } } } private static void TryRemove(MonsterAI monster) { //IL_0052: Unknown result type (might be due to invalid IL or missing references) try { ZNetView val = ((monster != null) ? ((Component)monster).GetComponent<ZNetView>() : null); if (!((Object)(object)val == (Object)null) && val.IsValid() && val.IsOwner() && RuntimeSpawnValidator.ShouldRemoveExisting(monster)) { Plugin.Log.LogWarning((object)("Removing out-of-progression existing creature " + $"'{((Object)((Component)monster).gameObject).name}' at {((Component)monster).transform.position}.")); val.Destroy(); } } catch (Exception arg) { Plugin.Log.LogError((object)$"Existing-creature safety check failed: {arg}"); } } } [HarmonyPatch(typeof(RandEventSystem), "SetRandomEvent", new Type[] { typeof(RandomEvent), typeof(Vector3) })] internal static class RaidProgressionSafetyPatch { [HarmonyPrefix] private static void Prefix(ref RandomEvent ev, Vector3 pos) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) if (ev != null && !ShouldAllow(ev, pos, out var _, out var _)) { RaidRuleRegistry.TryGetRequiredTier(ev.m_name, ev.m_requiredGlobalKeys, out var tier); ProgressionTier onlineSafetyTier = ProgressionSync.GetOnlineSafetyTier(); Plugin.Log.LogWarning((object)("Blocked random raid '" + ev.m_name + "': least-progressed online tier " + $"{onlineSafetyTier}, required {tier}.")); ev = null; } } internal static void EnforceActiveEvent() { //IL_0033: Unknown result type (might be due to invalid IL or missing references) ZNet instance = ZNet.instance; RandEventSystem instance2 = RandEventSystem.instance; if (!((Object)(object)instance == (Object)null) && instance.IsServer() && !((Object)(object)instance2 == (Object)null)) { RandomEvent currentRandomEvent = instance2.GetCurrentRandomEvent(); if (currentRandomEvent != null && !ShouldAllow(currentRandomEvent, currentRandomEvent.m_pos, out var requiredTier, out var onlineTier)) { Plugin.Log.LogWarning((object)("Stopped random raid '" + currentRandomEvent.m_name + "' after online progression changed: " + $"least-progressed online tier {onlineTier}, required {requiredTier}.")); instance2.ResetRandomEvent(); } } } private static bool ShouldAllow(RandomEvent ev, Vector3 eventPosition, out ProgressionTier requiredTier, out ProgressionTier onlineTier) { //IL_0043: Unknown result type (might be due to invalid IL or missing references) requiredTier = ProgressionTier.None; onlineTier = ProgressionTier.None; ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer() || ev == null || !ev.m_random) { return true; } GateSettings gateSettings = ModConfig.Snapshot(); if (!gateSettings.Enabled || !gateSettings.LimitRaidsToOnlineProgression) { return true; } if (IsAshlandsTormentSpawnerRaid(ev, eventPosition)) { return true; } if (!RaidRuleRegistry.TryGetRequiredTier(ev.m_name, ev.m_requiredGlobalKeys, out requiredTier)) { Plugin.Debug("Random raid '" + ev.m_name + "' has no managed boss-tier rule; vanilla eligibility remains authoritative."); return true; } onlineTier = ProgressionSync.GetOnlineSafetyTier(); return onlineTier >= requiredTier; } private static bool IsAshlandsTormentSpawnerRaid(RandomEvent ev, Vector3 eventPosition) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) if (RuntimeSpawnValidator.TryGetBiome(eventPosition, out var biome)) { return RaidRuleRegistry.IsProgressionExempt(ev.m_name, biome); } return false; } } internal static class RuntimeSpawnValidator { private const string SkaldHallEnemyTagKey = "arenaguard.enemy_tag"; private const string SkaldHallArenaIdKey = "arenaguard.arena_id"; private const string SkaldHallSessionIdKey = "arenaguard.session_id"; private const string SkaldHallEnemyIdKey = "arenaguard.enemy_id"; internal static bool ShouldAllow(SpawnData spawn, Vector3 spawnPoint, bool eventSpawner) { //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) if (!HasAuthoritativeState()) { return true; } if (spawn == null || (Object)(object)spawn.m_prefab == (Object)null) { return true; } if (!SpawnRuleRegistry.TryGet(spawn.m_requiredGlobalKey, ((Object)spawn.m_prefab).name, out var rule)) { return true; } GateSettings effectiveSettings = GetEffectiveSettings(); if (!effectiveSettings.Enabled || eventSpawner || !SpawnRuleRegistry.IsGateEnabled(rule, effectiveSettings)) { return true; } NativeBiome biome; bool flag = TryGetBiome(spawnPoint, out biome); bool inNativeBiome = flag && rule.NativeBiome != NativeBiome.Unknown && (rule.NativeBiome & biome) != 0; List<ProgressionTier> relevantTiers = GetRelevantTiers(spawnPoint, effectiveSettings); SpawnDecision spawnDecision = SpawnDecisionEngine.Evaluate(rule, effectiveSettings, eventSpawner: false, inNativeBiome, relevantTiers); if (spawnDecision.Block) { if (effectiveSettings.LogBlockedSpawns) { Plugin.Log.LogInfo((object)("Blocked ambient " + ((Object)spawn.m_prefab).name + " at " + (flag ? biome.ToString() : "unresolved biome") + ": " + $"effective safety tier {spawnDecision.EffectiveTier}, required {rule.RequiredTier}.")); } return false; } if (spawnDecision.Reason == SpawnDecisionReason.AllowedProgression && effectiveSettings.LogAllowedProgressionSpawns) { Plugin.Log.LogInfo((object)($"Allowed ambient {((Object)spawn.m_prefab).name} at {biome}: " + $"effective safety tier {spawnDecision.EffectiveTier}, required {rule.RequiredTier}.")); } return true; } internal static bool ShouldRemoveExisting(MonsterAI monster) { //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) if (!HasAuthoritativeState()) { return false; } if ((Object)(object)monster == (Object)null || monster.IsEventCreature()) { return false; } if (IsSkaldHallArenaEnemy(monster)) { return false; } Character component = ((Component)monster).GetComponent<Character>(); if ((Object)(object)component == (Object)null || component.IsBoss() || component.IsTamed()) { return false; } if (!SpawnRuleRegistry.TryGetCreatureSafetyRule(((Object)((Component)monster).gameObject).name, out var rule)) { return false; } GateSettings effectiveSettings = GetEffectiveSettings(); if (!effectiveSettings.Enabled || !SpawnRuleRegistry.IsGateEnabled(rule, effectiveSettings)) { return false; } NativeBiome biome; bool inNativeBiome = TryGetBiome(((Component)monster).transform.position, out biome) && (rule.NativeBiome & biome) != 0; List<ProgressionTier> relevantTiers = GetRelevantTiers(((Component)monster).transform.position, effectiveSettings); return SpawnDecisionEngine.Evaluate(rule, effectiveSettings, eventSpawner: false, inNativeBiome, relevantTiers).Block; } private static bool IsSkaldHallArenaEnemy(MonsterAI monster) { ZNetView val = (((Object)(object)monster == (Object)null) ? null : ((Component)monster).GetComponent<ZNetView>()); if ((Object)(object)val == (Object)null || !val.IsValid()) { return false; } ZDO zDO = val.GetZDO(); if (zDO != null) { return ArenaEnemyExemptionPolicy.IsSkaldHallArenaEnemy(zDO.GetInt("arenaguard.enemy_tag", 0), zDO.GetString("arenaguard.arena_id", string.Empty), zDO.GetString("arenaguard.session_id", string.Empty), zDO.GetString("arenaguard.enemy_id", string.Empty)); } return false; } internal static bool HasAuthoritativeState() { ZNet instance = ZNet.instance; bool num = (Object)(object)instance != (Object)null; bool isServer = num && instance.IsServer(); return AuthoritativeStatePolicy.CanEnforce(num, isServer, ClientProgressionState.HasServerSnapshot); } private static GateSettings GetEffectiveSettings() { ZNet instance = ZNet.instance; if (!((Object)(object)instance != (Object)null) || !instance.IsServer()) { return ClientProgressionState.GetSettings(); } return ModConfig.Snapshot(); } private static List<ProgressionTier> GetRelevantTiers(Vector3 spawnPoint, GateSettings settings) { //IL_0039: Unknown result type (might be due to invalid IL or missing references) if (settings.ProtectLeastProgressedOnline) { ZNet instance = ZNet.instance; ProgressionTier item = (((Object)(object)instance != (Object)null && instance.IsServer()) ? ProgressionSync.GetOnlineSafetyTier() : ClientProgressionState.OnlineSafetyTier); return new List<ProgressionTier> { item }; } return GetNearbyTiers(spawnPoint, settings.NearbyPlayerRadius); } private static List<ProgressionTier> GetNearbyTiers(Vector3 spawnPoint, float radius) { //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) List<ProgressionTier> list = new List<ProgressionTier>(); float num = radius * radius; ZNet instance = ZNet.instance; bool flag = (Object)(object)instance != (Object)null && instance.IsServer(); foreach (Player allPlayer in Player.GetAllPlayers()) { if (!((Object)(object)allPlayer == (Object)null)) { Vector3 val = ((Component)allPlayer).transform.position - spawnPoint; if (!(val.x * val.x + val.z * val.z > num)) { long playerID = allPlayer.GetPlayerID(); ProgressionTier item = (flag ? ProgressionRegistry.GetTier(playerID) : ClientProgressionState.GetTier(playerID)); list.Add(item); } } } return list; } internal static bool TryGetBiome(Vector3 spawnPoint, out NativeBiome biome) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) biome = NativeBiome.Unknown; ZoneSystem instance = ZoneSystem.instance; if ((Object)(object)instance != (Object)null) { try { Vector3 val = spawnPoint; Vector3 val2 = default(Vector3); Biome biome2 = default(Biome); BiomeArea val3 = default(BiomeArea); Heightmap val4 = default(Heightmap); instance.GetGroundData(ref val, ref val2, ref biome2, ref val3, ref val4); biome = ConvertBiome(biome2); if (biome != NativeBiome.Unknown) { return true; } } catch (Exception ex) { Plugin.Debug("Loaded-zone biome resolution failed: " + ex.Message); } } WorldGenerator instance2 = WorldGenerator.instance; if (instance2 == null) { return false; } try { biome = ConvertBiome(instance2.GetBiome(spawnPoint)); return biome != NativeBiome.Unknown; } catch (Exception ex2) { Plugin.Debug("World biome resolution failed: " + ex2.Message); return false; } } private static NativeBiome ConvertBiome(Biome biome) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) NativeBiome nativeBiome = NativeBiome.Unknown; if ((biome & 1) != 0) { nativeBiome |= NativeBiome.Meadows; } if ((biome & 8) != 0) { nativeBiome |= NativeBiome.BlackForest; } if ((biome & 2) != 0) { nativeBiome |= NativeBiome.Swamp; } if ((biome & 4) != 0) { nativeBiome |= NativeBiome.Mountain; } if ((biome & 0x10) != 0) { nativeBiome |= NativeBiome.Plains; } if ((biome & 0x200) != 0) { nativeBiome |= NativeBiome.Mistlands; } if ((biome & 0x20) != 0) { nativeBiome |= NativeBiome.Ashlands; } return nativeBiome; } } [HarmonyPatch(typeof(SpawnSystem), "Spawn", new Type[] { typeof(SpawnData), typeof(Vector3), typeof(bool) })] internal static class AmbientSpawnPatch { [HarmonyPrefix] private static bool Prefix(SpawnData critter, Vector3 spawnPoint, bool eventSpawner) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) if (!TormentSpawnerSafety.ShouldAllowSpawn(critter, spawnPoint)) { return false; } if (eventSpawner) { return true; } return RuntimeSpawnValidator.ShouldAllow(critter, spawnPoint, eventSpawner: false); } } [HarmonyPatch(typeof(SpawnSystem), "Awake")] internal static class SpawnRuleDiscoveryPatch { private static readonly HashSet<string> Seen = new HashSet<string>(StringComparer.Ordinal); [HarmonyPostfix] private static void Postfix(SpawnSystem __instance) { //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Expected I4, but got Unknown if (__instance?.m_spawnLists == null) { return; } foreach (SpawnSystemList spawnList in __instance.m_spawnLists) { if (spawnList?.m_spawners == null) { continue; } foreach (SpawnData spawner in spawnList.m_spawners) { if ((Object)(object)spawner?.m_prefab == (Object)null || string.IsNullOrEmpty(spawner.m_requiredGlobalKey)) { continue; } string text = spawner.m_requiredGlobalKey + "|" + ((Object)spawner.m_prefab).name + "|" + (int)spawner.m_biome; if (Seen.Add(text)) { if (SpawnRuleRegistry.TryGet(spawner.m_requiredGlobalKey, ((Object)spawner.m_prefab).name, out var rule)) { Plugin.Debug($"Discovered managed spawn rule {text} => {rule.RequiredTier}, native {rule.NativeBiome}."); } else if (spawner.m_requiredGlobalKey.StartsWith("defeated_", StringComparison.OrdinalIgnoreCase)) { Plugin.Debug("Discovered unmanaged defeated-key spawn rule " + text + "; it remains fail-open."); } } } } } } internal static class TormentSpawnerSafety { private const string EventPrefab = "spawnercharredstoneevent"; internal static bool ShouldAllowSpawn(SpawnData spawn, Vector3 spawnPoint) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)spawn?.m_prefab == (Object)null || !IsEventTormentSpawner(((Object)spawn.m_prefab).name)) { return true; } if (!TryGetBlockReason(spawnPoint, out var reason)) { return true; } if (GetEffectiveSettings().LogBlockedSpawns) { Plugin.Log.LogInfo((object)$"Blocked event Monument of Torment at {spawnPoint}: {reason}."); } return false; } internal static bool IsEventTormentSpawner(string prefabName) { return string.Equals(SpawnRuleRegistry.NormalizePrefab(prefabName), "spawnercharredstoneevent", StringComparison.Ordinal); } internal static bool TryGetBlockReason(Vector3 position, out string reason) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) reason = string.Empty; if (!RuntimeSpawnValidator.HasAuthoritativeState()) { return false; } GateSettings effectiveSettings = GetEffectiveSettings(); if (!effectiveSettings.Enabled) { return false; } if (effectiveSettings.BlockTormentSpawnersOutsideAshlands) { NativeBiome biome; bool flag = RuntimeSpawnValidator.TryGetBiome(position, out biome); if (!flag || (biome & NativeBiome.Ashlands) == 0) { reason = (flag ? $"biome is {biome}, not Ashlands" : "biome could not be proven to be Ashlands"); return true; } } return false; } private static GateSettings GetEffectiveSettings() { ZNet instance = ZNet.instance; if (!((Object)(object)instance != (Object)null) || !instance.IsServer()) { return ClientProgressionState.GetSettings(); } return ModConfig.Snapshot(); } } [HarmonyPatch(typeof(SpawnArea), "Awake")] internal static class ExistingTormentSpawnerSafetyPatch { [HarmonyPostfix] private static void Postfix(SpawnArea __instance) { TryRemove(__instance); } internal static void SweepLoaded() { SpawnArea[] array = Resources.FindObjectsOfTypeAll<SpawnArea>(); for (int i = 0; i < array.Length; i++) { TryRemove(array[i]); } } private static void TryRemove(SpawnArea spawnArea) { //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) try { if ((Object)(object)spawnArea == (Object)null) { return; } ZNetView val = ((Component)spawnArea).GetComponent<ZNetView>() ?? ((Component)spawnArea).GetComponentInParent<ZNetView>(); if (!((Object)(object)val == (Object)null) && val.IsValid() && val.IsOwner()) { string text = ResolvePrefabName(val, ((Object)((Component)spawnArea).gameObject).name); if (TormentSpawnerSafety.IsEventTormentSpawner(text) && TormentSpawnerSafety.TryGetBlockReason(((Component)spawnArea).transform.position, out var reason)) { Plugin.Log.LogWarning((object)("Removing event Monument of Torment '" + text + "' at " + $"{((Component)spawnArea).transform.position}: {reason}.")); val.Destroy(); } } } catch (Exception arg) { Plugin.Log.LogError((object)$"Existing Monument of Torment safety check failed: {arg}"); } } private static string ResolvePrefabName(ZNetView networkView, string fallback) { ZDO zDO = networkView.GetZDO(); object obj; if (zDO != null) { ZNetScene instance = ZNetScene.instance; obj = ((instance != null) ? instance.GetPrefab(zDO.GetPrefab()) : null); } else { obj = null; } return ((obj != null) ? ((Object)obj).name : null) ?? ((Object)((Component)networkView).gameObject).name ?? fallback; } } } namespace WorldStageDirector.Progression { public sealed class PlayerProgressRecord { public long PlayerId { get; } public string PlayerName { get; } public ProgressionTier Tier { get; } public BossCreditFlags KillReceipts { get; } public BossCreditFlags BossCredits { get; } public PlayerProgressRecord(long playerId, string playerName, ProgressionTier tier) : this(playerId, playerName, tier, BossCreditFlags.None, BossDefinitionRegistry.AllThrough(tier)) { } public PlayerProgressRecord(long playerId, string playerName, ProgressionTier tier, BossCreditFlags killReceipts, BossCreditFlags bossCredits) { PlayerId = playerId; PlayerName = playerName ?? string.Empty; Tier = tier; KillReceipts = killReceipts; BossCredits = bossCredits; } } internal static class ProgressionPersistence { [DataContract] private sealed class RegistryFile { [DataMember(Name = "schemaVersion", Order = 1)] public int SchemaVersion; [DataMember(Name = "worldUid", Order = 2)] public long WorldUid; [DataMember(Name = "players", Order = 3)] public List<PlayerRecord> Players = new List<PlayerRecord>(); [DataMember(Name = "offerings", Order = 4)] public List<OfferingRecord> Offerings = new List<OfferingRecord>(); } [DataContract] private sealed class PlayerRecord { [DataMember(Name = "playerId", Order = 1)] public long PlayerId; [DataMember(Name = "name", Order = 2)] public string PlayerName = string.Empty; [DataMember(Name = "tier", Order = 3)] public int Tier; [DataMember(Name = "killReceipts", Order = 4)] public int KillReceipts; [DataMember(Name = "bossCredits", Order = 5)] public int BossCredits; } private const int SchemaVersion = 3; internal static List<OfferingRecord> LoadedOfferings = new List<OfferingRecord>(); internal static bool RecoveryRequired { get; private set; } internal static List<PlayerProgressRecord> Load(long worldUid) { //IL_00c8: Unknown result type (might be due to invalid IL or missing references) RecoveryRequired = false; LoadedOfferings = new List<OfferingRecord>(); string text = GetPath(worldUid); string legacyPath = GetLegacyPath(worldUid); if (IdentityPaths.ShouldMigrate(File.Exists(text), File.Exists(legacyPath))) { try { File.Copy(legacyPath, text, overwrite: false); Plugin.Log.LogInfo((object)("Copied legacy progression registry from '" + legacyPath + "' to '" + text + "'.")); } catch (Exception ex) { text = legacyPath; Plugin.Log.LogWarning((object)("Could not copy legacy progression registry to its new filename. Loading the preserved legacy file instead. " + ex.Message)); } } if (!File.Exists(text)) { return new List<PlayerProgressRecord>(); } try { using FileStream fileStream = File.OpenRead(text); if (fileStream.Length > 33554432) { throw new SerializationException("Registry exceeds 32 MiB."); } RegistryFile registryFile = (RegistryFile)((XmlObjectSerializer)new DataContractJsonSerializer(typeof(RegistryFile))).ReadObject((Stream)fileStream); if (registryFile == null || registryFile.SchemaVersion < 1 || registryFile.SchemaVersion > 3 || registryFile.WorldUid != worldUid) { throw new SerializationException("Registry schema or world UID does not match."); } if (registryFile.Offerings != null) { HashSet<string> hashSet = new HashSet<string>(); if (registryFile.Offerings.Count > 100000) { throw new SerializationException("Offering ledger exceeds its safe limit."); } foreach (OfferingRecord offering in registryFile.Offerings) { if (offering == null || !offering.IsValid() || !hashSet.Add(offering.Id)) { throw new SerializationException("Invalid or duplicate offering record."); } } LoadedOfferings = registryFile.Offerings; } List<PlayerProgressRecord> list = new List<PlayerProgressRecord>(); if (registryFile.Players == null) { return list; } if (registryFile.Players.Count > 10000) { throw new SerializationException("Registry exceeds 10000 players."); } HashSet<long> hashSet2 = new HashSet<long>(); foreach (PlayerRecord player in registryFile.Players) { if (player == null || !PlayerIdentityRules.IsValid(player.PlayerId) || !hashSet2.Add(player.PlayerId) || player.Tier < 0 || player.Tier > 7 || (player.PlayerName?.Length ?? 0) > 256) { throw new SerializationException("Invalid or duplicate player record."); } ProgressionTier tier = (ProgressionTier)player.Tier; if (!BossProgressionMigration.TryNormalize(Math.Min(2, registryFile.SchemaVersion), tier, player.KillReceipts, player.BossCredits, out var killReceipts, out var bossCredits)) { throw new SerializationException("Invalid boss flags."); } list.Add(new PlayerProgressRecord(player.PlayerId, player.PlayerName, tier, killReceipts, bossCredits)); } return list; } catch (Exception arg) { RecoveryRequired = true; LoadedOfferings.Clear(); string text2 = text + ".corrupt-" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture); try { File.Copy(text, text2, overwrite: false); } catch { } Plugin.Log.LogError((object)$"Could not load progression registry '{text}'. Progression writes are blocked; restore a validated world backup and run wsd reload. Preserved copy: '{text2}'. {arg}"); return new List<PlayerProgressRecord>(); } } internal static void Save(long worldUid, IReadOnlyCollection<PlayerProgressRecord> records, IReadOnlyCollection<OfferingRecord> offerings) { //IL_00ed: Unknown result type (might be due to invalid IL or missing references) if (worldUid == 0L || RecoveryRequired) { throw new IOException("Progression storage is unavailable; recover the world registry before changing progression."); } string path = GetPath(worldUid); string text = path + ".tmp"; string destinationBackupFileName = path + ".bak"; try { Directory.CreateDirectory(Path.GetDirectoryName(path)); RegistryFile registryFile = new RegistryFile { SchemaVersion = 3, WorldUid = worldUid, Offerings = new List<OfferingRecord>(offerings) }; foreach (PlayerProgressRecord record in records) { registryFile.Players.Add(new PlayerRecord { PlayerId = record.PlayerId, PlayerName = record.PlayerName, Tier = (int)record.Tier, KillReceipts = (int)record.KillReceipts, BossCredits = (int)record.BossCredits }); } using (FileStream fileStream = File.Create(text)) { ((XmlObjectSerializer)new DataContractJsonSerializer(typeof(RegistryFile))).WriteObject((Stream)fileStream, (object)registryFile); fileStream.Flush(flushToDisk: true); } if (File.Exists(path)) { File.Replace(text, path, destinationBackupFileName); } else { File.Move(text, path); } } catch (Exception ex) { Plugin.Log.LogError((object)$"Could not save progression registry '{path}': {ex}"); try { if (File.Exists(text)) { File.Delete(text); } } catch { } throw new IOException("Progression was not committed. Existing registry and backup were preserved.", ex); } } internal static string GetPath(long worldUid) { return Path.Combine(Paths.ConfigPath, IdentityPaths.RegistryFileName("jg224.worldstagedirector", worldUid)); } internal static string GetLegacyPath(long worldUid) { return Path.Combine(Paths.ConfigPath, IdentityPaths.RegistryFileName("jg224.progressguard", worldUid)); } } internal static class ProgressionRegistry { private static readonly object Sync = new object(); private static readonly Dictionary<long, PlayerProgressRecord> Records = new Dictionary<long, PlayerProgressRecord>(); private static readonly Dictionary<string, OfferingRecord> Offerings = new Dictionary<string, OfferingRecord>(); private static List<PlayerProgressRecord> _committedRecords = new List<PlayerProgressRecord>(); private static List<OfferingRecord> _committedOfferings = new List<OfferingRecord>(); private static long _worldUid; internal static long WorldUid { get { lock (Sync) { return _worldUid; } } } internal static int Count { get { lock (Sync) { return Records.Count; } } } internal static void LoadForWorld(long worldUid) { if (worldUid == 0L) { throw new ArgumentOutOfRangeException("worldUid", "The authoritative world is not ready."); } lock (Sync) { _worldUid = worldUid; Records.Clear(); foreach (PlayerProgressRecord item in ProgressionPersistence.Load(worldUid)) { if (PlayerIdentityRules.IsValid(item.PlayerId)) { Records[item.PlayerId] = item; } } Offerings.Clear(); foreach (OfferingRecord loadedOffering in ProgressionPersistence.LoadedOfferings) { Offerings.Add(loadedOffering.Id, loadedOffering); } _committedRecords = Records.Values.ToList(); _committedOfferings = Offerings.Values.ToList(); } Plugin.Log.LogInfo((object)$"Progression registry loaded for world {worldUid}: {Count} player(s)."); } internal static bool EnsureLoadedForWorld(long worldUid) { if (worldUid == 0L) { return false; } if (WorldUid != worldUid) { LoadForWorld(worldUid); } return !ProgressionPersistence.RecoveryRequired; } internal static bool Promote(long playerId, string playerName, ProgressionTier tier) { return ImportLegacy(playerId, playerName, BossDefinitionRegistry.AllThrough(tier)); } internal static bool ImportLegacy(long playerId, string playerName, BossCreditFlags importedCredits) { if (!PlayerIdentityRules.IsValid(playerId) || importedCredits == BossCreditFlags.None || !BossDefinitionRegistry.IsValid(importedCredits)) { return false; } ProgressionTier progressionTier = BossDefinitionRegistry.HighestTier(importedCredits); lock (Sync) { if (Records.TryGetValue(playerId, out var value)) { ProgressionTier progressionTier2 = ((value.Tier > progressionTier) ? value.Tier : progressionTier); string text = (string.IsNullOrWhiteSpace(playerName) ? value.PlayerName : playerName); BossCreditFlags bossCreditFlags = value.BossCredits | importedCredits; BossCreditFlags bossCreditFlags2 = BossProgressionRules.OpenReceipts(value.KillReceipts, bossCreditFlags); if (progressionTier2 == value.Tier && bossCreditFlags2 == value.KillReceipts && bossCreditFlags == value.BossCredits && string.Equals(text, value.PlayerName, StringComparison.Ordinal)) { return false; } Records[playerId] = new PlayerProgressRecord(playerId, text, progressionTier2, bossCreditFlags2, bossCreditFlags); return true; } Records[playerId] = new PlayerProgressRecord(playerId, playerName, progressionTier, BossCreditFlags.None, importedCredits); return true; } } internal static bool RecordKill(long playerId, string playerName, ProgressionTier tier) { if (!PlayerIdentityRules.IsValid(playerId) || !BossDefinitionRegistry.TryFromTier(tier, out var _)) { return false; } lock (Sync) { BossCreditFlags bossCreditFlags = BossDefinitionRegistry.FlagFor(tier); if (Records.TryGetValue(playerId, out var value)) { string text = (string.IsNullOrWhiteSpace(playerName) ? value.PlayerName : playerName); if (BossDefinitionRegistry.Contains(value.BossCredits, tier)) { return false; } if ((value.KillReceipts & bossCreditFlags) == bossCreditFlags && string.Equals(text, value.PlayerName, StringComparison.Ordinal)) { return false; } Records[playerId] = new PlayerProgressRecord(playerId, text, value.Tier, value.KillReceipts | bossCreditFlags, value.BossCredits); return true; } Records[playerId] = new PlayerProgressRecord(playerId, playerName, ProgressionTier.None, bossCreditFlags, BossCreditFlags.None); return true; } } internal static bool GrantBossCredit(long playerId, string playerName, ProgressionTier tier) { if (!PlayerIdentityRules.IsValid(playerId) || !BossDefinitionRegistry.TryFromTier(tier, out var _)) { return false; } lock (Sync) { BossCreditFlags bossCreditFlags = BossDefinitionRegistry.FlagFor(tier); if (!Records.TryGetValue(playerId, out var value) || (value.KillReceipts & bossCreditFlags) != bossCreditFlags) { return false; } string text = (string.IsNullOrWhiteSpace(playerName) ? value.PlayerName : playerName); ProgressionTier progressionTier = ((value.Tier > tier) ? value.Tier : tier); if ((value.BossCredits & bossCreditFlags) == bossCreditFlags && progressionTier == value.Tier && string.Equals(text, value.PlayerName, StringComparison.Ordinal)) { return false; } BossCreditFlags bossCredits = value.BossCredits | bossCreditFlags; Records[playerId] = new PlayerProgressRecord(playerId, text, progressionTier, BossProgressionRules.OpenReceipts(value.KillReceipts, bossCredits), bossCredits); return true; } } internal static bool HasKillReceipt(long playerId, ProgressionTier tier) { lock (Sync) { PlayerProgressRecord value; return PlayerIdentityRules.IsValid(playerId) && Records.TryGetValue(playerId, out value) && BossProgressionRules.HasOpenReceipt(value.KillReceipts, value.BossCredits, tier); } } internal static bool HasBossCredit(long playerId, ProgressionTier tier) { lock (Sync) { PlayerProgressRecord value; return PlayerIdentityRules.IsValid(playerId) && Records.TryGetValue(playerId, out value) && BossDefinitionRegistry.Contains(value.BossCredits, tier); } } internal static PlayerProgressRecord Get(long playerId) { lock (Sync) { PlayerProgressRecord value; return (PlayerIdentityRules.IsValid(playerId) && Records.TryGetValue(playerId, out value)) ? value : null; } } internal static bool Set(long playerId, string playerName, ProgressionTier tier) { if (!PlayerIdentityRules.IsValid(playerId) || tier < ProgressionTier.None || tier > ProgressionTier.Fader) { return false; } lock (Sync) { Records[playerId] = new PlayerProgressRecord(playerId, playerName, tier); } return true; } internal static bool Reset(long playerId) { lock (Sync) { return Records.Remove(playerId); } } internal static ProgressionTier GetTier(long playerId) { lock (Sync) { PlayerProgressRecord value; return (PlayerIdentityRules.IsValid(playerId) && Records.TryGetValue(playerId, out value)) ? value.Tier : ProgressionTier.None; } } internal static List<PlayerProgressRecord> Snapshot() { lock (Sync) { return Records.Values.OrderBy<PlayerProgressRecord, string>((PlayerProgressRecord r) => r.PlayerName, StringComparer.OrdinalIgnoreCase).ThenBy((PlayerProgressRecord r) => r.PlayerId).ToList(); } } internal static PlayerProgressRecord Find(string playerIdOrName) { if (string.IsNullOrWhiteSpace(playerIdOrName)) { return null; } lock (Sync) { if (long.TryParse(playerIdOrName, out var result) && Records.TryGetValue(result, out var value)) { return value; } return Records.Values.FirstOrDefault((PlayerProgressRecord record) => string.Equals(record.PlayerName, playerIdOrName, StringComparison.OrdinalIgnoreCase)); } } internal static OfferingRecord FindOffering(string id) { lock (Sync) { OfferingRecord value; return (id != null && Offerings.TryGetValue(id, out value)) ? value : null; } } internal static void RecordOffering(OfferingRecord record) { lock (Sync) { if (record == null || !record.IsValid() || Offerings.ContainsKey(record.Id) || Offerings.Count >= 100000) { throw new InvalidOperationException("Invalid, duplicate or full offering ledger."); } Offerings.Add(record.Id, record); } } internal static List<OfferingRecord> OfferingHistory(long playerId) { lock (Sync) { return (from r in Offerings.Values where r.PlayerId == playerId orderby r.UtcTicks descending select r).ToList(); } } internal static List<ProgressionTier> CommittedOfferingTiers() { lock (Sync) { return (from r in Offerings.Values where r.Accepted && !r.Recovery select r.Tier).Distinct().ToList(); } } internal static void Save() { lock (Sync) { if (_worldUid == 0L) { return; } try { ProgressionPersistence.Save(_worldUid, Records.Values.ToList(), Offerings.Values.ToList()); _committedRecords = Records.Values.ToList(); _committedOfferings = Offerings.Values.ToList(); } catch { RollbackPending(); throw; } } } internal static void RollbackPending() { lock (Sync) { Records.Clear(); foreach (PlayerProgressRecord committedRecord in _committedRecords) { Records.Add(committedRecord.PlayerId, committedRecord); } Offerings.Clear(); foreach (OfferingRecord committedOffering in _committedOfferings) { Offerings.Add(committedOffering.Id, committedOffering); } } } internal static void Clear() { lock (Sync) { Records.Clear(); Offerings.Clear(); _committedRecords.Clear(); _committedOfferings.Clear(); _worldUid = 0L; } } } } namespace WorldStageDirector.Networking { internal static class ClientProgressionState { private static readonly object Sync = new object(); private static Dictionary<long, ProgressionTier> _tiers = new Dictionary<long, ProgressionTier>(); private static Dictionary<long, BossCreditFlags> _killReceipts = new Dictionary<long, BossCreditFlags>(); private static Dictionary<long, BossCreditFlags> _bossCredits = new Dictionary<long, BossCreditFlags>(); private static GateSettings _settings = new GateSettings(); internal static bool HasServerSnapshot { get; private set; } internal static long WorldUid { get; private set; } internal static ProgressionTier OnlineSafetyTier { get; private set; } = ProgressionTier.None; internal static void Replace(long worldUid, GateSettings settings, ProgressionTier onlineSafetyTier, Dictionary<long, ProgressionTier> tiers, Dictionary<long, BossCreditFlags> killReceipts, Dictionary<long, BossCreditFlags> bossCredits) { lock (Sync) { WorldUid = worldUid; _settings = settings?.Clone() ?? new GateSettings(); OnlineSafetyTier = onlineSafetyTier; _tiers = tiers ?? new Dictionary<long, ProgressionTier>(); _killReceipts = killReceipts ?? new Dictionary<long, BossCreditFlags>(); _bossCredits = bossCredits ?? new Dictionary<long, BossCreditFlags>(); HasServerSnapshot = true; } ExistingCreatureSafetyPatch.SweepLoaded(); ExistingTormentSpawnerSafetyPatch.SweepLoaded(); TrophyRules.ApplyLoadedPrefabs(); BossStonePresentation.RefreshLoadedStones(); } internal static GateSettings GetSettings() { lock (Sync) { return _settings.Clone(); } } internal static ProgressionTier GetTier(long playerId) { lock (Sync) { ProgressionTier value; return (PlayerIdentityRules.IsValid(playerId) && _tiers.TryGetValue(playerId, out value)) ? value : ProgressionTier.None; } } internal static BossCreditFlags GetKillReceipts(long playerId) { lock (Sync) { BossCreditFlags value; return (PlayerIdentityRules.IsValid(playerId) && _killReceipts.TryGetValue(playerId, out value)) ? value : BossCreditFlags.None; } } internal static BossCreditFlags GetBossCredits(long playerId) { lock (Sync) { BossCreditFlags value; return (PlayerIdentityRules.IsValid(playerId) && _bossCredits.TryGetValue(playerId, out value)) ? value : BossCreditFlags.None; } } internal static bool HasLocalKillReceipt(ProgressionTier tier) { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null) { return BossDefinitionRegistry.Contains(GetKillReceipts(localPlayer.GetPlayerID()), tier); } return false; } internal static bool HasLocalBossCredit(ProgressionTier tier) { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null) { return BossDefinitionRegistry.Contains(GetBossCredits(localPlayer.GetPlayerID()), tier); } return false; } internal static void Reset() { lock (Sync) { WorldUid = 0L; OnlineSafetyTier = ProgressionTier.None; _settings = new GateSettings(); _tiers = new Dictionary<long, ProgressionTier>(); _killReceipts = new Dictionary<long, BossCreditFlags>(); _bossCredits = new Dictionary<long, BossCreditFlags>(); HasServerSnapshot = false; } BossStonePresentation.Clear(); } } internal static class LegacyProgressionImport { internal const string ImportRpcName = "jg224.worldstagedirector.ImportLegacyProgression"; internal const string ImportAckRpcName = "jg224.worldstagedirector.ImportLegacyProgressionAck"; private const int ImportAttempts = 10; private const float InitialReplicationDelaySeconds = 2f; private const float RetryDelaySeconds = 2f; private static bool _importAcknowledged; internal static void Register(ZRoutedRpc rpc) { rpc.Register<ZPackage>("jg224.worldstagedirector.ImportLegacyProgression", (Action<long, ZPackage>)OnImportRequest); rpc.Register<ZPackage>("jg224.worldstagedirector.ImportLegacyProgressionAck", (Action<long, ZPackage>)OnImportAcknowledged); } internal static void Submit(Player player) { if (!Plugin.IsActive || (Object)(object)player == (Object)null || (Object)(object)player != (Object)(object)Player.m_localPlayer) { return; } BossCreditFlags bossCreditFlags = LegacyProgressionDetector.DetectFlags(player.GetUniqueKeys()); if (bossCreditFlags == BossCreditFlags.None) { return; } _importAcknowledged = false; ZNet instance = ZNet.instance; ZRoutedRpc instance2 = ZRoutedRpc.instance; if (!((Object)(object)instance == (Object)null) && instance2 != null) { if (instance.IsServer()) { Apply(player, bossCreditFlags); } else { ((MonoBehaviour)player).StartCoroutine(SubmitAfterReplication(player, bossCreditFlags)); } } } private static IEnumerator SubmitAfterReplication(Player player, BossCreditFlags credits) { yield return (object)new WaitForSeconds(2f); for (int attempt = 1; attempt <= 10; attempt++) { if (!Plugin.IsActive) { break; } if (_importAcknowledged) { break; } if ((Object)(object)player == (Object)null) { break; } if ((Object)(object)player != (Object)(object)Player.m_localPlayer) { break; } ZNet instance = ZNet.instance; ZRoutedRpc instance2 = ZRoutedRpc.instance; if ((Object)(object)instance == (Object)null || instance2 == null || instance.IsServer()) { break; } ZPackage val = new ZPackage(); val.Write((int)credits); instance2.InvokeRoutedRPC("jg224.worldstagedirector.ImportLegacyProgression", new object[1] { val }); Plugin.Debug($"Submitted exact legacy boss-stone credits {credits} for local character " + $"{player.GetPlayerID()} (migration attempt {attempt}/{10})."); if (attempt < 10) { yield return (object)new WaitForSeconds(2f); } } } private static void OnImportRequest(long sender, ZPackage package) { //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Expected O, but got Unknown if (!Plugin.IsActive) { return; } ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return; } BossCreditFlags bossCreditFlags = (BossCreditFlags)package.ReadInt(); if (bossCreditFlags == BossCreditFlags.None || !BossDefinitionRegistry.IsValid(bossCreditFlags)) { Plugin.Log.LogWarning((object)$"Rejected invalid legacy boss credits {(int)bossCreditFlags} from peer {sender}."); return; } try { if (!PeerCharacterIdentity.TryResolve(instance.GetPeer(sender), out var playerId, out var playerName)) { Plugin.Log.LogWarning((object)$"Rejected legacy progression import from unresolved peer {sender}."); return; } Apply(playerId, playerName, bossCreditFlags); ZPackage val = new ZPackage(); val.Write((int)bossCreditFlags); ZRoutedRpc instance2 = ZRoutedRpc.instance; if (instance2 != null) { instance2.InvokeRoutedRPC(sender, "jg224.worldstagedirector.ImportLegacyProgressionAck", new object[1] { val }); } } catch (Exception arg) { Plugin.Log.LogError((object)$"Legacy progression import failed for peer {sender}: {arg}"); } } private static void OnImportAcknowledged(long sender, ZPackage package) { if (!Plugin.IsActive) { return; } ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || instance.IsServer()) { return; } ZNetPeer serverPeer = instance.GetServerPeer(); if (serverPeer != null && serverPeer.m_uid == sender) { BossCreditFlags bossCreditFlags = (BossCreditFlags)package.ReadInt(); if (bossCreditFlags != BossCreditFlags.None && BossDefinitionRegistry.IsValid(bossCreditFlags)) { _importAcknowledged = true; Plugin.Debug($"Server acknowledged preserved legacy boss credits: {bossCreditFlags}."); } } } private static void Apply(Player player, BossCreditFlags credits) { if (!((Object)(object)player == (Object)null)) { Apply(player.GetPlayerID(), player.GetPlayerName(), credits); } } private static void Apply(long playerId, string playerName, BossCreditFlags credits) { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return; } if (!PlayerIdentityRules.IsValid(playerId)) { Plugin.Log.LogWarning((object)$"Rejected legacy progression import for invalid PlayerID {playerId}."); return; } if (ProgressionRegistry.WorldUid == 0L) { ProgressionRegistry.LoadForWorld(instance.GetWorldUID()); } bool num = ProgressionRegistry.ImportLegacy(playerId, playerName, credits); if (num) { ProgressionRegistry.Save(); } ProgressionSync.BroadcastState(); if (num) { Plugin.Log.LogInfo((object)("Imported legacy boss-stone progression for " + $"'{playerName}' ({playerId}): {credits} " + $"(highest {BossDefinitionRegistry.HighestTier(credits)}).")); } } } [HarmonyPatch(typeof(Player), "OnSpawned", new Type[] { typeof(bool) })] internal static class PlayerSpawnedLegacyImportPatch { [HarmonyPostfix] private static void Postfix(Player __instance) { LegacyProgressionImport.Submit(__instance); } } [HarmonyPatch(typeof(ZNet), "Awake")] internal static class ZNetAwakePatch { [HarmonyPostfix] private static void Postfix() { ClientProgressionState.Reset(); ServerBossFightLedger.Clear(); PlayerGuidance.Reset(); ProgressionSync.Register(ZRoutedRpc.instance); } } [HarmonyPatch(typeof(ZNet), "Start")] internal static class ZNetStartPatch { [HarmonyPostfix] private static void Postfix() { ZNet instance = ZNet.instance; if (!((Object)(object)instance == (Object)null) && instance.IsServer() && ProgressionRegistry.EnsureLoadedForWorld(instance.GetWorldUID())) { TrophyRules.ApplyLoadedPrefabs(); ProgressionSync.BroadcastState(); } } } [HarmonyPatch(typeof(Game), "OnDestroy")] internal static class GameDestroyPatch { [HarmonyPostfix] private static void Postfix() { ZNet instance = ZNet.instance; try { if ((Object)(object)instance != (Object)null && instance.IsServer()) { ProgressionRegistry.Save(); } } catch (Exception ex) { Plugin.Log.LogError((object)("World shutdown retained the previous durable registry: " + ex.Message)); } ProgressionRegistry.Clear(); ClientProgressionState.Reset(); ServerBossFightLedger.Clear(); BossStonePresentation.Clear(); PlayerGuidance.Reset(); } } [HarmonyPatch(typeof(ZNet), "RPC_CharacterID", new Type[] { typeof(ZRpc), typeof(ZDOID) })] internal static class CharacterIdReceivedPatch { [HarmonyPostfix] private static void Postfix() { if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { ProgressionSync.BroadcastState(); } } } [HarmonyPatch(typeof(ZNet), "Disconnect", new Type[] { typeof(ZNetPeer) })] internal static class PeerDisconnectedPatch { [HarmonyPostfix] private static void Postfix() { if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { ProgressionSync.BroadcastState(); } } } internal static class PeerCharacterIdentity { internal static bool TryResolve(ZNetPeer peer, out long playerId, out string playerName) { Vector3 position; return TryResolve(peer, out playerId, out playerName, out position); } internal static bool TryResolve(ZNetPeer peer, out long playerId, out string playerName, out Vector3 position) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) playerId = 0L; playerName = peer?.m_playerName ?? string.Empty; position = Vector3.zero; if (peer == null || peer.m_characterID == ZDOID.None || ZDOMan.instance == null) { return false; } ZDO zDO = ZDOMan.instance.GetZDO(peer.m_characterID); if (zDO == null || zDO.GetOwner() != peer.m_uid) { return false; } ZNetScene instance = ZNetScene.instance; GameObject val = ((instance != null) ? instance.GetPrefab(zDO.GetPrefab()) : null); if ((Object)(object)val == (Object)null || (Object)(object)val.GetComponent<Player>() == (Object)null) { return false; } playerId = zDO.GetLong(ZDOVars.s_playerID, 0L); string text = zDO.GetString(ZDOVars.s_playerName, string.Empty); if (!string.IsNullOrWhiteSpace(text)) { playerName = text; } position = zDO.GetPosition(); return PlayerIdentityRules.IsValid(playerId); } } internal static class ProgressionSync { internal const int ProtocolVersion = 9; private const int MaximumRecords = 10000; internal const string SyncRpcName = "jg224.worldstagedirector.SyncState"; private static ZRoutedRpc _registeredInstance; private static Action<long> _newPeerHandler; internal static void Register(ZRoutedRpc rpc) { if (rpc == null) { return; } if (_registeredInstance != rpc) { if (_registeredInstance != null && _newPeerHandler != null) { ZRoutedRpc registeredInstance = _registeredInstance; registeredInstance.m_onNewPeer = (Action<long>)Delegate.Remove(registeredInstance.m_onNewPeer, _newPeerHandler); } _newPeerHandler = null; rpc.Register<ZPackage>("jg224.worldstagedirector.SyncState", (Action<long, ZPackage>)OnSyncState); LegacyProgressionImport.Register(rpc); BossProgressionRpc.Register(rpc); _registeredInstance = rpc; Plugin.Debug("Registered progression synchronization and legacy-import routed RPCs."); } if (_newPeerHandler != null) { rpc.m_onNewPeer = (Action<long>)Delegate.Remove(rpc.m_onNewPeer, _newPeerHandler); _newPeerHandler = null; } ZNet instance = ZNet.instance; if ((Object)(object)instance != (Object)null && instance.IsServer()) { _newPeerHandler = OnNewPeer; rpc.m_onNewPeer = (Action<long>)Delegate.Combine(rpc.m_onNewPeer, _newPeerHandler); } } private static void OnNewPeer(long peerUid) { if (Plugin.IsActive) { BroadcastState(); } } internal static void BroadcastState() { if (!Plugin.IsActive) { return; } ZNet instance = ZNet.instance; ZRoutedRpc instance2 = ZRoutedRpc.instance; if ((Object)(object)instance == (Object)null || instance2 == null || !instance.IsServer() || !AuthoritativeStatePolicy.IsValidWorldSnapshot(ProgressionRegistry.WorldUid) || ProgressionPersistence.RecoveryRequired) { return; } RaidProgressionSafetyPatch.EnforceActiveEvent(); foreach (ZNetPeer connectedPeer in instance.GetConnectedPeers()) { if (connectedPeer != null) { SendState(connectedPeer.m_uid); } } ExistingCreatureSafetyPatch.SweepLoaded(); ExistingTormentSpawnerSafetyPatch.SweepLoaded(); } internal static void SendState(long peerUid) { if (!Plugin.IsActive) { return; } ZNet instance = ZNet.instance; ZRoutedRpc instance2 = ZRoutedRpc.instance; if ((Object)(object)instance == (Object)null || instance2 == null || !instance.IsServer() || !AuthoritativeStatePolicy.IsValidWorldSnapshot(ProgressionRegistry.WorldUid) || ProgressionPersistence.RecoveryRequired) { return; } try { ZPackage val = BuildPackage(); instance2.InvokeRoutedRPC(peerUid, "jg224.worldstagedirector.SyncState", new object[1] { val }); Plugin.Debug($"Sent progression snapshot to peer {peerUid} ({ProgressionRegistry.Count} records)."); } catch (Exception ex) { Plugin.Log.LogWarning((object)$"Could not send progression snapshot to peer {peerUid}: {ex.Message}"); } } internal static void Shutdown() { if (_registeredInstance != null && _newPeerHandler != null) { ZRoutedRpc registeredInstance = _registeredInstance; registeredInstance.m_onNewPeer = (Action<long>)Delegate.Remove(registeredInstance.m_onNewPeer, _newPeerHandler); } _registeredInstance = null; _newPeerHandler = null; ClientProgressionState.Reset(); BossProgressionRpc.Shutdown(); } internal static ProgressionTier GetOnlineSafetyTier() { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return ClientProgressionState.OnlineSafetyTier; } bool flag = false; ProgressionTier progressionTier = ProgressionTier.Fader; Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null) { flag = true; progressionTier = ProgressionRegistry.GetTier(localPlayer.GetPlayerID()); } foreach (ZNetPeer connectedPeer in instance.GetConnectedPeers()) { if (connectedPeer != null) { flag = true; long playerId; string playerName; ProgressionTier progressionTier2 = (PeerCharacterIdentity.TryResolve(connectedPeer, out playerId, out playerName) ? ProgressionRegistry.GetTier(playerId) : ProgressionTier.None); if (progressionTier2 < progressionTier) { progressionTier = progressionTier2; } } } if (!flag) { return ProgressionTier.None; } return progressionTier; } private static ZPackage BuildPackage() { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Expected O, but got Unknown GateSettings settings = ModConfig.Snapshot(); List<PlayerProgressRecord> list = ProgressionRegistry.Snapshot(); ZPackage val = new ZPackage(); val.Write(9); val.Write(ProgressionRegistry.WorldUid); WriteSettings(val, settings); val.Write((int)GetOnlineSafetyTier()); val.Write(list.Count); foreach (PlayerProgressRecord item in list) { val.Write(item.PlayerId); val.Write((int)item.Tier); val.Write((int)item.KillReceipts); val.Write((int)item.BossCredits); } return val; } private static void OnSyncState(long sender, ZPackage package) { if (!Plugin.IsActive) { return; } ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || instance.IsServer()) { return; } ZNetPeer serverPeer = instance.GetServerPeer(); if (serverPeer == null || sender != serverPeer.m_uid) { Plugin.Log.LogWarning((object)$"Ignored progression snapshot from non-server peer {sender}."); return; } try { int num = package.ReadInt(); if (num != 9) { throw new InvalidOperationException($"Unsupported WorldStageDirector protocol {num}; expected {9}."); } long num2 = package.ReadLong(); if (!AuthoritativeStatePolicy.IsValidWorldSnapshot(num2)) { Plugin.Log.LogWarning((object)"Ignored progression snapshot before the server's world registry was ready."); return; } GateSettings settings = ReadSettings(package); int num3 = package.ReadInt(); if (num3 < 0 || num3 > 7) { throw new InvalidOperationException($"Invalid online safety tier {num3}."); } int num4 = package.ReadInt(); if (num4 < 0 || num4 > 10000) { throw new InvalidOperationException($"Invalid progression record count {num4}."); } Dictionary<long, ProgressionTier> dictionary = new Dictionary<long, ProgressionTier>(num4); Dictionary<long, BossCreditFlags> dictionary2 = new Dictionary<long, BossCreditFlags>(num4); Dictionary<long, BossCreditFlags> dictionary3 = new Dictionary<long, BossCreditFlags>(num4); for (int i = 0; i < num4; i++) { long num5 = package.ReadLong(); int num6 = package.ReadInt(); BossCreditFlags bossCreditFlags = (BossCreditFlags)package.ReadInt(); BossCreditFlags bossCreditFlags2 = (BossCreditFlags)package.ReadInt(); if (PlayerIdentityRules.IsValid(num5) && num6 >= 0 && num6 <= 7 && BossDefinitionRegistry.IsValid(bossCreditFlags) && BossDefinitionRegistry.IsValid(bossCreditFlags2)) { dictionary[num5] = (ProgressionTier)num6; dictionary2[num5] = bossCreditFlags; dictionary3[num5] = bossCreditFlags2; } } ProgressionTier progressionTier = (ProgressionTier)num3; ClientProgressionState.Replace(num2, settings, progressionTier, dictionary, dictionary2, dictionary3); Plugin.Log.LogInfo((object)($"Received authoritative progression snapshot for world {num2}: " + $"{dictionary.Count} player(s), online safety tier {progressionTier}.")); } catch (Exception arg) { Plugin.Log.LogError((object)$"Rejected invalid progression snapshot: {arg}"); ClientProgressionState.Reset(); } } private static void WriteSettings(ZPackage package, GateSettings settings) { package.Write(settings.Enabled); package.Write(settings.NearbyPlayerRadius); package.Write(settings.ProtectLeastProgressedOnline); package.Write(settings.UseLowestNearbyProgression); package.Write(settings.GateYagluthRoamers); package.Write(settings.GateQueenRoamers); package.Write(settings.GateFaderRoamers); package.Write(settings.GateEarlyGameRoamers); package.Write(settings.BlockTormentSpawnersOutsideAshlands); package.Write(settings.LimitRaidsToOnlineProgression); package.Write(settings.BossProgressionEnabled); package.Write(settings.AutoEnablePlayerEvents); package.Write(settings.BossKillWitnessRadius); package.Write(settings.SacrificeTrophyRange); package.Write(settings.BossLootOnSacrifice); package.Write((int)settings.CreditedPlayerDeathDrops); package.Write(settings.SacrificeLootDelaySeconds); package.Write(settings.BossTrophyWeight); package.Write(settings.BossTrophyMaxStackSize); package.Write(settings.BossTrophyAutoPickup); package.Write((int)settings.BossTrophyPortalRestriction); package.Write(settings.VerboseLogging); package.Write(settings.LogBlockedSpawns); package.Write(settings.LogAllowedProgressionSpawns); } private static GateSettings ReadSettings(ZPackage package) { return new GateSettings { Enabled = package.ReadBool(), NearbyPlayerRadius = package.ReadSingle(), ProtectLeastProgressedOnline = package.ReadBool(), UseLowestNearbyProgression = package.ReadBool(), GateYagluthRoamers = package.ReadBool(), GateQueenRoamers = package.ReadBool(), GateFaderRoamers = package.ReadBool(), GateEarlyGameRoamers = package.ReadBool(), BlockTormentSpawnersOutsideAshlands = package.ReadBool(), LimitRaidsToOnlineProgression = package.ReadBool(), BossProgressionEnabled = package.ReadBool(), AutoEnablePlayerEvents = package.ReadBool(), BossKillWitnessRadius = package.ReadSingle(), SacrificeTrophyRange = package.ReadSingle(), BossLootOnSacrifice = package.ReadBool(), CreditedPlayerDeathDrops = (CreditedPlayerDropMode)package.ReadInt(), SacrificeLootDelaySeconds = package.ReadSingle(), BossTrophyWeight = package.ReadSingle(), BossTrophyMaxStackSize = package.ReadInt(), BossTrophyAutoPickup = package.ReadBool(), BossTrophyPortalRestriction = (TrophyPortalRestriction)package.ReadInt(), VerboseLogging = package.ReadBool(), LogBlockedSpawns = package.ReadBool(), LogAllowedProgressionSpawns = package.ReadBool() }; } } } namespace WorldStageDirector.Core { public static class ArenaEnemyExemptionPolicy { public static bool IsSkaldHallArenaEnemy(int enemyTag, string arenaId, string sessionId, string enemyId) { if (enemyTag == 1 && IsGuid(arenaId) && IsGuid(sessionId)) { return IsGuid(enemyId); } return false; } private static bool IsGuid(string value) { Guid result; if (!string.IsNullOrWhiteSpace(value)) { return Guid.TryParse(value, out result); } return false; } } internal static class AuthoritativeStatePolicy { internal static bool IsValidWorldSnapshot(long worldUid) { return worldUid != 0; } internal static bool CanEnforce(bool networkAvailable, bool isServer, bool hasServerSnapshot) { if (networkAvailable) { return isServer || hasServerSnapshot; } return false; } } [Flags] public enum BossCreditFlags { None = 0, Eikthyr = 1, Elder = 2, Bonemass = 4, Moder = 8, Yagluth = 0x10, Queen = 0x20, Fader = 0x40, All = 0x7F } public sealed class BossDefinition { public ProgressionTier Tier { get; } public string DefeatKey { get; } public string GuardianPowerName { get; } public string TrophyPrefabName { get; } public string BossPrefabName { get; } public BossDefinition(ProgressionTier tier, string defeatKey, string guardianPowerName, string trophyPrefabName, string bossPrefabName) { Tier = tier; DefeatKey = defeatKey; GuardianPowerName = guardianPowerName; TrophyPrefabName = trophyPrefabName; BossPrefabName = bossPrefabName; } } public static class BossDefinitionRegistry { private static readonly BossDefinition[] Definitions; private static readonly Dictionary<string, BossDefinition> ByDefeatKey; private static readonly Dictionary<string, BossDefinition> ByGuardianPower; private static readonly Dictionary<string, BossDefinition> ByTrophy; private static readonly Dictionary<string, BossDefinition> ByBossPrefab; public static IReadOnlyList<BossDefinition> All => Definitions; static BossDefinitionRegistry() { Definitions = new BossDefinition[7] { new BossDefinition(ProgressionTier.Eikthyr, "defeated_eikthyr", "GP_Eikthyr", "TrophyEikthyr", "Eikthyr"), new BossDefinition(ProgressionTier.Elder, "defeated_gdking", "GP_TheElder", "TrophyTheElder", "gd_king"), new BossDefinition(ProgressionTier.Bonemass, "defeated_bonemass", "GP_Bonemass", "TrophyBonemass", "Bonemass"), new BossDefinition(ProgressionTier.Moder, "defeated_dragon", "GP_Moder", "TrophyDragonQueen", "Dragon"), new BossDefinition(ProgressionTier.Yagluth, "defeated_goblinking", "GP_Yagluth", "TrophyGoblinKing", "GoblinKing"), new BossDefinition(ProgressionTier.Queen, "defeated_queen", "GP_Queen", "TrophySeekerQueen", "SeekerQueen"), new BossDefinition(ProgressionTier.Fader, "defeated_fader", "GP_Ashlands", "TrophyFader", "Fader") }; ByDefeatKey = Build((BossDefinition definition) => definition.DefeatKey); ByGuardianPower = Build((BossDefinition definition) => definition.GuardianPowerName); ByTrophy = Build((BossDefinition definition) => definition.TrophyPrefabName); ByBossPrefab = Build((BossDefinition definition) => definition.BossPrefabName); ByGuardianPower["GP_Fader"] = Definitions[6]; } public static bool TryFromDefeatKey(string defeatKey, out BossDefinition definition) { return TryGet(ByDefeatKey, defeatKey, out definition); } public static bool TryFromGuardianPower(string guardianPowerName, out BossDefinition definition) { return TryGet(ByGuardianPower, guardianPowerName, out definition); } public static bool TryFromTrophy(string trophyPrefabName, out BossDefinition definition) { return TryGet(ByTrophy, trophyPrefabName, out definition); } public static bool TryFromBossPrefab(string bossPrefabName, out BossDefinition definition) { return TryGet(ByBossPrefab, bossPrefabName, out definition); } public static bool TryFromTier(ProgressionTier tier, out BossDefinition definition) { if (tier > ProgressionTier.None && tier <= ProgressionTier.Fader) { definition = Definitions[(int)(tier - 1)]; return true; } definition = null; return false; } public static BossCreditFlags FlagFor(ProgressionTier tier) { if (tier <= ProgressionTier.None || tier > ProgressionTier.Fader) { return BossCreditFlags.None; } return (BossCreditFlags)(1 << (int)(tier - 1)); } public static BossCreditFlags AllThrough(ProgressionTier tier) { if (tier <= ProgressionTier.None) { return BossCreditFlags.None; } if (tier >= ProgressionTier.Fader) { return BossCreditFlags.All; } return (BossCreditFlags)((1 << (int)tier) - 1); } public static bool Contains(BossCreditFlags flags, ProgressionTier tier) { BossCreditFlags bossCreditFlags = FlagFor(tier); if (bossCreditFlags != BossCreditFlags.None) { return (flags & bossCreditFlags) == bossCreditFlags; } return false; } public static bool IsValid(BossCreditFlags flags) { return (flags & ~BossCreditFlags.All) == 0; } public static ProgressionTier HighestTier(BossCreditFlags flags) { for (int num = 7; num > 0; num--) { ProgressionTier progressionTier = (ProgressionTier)num; if (Contains(flags, progressionTier)) { return progressionTier; } } return ProgressionTier.None; } private static Dictionary<string, BossDefinition> Build(Func<BossDefinition, string> keySelector) { Dictionary<string, BossDefinition> dictionary = new Dictionary<string, BossDefinition>(StringComparer.OrdinalIgnoreCase); BossDefinition[] definitions = Definitions; foreach (BossDefinition bossDefinition in definitions) { dictionary[keySelector(bossDefinition)] = bossDefinition; } return dictionary; } private static bool TryGet(Dictionary<string, BossDefinition> map, string key, out BossDefinition definition) { if (!string.IsNullOrWhiteSpace(key) && map.TryGetValue(key.Trim(), out definition)) { return true; } definition = null; return false; } } public sealed class BossParticipationLedger<TBossId> { private sealed class Fight { internal readonly HashSet<long> Participants = new HashSet<long>(); internal bool Settling; internal bool Completed; } private readonly Dictionary<TBossId, Fight> _fights = new Dictionary<TBossId, Fight>(); private readonly int _maximumBosses; private readonly int _maximumParticipants; public bool AtCapacity => _fights.Count >= _maximumBosses; public BossParticipationLedger(int maximumBosses = 1024, int maximumParticipants = 100) { if (maximumBosses <= 0) { throw new ArgumentOutOfRangeException("maximumBosses"); } if (maximumParticipants <= 0) { throw new ArgumentOutOfRangeException("maximumParticipants"); } _maximumBosses = maximumBosses; _maximumParticipants = maximumParticipants; } public bool RecordDamage(TBossId bossId, long playerId) { if (!PlayerIdentityRules.IsValid(playerId) || !TryGetOrCreate(bossId, out var fight) || fight.Settling || fight.Completed) { return false; } if (fight.Participants.Contains(playerId)) { return true; } if (fight.Participants.Count < _maximumParticipants) { return fight.Participants.Add(playerId); } return false; } public bool HasDamaged(TBossId bossId, long playerId) { if (IsValidBoss(bossId) && PlayerIdentityRules.IsValid(playerId) && _fights.TryGetValue(bossId, out var value)) { return value.Participants.Contains(playerId); } return false; } public bool TrySettle(TBossId bossId) { if (!TryGetOrCreate(bossId, out var fight) || fight.Settling || fight.Completed) { return false; } fight.Settling = true; return true; } public void Retry(TBossId bossId) { if (IsValidBoss(bossId) && _fights.TryGetValue(bossId, out var value) && !value.Completed) { value.Settling = false; } } public void Complete(TBossId bossId) { if (IsValidBoss(bossId) && _fights.TryGetValue(bossId, out var value) && value.Settling) { value.Completed = true; value.Participants.Clear(); } } public void RemoveMissing(Func<TBossId, bool> bossExists) { if (bossExists == null) { throw new ArgumentNullException("bossExists"); } List<TBossId> list = new List<TBossId>(); foreach (KeyValuePair<TBossId, Fight> fight in _fights) { if ((!fight.Value.Settling || fight.Value.Completed) && !bossExists(fight.Key)) { list.Add(fight.Key); } } foreach (TBossId item in list) { _fights.Remove(item); } } public void Clear() { _fights.Clear(); } private bool TryGetOrCreate(TBossId bossId, out Fight fight) { fight = null; if (!IsValidBoss(bossId)) { return false; } if (_fights.TryGetValue(bossId, out fight)) { return true; } if (AtCapacity) { return false; } _fights.Add(bossId, fight = new Fight()); return true; } private static bool IsValidBoss(TBossId bossId) { return !EqualityComparer<TBossId>.Default.Equals(bossId, default(TBossId)); } } public static class BossDamageParticipation { public static bool AcceptedDamageOverTime(float mitigatedDamage, bool accepted) { if (accepted && !float.IsNaN(mitigatedDamage) && !float.IsInfinity(mitigatedDamage)) { return mitigatedDamage > 0f; } return false; } public static bool PoisonDamageIncreased(float before, float after, float mitigatedDamage) { if (AcceptedDamageOverTime(mitigatedDamage, accepted: true) && !float.IsNaN(before) && !float.IsInfinity(before) && after == mitigatedDamage) { return after > before; } return false; } public static bool CausedHealthLoss(float before, float after) { if (!float.IsNaN(before) && !float.IsInfinity(before) && !float.IsNaN(after) && !float.IsInfinity(after) && before > 0f) { return after < before; } return false; } } public static class BossProgressionMigration { public static bool TryNormalize(int schemaVersion, ProgressionTier tier, int storedKillReceipts, int storedBossCredits, out BossCreditFlags killReceipts, out BossCreditFlags bossCredits) { killReceipts = BossCreditFlags.None; bossCredits = BossCreditFlags.None; if (tier < ProgressionTier.None || tier > ProgressionTier.Fader) { return false; } switch (schemaVersion) { case 1: bossCredits = BossDefinitionRegistry.AllThrough(tier); killReceipts = BossCreditFlags.None; return true; default: return false; case 2: bossCredits = (BossCreditFlags)storedBossCredits; if (!BossDefinitionRegistry.IsValid((BossCreditFlags)storedKillReceipts) || !BossDefinitionRegistry.IsValid(bossCredits)) { return false; } killReceipts = BossProgressionRules.OpenReceipts((BossCreditFlags)storedKillReceipts, bossCredits); return true; } } } public static class BossProgressionRules { public static bool CanRecordKill(float horizontalDistance, float witnessRadius) { if (witnessRadius > 0f && horizontalDistance >= 0f) { return horizontalDistance <= witnessRadius; } return false; } public static bool CanRecordParticipatingKill(bool dealtDamage, float horizontalDistance, float witnessRadius) { if (dealtDamage) { return CanRecordKill(horizontalDistance, witnessRadius); } return false; } public static bool CanRequestSacrifice(BossCreditFlags killReceipts, ProgressionTier tier, bool trophyMatches, float horizontalDistance, float sacrificeRange) { if (trophyMatches && sacrificeRange > 0f && horizontalDistance >= 0f && horizontalDistance <= sacrificeRange) { return BossDefinitionRegistry.Contains(killReceipts, tier); } return false; } public static bool CanReceiveCredit(BossCreditFlags killReceipts, ProgressionTier tier, float horizontalDistance, float sacrificeRange) { if (sacrificeRange > 0f && horizontalDistance >= 0f && horizontalDistance <= sacrificeRange) { return BossDefinitionRegistry.Contains(killReceipts, tier); } return false; } public static bool ShouldPlaySacrificeEffects(bool effectRequested, bool sacrificeActive) { return effectRequested && sacrificeActive; } public static bool HasOpenReceipt(BossCreditFlags killReceipts, BossCreditFlags bossCredits, ProgressionTier tier) { if (BossDefinitionRegistry.Contains(killReceipts, tier)) { return !BossDefinitionRegistry.Contains(bossCredits, tier); } return false; } public static BossCreditFlags OpenReceipts(BossCreditFlags killReceipts, BossCreditFlags bossCredits) { return killReceipts & ~bossCredits & BossCreditFlags.All; } public static bool CanReceivePersonalDeathLoot(BossCreditFlags killReceipts, BossCreditFlags bossCredits, ProgressionTier tier, bool bossLootOnSacrifice, CreditedPlayerDropMode creditedPlayerDropMode) { if (BossDefinitionRegistry.TryFromTier(tier, out var _) && creditedPlayerDropMode >= CreditedPlayerDropMode.Nothing && creditedPlayerDropMode <= CreditedPlayerDropMode.ReservedLoot) { if (!BossDefinitionRegistry.Contains(bossCredits, tier)) { return !BossDefinitionRegistry.Contains(killReceipts, tier); } return creditedPlayerDropMode == CreditedPlayerDropMode.ReservedLoot; } return false; } public static int ScaleRewardAmount(int rolledAmount, int eligiblePlayers, bool onePerPlayer) { if (rolledAmount <= 0 || eligiblePlayers <= 0) { return 0; } if (onePerPlayer) { return eligiblePlayers; } return (int)Math.Min((long)rolledAmount * (long)eligiblePlayers, 10000L); } } public static class BossTierMapping { public static bool TryMapGuardianPower(string guardianPowerName, out ProgressionTier tier) { if (BossDefinitionRegistry.TryFromGuardianPower(guardianPowerName, out var definition)) { tier = definition.Tier; return true; } tier = ProgressionTier.None; return false; } } public enum CreditedPlayerDropMode { Nothing, ReservedLoot } public sealed class GateSettings { public bool Enabled { get; set; } = true; public float NearbyPlayerRadius { get; set; } = 100f; public bool ProtectLeastProgressedOnline { get; set; } = true; public bool UseLowestNearbyProgression { get; set; } = true; public bool GateYagluthRoamers { get; set; } = true; public bool GateQueenRoamers { get; set; } = true; public bool GateFaderRoamers { get; set; } = true; public bool GateEarlyGameRoamers { get; set; } = true; public bool BlockTormentSpawnersOutsideAshlands { get; set; } = true; public bool LimitRaidsToOnlineProgression { get; set; } = true; public bool BossProgressionEnabled { get; set; } = true; public bool AutoEnablePlayerEvents { get; set; } = true; public float BossKillWitnessRadius { get; set; } = 100f; public float SacrificeTrophyRange { get; set; } = 25f; public bool BossLootOnSacrifice { get; set; } public CreditedPlayerDropMode CreditedPlayerDeathDrops { get; set; } = CreditedPlayerDropMode.ReservedLoot; public float SacrificeLootDelaySeconds { get; set; } = 12f; public float BossTrophyWeight { get; set; } = -1f; public int BossTrophyMaxStackSize { get; set; } = 1; public bool BossTrophyAutoPickup { get; set; } public TrophyPortalRestriction BossTrophyPortalRestriction { get; set; } = TrophyPortalRestriction.Vanilla; public bool VerboseLogging { get; set; } public bool LogBlockedSpawns { get; set; } = true; public bool LogAllowedProgressionSpawns { get; set; } public GateSettings Clone() { return new GateSettings { Enabled = Enabled, NearbyPlayerRadius = NearbyPlayerRadius, ProtectLeastProgressedOnline = ProtectLeastProgressedOnline, UseLowestNearbyProgression = UseLowestNearbyProgression, GateYagluthRoamers = GateYagluthRoamers, GateQueenRoamers = GateQueenRoamers, GateFaderRoamers = GateFaderRoamers, GateEarlyGameRoamers = GateEarlyGameRoamers, BlockTormentSpawnersOutsideAshlands = BlockTormentSpawnersOutsideAshlands, LimitRaidsToOnlineProgression = LimitRaidsToOnlineProgression, BossProgressionEnabled = BossProgressionEnabled, AutoEnablePlayerEvents = AutoEnablePlayerEvents, BossKillWitnessRadius = BossKillWitnessRadius, SacrificeTrophyRange = SacrificeTrophyRange, BossLootOnSacrifice = BossLootOnSacrifice, CreditedPlayerDeathDrops = CreditedPlayerDeathDrops, SacrificeLootDelaySeconds = SacrificeLootDelaySeconds, BossTrophyWeight = BossTrophyWeight, BossTrophyMaxStackSize = BossTrophyMaxStackSize, BossTrophyAutoPickup = BossTrophyAutoPickup, BossTrophyPortalRestriction = BossTrophyPortalRestriction, VerboseLogging = VerboseLogging, LogBlockedSpawns = LogBlockedSpawns, LogAllowedProgressionSpawns = LogAllowedProgressionSpawns }; } } public static class IdentityPaths { public const string CurrentPluginGuid = "jg224.worldstagedirector"; public const string LegacyPluginGuid = "jg224.progressguard"; public static bool ShouldMigrate(bool currentExists, bool legacyExists) { return !currentExists && legacyExists; } public static string ConfigFileName(string pluginGuid) { return pluginGuid + ".cfg"; } public static string RegistryFileName(string pluginGuid, long worldUid) { return pluginGuid + ".world-" + worldUid.ToString(CultureInfo.InvariantCulture) + ".json"; } } public static class LegacyProgressionDetector { private static readonly KeyValuePair<string, ProgressionTier>[] GuardianPowerKeys = new KeyValuePair<string, ProgressionTier>[8] { new KeyValuePair<string, ProgressionTier>("GP_Eikthyr", ProgressionTier.Eikthyr), new KeyValuePair<string, ProgressionTier>("GP_TheElder", ProgressionTier.Elder), new KeyValuePair<string, ProgressionTier>("GP_Bonemass", ProgressionTier.Bonemass), new KeyValuePair<string, ProgressionTier>("GP_Moder", ProgressionTier.Moder), new KeyValuePair<string, ProgressionTier>("GP_Yagluth", ProgressionTier.Yagluth), new KeyValuePair<string, ProgressionTier>("GP_Queen", ProgressionTier.Queen), new KeyValuePair<string, ProgressionTier>("GP_Fader", ProgressionTier.Fader), new KeyValuePair<string, ProgressionTier>("GP_Ashlands", ProgressionTier.Fader) }; public static ProgressionTier DetectHighest(IEnumerable<string> uniqueKeys) { return BossDefinitionRegistry.HighestTier(DetectFlags(uniqueKeys)); } public static BossCreditFlags DetectFlags(IEnumerable<string> uniqueKeys) { if (uniqueKeys == null) { return BossCreditFlags.None; } HashSet<string> hashSet = new HashSet<string>(uniqueKeys, StringComparer.OrdinalIgnoreCase); BossCreditFlags bossCreditFlags = BossCreditFlags.None; KeyValuePair<string, ProgressionTier>[] guardianPowerKeys = GuardianPowerKeys; for (int i = 0; i < guardianPowerKeys.Length; i++) { KeyValuePair<string, ProgressionTier> keyValuePair = guardianPowerKeys[i]; if (hashSet.Contains(keyValuePair.Key)) { bossCreditFlags |= BossDefinitionRegistry.FlagFor(keyValuePair.Value); } } return bossCreditFlags; } } [DataContract] public sealed class OfferingRecord { [DataMember(Order = 1)] public string Id; [DataMember(Order = 2)] public long PlayerId; [DataMember(Order = 3)] public ProgressionTier Tier; [DataMember(Order = 4)] public bool Accepted; [DataMember(Order = 5)] public bool Recovery; [DataMember(Order = 6)] public long UtcTicks; [DataMember(Order = 7)] public string Reason = string.Empty; public bool IsValid() { if (Guid.TryParseExact(Id, "N", out var _) && PlayerIdentityRules.IsValid(PlayerId) && Tier >= ProgressionTier.Eikthyr && Tier <= ProgressionTier.Fader && UtcTicks > 0) { long utcTicks = UtcTicks; DateTime maxValue = DateTime.MaxValue; if (utcTicks <= maxValue.Ticks) { if (Reason != null) { if (Reason.Length <= 120) { return !Reason.Any(char.IsControl); } return false; } return true; } } return false; } public bool Matches(long playerId, ProgressionTier tier) { if (PlayerId == playerId && Tier == tier) { return !Recovery; } return false; } } public enum OfferingPhase { Prepared, Removed, Refunding, Accepted } [DataContract] public sealed class PendingOffering { [DataMember] public int Schema = 1; [DataMember] public string Id; [DataMember] public long World; [DataMember] public long Player; [DataMember] public ProgressionTier Tier; [DataMember] public string Trophy; [DataMember] public OfferingPhase Phase; public static PendingOffering Read(string path, long world, long player) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) if (!File.Exists(path)) { return null; } using FileStream fileStream = File.OpenRead(path); if (fileStream.Length > 4096) { throw new IOException("Offering journal exceeds 4 KiB."); } PendingOffering obj = (PendingOffering)((XmlObjectSerializer)new DataContractJsonSerializer(typeof(PendingOffering))).ReadObject((Stream)fileStream); obj.Validate(world, player); return obj; } public void Write(string path) { //IL_003e: Unknown result type (might be due to invalid IL or missing references) Validate(World, Player); Directory.CreateDirectory(Path.GetDirectoryName(path)); string text = path + ".tmp"; using (FileStream fileStream = new FileStream(text, FileMode.Create, FileAccess.Write, FileShare.None)) { ((XmlObjectSerializer)new DataContractJsonSerializer(typeof(PendingOffering))).WriteObject((Stream)fileStream, (object)this); fileStream.Flush(flushToDisk: true); } if (File.Exists(path)) { File.Replace(text, path, path + ".bak"); } else { File.Move(text, path); } } public void Validate(long world, long player) { if (Schema != 1 || World == 0L || World != world || !PlayerIdentityRules.IsValid(Player) || Player != player || !Guid.TryParseExact(Id, "N", out var _) || Phase < OfferingPhase.Prepared || Phase > OfferingPhase.Accepted || !BossDefinitionRegistry.TryFromTrophy(Trophy, out var definition) || definition.Tier != Tier) { throw new IOException("Invalid or mismatched offering journal; preserve it for administrator review."); } } } public static class PersonalDropReservation { public static string EncodeOwner(long playerId) { if (!PlayerIdentityRules.IsValid(playerId)) { return string.Empty; } return playerId.ToString(CultureInfo.InvariantCulture); } public static bool TryDecodeOwner(string value, out long playerId) { if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out playerId)) { return PlayerIdentityRules.IsValid(playerId); } return false; } public static bool CanClaim(long reservedPlayerId, long claimingPlayerId) { if (PlayerIdentityRules.IsValid(reservedPlayerId)) { return reservedPlayerId == claimingPlayerId; } return false; } } internal static class PlayerIdentityRules { internal static bool IsValid(long playerId) { return playerId != 0; } } public enum ProgressionTier { None, Eikthyr, Elder, Bonemass, Moder, Yagluth, Queen, Fader } public static class RaidRuleRegistry { private static readonly Dictionary<string, ProgressionTier> ExplicitTiers = new Dictionary<string, ProgressionTier>(StringComparer.OrdinalIgnoreCase) { ["army_eikthyr"] = ProgressionTier.None, ["hildirboss1"] = ProgressionTier.Eikthyr, ["hildirboss2"] = ProgressionTier.Bonemass, ["hildirboss3"] = ProgressionTier.Moder }; public static bool TryGetRequiredTier(string eventName, IEnumerable<string> requiredGlobalKeys, out ProgressionTier tier) { bool flag = false; tier = ProgressionTier.None; if (requiredGlobalKeys != null) { foreach (string requiredGlobalKey in requiredGlobalKeys) { if (SpawnRuleRegistry.TryGetRequiredTier(requiredGlobalKey, out var tier2)) { if (!flag || tier2 > tier) { tier = tier2; } flag = true; } } } if (flag) { return true; } return ExplicitTiers.TryGetValue((eventName ?? string.Empty).Trim(), out tier); } public static bool IsProgressionExempt(string eventName, NativeBiome eventBiome) { if (string.Equals((eventName ?? string.Empty).Trim(), "army_charredspawners", StringComparison.OrdinalIgnoreCase)) { return (eventBiome & NativeBiome.Ashlands) != 0; } return false; } } public enum SpawnDecisionReason { AllowedDisabled, AllowedEvent, AllowedUnmanaged, AllowedGateDisabled, AllowedNativeBiome, AllowedNoNearbyPlayers, AllowedProgression, BlockedProgression } public sealed class SpawnDecision { public bool Block { get; } public SpawnDecisionReason Reason { get; } public ProgressionTier EffectiveTier { get; } public SpawnDecision(bool block, SpawnDecisionReason reason, ProgressionTier effectiveTier) { Block = block; Reason = reason; EffectiveTier = effectiveTier; } } public static class SpawnDecisionEngine { public static SpawnDecision Evaluate(SpawnRule rule, GateSettings settings, bool eventSpawner, bool inNativeBiome, IReadOnlyList<ProgressionTier> nearbyTiers) { if (settings == null || !settings.Enabled) { return Allow(SpawnDecisionReason.AllowedDisabled); } if (eventSpawner) { return Allow(SpawnDecisionReason.AllowedEvent); } if (rule == null) { return Allow(SpawnDecisionReason.AllowedUnmanaged); } if (!SpawnRuleRegistry.IsGateEnabled(rule, settings)) { return Allow(SpawnDecisionReason.AllowedGateDisabled); } if (inNativeBiome) { return Allow(SpawnDecisionReason.AllowedNativeBiome); } if (nearbyTiers == null || nearbyTiers.Count == 0) { return Allow(SpawnDecisionReason.AllowedNoNearbyPlayers); } ProgressionTier progressionTier = nearbyTiers[0]; for (int i = 1; i < nearbyTiers.Count; i++) { ProgressionTier progressionTier2 = nearbyTiers[i]; if (settings.UseLowestNearbyProgression) { if (progressionTier2 < progressionTier) { progressionTier = progressionTier2; } } else if (progressionTier2 > progressionTier) { progressionTier = progressionTier2; } } if (progressionTier >= rule.RequiredTier) { return new SpawnDecision(block: false, SpawnDecisionReason.AllowedProgression, progressionTier); } return new SpawnDecision(block: true, SpawnDecisionReason.BlockedProgression, progressionTier); } private static SpawnDecision Allow(SpawnDecisionReason reason) { return new SpawnDecision(block: false, reason, ProgressionTier.None); } } [Flags] public enum NativeBiome { Unknown = 0, Meadows = 1, BlackForest = 2, Swamp = 4, Mountain = 8, Plains = 0x10, Mistlands = 0x20, Ashlands = 0x40 } public sealed class SpawnRule { public string RequiredGlobalKey { get; } public string PrefabName { get; } public ProgressionTier RequiredTier { get; } public NativeBiome NativeBiome { get; } public SpawnRule(string requiredGlobalKey, string prefabName, ProgressionTier requiredTier, NativeBiome nativeBiome) { RequiredGlobalKey = requiredGlobalKey; PrefabName = prefabName; RequiredTier = requiredTier; NativeBiome = nativeBiome; } } public static class SpawnRuleRegistry { private static readonly Dictionary<string, SpawnRule> Rules = BuildRules(); private static readonly Dictionary<string, SpawnRule> CreatureSafetyRules = BuildCreatureSafetyRules(); public static bool TryGet(string requiredGlobalKey, string prefabName, out SpawnRule rule) { string key = BuildKey(requiredGlobalKey, prefabName); if (Rules.TryGetValue(key, out rule)) { return true; } if (NormalizePrefab(prefabName) == "odin") { rule = null; return false; } if (TryGetRequiredTier(requiredGlobalKey, out var tier)) { rule = new SpawnRule(requiredGlobalKey, prefabName, tier, NativeBiome.Unknown); return true; } rule = null; return false; } public static bool TryGetCreatureSafetyRule(string prefabName, out SpawnRule rule) { return CreatureSafetyRules.TryGetValue(NormalizePrefab(prefabName), out rule); } public static bool TryGetRequiredTier(string requiredGlobalKey, out ProgressionTier tier) { switch ((requiredGlobalKey ?? string.Empty).Trim().ToLowerInvariant()) { case "defeated_eikthyr": tier = ProgressionTier.Eikthyr; return true; case "defeated_gdking": tier = ProgressionTier.Elder; return true; case "defeated_bonemass": tier = ProgressionTier.Bonemass; return true; case "defeated_dragon": tier = ProgressionTier.Moder; return true; case "defeated_goblinking": tier = ProgressionTier.Yagluth; return true; case "defeated_queen": tier = ProgressionTier.Queen; return true; case "defeated_fader": tier = ProgressionTier.Fader; return true; default: tier = ProgressionTier.None; return false; } } public static bool IsGateEnabled(SpawnRule rule, GateSettings settings) { if (rule == null || settings == null) { return false; } return rule.RequiredTier switch { ProgressionTier.Yagluth => settings.GateYagluthRoamers, ProgressionTier.Queen => settings.GateQueenRoamers, ProgressionTier.Fader => settings.GateFaderRoamers, _ => settings.GateEarlyGameRoamers, }; } public static string NormalizePrefab(string value) { if (string.IsNullOrWhiteSpace(value)) { return string.Empty; } StringBuilder stringBuilder = new StringBuilder(value.Length); foreach (char c in value) { if (char.IsLetterOrDigit(c)) { stringBuilder.Append(char.ToLowerInvariant(c)); } } if (stringBuilder.Length >= "clone".Length && stringBuilder.ToString().EndsWith("clone", StringComparison.Ordinal)) { stringBuilder.Length -= "clone".Length; } return stringBuilder.ToString(); } private static Dictionary<string, SpawnRule> BuildRules() { Dictionary<string, SpawnRule> dictionary = new Dictionary<string, SpawnRule>(StringComparer.Ordinal); Add(dictionary, "defeated_eikthyr", "Greydwarf", ProgressionTier.Eikthyr, NativeBiome.BlackForest); Add(dictionary, "defeated_gdking", "Draugr", ProgressionTier.Elder, NativeBiome.Swamp); Add(dictionary, "defeated_gdking", "Greydwarf_Elite", ProgressionTier.Elder, NativeBiome.BlackForest); Add(dictionary, "defeated_gdking", "Greydwarf_Shaman", ProgressionTier.Elder, NativeBiome.BlackForest); Add(dictionary, "defeated_bonemass", "Skeleton", ProgressionTier.Bonemass, NativeBiome.BlackForest | NativeBiome.Swamp); Add(dictionary, "defeated_goblinking", "Goblin", ProgressionTier.Yagluth, NativeBiome.Plains); Add(dictionary, "defeated_queen", "Seeker", ProgressionTier.Queen, NativeBiome.Mistlands); Add(dictionary, "defeated_queen", "SeekerBrood", ProgressionTier.Queen, NativeBiome.Mistlands); Add(dictionary, "defeated_queen", "Tick", ProgressionTier.Queen, NativeBiome.Mistlands); Add(dictionary, "defeated_fader", "Charred_Archer", ProgressionTier.Fader, NativeBiome.Ashlands); Add(dictionary, "defeated_fader", "Charred_Melee", ProgressionTier.Fader, NativeBiome.Ashlands); return dictionary; } private static Dictionary<string, SpawnRule> BuildCreatureSafetyRules() { Dictionary<string, SpawnRule> dictionary = new Dictionary<string, SpawnRule>(StringComparer.Ordinal); foreach (SpawnRule value in Rules.Values) { dictionary[NormalizePrefab(value.PrefabName)] = value; } return dictionary; } private static void Add(Dictionary<string, SpawnRule> rules, string globalKey, string prefab, ProgressionTier tier, NativeBiome biome) { SpawnRule value = new SpawnRule(globalKey, prefab, tier,