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 ProgressGuard v0.1.0
ProgressGuard.dll
Decompiled 10 hours agousing 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.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Runtime.Serialization.Json; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Jotunn.Utils; using Microsoft.CodeAnalysis; using ProgressGuard.Commands; using ProgressGuard.Config; using ProgressGuard.Core; using ProgressGuard.Integration; using ProgressGuard.Networking; using ProgressGuard.Progression; using ProgressGuard.Spawning; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("jg224")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Per-character progression gates for Valheim ambient post-boss roamers.")] [assembly: AssemblyFileVersion("0.1.0.0")] [assembly: AssemblyInformationalVersion("0.1.0+5d1bc7e28a96d49e3ea7a369558302aecdad7a8c")] [assembly: AssemblyProduct("ProgressGuard")] [assembly: AssemblyTitle("ProgressGuard")] [assembly: AssemblyVersion("0.1.0.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 ProgressGuard { [BepInPlugin("jg224.progressguard", "ProgressGuard", "0.1.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [NetworkCompatibility(/*Could not decode attribute arguments.*/)] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "jg224.progressguard"; public const string PluginName = "ProgressGuard"; public const string PluginVersion = "0.1.0"; public const string JotunnGuid = "com.jotunn.jotunn"; public const string ZenBossStoneGuid = "ZenDragon.ZenBossStone"; private Harmony _harmony; internal static ManualLogSource Log { get; private set; } private void Awake() { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; ModConfig.Bind(((BaseUnityPlugin)this).Config); ZenBossStoneIntegration.Initialize(); _harmony = new Harmony("jg224.progressguard"); _harmony.PatchAll(typeof(Plugin).Assembly); AdminCommands.Register(); Log.LogInfo((object)("ProgressGuard v0.1.0 loaded. Required on server and every client. ZenBossStone " + ZenBossStoneIntegration.DetectedVersion + " detected.")); } private void OnDestroy() { ProgressionRegistry.Save(); ProgressionSync.Shutdown(); Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } 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 ProgressGuard.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}"); } } } internal static class RuntimeSpawnValidator { internal static bool ShouldAllow(SpawnData spawn, Vector3 spawnPoint, bool eventSpawner) { //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Unknown result type (might be due to invalid IL or missing references) 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_006a: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)monster == (Object)null || monster.IsEventCreature()) { 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 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_0015: 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_0028: 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) { return false; } 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); return biome != NativeBiome.Unknown; } catch (Exception ex) { Plugin.Debug("Biome resolution failed: " + ex.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), typeof(int), typeof(float) })] internal static class AmbientSpawnPatch { [HarmonyPrefix] private static bool Prefix(SpawnData critter, Vector3 spawnPoint, bool eventSpawner) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) 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."); } } } } } } } namespace ProgressGuard.Progression { public sealed class PlayerProgressRecord { public long PlayerId { get; } public string PlayerName { get; } public ProgressionTier Tier { get; } public PlayerProgressRecord(long playerId, string playerName, ProgressionTier tier) { PlayerId = playerId; PlayerName = playerName ?? string.Empty; Tier = tier; } } 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>(); } [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; } private const int SchemaVersion = 1; internal static List<PlayerProgressRecord> Load(long worldUid) { string path = GetPath(worldUid); if (!File.Exists(path)) { return new List<PlayerProgressRecord>(); } try { using FileStream stream = File.OpenRead(path); RegistryFile registryFile = (RegistryFile)new DataContractJsonSerializer(typeof(RegistryFile)).ReadObject(stream); if (registryFile == null || registryFile.SchemaVersion != 1 || registryFile.WorldUid != worldUid) { throw new SerializationException("Registry schema or world UID does not match."); } List<PlayerProgressRecord> list = new List<PlayerProgressRecord>(); if (registryFile.Players == null) { return list; } foreach (PlayerRecord player in registryFile.Players) { if (player.PlayerId > 0 && player.Tier >= 0 && player.Tier <= 7) { list.Add(new PlayerProgressRecord(player.PlayerId, player.PlayerName, (ProgressionTier)player.Tier)); } } return list; } catch (Exception arg) { string text = path + ".corrupt-" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture); try { File.Copy(path, text, overwrite: false); } catch { } Plugin.Log.LogError((object)$"Could not load progression registry '{path}'. Starting empty; preserved copy: '{text}'. {arg}"); return new List<PlayerProgressRecord>(); } } internal static void Save(long worldUid, IReadOnlyCollection<PlayerProgressRecord> records) { if (worldUid == 0L) { return; } string path = GetPath(worldUid); string text = path + ".tmp"; string destinationBackupFileName = path + ".bak"; try { Directory.CreateDirectory(Path.GetDirectoryName(path)); RegistryFile registryFile = new RegistryFile { SchemaVersion = 1, WorldUid = worldUid }; foreach (PlayerProgressRecord record in records) { registryFile.Players.Add(new PlayerRecord { PlayerId = record.PlayerId, PlayerName = record.PlayerName, Tier = (int)record.Tier }); } using (FileStream fileStream = File.Create(text)) { new DataContractJsonSerializer(typeof(RegistryFile)).WriteObject(fileStream, registryFile); fileStream.Flush(flushToDisk: true); } if (File.Exists(path)) { try { File.Replace(text, path, destinationBackupFileName); return; } catch { File.Copy(text, path, overwrite: true); File.Delete(text); return; } } File.Move(text, path); } catch (Exception arg) { Plugin.Log.LogError((object)$"Could not save progression registry '{path}': {arg}"); try { if (File.Exists(text)) { File.Delete(text); } } catch { } } } internal static string GetPath(long worldUid) { return Path.Combine(Paths.ConfigPath, "jg224.progressguard.world-" + worldUid.ToString(CultureInfo.InvariantCulture) + ".json"); } } internal static class ProgressionRegistry { private static readonly object Sync = new object(); private static readonly Dictionary<long, PlayerProgressRecord> Records = new Dictionary<long, PlayerProgressRecord>(); 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) { lock (Sync) { _worldUid = worldUid; Records.Clear(); foreach (PlayerProgressRecord item in ProgressionPersistence.Load(worldUid)) { if (item.PlayerId > 0) { Records[item.PlayerId] = item; } } } Plugin.Log.LogInfo((object)$"Progression registry loaded for world {worldUid}: {Count} player(s)."); } internal static bool Promote(long playerId, string playerName, ProgressionTier tier) { if (playerId <= 0 || tier <= ProgressionTier.None || tier > ProgressionTier.Fader) { return false; } lock (Sync) { if (Records.TryGetValue(playerId, out var value)) { ProgressionTier progressionTier = ((value.Tier > tier) ? value.Tier : tier); string text = (string.IsNullOrWhiteSpace(playerName) ? value.PlayerName : playerName); if (progressionTier == value.Tier && string.Equals(text, value.PlayerName, StringComparison.Ordinal)) { return false; } Records[playerId] = new PlayerProgressRecord(playerId, text, progressionTier); return progressionTier != value.Tier; } Records[playerId] = new PlayerProgressRecord(playerId, playerName, tier); return true; } } internal static bool Set(long playerId, string playerName, ProgressionTier tier) { if (playerId <= 0 || 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 (playerId > 0 && 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 void Save() { long worldUid; List<PlayerProgressRecord> records; lock (Sync) { worldUid = _worldUid; if (worldUid == 0L) { return; } records = Records.Values.ToList(); } ProgressionPersistence.Save(worldUid, records); } internal static void Clear() { lock (Sync) { Records.Clear(); _worldUid = 0L; } } } } namespace ProgressGuard.Networking { internal static class ClientProgressionState { private static readonly object Sync = new object(); private static Dictionary<long, ProgressionTier> _tiers = new Dictionary<long, ProgressionTier>(); 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) { lock (Sync) { WorldUid = worldUid; _settings = settings?.Clone() ?? new GateSettings(); OnlineSafetyTier = onlineSafetyTier; _tiers = tiers ?? new Dictionary<long, ProgressionTier>(); HasServerSnapshot = true; } ExistingCreatureSafetyPatch.SweepLoaded(); } internal static GateSettings GetSettings() { lock (Sync) { return _settings.Clone(); } } internal static ProgressionTier GetTier(long playerId) { lock (Sync) { ProgressionTier value; return (playerId > 0 && _tiers.TryGetValue(playerId, out value)) ? value : ProgressionTier.None; } } internal static void Reset() { lock (Sync) { WorldUid = 0L; OnlineSafetyTier = ProgressionTier.None; _settings = new GateSettings(); _tiers = new Dictionary<long, ProgressionTier>(); HasServerSnapshot = false; } } } internal static class LegacyProgressionImport { private const string ImportRpcName = "jg224.progressguard.ImportLegacyProgression"; internal static void Register(ZRoutedRpc rpc) { rpc.Register<int>("jg224.progressguard.ImportLegacyProgression", (Action<long, int>)OnImportRequest); } internal static void Submit(Player player) { if ((Object)(object)player == (Object)null || (Object)(object)player != (Object)(object)Player.m_localPlayer) { return; } ProgressionTier progressionTier = LegacyProgressionDetector.DetectHighest(player.GetUniqueKeys()); if (progressionTier == ProgressionTier.None) { return; } ZNet instance = ZNet.instance; ZRoutedRpc instance2 = ZRoutedRpc.instance; if (!((Object)(object)instance == (Object)null) && instance2 != null) { if (instance.IsServer()) { Apply(player, progressionTier); } else { ((MonoBehaviour)player).StartCoroutine(SubmitAfterReplication(player, progressionTier)); } } } private static IEnumerator SubmitAfterReplication(Player player, ProgressionTier tier) { yield return (object)new WaitForSeconds(2f); if (!((Object)(object)player == (Object)null) && !((Object)(object)player != (Object)(object)Player.m_localPlayer)) { ZNet instance = ZNet.instance; ZRoutedRpc instance2 = ZRoutedRpc.instance; if (!((Object)(object)instance == (Object)null) && instance2 != null && !instance.IsServer()) { instance2.InvokeRoutedRPC("jg224.progressguard.ImportLegacyProgression", new object[1] { (int)tier }); Plugin.Debug($"Submitted legacy ZenBossStone tier {tier} for local character {player.GetPlayerID()}."); } } } private static void OnImportRequest(long sender, int rawTier) { //IL_005a: Unknown result type (might be due to invalid IL or missing references) ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return; } if (rawTier <= 0 || rawTier > 7) { Plugin.Log.LogWarning((object)$"Rejected invalid legacy progression tier {rawTier} from peer {sender}."); return; } try { ZNetPeer peer = instance.GetPeer(sender); object obj; if (peer != null) { ZNetScene instance2 = ZNetScene.instance; obj = ((instance2 != null) ? instance2.FindInstance(peer.m_characterID) : null); } else { obj = null; } Player val = ((obj != null) ? ((GameObject)obj).GetComponent<Player>() : null); if (peer == null || (Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)$"Rejected legacy progression import from unresolved peer {sender}."); } else { Apply(val, (ProgressionTier)rawTier); } } catch (Exception arg) { Plugin.Log.LogError((object)$"Legacy progression import failed for peer {sender}: {arg}"); } } private static void Apply(Player player, ProgressionTier tier) { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return; } long playerID = player.GetPlayerID(); if (playerID <= 0) { Plugin.Log.LogWarning((object)$"Rejected legacy progression import for invalid PlayerID {playerID}."); return; } if (ProgressionRegistry.WorldUid == 0L) { ProgressionRegistry.LoadForWorld(instance.GetWorldUID()); } bool num = ProgressionRegistry.Promote(playerID, player.GetPlayerName(), tier); if (num) { ProgressionRegistry.Save(); } ProgressionSync.BroadcastState(); if (num) { Plugin.Log.LogInfo((object)("Imported legacy ZenBossStone progression for " + $"'{player.GetPlayerName()}' ({playerID}): {tier}.")); } } } [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(); ProgressionSync.Register(ZRoutedRpc.instance); } } [HarmonyPatch(typeof(Game), "Awake")] internal static class GameAwakePatch { [HarmonyPostfix] private static void Postfix() { ZNet instance = ZNet.instance; if (!((Object)(object)instance == (Object)null) && instance.IsServer()) { ProgressionRegistry.LoadForWorld(instance.GetWorldUID()); ProgressionSync.BroadcastState(); } } } [HarmonyPatch(typeof(Game), "OnDestroy")] internal static class GameDestroyPatch { [HarmonyPostfix] private static void Postfix() { ZNet instance = ZNet.instance; if ((Object)(object)instance != (Object)null && instance.IsServer()) { ProgressionRegistry.Save(); } ProgressionRegistry.Clear(); ClientProgressionState.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 ProgressionSync { private const int ProtocolVersion = 2; private const int MaximumRecords = 10000; private const string SyncRpcName = "jg224.progressguard.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.progressguard.SyncState", (Action<long, ZPackage>)OnSyncState); LegacyProgressionImport.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) { BroadcastState(); } internal static void BroadcastState() { ZNet instance = ZNet.instance; ZRoutedRpc instance2 = ZRoutedRpc.instance; if ((Object)(object)instance == (Object)null || instance2 == null || !instance.IsServer()) { return; } foreach (ZNetPeer connectedPeer in instance.GetConnectedPeers()) { if (connectedPeer != null) { SendState(connectedPeer.m_uid); } } ExistingCreatureSafetyPatch.SweepLoaded(); } internal static void SendState(long peerUid) { ZNet instance = ZNet.instance; ZRoutedRpc instance2 = ZRoutedRpc.instance; if ((Object)(object)instance == (Object)null || instance2 == null || !instance.IsServer()) { return; } try { ZPackage val = BuildPackage(); instance2.InvokeRoutedRPC(peerUid, "jg224.progressguard.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(); } internal static ProgressionTier GetOnlineSafetyTier() { //IL_006a: Unknown result type (might be due to invalid IL or missing references) 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; ZNetScene instance2 = ZNetScene.instance; GameObject obj = ((instance2 != null) ? instance2.FindInstance(connectedPeer.m_characterID) : null); Player val = ((obj != null) ? obj.GetComponent<Player>() : null); ProgressionTier progressionTier2 = ((!((Object)(object)val == (Object)null)) ? ProgressionRegistry.GetTier(val.GetPlayerID()) : 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(2); 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); } return val; } private static void OnSyncState(long sender, ZPackage package) { 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 != 2) { throw new InvalidOperationException($"Unsupported ProgressGuard protocol {num}; expected {2}."); } long num2 = package.ReadLong(); 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); for (int i = 0; i < num4; i++) { long num5 = package.ReadLong(); int num6 = package.ReadInt(); if (num5 > 0 && num6 >= 0 && num6 <= 7) { dictionary[num5] = (ProgressionTier)num6; } } ProgressionTier progressionTier = (ProgressionTier)num3; ClientProgressionState.Replace(num2, settings, progressionTier, dictionary); 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.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(), VerboseLogging = package.ReadBool(), LogBlockedSpawns = package.ReadBool(), LogAllowedProgressionSpawns = package.ReadBool() }; } } } namespace ProgressGuard.Integration { internal static class ZenBossStoneIntegration { private static FieldInfo _sacrificeRangeField; internal static MethodInfo SacrificeHandler { get; private set; } internal static string DetectedVersion { get; private set; } = "unknown"; internal static void Initialize() { if (Chainloader.PluginInfos.TryGetValue("ZenDragon.ZenBossStone", out var value)) { DetectedVersion = value.Metadata.Version?.ToString() ?? "unknown"; } Type type = AccessTools.TypeByName("ZenBossStone.State"); SacrificeHandler = ((type == null) ? null : AccessTools.Method(type, "RPC_BossStoneSacrifice", new Type[2] { typeof(long), typeof(ZDOID) }, (Type[])null)); Type type2 = AccessTools.TypeByName("ZenBossStone.Configs"); _sacrificeRangeField = ((type2 == null) ? null : AccessTools.Field(type2, "SacrificeTrophyRange")); if (SacrificeHandler == null) { Plugin.Log.LogError((object)"ZenBossStone RPC_BossStoneSacrifice(long, ZDOID) was not found. Progress observation is disabled."); } if (_sacrificeRangeField == null) { Plugin.Log.LogWarning((object)"ZenBossStone SacrificeTrophyRange was not found. Falling back to 25 meters."); } } internal static int GetSacrificeRange() { try { if (_sacrificeRangeField?.GetValue(null) is ConfigEntry<int> val) { return Math.Max(1, val.Value); } } catch (Exception ex) { Plugin.Debug("Could not read ZenBossStone sacrifice range: " + ex.Message); } return 25; } internal static bool TryResolveSacrifice(ZDOID itemStandId, out BossStone stone, out ProgressionTier tier, out string guardianPowerName) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) stone = null; tier = ProgressionTier.None; guardianPowerName = string.Empty; try { ZNetScene instance = ZNetScene.instance; if ((Object)(object)instance == (Object)null) { return false; } GameObject val = instance.FindInstance(itemStandId); if ((Object)(object)val == (Object)null) { return false; } stone = val.GetComponentInParent<BossStone>(); if ((Object)(object)stone == (Object)null || (Object)(object)stone.m_itemStand == (Object)null || (Object)(object)stone.m_itemStand.m_guardianPower == (Object)null) { return false; } guardianPowerName = ((Object)stone.m_itemStand.m_guardianPower).name ?? string.Empty; return BossTierMapping.TryMapGuardianPower(guardianPowerName, out tier); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not resolve ZenBossStone sacrifice target: " + ex.Message)); return false; } } } [HarmonyPatch] internal static class ZenBossStoneSacrificePatch { private static bool Prepare() { return ZenBossStoneIntegration.SacrificeHandler != null; } private static MethodBase TargetMethod() { return ZenBossStoneIntegration.SacrificeHandler; } [HarmonyPostfix] private static void Postfix(long sender, ZDOID itemStandID) { //IL_0019: 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_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00c7: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_00d0: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Unknown result type (might be due to invalid IL or missing references) ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return; } try { if (!ZenBossStoneIntegration.TryResolveSacrifice(itemStandID, out var stone, out var tier, out var guardianPowerName)) { Plugin.Log.LogWarning((object)$"ZenBossStone sacrifice could not be mapped for ZDO {itemStandID}."); return; } if (ProgressionRegistry.WorldUid == 0L) { ProgressionRegistry.LoadForWorld(instance.GetWorldUID()); } int sacrificeRange = ZenBossStoneIntegration.GetSacrificeRange(); float num = sacrificeRange * sacrificeRange; Vector3 position = ((Component)stone).transform.position; if (instance.IsDedicated() && !IsValidRemoteSacrifice(instance, sender, position, num)) { return; } int num2 = 0; int num3 = 0; foreach (Player allPlayer in Player.GetAllPlayers()) { if ((Object)(object)allPlayer == (Object)null) { continue; } Vector3 val = ((Component)allPlayer).transform.position - position; if (val.x * val.x + val.z * val.z > num) { continue; } long playerID = allPlayer.GetPlayerID(); if (playerID <= 0) { Plugin.Log.LogWarning((object)$"Skipped in-range player '{allPlayer.GetPlayerName()}' with invalid PlayerID {playerID}."); continue; } num2++; if (ProgressionRegistry.Promote(playerID, allPlayer.GetPlayerName(), tier)) { num3++; } } if (num3 > 0) { ProgressionRegistry.Save(); ProgressionSync.BroadcastState(); } Plugin.Log.LogInfo((object)($"ZenBossStone sacrifice '{guardianPowerName}' => {tier}; " + $"{num2} player(s) in {sacrificeRange}m range, {num3} progression record(s) advanced.")); } catch (Exception arg) { Plugin.Log.LogError((object)$"Progression observation failed after ZenBossStone sacrifice: {arg}"); } } private static bool IsValidRemoteSacrifice(ZNet znet, long sender, Vector3 stonePosition, float rangeSquared) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) ZNetPeer peer = znet.GetPeer(sender); if (peer == null) { Plugin.Log.LogWarning((object)$"Ignored ZenBossStone sacrifice from unknown routed peer {sender}."); return false; } ZNetScene instance = ZNetScene.instance; GameObject obj = ((instance != null) ? instance.FindInstance(peer.m_characterID) : null); Player val = ((obj != null) ? obj.GetComponent<Player>() : null); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)$"Ignored ZenBossStone sacrifice because peer {sender}'s player could not be resolved."); return false; } Vector3 val2 = ((Component)val).transform.position - stonePosition; if (val2.x * val2.x + val2.z * val2.z > rangeSquared) { Plugin.Log.LogWarning((object)("Ignored out-of-range ZenBossStone sacrifice from " + $"'{val.GetPlayerName()}' (peer {sender}).")); return false; } return true; } } } namespace ProgressGuard.Core { public static class BossTierMapping { public static bool TryMapGuardianPower(string guardianPowerName, out ProgressionTier tier) { string text = Normalize(guardianPowerName); if (text.Contains("eikthyr")) { return Found(ProgressionTier.Eikthyr, out tier); } if (text.Contains("theelder") || text == "elder" || text.EndsWith("elder", StringComparison.Ordinal)) { return Found(ProgressionTier.Elder, out tier); } if (text.Contains("bonemass")) { return Found(ProgressionTier.Bonemass, out tier); } if (text.Contains("moder")) { return Found(ProgressionTier.Moder, out tier); } if (text.Contains("yagluth") || text.Contains("goblinking")) { return Found(ProgressionTier.Yagluth, out tier); } if (text.Contains("queen")) { return Found(ProgressionTier.Queen, out tier); } if (text.Contains("fader")) { return Found(ProgressionTier.Fader, out tier); } tier = ProgressionTier.None; return false; } private static bool Found(ProgressionTier value, out ProgressionTier tier) { tier = value; return true; } private static string Normalize(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)); } } return stringBuilder.ToString(); } } 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 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, VerboseLogging = VerboseLogging, LogBlockedSpawns = LogBlockedSpawns, LogAllowedProgressionSpawns = LogAllowedProgressionSpawns }; } } public static class LegacyProgressionDetector { private static readonly KeyValuePair<string, ProgressionTier>[] GuardianPowerKeys = new KeyValuePair<string, ProgressionTier>[7] { 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) }; public static ProgressionTier DetectHighest(IEnumerable<string> uniqueKeys) { if (uniqueKeys == null) { return ProgressionTier.None; } HashSet<string> hashSet = new HashSet<string>(uniqueKeys, StringComparer.OrdinalIgnoreCase); ProgressionTier progressionTier = ProgressionTier.None; KeyValuePair<string, ProgressionTier>[] guardianPowerKeys = GuardianPowerKeys; for (int i = 0; i < guardianPowerKeys.Length; i++) { KeyValuePair<string, ProgressionTier> keyValuePair = guardianPowerKeys[i]; if (keyValuePair.Value > progressionTier && hashSet.Contains(keyValuePair.Key)) { progressionTier = keyValuePair.Value; } } return progressionTier; } } public enum ProgressionTier { None, Eikthyr, Elder, Bonemass, Moder, Yagluth, Queen, Fader } 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, biome); rules.Add(BuildKey(globalKey, prefab), value); } private static string BuildKey(string globalKey, string prefab) { return (globalKey ?? string.Empty).Trim().ToLowerInvariant() + "|" + NormalizePrefab(prefab); } } } namespace ProgressGuard.Config { internal static class ModConfig { internal static ConfigEntry<bool> Enabled; internal static ConfigEntry<float> NearbyPlayerRadius; internal static ConfigEntry<bool> ProtectLeastProgressedOnline; internal static ConfigEntry<bool> UseLowestNearbyProgression; internal static ConfigEntry<bool> GateYagluthRoamers; internal static ConfigEntry<bool> GateQueenRoamers; internal static ConfigEntry<bool> GateFaderRoamers; internal static ConfigEntry<bool> GateEarlyGameRoamers; internal static ConfigEntry<bool> VerboseLogging; internal static ConfigEntry<bool> LogBlockedSpawns; internal static ConfigEntry<bool> LogAllowedProgressionSpawns; internal static void Bind(ConfigFile config) { //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Expected O, but got Unknown Enabled = config.Bind<bool>("General", "Enabled", true, "Master switch. The server value is authoritative and is synchronized to clients."); NearbyPlayerRadius = config.Bind<float>("Progression", "NearbyPlayerRadius", 100f, new ConfigDescription("Players within this horizontal distance of an ambient spawn influence its progression gate.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(10f, 300f), Array.Empty<object>())); ProtectLeastProgressedOnline = config.Bind<bool>("Progression", "ProtectLeastProgressedOnline", true, "When true, the least-progressed currently connected character is the server-wide safety ceiling for boss-key ambient roamers."); UseLowestNearbyProgression = config.Bind<bool>("Progression", "UseLowestNearbyProgression", true, "Used only when ProtectLeastProgressedOnline is false. Chooses the least- or most-progressed nearby character."); GateYagluthRoamers = config.Bind<bool>("Spawn Gates", "GateYagluthRoamers", true, "Gate post-Yagluth Fulings outside the Plains."); GateQueenRoamers = config.Bind<bool>("Spawn Gates", "GateQueenRoamers", true, "Gate post-Queen Seekers, Seeker Broods, and Ticks outside the Mistlands."); GateFaderRoamers = config.Bind<bool>("Spawn Gates", "GateFaderRoamers", true, "Gate post-Fader Charred outside the Ashlands."); GateEarlyGameRoamers = config.Bind<bool>("Spawn Gates", "GateEarlyGameRoamers", true, "Gate the Eikthyr, Elder, Bonemass, and Moder boss-key ambient-roamer families."); VerboseLogging = config.Bind<bool>("Debug", "VerboseLogging", false, "Log protocol, discovery, and validation detail at Info level."); LogBlockedSpawns = config.Bind<bool>("Debug", "LogBlockedSpawns", true, "Log each managed ambient spawn blocked for progression."); LogAllowedProgressionSpawns = config.Bind<bool>("Debug", "LogAllowedProgressionSpawns", false, "Log managed ambient spawns allowed by nearby progression. This can be noisy."); Subscribe<bool>(Enabled); Subscribe<float>(NearbyPlayerRadius); Subscribe<bool>(ProtectLeastProgressedOnline); Subscribe<bool>(UseLowestNearbyProgression); Subscribe<bool>(GateYagluthRoamers); Subscribe<bool>(GateQueenRoamers); Subscribe<bool>(GateFaderRoamers); Subscribe<bool>(GateEarlyGameRoamers); Subscribe<bool>(VerboseLogging); Subscribe<bool>(LogBlockedSpawns); Subscribe<bool>(LogAllowedProgressionSpawns); } internal static GateSettings Snapshot() { return new GateSettings { Enabled = Enabled.Value, NearbyPlayerRadius = NearbyPlayerRadius.Value, ProtectLeastProgressedOnline = ProtectLeastProgressedOnline.Value, UseLowestNearbyProgression = UseLowestNearbyProgression.Value, GateYagluthRoamers = GateYagluthRoamers.Value, GateQueenRoamers = GateQueenRoamers.Value, GateFaderRoamers = GateFaderRoamers.Value, GateEarlyGameRoamers = GateEarlyGameRoamers.Value, VerboseLogging = VerboseLogging.Value, LogBlockedSpawns = LogBlockedSpawns.Value, LogAllowedProgressionSpawns = LogAllowedProgressionSpawns.Value }; } private static void Subscribe<T>(ConfigEntry<T> entry) { entry.SettingChanged += OnSettingChanged; } private static void OnSettingChanged(object sender, EventArgs args) { ProgressionSync.BroadcastState(); } } } namespace ProgressGuard.Commands { internal static class AdminCommands { [CompilerGenerated] private static class <>O { public static ConsoleEvent <0>__Run; public static Func<PlayerProgressRecord, string> <1>__FormatRecord; } private static bool _registered; internal static void Register() { //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Expected O, but got Unknown if (_registered) { return; } try { object obj = <>O.<0>__Run; if (obj == null) { ConsoleEvent val = Run; <>O.<0>__Run = val; obj = (object)val; } new ConsoleCommand("pg", "ProgressGuard admin: pg list | get <id|name> | set <id> <tier> [name] | reset <id> | reload", (ConsoleEvent)obj, false, false, true, false, false, (ConsoleOptionsFetcher)null, false, true, true); _registered = true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not register ProgressGuard admin command: " + ex.Message)); } } private static void Run(ConsoleEventArgs args) { try { if (args.Length < 2) { Reply(args, "Usage: pg list | get <id|name> | set <id> <tier> [name] | reset <id> | reload"); return; } string text = args[1].ToLowerInvariant(); switch (text) { case "list": List(args); break; case "get": Get(args); break; case "set": Set(args); break; case "reset": Reset(args); break; case "reload": Reload(args); break; default: Reply(args, "Unknown subcommand '" + text + "'."); break; } } catch (Exception ex) { Reply(args, "ProgressGuard command failed: " + ex.Message); } } private static void List(ConsoleEventArgs args) { List<PlayerProgressRecord> list = ProgressionRegistry.Snapshot(); Reply(args, (list.Count == 0) ? "ProgressGuard registry is empty." : string.Join("\n", list.Select(FormatRecord))); } private static void Get(ConsoleEventArgs args) { if (args.Length < 3) { Reply(args, "Usage: pg get <playerID|name>"); return; } PlayerProgressRecord playerProgressRecord = ProgressionRegistry.Find(args[2]); Reply(args, (playerProgressRecord == null) ? ("No progression record for '" + args[2] + "'.") : FormatRecord(playerProgressRecord)); } private static void Set(ConsoleEventArgs args) { if (args.Length < 4 || !long.TryParse(args[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) || !TryParseTier(args[3], out var tier)) { Reply(args, "Usage: pg set <playerID> <None|Eikthyr|Elder|Bonemass|Moder|Yagluth|Queen|Fader|0-7> [name]"); return; } string text = ((args.Length >= 5) ? args[4] : (ProgressionRegistry.Find(args[2])?.PlayerName ?? string.Empty)); if (!ProgressionRegistry.Set(result, text, tier)) { Reply(args, "Invalid player ID or tier."); return; } Commit(); Reply(args, $"Set {result} ({text}) to {tier}."); } private static void Reset(ConsoleEventArgs args) { if (args.Length < 3 || !long.TryParse(args[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { Reply(args, "Usage: pg reset <playerID>"); return; } bool flag = ProgressionRegistry.Reset(result); Commit(); Reply(args, flag ? $"Reset progression for {result}." : $"No record existed for {result}."); } private static void Reload(ConsoleEventArgs args) { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { Reply(args, "Registry reload is server-only."); return; } ProgressionRegistry.LoadForWorld(instance.GetWorldUID()); ProgressionSync.BroadcastState(); Reply(args, $"Reloaded {ProgressionRegistry.Count} progression record(s)."); } private static void Commit() { ProgressionRegistry.Save(); ProgressionSync.BroadcastState(); } private static bool TryParseTier(string value, out ProgressionTier tier) { if (Enum.TryParse<ProgressionTier>(value, ignoreCase: true, out tier) && tier >= ProgressionTier.None && tier <= ProgressionTier.Fader) { return true; } if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) && result >= 0 && result <= 7) { tier = (ProgressionTier)result; return true; } tier = ProgressionTier.None; return false; } private static string FormatRecord(PlayerProgressRecord record) { return $"{record.PlayerId} | {record.PlayerName} | {record.Tier} ({(int)record.Tier})"; } private static void Reply(ConsoleEventArgs args, string message) { Terminal context = args.Context; if (context != null) { context.AddString(message); } Plugin.Log.LogInfo((object)("[admin] " + message)); } } }