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.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 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() : 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 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(); 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 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 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 { item }; } return GetNearbyTiers(spawnPoint, settings.NearbyPlayerRadius); } private static List 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 list = new List(); 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 Seen = new HashSet(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 Players = new List(); } [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 Load(long worldUid) { string path = GetPath(worldUid); if (!File.Exists(path)) { return new List(); } 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 list = new List(); 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(); } } internal static void Save(long worldUid, IReadOnlyCollection 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 Records = new Dictionary(); 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 Snapshot() { lock (Sync) { return Records.Values.OrderBy((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 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 _tiers = new Dictionary(); 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 tiers) { lock (Sync) { WorldUid = worldUid; _settings = settings?.Clone() ?? new GateSettings(); OnlineSafetyTier = onlineSafetyTier; _tiers = tiers ?? new Dictionary(); 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(); HasServerSnapshot = false; } } } internal static class LegacyProgressionImport { private const string ImportRpcName = "jg224.progressguard.ImportLegacyProgression"; internal static void Register(ZRoutedRpc rpc) { rpc.Register("jg224.progressguard.ImportLegacyProgression", (Action)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() : 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 _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)Delegate.Remove(registeredInstance.m_onNewPeer, _newPeerHandler); } _newPeerHandler = null; rpc.Register("jg224.progressguard.SyncState", (Action)OnSyncState); LegacyProgressionImport.Register(rpc); _registeredInstance = rpc; Plugin.Debug("Registered progression synchronization and legacy-import routed RPCs."); } if (_newPeerHandler != null) { rpc.m_onNewPeer = (Action)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)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)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() : 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 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 dictionary = new Dictionary(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 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(); 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() : 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[] GuardianPowerKeys = new KeyValuePair[7] { new KeyValuePair("GP_Eikthyr", ProgressionTier.Eikthyr), new KeyValuePair("GP_TheElder", ProgressionTier.Elder), new KeyValuePair("GP_Bonemass", ProgressionTier.Bonemass), new KeyValuePair("GP_Moder", ProgressionTier.Moder), new KeyValuePair("GP_Yagluth", ProgressionTier.Yagluth), new KeyValuePair("GP_Queen", ProgressionTier.Queen), new KeyValuePair("GP_Fader", ProgressionTier.Fader) }; public static ProgressionTier DetectHighest(IEnumerable uniqueKeys) { if (uniqueKeys == null) { return ProgressionTier.None; } HashSet hashSet = new HashSet(uniqueKeys, StringComparer.OrdinalIgnoreCase); ProgressionTier progressionTier = ProgressionTier.None; KeyValuePair[] guardianPowerKeys = GuardianPowerKeys; for (int i = 0; i < guardianPowerKeys.Length; i++) { KeyValuePair 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 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 Rules = BuildRules(); private static readonly Dictionary 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 BuildRules() { Dictionary dictionary = new Dictionary(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 BuildCreatureSafetyRules() { Dictionary dictionary = new Dictionary(StringComparer.Ordinal); foreach (SpawnRule value in Rules.Values) { dictionary[NormalizePrefab(value.PrefabName)] = value; } return dictionary; } private static void Add(Dictionary 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 Enabled; internal static ConfigEntry NearbyPlayerRadius; internal static ConfigEntry ProtectLeastProgressedOnline; internal static ConfigEntry UseLowestNearbyProgression; internal static ConfigEntry GateYagluthRoamers; internal static ConfigEntry GateQueenRoamers; internal static ConfigEntry GateFaderRoamers; internal static ConfigEntry GateEarlyGameRoamers; internal static ConfigEntry VerboseLogging; internal static ConfigEntry LogBlockedSpawns; internal static ConfigEntry 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("General", "Enabled", true, "Master switch. The server value is authoritative and is synchronized to clients."); NearbyPlayerRadius = config.Bind("Progression", "NearbyPlayerRadius", 100f, new ConfigDescription("Players within this horizontal distance of an ambient spawn influence its progression gate.", (AcceptableValueBase)(object)new AcceptableValueRange(10f, 300f), Array.Empty())); ProtectLeastProgressedOnline = config.Bind("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("Progression", "UseLowestNearbyProgression", true, "Used only when ProtectLeastProgressedOnline is false. Chooses the least- or most-progressed nearby character."); GateYagluthRoamers = config.Bind("Spawn Gates", "GateYagluthRoamers", true, "Gate post-Yagluth Fulings outside the Plains."); GateQueenRoamers = config.Bind("Spawn Gates", "GateQueenRoamers", true, "Gate post-Queen Seekers, Seeker Broods, and Ticks outside the Mistlands."); GateFaderRoamers = config.Bind("Spawn Gates", "GateFaderRoamers", true, "Gate post-Fader Charred outside the Ashlands."); GateEarlyGameRoamers = config.Bind("Spawn Gates", "GateEarlyGameRoamers", true, "Gate the Eikthyr, Elder, Bonemass, and Moder boss-key ambient-roamer families."); VerboseLogging = config.Bind("Debug", "VerboseLogging", false, "Log protocol, discovery, and validation detail at Info level."); LogBlockedSpawns = config.Bind("Debug", "LogBlockedSpawns", true, "Log each managed ambient spawn blocked for progression."); LogAllowedProgressionSpawns = config.Bind("Debug", "LogAllowedProgressionSpawns", false, "Log managed ambient spawns allowed by nearby progression. This can be noisy."); Subscribe(Enabled); Subscribe(NearbyPlayerRadius); Subscribe(ProtectLeastProgressedOnline); Subscribe(UseLowestNearbyProgression); Subscribe(GateYagluthRoamers); Subscribe(GateQueenRoamers); Subscribe(GateFaderRoamers); Subscribe(GateEarlyGameRoamers); Subscribe(VerboseLogging); Subscribe(LogBlockedSpawns); Subscribe(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(ConfigEntry 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 <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 | set [name] | reset | 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 | set [name] | reset | 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 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 "); 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 [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 "); 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(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)); } } }