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.Configuration; using BepInEx.Logging; using HarmonyLib; using Jotunn.Utils; using Microsoft.CodeAnalysis; using TMPro; using UnityEngine; using UnityEngine.UI; using WorldStageDirector.Bosses; using WorldStageDirector.Commands; using WorldStageDirector.Config; using WorldStageDirector.Core; using WorldStageDirector.Networking; using WorldStageDirector.Progression; using WorldStageDirector.Spawning; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("jg224")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyDescription("Server-side progression control. Protects players from content beyond their progress and provides expanded boss-stone progression management.")] [assembly: AssemblyFileVersion("0.2.6.0")] [assembly: AssemblyInformationalVersion("0.2.6+6f063ac1b01544e66ff3a9b72dea5a44b443c208")] [assembly: AssemblyProduct("WorldStageDirector")] [assembly: AssemblyTitle("WorldStageDirector")] [assembly: AssemblyVersion("0.2.6.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace WorldStageDirector { [BepInPlugin("jg224.worldstagedirector", "WorldStageDirector", "0.2.6")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInIncompatibility("ZenDragon.ZenBossStone")] [BepInIncompatibility("jg224.progressguard")] [NetworkCompatibility(/*Could not decode attribute arguments.*/)] public sealed class Plugin : BaseUnityPlugin { public const string PluginGuid = "jg224.worldstagedirector"; internal const string LegacyPluginGuid = "jg224.progressguard"; public const string PluginName = "WorldStageDirector"; public const string PluginVersion = "0.2.6"; public const string JotunnGuid = "com.jotunn.jotunn"; private Harmony _harmony; internal static ManualLogSource Log { get; private set; } private void Awake() { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; LegacyIdentityMigration.TryMigrateConfig(((BaseUnityPlugin)this).Config); ModConfig.Bind(((BaseUnityPlugin)this).Config); _harmony = new Harmony("jg224.worldstagedirector"); _harmony.PatchAll(typeof(Plugin).Assembly); AdminCommands.Register(); Log.LogInfo((object)"WorldStageDirector v0.2.6 loaded. Required on server and every client. Boss deaths now advance the world only after an eligible trophy sacrifice."); } 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 WorldStageDirector.Spawning { [HarmonyPatch(typeof(MonsterAI), "Start")] internal static class ExistingCreatureSafetyPatch { [HarmonyPostfix] private static void Postfix(MonsterAI __instance) { TryRemove(__instance); } internal static void SweepLoaded() { BaseAI[] array = BaseAI.GetAllInstances().ToArray(); foreach (BaseAI obj in array) { MonsterAI val = (MonsterAI)(object)((obj is MonsterAI) ? obj : null); if (val != null) { TryRemove(val); } } } private static void TryRemove(MonsterAI monster) { //IL_0052: Unknown result type (might be due to invalid IL or missing references) try { ZNetView val = ((monster != null) ? ((Component)monster).GetComponent() : null); if (!((Object)(object)val == (Object)null) && val.IsValid() && val.IsOwner() && RuntimeSpawnValidator.ShouldRemoveExisting(monster)) { Plugin.Log.LogWarning((object)("Removing out-of-progression existing creature " + $"'{((Object)((Component)monster).gameObject).name}' at {((Component)monster).transform.position}.")); val.Destroy(); } } catch (Exception arg) { Plugin.Log.LogError((object)$"Existing-creature safety check failed: {arg}"); } } } [HarmonyPatch(typeof(RandEventSystem), "SetRandomEvent", new Type[] { typeof(RandomEvent), typeof(Vector3) })] internal static class RaidProgressionSafetyPatch { [HarmonyPrefix] private static void Prefix(ref RandomEvent ev, Vector3 pos) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) if (ev != null && !ShouldAllow(ev, pos, out var _, out var _)) { RaidRuleRegistry.TryGetRequiredTier(ev.m_name, ev.m_requiredGlobalKeys, out var tier); ProgressionTier onlineSafetyTier = ProgressionSync.GetOnlineSafetyTier(); Plugin.Log.LogWarning((object)("Blocked random raid '" + ev.m_name + "': least-progressed online tier " + $"{onlineSafetyTier}, required {tier}.")); ev = null; } } internal static void EnforceActiveEvent() { //IL_0033: Unknown result type (might be due to invalid IL or missing references) ZNet instance = ZNet.instance; RandEventSystem instance2 = RandEventSystem.instance; if (!((Object)(object)instance == (Object)null) && instance.IsServer() && !((Object)(object)instance2 == (Object)null)) { RandomEvent currentRandomEvent = instance2.GetCurrentRandomEvent(); if (currentRandomEvent != null && !ShouldAllow(currentRandomEvent, currentRandomEvent.m_pos, out var requiredTier, out var onlineTier)) { Plugin.Log.LogWarning((object)("Stopped random raid '" + currentRandomEvent.m_name + "' after online progression changed: " + $"least-progressed online tier {onlineTier}, required {requiredTier}.")); instance2.ResetRandomEvent(); } } } private static bool ShouldAllow(RandomEvent ev, Vector3 eventPosition, out ProgressionTier requiredTier, out ProgressionTier onlineTier) { //IL_0043: Unknown result type (might be due to invalid IL or missing references) requiredTier = ProgressionTier.None; onlineTier = ProgressionTier.None; ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer() || ev == null || !ev.m_random) { return true; } GateSettings gateSettings = ModConfig.Snapshot(); if (!gateSettings.Enabled || !gateSettings.LimitRaidsToOnlineProgression) { return true; } if (IsAshlandsTormentSpawnerRaid(ev, eventPosition)) { return true; } if (!RaidRuleRegistry.TryGetRequiredTier(ev.m_name, ev.m_requiredGlobalKeys, out requiredTier)) { Plugin.Debug("Random raid '" + ev.m_name + "' has no managed boss-tier rule; vanilla eligibility remains authoritative."); return true; } onlineTier = ProgressionSync.GetOnlineSafetyTier(); return onlineTier >= requiredTier; } private static bool IsAshlandsTormentSpawnerRaid(RandomEvent ev, Vector3 eventPosition) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) if (RuntimeSpawnValidator.TryGetBiome(eventPosition, out var biome)) { return RaidRuleRegistry.IsProgressionExempt(ev.m_name, biome); } return false; } } internal static class RuntimeSpawnValidator { internal static bool ShouldAllow(SpawnData spawn, Vector3 spawnPoint, bool eventSpawner) { //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) if (!HasAuthoritativeState()) { return true; } if (spawn == null || (Object)(object)spawn.m_prefab == (Object)null) { return true; } if (!SpawnRuleRegistry.TryGet(spawn.m_requiredGlobalKey, ((Object)spawn.m_prefab).name, out var rule)) { return true; } GateSettings effectiveSettings = GetEffectiveSettings(); if (!effectiveSettings.Enabled || eventSpawner || !SpawnRuleRegistry.IsGateEnabled(rule, effectiveSettings)) { return true; } NativeBiome biome; bool flag = TryGetBiome(spawnPoint, out biome); bool inNativeBiome = flag && rule.NativeBiome != NativeBiome.Unknown && (rule.NativeBiome & biome) != 0; List 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_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) if (!HasAuthoritativeState()) { return false; } 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; } internal static bool HasAuthoritativeState() { ZNet instance = ZNet.instance; bool num = (Object)(object)instance != (Object)null; bool isServer = num && instance.IsServer(); return AuthoritativeStatePolicy.CanEnforce(num, isServer, ClientProgressionState.HasServerSnapshot); } private static GateSettings GetEffectiveSettings() { ZNet instance = ZNet.instance; if (!((Object)(object)instance != (Object)null) || !instance.IsServer()) { return ClientProgressionState.GetSettings(); } return ModConfig.Snapshot(); } private static List 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_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) biome = NativeBiome.Unknown; ZoneSystem instance = ZoneSystem.instance; if ((Object)(object)instance != (Object)null) { try { Vector3 val = spawnPoint; Vector3 val2 = default(Vector3); Biome biome2 = default(Biome); BiomeArea val3 = default(BiomeArea); Heightmap val4 = default(Heightmap); instance.GetGroundData(ref val, ref val2, ref biome2, ref val3, ref val4); biome = ConvertBiome(biome2); if (biome != NativeBiome.Unknown) { return true; } } catch (Exception ex) { Plugin.Debug("Loaded-zone biome resolution failed: " + ex.Message); } } WorldGenerator instance2 = WorldGenerator.instance; if (instance2 == null) { return false; } try { biome = ConvertBiome(instance2.GetBiome(spawnPoint)); return biome != NativeBiome.Unknown; } catch (Exception ex2) { Plugin.Debug("World biome resolution failed: " + ex2.Message); return false; } } private static NativeBiome ConvertBiome(Biome biome) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) NativeBiome nativeBiome = NativeBiome.Unknown; if ((biome & 1) != 0) { nativeBiome |= NativeBiome.Meadows; } if ((biome & 8) != 0) { nativeBiome |= NativeBiome.BlackForest; } if ((biome & 2) != 0) { nativeBiome |= NativeBiome.Swamp; } if ((biome & 4) != 0) { nativeBiome |= NativeBiome.Mountain; } if ((biome & 0x10) != 0) { nativeBiome |= NativeBiome.Plains; } if ((biome & 0x200) != 0) { nativeBiome |= NativeBiome.Mistlands; } if ((biome & 0x20) != 0) { nativeBiome |= NativeBiome.Ashlands; } return nativeBiome; } } [HarmonyPatch(typeof(SpawnSystem), "Spawn", new Type[] { typeof(SpawnData), typeof(Vector3), typeof(bool), typeof(int), typeof(float) })] internal static class AmbientSpawnPatch { [HarmonyPrefix] private static bool Prefix(SpawnData critter, Vector3 spawnPoint, bool eventSpawner) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) if (!TormentSpawnerSafety.ShouldAllowSpawn(critter, spawnPoint)) { return false; } if (eventSpawner) { return true; } return RuntimeSpawnValidator.ShouldAllow(critter, spawnPoint, eventSpawner: false); } } [HarmonyPatch(typeof(SpawnSystem), "Awake")] internal static class SpawnRuleDiscoveryPatch { private static readonly HashSet 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."); } } } } } } internal static class TormentSpawnerSafety { private const string EventPrefab = "spawnercharredstoneevent"; internal static bool ShouldAllowSpawn(SpawnData spawn, Vector3 spawnPoint) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)spawn?.m_prefab == (Object)null || !IsEventTormentSpawner(((Object)spawn.m_prefab).name)) { return true; } if (!TryGetBlockReason(spawnPoint, out var reason)) { return true; } if (GetEffectiveSettings().LogBlockedSpawns) { Plugin.Log.LogInfo((object)$"Blocked event Monument of Torment at {spawnPoint}: {reason}."); } return false; } internal static bool IsEventTormentSpawner(string prefabName) { return string.Equals(SpawnRuleRegistry.NormalizePrefab(prefabName), "spawnercharredstoneevent", StringComparison.Ordinal); } internal static bool TryGetBlockReason(Vector3 position, out string reason) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) reason = string.Empty; if (!RuntimeSpawnValidator.HasAuthoritativeState()) { return false; } GateSettings effectiveSettings = GetEffectiveSettings(); if (!effectiveSettings.Enabled) { return false; } if (effectiveSettings.BlockTormentSpawnersOutsideAshlands) { NativeBiome biome; bool flag = RuntimeSpawnValidator.TryGetBiome(position, out biome); if (!flag || (biome & NativeBiome.Ashlands) == 0) { reason = (flag ? $"biome is {biome}, not Ashlands" : "biome could not be proven to be Ashlands"); return true; } } return false; } private static GateSettings GetEffectiveSettings() { ZNet instance = ZNet.instance; if (!((Object)(object)instance != (Object)null) || !instance.IsServer()) { return ClientProgressionState.GetSettings(); } return ModConfig.Snapshot(); } } [HarmonyPatch(typeof(SpawnArea), "Awake")] internal static class ExistingTormentSpawnerSafetyPatch { [HarmonyPostfix] private static void Postfix(SpawnArea __instance) { TryRemove(__instance); } internal static void SweepLoaded() { SpawnArea[] array = Resources.FindObjectsOfTypeAll(); for (int i = 0; i < array.Length; i++) { TryRemove(array[i]); } } private static void TryRemove(SpawnArea spawnArea) { //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) try { if ((Object)(object)spawnArea == (Object)null) { return; } ZNetView val = ((Component)spawnArea).GetComponent() ?? ((Component)spawnArea).GetComponentInParent(); if (!((Object)(object)val == (Object)null) && val.IsValid() && val.IsOwner()) { string text = ResolvePrefabName(val, ((Object)((Component)spawnArea).gameObject).name); if (TormentSpawnerSafety.IsEventTormentSpawner(text) && TormentSpawnerSafety.TryGetBlockReason(((Component)spawnArea).transform.position, out var reason)) { Plugin.Log.LogWarning((object)("Removing event Monument of Torment '" + text + "' at " + $"{((Component)spawnArea).transform.position}: {reason}.")); val.Destroy(); } } } catch (Exception arg) { Plugin.Log.LogError((object)$"Existing Monument of Torment safety check failed: {arg}"); } } private static string ResolvePrefabName(ZNetView networkView, string fallback) { ZDO zDO = networkView.GetZDO(); object obj; if (zDO != null) { ZNetScene instance = ZNetScene.instance; obj = ((instance != null) ? instance.GetPrefab(zDO.GetPrefab()) : null); } else { obj = null; } return ((obj != null) ? ((Object)obj).name : null) ?? ((Object)((Component)networkView).gameObject).name ?? fallback; } } } namespace WorldStageDirector.Progression { public sealed class PlayerProgressRecord { public long PlayerId { get; } public string PlayerName { get; } public ProgressionTier Tier { get; } public BossCreditFlags KillReceipts { get; } public BossCreditFlags BossCredits { get; } public PlayerProgressRecord(long playerId, string playerName, ProgressionTier tier) : this(playerId, playerName, tier, BossCreditFlags.None, BossDefinitionRegistry.AllThrough(tier)) { } public PlayerProgressRecord(long playerId, string playerName, ProgressionTier tier, BossCreditFlags killReceipts, BossCreditFlags bossCredits) { PlayerId = playerId; PlayerName = playerName ?? string.Empty; Tier = tier; KillReceipts = killReceipts; BossCredits = bossCredits; } } internal static class ProgressionPersistence { [DataContract] private sealed class RegistryFile { [DataMember(Name = "schemaVersion", Order = 1)] public int SchemaVersion; [DataMember(Name = "worldUid", Order = 2)] public long WorldUid; [DataMember(Name = "players", Order = 3)] public List 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; [DataMember(Name = "killReceipts", Order = 4)] public int KillReceipts; [DataMember(Name = "bossCredits", Order = 5)] public int BossCredits; } private const int SchemaVersion = 2; internal static List Load(long worldUid) { string text = GetPath(worldUid); string legacyPath = GetLegacyPath(worldUid); if (IdentityPaths.ShouldMigrate(File.Exists(text), File.Exists(legacyPath))) { try { File.Copy(legacyPath, text, overwrite: false); Plugin.Log.LogInfo((object)("Copied legacy progression registry from '" + legacyPath + "' to '" + text + "'.")); } catch (Exception ex) { text = legacyPath; Plugin.Log.LogWarning((object)("Could not copy legacy progression registry to its new filename. Loading the preserved legacy file instead. " + ex.Message)); } } if (!File.Exists(text)) { return new List(); } try { using FileStream stream = File.OpenRead(text); RegistryFile registryFile = (RegistryFile)new DataContractJsonSerializer(typeof(RegistryFile)).ReadObject(stream); if (registryFile == null || (registryFile.SchemaVersion != 1 && registryFile.SchemaVersion != 2) || 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) { ProgressionTier tier = (ProgressionTier)player.Tier; if (BossProgressionMigration.TryNormalize(registryFile.SchemaVersion, tier, player.KillReceipts, player.BossCredits, out var killReceipts, out var bossCredits)) { list.Add(new PlayerProgressRecord(player.PlayerId, player.PlayerName, tier, killReceipts, bossCredits)); } } } return list; } catch (Exception arg) { string text2 = text + ".corrupt-" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture); try { File.Copy(text, text2, overwrite: false); } catch { } Plugin.Log.LogError((object)$"Could not load progression registry '{text}'. Starting empty; preserved copy: '{text2}'. {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 = 2, WorldUid = worldUid }; foreach (PlayerProgressRecord record in records) { registryFile.Players.Add(new PlayerRecord { PlayerId = record.PlayerId, PlayerName = record.PlayerName, Tier = (int)record.Tier, KillReceipts = (int)record.KillReceipts, BossCredits = (int)record.BossCredits }); } using (FileStream fileStream = File.Create(text)) { 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, IdentityPaths.RegistryFileName("jg224.worldstagedirector", worldUid)); } internal static string GetLegacyPath(long worldUid) { return Path.Combine(Paths.ConfigPath, IdentityPaths.RegistryFileName("jg224.progressguard", worldUid)); } } internal static class ProgressionRegistry { private static readonly object Sync = new object(); private static readonly Dictionary 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) { return ImportLegacy(playerId, playerName, BossDefinitionRegistry.AllThrough(tier)); } internal static bool ImportLegacy(long playerId, string playerName, BossCreditFlags importedCredits) { if (playerId <= 0 || importedCredits == BossCreditFlags.None || !BossDefinitionRegistry.IsValid(importedCredits)) { return false; } ProgressionTier progressionTier = BossDefinitionRegistry.HighestTier(importedCredits); lock (Sync) { if (Records.TryGetValue(playerId, out var value)) { ProgressionTier progressionTier2 = ((value.Tier > progressionTier) ? value.Tier : progressionTier); string text = (string.IsNullOrWhiteSpace(playerName) ? value.PlayerName : playerName); BossCreditFlags bossCreditFlags = value.BossCredits | importedCredits; BossCreditFlags bossCreditFlags2 = BossProgressionRules.OpenReceipts(value.KillReceipts, bossCreditFlags); if (progressionTier2 == value.Tier && bossCreditFlags2 == value.KillReceipts && bossCreditFlags == value.BossCredits && string.Equals(text, value.PlayerName, StringComparison.Ordinal)) { return false; } Records[playerId] = new PlayerProgressRecord(playerId, text, progressionTier2, bossCreditFlags2, bossCreditFlags); return true; } Records[playerId] = new PlayerProgressRecord(playerId, playerName, progressionTier, BossCreditFlags.None, importedCredits); return true; } } internal static bool RecordKill(long playerId, string playerName, ProgressionTier tier) { if (playerId <= 0 || !BossDefinitionRegistry.TryFromTier(tier, out var _)) { return false; } lock (Sync) { BossCreditFlags bossCreditFlags = BossDefinitionRegistry.FlagFor(tier); if (Records.TryGetValue(playerId, out var value)) { string text = (string.IsNullOrWhiteSpace(playerName) ? value.PlayerName : playerName); if (BossDefinitionRegistry.Contains(value.BossCredits, tier)) { return false; } if ((value.KillReceipts & bossCreditFlags) == bossCreditFlags && string.Equals(text, value.PlayerName, StringComparison.Ordinal)) { return false; } Records[playerId] = new PlayerProgressRecord(playerId, text, value.Tier, value.KillReceipts | bossCreditFlags, value.BossCredits); return true; } Records[playerId] = new PlayerProgressRecord(playerId, playerName, ProgressionTier.None, bossCreditFlags, BossCreditFlags.None); return true; } } internal static bool GrantBossCredit(long playerId, string playerName, ProgressionTier tier) { if (playerId <= 0 || !BossDefinitionRegistry.TryFromTier(tier, out var _)) { return false; } lock (Sync) { BossCreditFlags bossCreditFlags = BossDefinitionRegistry.FlagFor(tier); if (!Records.TryGetValue(playerId, out var value) || (value.KillReceipts & bossCreditFlags) != bossCreditFlags) { return false; } string text = (string.IsNullOrWhiteSpace(playerName) ? value.PlayerName : playerName); ProgressionTier progressionTier = ((value.Tier > tier) ? value.Tier : tier); if ((value.BossCredits & bossCreditFlags) == bossCreditFlags && progressionTier == value.Tier && string.Equals(text, value.PlayerName, StringComparison.Ordinal)) { return false; } BossCreditFlags bossCredits = value.BossCredits | bossCreditFlags; Records[playerId] = new PlayerProgressRecord(playerId, text, progressionTier, BossProgressionRules.OpenReceipts(value.KillReceipts, bossCredits), bossCredits); return true; } } internal static bool HasKillReceipt(long playerId, ProgressionTier tier) { lock (Sync) { PlayerProgressRecord value; return playerId > 0 && Records.TryGetValue(playerId, out value) && BossProgressionRules.HasOpenReceipt(value.KillReceipts, value.BossCredits, tier); } } internal static bool HasBossCredit(long playerId, ProgressionTier tier) { lock (Sync) { PlayerProgressRecord value; return playerId > 0 && Records.TryGetValue(playerId, out value) && BossDefinitionRegistry.Contains(value.BossCredits, tier); } } internal static PlayerProgressRecord Get(long playerId) { lock (Sync) { PlayerProgressRecord value; return (playerId > 0 && Records.TryGetValue(playerId, out value)) ? value : null; } } 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 WorldStageDirector.Networking { internal static class ClientProgressionState { private static readonly object Sync = new object(); private static Dictionary _tiers = new Dictionary(); private static Dictionary _killReceipts = new Dictionary(); private static Dictionary _bossCredits = 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, Dictionary killReceipts, Dictionary bossCredits) { lock (Sync) { WorldUid = worldUid; _settings = settings?.Clone() ?? new GateSettings(); OnlineSafetyTier = onlineSafetyTier; _tiers = tiers ?? new Dictionary(); _killReceipts = killReceipts ?? new Dictionary(); _bossCredits = bossCredits ?? new Dictionary(); HasServerSnapshot = true; } ExistingCreatureSafetyPatch.SweepLoaded(); ExistingTormentSpawnerSafetyPatch.SweepLoaded(); TrophyRules.ApplyLoadedPrefabs(); BossStonePresentation.RefreshLoadedStones(); } internal static GateSettings GetSettings() { lock (Sync) { return _settings.Clone(); } } internal static ProgressionTier GetTier(long playerId) { lock (Sync) { ProgressionTier value; return (playerId > 0 && _tiers.TryGetValue(playerId, out value)) ? value : ProgressionTier.None; } } internal static BossCreditFlags GetKillReceipts(long playerId) { lock (Sync) { BossCreditFlags value; return (playerId > 0 && _killReceipts.TryGetValue(playerId, out value)) ? value : BossCreditFlags.None; } } internal static BossCreditFlags GetBossCredits(long playerId) { lock (Sync) { BossCreditFlags value; return (playerId > 0 && _bossCredits.TryGetValue(playerId, out value)) ? value : BossCreditFlags.None; } } internal static bool HasLocalKillReceipt(ProgressionTier tier) { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null) { return BossDefinitionRegistry.Contains(GetKillReceipts(localPlayer.GetPlayerID()), tier); } return false; } internal static bool HasLocalBossCredit(ProgressionTier tier) { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null) { return BossDefinitionRegistry.Contains(GetBossCredits(localPlayer.GetPlayerID()), tier); } return false; } internal static void Reset() { lock (Sync) { WorldUid = 0L; OnlineSafetyTier = ProgressionTier.None; _settings = new GateSettings(); _tiers = new Dictionary(); _killReceipts = new Dictionary(); _bossCredits = new Dictionary(); HasServerSnapshot = false; } BossStonePresentation.Clear(); } } internal static class LegacyProgressionImport { private const string ImportRpcName = "jg224.worldstagedirector.ImportLegacyProgression"; private const string ImportAckRpcName = "jg224.worldstagedirector.ImportLegacyProgressionAck"; private const int ImportAttempts = 10; private const float InitialReplicationDelaySeconds = 2f; private const float RetryDelaySeconds = 2f; private static bool _importAcknowledged; internal static void Register(ZRoutedRpc rpc) { rpc.Register("jg224.worldstagedirector.ImportLegacyProgression", (Action)OnImportRequest); rpc.Register("jg224.worldstagedirector.ImportLegacyProgressionAck", (Action)OnImportAcknowledged); } internal static void Submit(Player player) { if ((Object)(object)player == (Object)null || (Object)(object)player != (Object)(object)Player.m_localPlayer) { return; } BossCreditFlags bossCreditFlags = LegacyProgressionDetector.DetectFlags(player.GetUniqueKeys()); if (bossCreditFlags == BossCreditFlags.None) { return; } _importAcknowledged = false; ZNet instance = ZNet.instance; ZRoutedRpc instance2 = ZRoutedRpc.instance; if (!((Object)(object)instance == (Object)null) && instance2 != null) { if (instance.IsServer()) { Apply(player, bossCreditFlags); } else { ((MonoBehaviour)player).StartCoroutine(SubmitAfterReplication(player, bossCreditFlags)); } } } private static IEnumerator SubmitAfterReplication(Player player, BossCreditFlags credits) { yield return (object)new WaitForSeconds(2f); for (int attempt = 1; attempt <= 10; attempt++) { if (_importAcknowledged) { break; } if ((Object)(object)player == (Object)null) { break; } if ((Object)(object)player != (Object)(object)Player.m_localPlayer) { break; } ZNet instance = ZNet.instance; ZRoutedRpc instance2 = ZRoutedRpc.instance; if ((Object)(object)instance == (Object)null || instance2 == null || instance.IsServer()) { break; } ZPackage val = new ZPackage(); val.Write((int)credits); instance2.InvokeRoutedRPC("jg224.worldstagedirector.ImportLegacyProgression", new object[1] { val }); Plugin.Debug($"Submitted exact legacy boss-stone credits {credits} for local character " + $"{player.GetPlayerID()} (migration attempt {attempt}/{10})."); if (attempt < 10) { yield return (object)new WaitForSeconds(2f); } } } private static void OnImportRequest(long sender, ZPackage package) { //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Expected O, but got Unknown ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return; } BossCreditFlags bossCreditFlags = (BossCreditFlags)package.ReadInt(); if (bossCreditFlags == BossCreditFlags.None || !BossDefinitionRegistry.IsValid(bossCreditFlags)) { Plugin.Log.LogWarning((object)$"Rejected invalid legacy boss credits {(int)bossCreditFlags} from peer {sender}."); return; } try { if (!PeerCharacterIdentity.TryResolve(instance.GetPeer(sender), out var playerId, out var playerName)) { Plugin.Log.LogWarning((object)$"Rejected legacy progression import from unresolved peer {sender}."); return; } Apply(playerId, playerName, bossCreditFlags); ZPackage val = new ZPackage(); val.Write((int)bossCreditFlags); ZRoutedRpc instance2 = ZRoutedRpc.instance; if (instance2 != null) { instance2.InvokeRoutedRPC(sender, "jg224.worldstagedirector.ImportLegacyProgressionAck", new object[1] { val }); } } catch (Exception arg) { Plugin.Log.LogError((object)$"Legacy progression import failed for peer {sender}: {arg}"); } } private static void OnImportAcknowledged(long sender, ZPackage package) { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || instance.IsServer()) { return; } ZNetPeer serverPeer = instance.GetServerPeer(); if (serverPeer != null && serverPeer.m_uid == sender) { BossCreditFlags bossCreditFlags = (BossCreditFlags)package.ReadInt(); if (bossCreditFlags != BossCreditFlags.None && BossDefinitionRegistry.IsValid(bossCreditFlags)) { _importAcknowledged = true; Plugin.Debug($"Server acknowledged preserved legacy boss credits: {bossCreditFlags}."); } } } private static void Apply(Player player, BossCreditFlags credits) { if (!((Object)(object)player == (Object)null)) { Apply(player.GetPlayerID(), player.GetPlayerName(), credits); } } private static void Apply(long playerId, string playerName, BossCreditFlags credits) { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return; } if (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.ImportLegacy(playerId, playerName, credits); if (num) { ProgressionRegistry.Save(); } ProgressionSync.BroadcastState(); if (num) { Plugin.Log.LogInfo((object)("Imported legacy boss-stone progression for " + $"'{playerName}' ({playerId}): {credits} " + $"(highest {BossDefinitionRegistry.HighestTier(credits)}).")); } } } [HarmonyPatch(typeof(Player), "OnSpawned", new Type[] { typeof(bool) })] internal static class PlayerSpawnedLegacyImportPatch { [HarmonyPostfix] private static void Postfix(Player __instance) { LegacyProgressionImport.Submit(__instance); } } [HarmonyPatch(typeof(ZNet), "Awake")] internal static class ZNetAwakePatch { [HarmonyPostfix] private static void Postfix() { ClientProgressionState.Reset(); BossFightTracker.Clear(); ServerBossFightLedger.Clear(); PlayerGuidance.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()); TrophyRules.ApplyLoadedPrefabs(); 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(); BossFightTracker.Clear(); ServerBossFightLedger.Clear(); BossStonePresentation.Clear(); PlayerGuidance.Reset(); } } [HarmonyPatch(typeof(ZNet), "RPC_CharacterID", new Type[] { typeof(ZRpc), typeof(ZDOID) })] internal static class CharacterIdReceivedPatch { [HarmonyPostfix] private static void Postfix() { if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { ProgressionSync.BroadcastState(); } } } [HarmonyPatch(typeof(ZNet), "Disconnect", new Type[] { typeof(ZNetPeer) })] internal static class PeerDisconnectedPatch { [HarmonyPostfix] private static void Postfix() { if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { ProgressionSync.BroadcastState(); } } } internal static class PeerCharacterIdentity { internal static bool TryResolve(ZNetPeer peer, out long playerId, out string playerName) { Vector3 position; return TryResolve(peer, out playerId, out playerName, out position); } internal static bool TryResolve(ZNetPeer peer, out long playerId, out string playerName, out Vector3 position) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) playerId = 0L; playerName = peer?.m_playerName ?? string.Empty; position = Vector3.zero; if (peer == null || peer.m_characterID == ZDOID.None || ZDOMan.instance == null) { return false; } ZDO zDO = ZDOMan.instance.GetZDO(peer.m_characterID); if (zDO == null || zDO.GetOwner() != peer.m_uid) { return false; } ZNetScene instance = ZNetScene.instance; GameObject val = ((instance != null) ? instance.GetPrefab(zDO.GetPrefab()) : null); if ((Object)(object)val == (Object)null || (Object)(object)val.GetComponent() == (Object)null) { return false; } playerId = zDO.GetLong(ZDOVars.s_playerID, 0L); string text = zDO.GetString(ZDOVars.s_playerName, string.Empty); if (!string.IsNullOrWhiteSpace(text)) { playerName = text; } position = zDO.GetPosition(); return playerId > 0; } } internal static class ProgressionSync { private const int ProtocolVersion = 4; private const int MaximumRecords = 10000; private const string SyncRpcName = "jg224.worldstagedirector.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.worldstagedirector.SyncState", (Action)OnSyncState); LegacyProgressionImport.Register(rpc); BossProgressionRpc.Register(rpc); _registeredInstance = rpc; Plugin.Debug("Registered progression synchronization and legacy-import routed RPCs."); } if (_newPeerHandler != null) { rpc.m_onNewPeer = (Action)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; } RaidProgressionSafetyPatch.EnforceActiveEvent(); foreach (ZNetPeer connectedPeer in instance.GetConnectedPeers()) { if (connectedPeer != null) { SendState(connectedPeer.m_uid); } } ExistingCreatureSafetyPatch.SweepLoaded(); ExistingTormentSpawnerSafetyPatch.SweepLoaded(); } internal static void SendState(long peerUid) { 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.worldstagedirector.SyncState", new object[1] { val }); Plugin.Debug($"Sent progression snapshot to peer {peerUid} ({ProgressionRegistry.Count} records)."); } catch (Exception ex) { Plugin.Log.LogWarning((object)$"Could not send progression snapshot to peer {peerUid}: {ex.Message}"); } } internal static void Shutdown() { if (_registeredInstance != null && _newPeerHandler != null) { ZRoutedRpc registeredInstance = _registeredInstance; registeredInstance.m_onNewPeer = (Action)Delegate.Remove(registeredInstance.m_onNewPeer, _newPeerHandler); } _registeredInstance = null; _newPeerHandler = null; ClientProgressionState.Reset(); BossProgressionRpc.Shutdown(); } internal static ProgressionTier GetOnlineSafetyTier() { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return ClientProgressionState.OnlineSafetyTier; } bool flag = false; ProgressionTier progressionTier = ProgressionTier.Fader; Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null) { flag = true; progressionTier = ProgressionRegistry.GetTier(localPlayer.GetPlayerID()); } foreach (ZNetPeer connectedPeer in instance.GetConnectedPeers()) { if (connectedPeer != null) { flag = true; long playerId; string playerName; ProgressionTier progressionTier2 = (PeerCharacterIdentity.TryResolve(connectedPeer, out playerId, out playerName) ? ProgressionRegistry.GetTier(playerId) : ProgressionTier.None); if (progressionTier2 < progressionTier) { progressionTier = progressionTier2; } } } if (!flag) { return ProgressionTier.None; } return progressionTier; } private static ZPackage BuildPackage() { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Expected O, but got Unknown GateSettings settings = ModConfig.Snapshot(); List list = ProgressionRegistry.Snapshot(); ZPackage val = new ZPackage(); val.Write(4); val.Write(ProgressionRegistry.WorldUid); WriteSettings(val, settings); val.Write((int)GetOnlineSafetyTier()); val.Write(list.Count); foreach (PlayerProgressRecord item in list) { val.Write(item.PlayerId); val.Write((int)item.Tier); val.Write((int)item.KillReceipts); val.Write((int)item.BossCredits); } return val; } private static void OnSyncState(long sender, ZPackage package) { 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 != 4) { throw new InvalidOperationException($"Unsupported WorldStageDirector protocol {num}; expected {4}."); } 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); Dictionary dictionary2 = new Dictionary(num4); Dictionary dictionary3 = new Dictionary(num4); for (int i = 0; i < num4; i++) { long num5 = package.ReadLong(); int num6 = package.ReadInt(); BossCreditFlags bossCreditFlags = (BossCreditFlags)package.ReadInt(); BossCreditFlags bossCreditFlags2 = (BossCreditFlags)package.ReadInt(); if (num5 > 0 && num6 >= 0 && num6 <= 7 && BossDefinitionRegistry.IsValid(bossCreditFlags) && BossDefinitionRegistry.IsValid(bossCreditFlags2)) { dictionary[num5] = (ProgressionTier)num6; dictionary2[num5] = bossCreditFlags; dictionary3[num5] = bossCreditFlags2; } } ProgressionTier progressionTier = (ProgressionTier)num3; ClientProgressionState.Replace(num2, settings, progressionTier, dictionary, dictionary2, dictionary3); Plugin.Log.LogInfo((object)($"Received authoritative progression snapshot for world {num2}: " + $"{dictionary.Count} player(s), online safety tier {progressionTier}.")); } catch (Exception arg) { Plugin.Log.LogError((object)$"Rejected invalid progression snapshot: {arg}"); ClientProgressionState.Reset(); } } private static void WriteSettings(ZPackage package, GateSettings settings) { package.Write(settings.Enabled); package.Write(settings.NearbyPlayerRadius); package.Write(settings.ProtectLeastProgressedOnline); package.Write(settings.UseLowestNearbyProgression); package.Write(settings.GateYagluthRoamers); package.Write(settings.GateQueenRoamers); package.Write(settings.GateFaderRoamers); package.Write(settings.GateEarlyGameRoamers); package.Write(settings.BlockTormentSpawnersOutsideAshlands); package.Write(settings.LimitRaidsToOnlineProgression); package.Write(settings.BossProgressionEnabled); package.Write(settings.AutoEnablePlayerEvents); package.Write(settings.BossKillWitnessRadius); package.Write(settings.SacrificeTrophyRange); package.Write(settings.BossLootOnSacrifice); package.Write(settings.SacrificeLootDelaySeconds); package.Write(settings.BossTrophyWeight); package.Write(settings.BossTrophyMaxStackSize); package.Write(settings.BossTrophyAutoPickup); package.Write((int)settings.BossTrophyPortalRestriction); package.Write(settings.VerboseLogging); package.Write(settings.LogBlockedSpawns); package.Write(settings.LogAllowedProgressionSpawns); } private static GateSettings ReadSettings(ZPackage package) { return new GateSettings { Enabled = package.ReadBool(), NearbyPlayerRadius = package.ReadSingle(), ProtectLeastProgressedOnline = package.ReadBool(), UseLowestNearbyProgression = package.ReadBool(), GateYagluthRoamers = package.ReadBool(), GateQueenRoamers = package.ReadBool(), GateFaderRoamers = package.ReadBool(), GateEarlyGameRoamers = package.ReadBool(), BlockTormentSpawnersOutsideAshlands = package.ReadBool(), LimitRaidsToOnlineProgression = package.ReadBool(), BossProgressionEnabled = package.ReadBool(), AutoEnablePlayerEvents = package.ReadBool(), BossKillWitnessRadius = package.ReadSingle(), SacrificeTrophyRange = package.ReadSingle(), BossLootOnSacrifice = package.ReadBool(), SacrificeLootDelaySeconds = package.ReadSingle(), BossTrophyWeight = package.ReadSingle(), BossTrophyMaxStackSize = package.ReadInt(), BossTrophyAutoPickup = package.ReadBool(), BossTrophyPortalRestriction = (TrophyPortalRestriction)package.ReadInt(), VerboseLogging = package.ReadBool(), LogBlockedSpawns = package.ReadBool(), LogAllowedProgressionSpawns = package.ReadBool() }; } } } namespace WorldStageDirector.Core { internal static class AuthoritativeStatePolicy { internal static bool CanEnforce(bool networkAvailable, bool isServer, bool hasServerSnapshot) { if (networkAvailable) { return isServer || hasServerSnapshot; } return false; } } [Flags] public enum BossCreditFlags { None = 0, Eikthyr = 1, Elder = 2, Bonemass = 4, Moder = 8, Yagluth = 0x10, Queen = 0x20, Fader = 0x40, All = 0x7F } public sealed class BossDefinition { public ProgressionTier Tier { get; } public string DefeatKey { get; } public string GuardianPowerName { get; } public string TrophyPrefabName { get; } public string BossPrefabName { get; } public BossDefinition(ProgressionTier tier, string defeatKey, string guardianPowerName, string trophyPrefabName, string bossPrefabName) { Tier = tier; DefeatKey = defeatKey; GuardianPowerName = guardianPowerName; TrophyPrefabName = trophyPrefabName; BossPrefabName = bossPrefabName; } } public static class BossDefinitionRegistry { private static readonly BossDefinition[] Definitions; private static readonly Dictionary ByDefeatKey; private static readonly Dictionary ByGuardianPower; private static readonly Dictionary ByTrophy; private static readonly Dictionary ByBossPrefab; public static IReadOnlyList All => Definitions; static BossDefinitionRegistry() { Definitions = new BossDefinition[7] { new BossDefinition(ProgressionTier.Eikthyr, "defeated_eikthyr", "GP_Eikthyr", "TrophyEikthyr", "Eikthyr"), new BossDefinition(ProgressionTier.Elder, "defeated_gdking", "GP_TheElder", "TrophyTheElder", "gd_king"), new BossDefinition(ProgressionTier.Bonemass, "defeated_bonemass", "GP_Bonemass", "TrophyBonemass", "Bonemass"), new BossDefinition(ProgressionTier.Moder, "defeated_dragon", "GP_Moder", "TrophyDragonQueen", "Dragon"), new BossDefinition(ProgressionTier.Yagluth, "defeated_goblinking", "GP_Yagluth", "TrophyGoblinKing", "GoblinKing"), new BossDefinition(ProgressionTier.Queen, "defeated_queen", "GP_Queen", "TrophySeekerQueen", "SeekerQueen"), new BossDefinition(ProgressionTier.Fader, "defeated_fader", "GP_Ashlands", "TrophyFader", "Fader") }; ByDefeatKey = Build((BossDefinition definition) => definition.DefeatKey); ByGuardianPower = Build((BossDefinition definition) => definition.GuardianPowerName); ByTrophy = Build((BossDefinition definition) => definition.TrophyPrefabName); ByBossPrefab = Build((BossDefinition definition) => definition.BossPrefabName); ByGuardianPower["GP_Fader"] = Definitions[6]; } public static bool TryFromDefeatKey(string defeatKey, out BossDefinition definition) { return TryGet(ByDefeatKey, defeatKey, out definition); } public static bool TryFromGuardianPower(string guardianPowerName, out BossDefinition definition) { return TryGet(ByGuardianPower, guardianPowerName, out definition); } public static bool TryFromTrophy(string trophyPrefabName, out BossDefinition definition) { return TryGet(ByTrophy, trophyPrefabName, out definition); } public static bool TryFromBossPrefab(string bossPrefabName, out BossDefinition definition) { return TryGet(ByBossPrefab, bossPrefabName, out definition); } public static bool TryFromTier(ProgressionTier tier, out BossDefinition definition) { if (tier > ProgressionTier.None && tier <= ProgressionTier.Fader) { definition = Definitions[(int)(tier - 1)]; return true; } definition = null; return false; } public static BossCreditFlags FlagFor(ProgressionTier tier) { if (tier <= ProgressionTier.None || tier > ProgressionTier.Fader) { return BossCreditFlags.None; } return (BossCreditFlags)(1 << (int)(tier - 1)); } public static BossCreditFlags AllThrough(ProgressionTier tier) { if (tier <= ProgressionTier.None) { return BossCreditFlags.None; } if (tier >= ProgressionTier.Fader) { return BossCreditFlags.All; } return (BossCreditFlags)((1 << (int)tier) - 1); } public static bool Contains(BossCreditFlags flags, ProgressionTier tier) { BossCreditFlags bossCreditFlags = FlagFor(tier); if (bossCreditFlags != BossCreditFlags.None) { return (flags & bossCreditFlags) == bossCreditFlags; } return false; } public static bool IsValid(BossCreditFlags flags) { return (flags & ~BossCreditFlags.All) == 0; } public static ProgressionTier HighestTier(BossCreditFlags flags) { for (int num = 7; num > 0; num--) { ProgressionTier progressionTier = (ProgressionTier)num; if (Contains(flags, progressionTier)) { return progressionTier; } } return ProgressionTier.None; } private static Dictionary Build(Func keySelector) { Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); BossDefinition[] definitions = Definitions; foreach (BossDefinition bossDefinition in definitions) { dictionary[keySelector(bossDefinition)] = bossDefinition; } return dictionary; } private static bool TryGet(Dictionary map, string key, out BossDefinition definition) { if (!string.IsNullOrWhiteSpace(key) && map.TryGetValue(key.Trim(), out definition)) { return true; } definition = null; return false; } } public static class BossProgressionMigration { public static bool TryNormalize(int schemaVersion, ProgressionTier tier, int storedKillReceipts, int storedBossCredits, out BossCreditFlags killReceipts, out BossCreditFlags bossCredits) { killReceipts = BossCreditFlags.None; bossCredits = BossCreditFlags.None; if (tier < ProgressionTier.None || tier > ProgressionTier.Fader) { return false; } switch (schemaVersion) { case 1: bossCredits = BossDefinitionRegistry.AllThrough(tier); killReceipts = BossCreditFlags.None; return true; default: return false; case 2: bossCredits = (BossCreditFlags)storedBossCredits; if (!BossDefinitionRegistry.IsValid((BossCreditFlags)storedKillReceipts) || !BossDefinitionRegistry.IsValid(bossCredits)) { return false; } killReceipts = BossProgressionRules.OpenReceipts((BossCreditFlags)storedKillReceipts, bossCredits); return true; } } } public static class BossProgressionRules { public static bool CanRecordKill(bool damagedBoss, float horizontalDistance, float witnessRadius) { if (damagedBoss && witnessRadius > 0f && horizontalDistance >= 0f) { return horizontalDistance <= witnessRadius; } return false; } public static bool CanRequestSacrifice(BossCreditFlags killReceipts, ProgressionTier tier, bool trophyMatches, float horizontalDistance, float sacrificeRange) { if (trophyMatches && sacrificeRange > 0f && horizontalDistance >= 0f && horizontalDistance <= sacrificeRange) { return BossDefinitionRegistry.Contains(killReceipts, tier); } return false; } public static bool CanReceiveCredit(BossCreditFlags killReceipts, ProgressionTier tier, float horizontalDistance, float sacrificeRange) { if (sacrificeRange > 0f && horizontalDistance >= 0f && horizontalDistance <= sacrificeRange) { return BossDefinitionRegistry.Contains(killReceipts, tier); } return false; } public static bool HasOpenReceipt(BossCreditFlags killReceipts, BossCreditFlags bossCredits, ProgressionTier tier) { if (BossDefinitionRegistry.Contains(killReceipts, tier)) { return !BossDefinitionRegistry.Contains(bossCredits, tier); } return false; } public static BossCreditFlags OpenReceipts(BossCreditFlags killReceipts, BossCreditFlags bossCredits) { return killReceipts & ~bossCredits & BossCreditFlags.All; } public static int ScaleRewardAmount(int rolledAmount, int eligiblePlayers, bool onePerPlayer) { if (rolledAmount <= 0 || eligiblePlayers <= 0) { return 0; } if (onePerPlayer) { return eligiblePlayers; } return (int)Math.Min((long)rolledAmount * (long)eligiblePlayers, 10000L); } } public static class BossTierMapping { public static bool TryMapGuardianPower(string guardianPowerName, out ProgressionTier tier) { if (BossDefinitionRegistry.TryFromGuardianPower(guardianPowerName, out var definition)) { tier = definition.Tier; return true; } tier = ProgressionTier.None; return false; } } public sealed class GateSettings { public bool Enabled { get; set; } = true; public float NearbyPlayerRadius { get; set; } = 100f; public bool ProtectLeastProgressedOnline { get; set; } = true; public bool UseLowestNearbyProgression { get; set; } = true; public bool GateYagluthRoamers { get; set; } = true; public bool GateQueenRoamers { get; set; } = true; public bool GateFaderRoamers { get; set; } = true; public bool GateEarlyGameRoamers { get; set; } = true; public bool BlockTormentSpawnersOutsideAshlands { get; set; } = true; public bool LimitRaidsToOnlineProgression { get; set; } = true; public bool BossProgressionEnabled { get; set; } = true; public bool AutoEnablePlayerEvents { get; set; } = true; public float BossKillWitnessRadius { get; set; } = 100f; public float SacrificeTrophyRange { get; set; } = 25f; public bool BossLootOnSacrifice { get; set; } = true; public float SacrificeLootDelaySeconds { get; set; } = 12f; public float BossTrophyWeight { get; set; } = 400f; public int BossTrophyMaxStackSize { get; set; } = 1; public bool BossTrophyAutoPickup { get; set; } public TrophyPortalRestriction BossTrophyPortalRestriction { get; set; } = TrophyPortalRestriction.WoodOnly; public bool VerboseLogging { get; set; } public bool LogBlockedSpawns { get; set; } = true; public bool LogAllowedProgressionSpawns { get; set; } public GateSettings Clone() { return new GateSettings { Enabled = Enabled, NearbyPlayerRadius = NearbyPlayerRadius, ProtectLeastProgressedOnline = ProtectLeastProgressedOnline, UseLowestNearbyProgression = UseLowestNearbyProgression, GateYagluthRoamers = GateYagluthRoamers, GateQueenRoamers = GateQueenRoamers, GateFaderRoamers = GateFaderRoamers, GateEarlyGameRoamers = GateEarlyGameRoamers, BlockTormentSpawnersOutsideAshlands = BlockTormentSpawnersOutsideAshlands, LimitRaidsToOnlineProgression = LimitRaidsToOnlineProgression, BossProgressionEnabled = BossProgressionEnabled, AutoEnablePlayerEvents = AutoEnablePlayerEvents, BossKillWitnessRadius = BossKillWitnessRadius, SacrificeTrophyRange = SacrificeTrophyRange, BossLootOnSacrifice = BossLootOnSacrifice, SacrificeLootDelaySeconds = SacrificeLootDelaySeconds, BossTrophyWeight = BossTrophyWeight, BossTrophyMaxStackSize = BossTrophyMaxStackSize, BossTrophyAutoPickup = BossTrophyAutoPickup, BossTrophyPortalRestriction = BossTrophyPortalRestriction, VerboseLogging = VerboseLogging, LogBlockedSpawns = LogBlockedSpawns, LogAllowedProgressionSpawns = LogAllowedProgressionSpawns }; } } public static class IdentityPaths { public const string CurrentPluginGuid = "jg224.worldstagedirector"; public const string LegacyPluginGuid = "jg224.progressguard"; public static bool ShouldMigrate(bool currentExists, bool legacyExists) { return !currentExists && legacyExists; } public static string ConfigFileName(string pluginGuid) { return pluginGuid + ".cfg"; } public static string RegistryFileName(string pluginGuid, long worldUid) { return pluginGuid + ".world-" + worldUid.ToString(CultureInfo.InvariantCulture) + ".json"; } } public static class LegacyProgressionDetector { private static readonly KeyValuePair[] GuardianPowerKeys = new KeyValuePair[8] { 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), new KeyValuePair("GP_Ashlands", ProgressionTier.Fader) }; public static ProgressionTier DetectHighest(IEnumerable uniqueKeys) { return BossDefinitionRegistry.HighestTier(DetectFlags(uniqueKeys)); } public static BossCreditFlags DetectFlags(IEnumerable uniqueKeys) { if (uniqueKeys == null) { return BossCreditFlags.None; } HashSet hashSet = new HashSet(uniqueKeys, StringComparer.OrdinalIgnoreCase); BossCreditFlags bossCreditFlags = BossCreditFlags.None; KeyValuePair[] guardianPowerKeys = GuardianPowerKeys; for (int i = 0; i < guardianPowerKeys.Length; i++) { KeyValuePair keyValuePair = guardianPowerKeys[i]; if (hashSet.Contains(keyValuePair.Key)) { bossCreditFlags |= BossDefinitionRegistry.FlagFor(keyValuePair.Value); } } return bossCreditFlags; } } public enum ProgressionTier { None, Eikthyr, Elder, Bonemass, Moder, Yagluth, Queen, Fader } public static class RaidRuleRegistry { private static readonly Dictionary ExplicitTiers = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["army_eikthyr"] = ProgressionTier.None, ["hildirboss1"] = ProgressionTier.Eikthyr, ["hildirboss2"] = ProgressionTier.Bonemass, ["hildirboss3"] = ProgressionTier.Moder }; public static bool TryGetRequiredTier(string eventName, IEnumerable requiredGlobalKeys, out ProgressionTier tier) { bool flag = false; tier = ProgressionTier.None; if (requiredGlobalKeys != null) { foreach (string requiredGlobalKey in requiredGlobalKeys) { if (SpawnRuleRegistry.TryGetRequiredTier(requiredGlobalKey, out var tier2)) { if (!flag || tier2 > tier) { tier = tier2; } flag = true; } } } if (flag) { return true; } return ExplicitTiers.TryGetValue((eventName ?? string.Empty).Trim(), out tier); } public static bool IsProgressionExempt(string eventName, NativeBiome eventBiome) { if (string.Equals((eventName ?? string.Empty).Trim(), "army_charredspawners", StringComparison.OrdinalIgnoreCase)) { return (eventBiome & NativeBiome.Ashlands) != 0; } return false; } } public enum SpawnDecisionReason { AllowedDisabled, AllowedEvent, AllowedUnmanaged, AllowedGateDisabled, AllowedNativeBiome, AllowedNoNearbyPlayers, AllowedProgression, BlockedProgression } public sealed class SpawnDecision { public bool Block { get; } public SpawnDecisionReason Reason { get; } public ProgressionTier EffectiveTier { get; } public SpawnDecision(bool block, SpawnDecisionReason reason, ProgressionTier effectiveTier) { Block = block; Reason = reason; EffectiveTier = effectiveTier; } } public static class SpawnDecisionEngine { public static SpawnDecision Evaluate(SpawnRule rule, GateSettings settings, bool eventSpawner, bool inNativeBiome, IReadOnlyList 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); } } public enum TrophyPortalRestriction { None, WoodOnly, WoodAndStone } } namespace WorldStageDirector.Config { internal static class LegacyIdentityMigration { internal static bool TryMigrateConfig(ConfigFile config) { if (config == null || string.IsNullOrWhiteSpace(config.ConfigFilePath)) { return false; } string configFilePath = config.ConfigFilePath; string directoryName = Path.GetDirectoryName(configFilePath); if (string.IsNullOrWhiteSpace(directoryName)) { return false; } string text = Path.Combine(directoryName, IdentityPaths.ConfigFileName("jg224.progressguard")); if (!IdentityPaths.ShouldMigrate(File.Exists(configFilePath), File.Exists(text))) { return false; } try { File.Copy(text, configFilePath, overwrite: false); config.Reload(); Plugin.Log.LogInfo((object)("Copied legacy settings from '" + text + "' to '" + configFilePath + "'.")); return true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not copy legacy settings from '" + text + "' to '" + configFilePath + "'. Defaults will be used until the file can be migrated. " + ex.Message)); return false; } } } internal static class ModConfig { private static ConfigFile _config; 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 BlockTormentSpawnersOutsideAshlands; internal static ConfigEntry LimitRaidsToOnlineProgression; internal static ConfigEntry BossProgressionEnabled; internal static ConfigEntry AutoEnablePlayerEvents; internal static ConfigEntry BossKillWitnessRadius; internal static ConfigEntry SacrificeTrophyRange; internal static ConfigEntry BossLootOnSacrifice; internal static ConfigEntry SacrificeLootDelaySeconds; internal static ConfigEntry BossTrophyWeight; internal static ConfigEntry BossTrophyMaxStackSize; internal static ConfigEntry BossTrophyAutoPickup; internal static ConfigEntry BossTrophyPortalRestriction; internal static ConfigEntry VerboseLogging; internal static ConfigEntry LogBlockedSpawns; internal static ConfigEntry LogAllowedProgressionSpawns; internal static void Bind(ConfigFile config) { //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Expected O, but got Unknown //IL_0190: Unknown result type (might be due to invalid IL or missing references) //IL_019a: Expected O, but got Unknown //IL_01c8: Unknown result type (might be due to invalid IL or missing references) //IL_01d2: Expected O, but got Unknown //IL_021b: Unknown result type (might be due to invalid IL or missing references) //IL_0225: Expected O, but got Unknown //IL_0253: Unknown result type (might be due to invalid IL or missing references) //IL_025d: Expected O, but got Unknown //IL_0280: Unknown result type (might be due to invalid IL or missing references) //IL_028a: Expected O, but got Unknown _config = config; 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."); BlockTormentSpawnersOutsideAshlands = config.Bind("Raid Safety", "BlockTormentSpawnersOutsideAshlands", true, "Prevent event Monuments of Torment from spawning outside the Ashlands and remove loaded event monuments found there."); LimitRaidsToOnlineProgression = config.Bind("Raid Safety", "LimitRaidsToOnlineProgression", true, "Allow a managed random raid only when every connected character has turned in at least the boss trophy tier that unlocks it. The Monument of Torment raid is always allowed in the Ashlands."); BossProgressionEnabled = config.Bind("Boss Progression", "Enabled", true, "Delay personal and world boss progression until a participating player sacrifices the matching trophy at its Forsaken altar."); AutoEnablePlayerEvents = config.Bind("Boss Progression", "AutoEnablePlayerEvents", true, "Enable Valheim's PlayerEvents world modifier on server startup, preserving per-player raid requirement behavior."); BossKillWitnessRadius = config.Bind("Boss Progression", "BossKillWitnessRadius", 100f, new ConfigDescription("A player must damage the boss and remain within this horizontal distance when it dies to receive a kill receipt.", (AcceptableValueBase)(object)new AcceptableValueRange(10f, 300f), Array.Empty())); SacrificeTrophyRange = config.Bind("Boss Progression", "SacrificeTrophyRange", 25f, new ConfigDescription("Players within this horizontal distance of a valid trophy sacrifice can receive personal boss credit.", (AcceptableValueBase)(object)new AcceptableValueRange(10f, 50f), Array.Empty())); BossLootOnSacrifice = config.Bind("Boss Progression", "BossLootOnSacrifice", true, "Withhold managed boss progression loot at death and release one reward set per nearby open kill receipt after sacrifice."); SacrificeLootDelaySeconds = config.Bind("Boss Progression", "SacrificeLootDelaySeconds", 12f, new ConfigDescription("Seconds between a valid trophy sacrifice and its altar loot drop.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 30f), Array.Empty())); BossTrophyWeight = config.Bind("Boss Trophies", "Weight", 400f, new ConfigDescription("Weight of managed boss trophies.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 3000f), Array.Empty())); BossTrophyMaxStackSize = config.Bind("Boss Trophies", "MaxStackSize", 1, new ConfigDescription("Maximum stack size of managed boss trophies.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 99), Array.Empty())); BossTrophyAutoPickup = config.Bind("Boss Trophies", "AutoPickup", false, "Allow managed boss trophies to be collected by automatic pickup."); BossTrophyPortalRestriction = config.Bind("Boss Trophies", "PortalRestriction", TrophyPortalRestriction.WoodOnly, "Restricted trophies show the non-teleportable icon. None allows every portal, WoodOnly blocks wooden portals, and WoodAndStone blocks every portal."); 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(BlockTormentSpawnersOutsideAshlands); Subscribe(LimitRaidsToOnlineProgression); Subscribe(BossProgressionEnabled); Subscribe(AutoEnablePlayerEvents); Subscribe(BossKillWitnessRadius); Subscribe(SacrificeTrophyRange); Subscribe(BossLootOnSacrifice); Subscribe(SacrificeLootDelaySeconds); Subscribe(BossTrophyWeight); Subscribe(BossTrophyMaxStackSize); Subscribe(BossTrophyAutoPickup); Subscribe(BossTrophyPortalRestriction); 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, BlockTormentSpawnersOutsideAshlands = BlockTormentSpawnersOutsideAshlands.Value, LimitRaidsToOnlineProgression = LimitRaidsToOnlineProgression.Value, BossProgressionEnabled = BossProgressionEnabled.Value, AutoEnablePlayerEvents = AutoEnablePlayerEvents.Value, BossKillWitnessRadius = BossKillWitnessRadius.Value, SacrificeTrophyRange = SacrificeTrophyRange.Value, BossLootOnSacrifice = BossLootOnSacrifice.Value, SacrificeLootDelaySeconds = SacrificeLootDelaySeconds.Value, BossTrophyWeight = BossTrophyWeight.Value, BossTrophyMaxStackSize = BossTrophyMaxStackSize.Value, BossTrophyAutoPickup = BossTrophyAutoPickup.Value, BossTrophyPortalRestriction = BossTrophyPortalRestriction.Value, VerboseLogging = VerboseLogging.Value, LogBlockedSpawns = LogBlockedSpawns.Value, LogAllowedProgressionSpawns = LogAllowedProgressionSpawns.Value }; } internal static bool ConsumeOnboardingLogin(long worldUid, long playerId) { //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Expected O, but got Unknown if (_config == null || worldUid == 0L || playerId <= 0) { return false; } string text = $"OnboardingShown_{worldUid}_{playerId}"; ConfigEntry val = _config.Bind("Client Guidance", text, 0, new ConfigDescription("Internal per-character count for the boss-progression login tutorial.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 2), Array.Empty())); if (val.Value >= 2) { return false; } int value = val.Value; val.Value = value + 1; _config.Save(); return true; } private static void Subscribe(ConfigEntry entry) { entry.SettingChanged += OnSettingChanged; } private static void OnSettingChanged(object sender, EventArgs args) { TrophyRules.ApplyLoadedPrefabs(); BossStonePresentation.RefreshLoadedStones(); ProgressionSync.BroadcastState(); } } } namespace WorldStageDirector.Commands { internal static class AdminCommands { [CompilerGenerated] private static class <>O { public static Func <0>__FormatRecord; public static ConsoleEvent <1>__Run; } private static bool _registered; internal static void Register() { if (_registered) { return; } try { RegisterCommand("wsd", "WorldStageDirector admin: wsd audit | list | get | set [name] | reset | reload"); RegisterCommand("pg", "Legacy alias for the WorldStageDirector wsd command."); _registered = true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not register WorldStageDirector admin command: " + ex.Message)); } } private static void Run(ConsoleEventArgs args) { try { if (args.Length < 2) { Reply(args, "Usage: wsd audit | list | get | set [name] | reset | reload"); return; } string text = args[1].ToLowerInvariant(); switch (text) { case "audit": Audit(args); break; 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, "WorldStageDirector command failed: " + ex.Message); } } private static void Audit(ConsoleEventArgs args) { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { Reply(args, "Migration audit is server-only."); return; } List list = new List(); Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null) { list.Add(FormatAudit(localPlayer.GetPlayerID(), localPlayer.GetPlayerName())); } foreach (ZNetPeer connectedPeer in instance.GetConnectedPeers()) { if (PeerCharacterIdentity.TryResolve(connectedPeer, out var playerId, out var playerName)) { list.Add(FormatAudit(playerId, playerName)); } else { list.Add("WAITING | " + (connectedPeer?.m_playerName ?? "unknown") + " | character identity has not replicated yet"); } } Reply(args, (list.Count == 0) ? "No connected player characters are available to audit." : ("Connected-player migration audit:\n" + string.Join("\n", list))); } private static void List(ConsoleEventArgs args) { List list = ProgressionRegistry.Snapshot(); Reply(args, (list.Count == 0) ? "WorldStageDirector registry is empty." : string.Join("\n", list.Select(FormatRecord))); } private static void Get(ConsoleEventArgs args) { if (args.Length < 3) { Reply(args, "Usage: wsd 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: wsd 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: wsd 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 void RegisterCommand(string name, string description) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Expected O, but got Unknown object obj = <>O.<1>__Run; if (obj == null) { ConsoleEvent val = Run; <>O.<1>__Run = val; obj = (object)val; } new ConsoleCommand(name, description, (ConsoleEvent)obj, false, false, true, false, false, (ConsoleOptionsFetcher)null, false, true, true); } 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} | tier={record.Tier} | " + $"kills={record.KillReceipts} | credits={record.BossCredits}"; } private static string FormatAudit(long playerId, string playerName) { PlayerProgressRecord playerProgressRecord = ProgressionRegistry.Get(playerId); if (playerProgressRecord != null) { return $"PRESERVED | {playerName} ({playerId}) | tier={playerProgressRecord.Tier} | credits={playerProgressRecord.BossCredits}"; } return $"MISSING | {playerName} ({playerId}) | reconnect with WorldStageDirector 0.2 to import character boss keys"; } private static void Reply(ConsoleEventArgs args, string message) { Terminal context = args.Context; if (context != null) { context.AddString(message); } Plugin.Log.LogInfo((object)("[admin] " + message)); } } } namespace WorldStageDirector.Bosses { internal readonly struct BossDeathObservation { internal ProgressionTier Tier { get; } internal ZDOID BossId { get; } internal Vector3 DeathPosition { get; } internal BossDeathObservation(ProgressionTier tier, ZDOID bossId, Vector3 deathPosition) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) Tier = tier; BossId = bossId; DeathPosition = deathPosition; } } [HarmonyPatch(typeof(Character), "Damage", new Type[] { typeof(HitData) })] internal static class BossDamageParticipationPatch { [HarmonyPrefix] private static void Prefix(Character __instance, HitData hit) { BossFightTracker.ObserveDamage(__instance, hit); } } [HarmonyPatch(typeof(Character), "OnDeath")] internal static class DelayedBossProgressionPatch { [HarmonyPrefix] private static void Prefix(Character __instance, out string __state) { __state = null; if (BossProgressionRuntime.IsEnabled() && BossProgressionRuntime.TryGetDefinition(__instance, out var _)) { __state = __instance.m_defeatSetGlobalKey; BossFightTracker.ObserveDeath(__instance); __instance.m_defeatSetGlobalKey = string.Empty; } } [HarmonyFinalizer] private static Exception Finalizer(Character __instance, string __state, Exception __exception) { if (__state != null && (Object)(object)__instance != (Object)null) { __instance.m_defeatSetGlobalKey = __state; } return __exception; } } internal static class BossFightTracker { private static readonly object Sync = new object(); private static readonly HashSet LocallyDamagedBosses = new HashSet(); internal static void ObserveDamage(Character target, HitData hit) { //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Unknown result type (might be due to invalid IL or missing references) if (!BossProgressionRuntime.IsEnabled() || (Object)(object)target == (Object)null || hit == null || !BossProgressionRuntime.TryGetDefinition(target, out var _) || hit.GetTotalDamage() <= 0f) { return; } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null || (Object)(object)hit.GetAttacker() != (Object)(object)localPlayer) { return; } ZNetView component = ((Component)target).GetComponent(); ZDO val = ((component != null) ? component.GetZDO() : null); if (val == null) { return; } lock (Sync) { if (LocallyDamagedBosses.Contains(val.m_uid)) { return; } } if (!BossProgressionRuntime.TryGetDefinition(target, out var definition2) || !BossProgressionRpc.SubmitParticipation(val.m_uid, definition2.Tier)) { return; } lock (Sync) { LocallyDamagedBosses.Add(val.m_uid); } } internal static void ObserveDeath(Character boss) { //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) if (BossProgressionRuntime.IsEnabled() && BossProgressionRuntime.TryGetDefinition(boss, out var definition)) { ZNetView component = ((Component)boss).GetComponent(); ZDO val = ((component != null) ? component.GetZDO() : null); if (val != null) { Forget(boss); BossProgressionRpc.SubmitDeathObservation(new BossDeathObservation(definition.Tier, val.m_uid, ((Component)boss).transform.position)); } } } internal static void Forget(Character boss) { //IL_0039: Unknown result type (might be due to invalid IL or missing references) object obj; if (!((Object)(object)boss == (Object)null)) { ZNetView component = ((Component)boss).GetComponent(); obj = ((component != null) ? component.GetZDO() : null); } else { obj = null; } ZDO val = (ZDO)obj; if (val == null) { return; } lock (Sync) { LocallyDamagedBosses.Remove(val.m_uid); } } internal static void Clear() { lock (Sync) { LocallyDamagedBosses.Clear(); } } } internal static class BossLootService { internal static List> CreateSacrificeLoot(BossDefinition definition, int eligiblePlayers) { List> list = new List>(); ZNetScene instance = ZNetScene.instance; GameObject val = ((instance != null) ? instance.GetPrefab(definition.BossPrefabName) : null); CharacterDrop val2 = (((Object)(object)val == (Object)null) ? null : val.GetComponent()); if ((Object)(object)val2 == (Object)null) { return list; } foreach (Drop drop in val2.m_drops) { if (!((Object)(object)drop?.m_prefab == (Object)null) && !BossDefinitionRegistry.TryFromTrophy(((Object)drop.m_prefab).name, out var _) && !(Random.value > drop.m_chance)) { int num = BossProgressionRules.ScaleRewardAmount((drop.m_dontScale || (Object)(object)Game.instance == (Object)null) ? Random.Range(drop.m_amountMin, drop.m_amountMax) : Game.instance.ScaleDrops(drop.m_prefab, drop.m_amountMin, drop.m_amountMax), eligiblePlayers, drop.m_onePerPlayer); if (num > 0) { list.Add(new KeyValuePair(drop.m_prefab, num)); } } } return list; } internal static void SpawnSacrificeLoot(BossStone stone, BossDefinition definition, int eligiblePlayers) { //IL_0024: 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_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0055: 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_0064: 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_006e: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)stone == (Object)null || !BossProgressionRuntime.GetSettings().BossLootOnSacrifice) { return; } List> list = CreateSacrificeLoot(definition, eligiblePlayers); Vector3 val = ((Component)stone).transform.position + Vector3.up * 1.5f; foreach (KeyValuePair item in list) { for (int i = 0; i < item.Value; i++) { Vector2 val2 = Random.insideUnitCircle * 1.5f; Object.Instantiate(item.Key, val + new Vector3(val2.x, 0f, val2.y), Random.rotation); } } int num = 0; foreach (KeyValuePair item2 in list) { num += item2.Value; } Plugin.Log.LogInfo((object)($"Spawned {num} {definition.Tier} sacrifice reward item(s) across " + $"{eligiblePlayers} consumed kill receipt(s).")); } internal static bool IsManagedBoss(CharacterDrop dropper, out BossDefinition definition) { definition = null; Character val = (((Object)(object)dropper == (Object)null) ? null : ((Component)dropper).GetComponent()); if ((Object)(object)val != (Object)null) { return BossProgressionRuntime.TryGetDefinition(val, out definition); } return false; } } [HarmonyPatch(typeof(CharacterDrop), "GenerateDropList")] internal static class ManagedBossDropPatch { [HarmonyPostfix] private static void Postfix(CharacterDrop __instance, ref List> __result) { GateSettings settings = BossProgressionRuntime.GetSettings(); if (settings.Enabled && settings.BossProgressionEnabled && settings.BossLootOnSacrifice && BossLootService.IsManagedBoss(__instance, out var definition) && __result != null) { __result.RemoveAll((KeyValuePair entry) => (Object)(object)entry.Key == (Object)null || !string.Equals(((Object)entry.Key).name, definition.TrophyPrefabName, StringComparison.OrdinalIgnoreCase)); } } } internal static class BossProgressionRpc { private const string ParticipationRpc = "jg224.worldstagedirector.BossParticipation"; private const string BossDeathRpc = "jg224.worldstagedirector.BossDeath"; private const string TrophyPickupRpc = "jg224.worldstagedirector.BossTrophyPickup"; private const string PlayerNoticeRpc = "jg224.worldstagedirector.BossPlayerNotice"; private const string OfferingReadinessRequestRpc = "jg224.worldstagedirector.BossOfferingReadinessRequest"; private const string OfferingReadinessResultRpc = "jg224.worldstagedirector.BossOfferingReadinessResult"; private const string SacrificeRequestRpc = "jg224.worldstagedirector.BossSacrificeRequest"; private const string SacrificeResultRpc = "jg224.worldstagedirector.BossSacrificeResult"; private const string SacrificeEventRpc = "jg224.worldstagedirector.BossSacrificeEvent"; private static ZRoutedRpc _registeredInstance; internal static void Register(ZRoutedRpc rpc) { if (rpc != null && _registeredInstance != rpc) { rpc.Register("jg224.worldstagedirector.BossParticipation", (Action)OnParticipation); rpc.Register("jg224.worldstagedirector.BossDeath", (Action)OnBossDeath); rpc.Register("jg224.worldstagedirector.BossTrophyPickup", (Action)OnTrophyPickup); rpc.Register("jg224.worldstagedirector.BossPlayerNotice", (Action)OnPlayerNotice); rpc.Register("jg224.worldstagedirector.BossOfferingReadinessRequest", (Action)OnOfferingReadinessRequest); rpc.Register("jg224.worldstagedirector.BossOfferingReadinessResult", (Action)OnOfferingReadinessResult); rpc.Register("jg224.worldstagedirector.BossSacrificeRequest", (Action)OnSacrificeRequest); rpc.Register("jg224.worldstagedirector.BossSacrificeResult", (Action)OnSacrificeResult); rpc.Register("jg224.worldstagedirector.BossSacrificeEvent", (Action)OnSacrificeEvent); _registeredInstance = rpc; Plugin.Debug("Registered boss participation and trophy-sacrifice RPCs."); } } internal static bool SubmitParticipation(ZDOID bossId, ProgressionTier tier) { //IL_0027: 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_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Expected O, but got Unknown //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) ZNet instance = ZNet.instance; ZRoutedRpc instance2 = ZRoutedRpc.instance; Player localPlayer = Player.m_localPlayer; if ((Object)(object)instance == (Object)null || instance2 == null || (Object)(object)localPlayer == (Object)null || bossId == ZDOID.None || !BossDefinitionRegistry.TryFromTier(tier, out var _)) { return false; } if (instance.IsServer()) { return RegisterParticipation(bossId, tier, localPlayer.GetPlayerID(), localPlayer.GetPlayerName()); } ZPackage val = new ZPackage(); val.Write((int)tier); val.Write(bossId); instance2.InvokeRoutedRPC("jg224.worldstagedirector.BossParticipation", new object[1] { val }); return true; } internal static void SubmitDeathObservation(BossDeathObservation observation) { //IL_001a: 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_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Expected O, but got Unknown //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) ZNet instance = ZNet.instance; ZRoutedRpc instance2 = ZRoutedRpc.instance; if (!((Object)(object)instance == (Object)null) && instance2 != null && !(observation.BossId == ZDOID.None)) { if (instance.IsServer()) { ProcessBossDeath(0L, observation.BossId, observation.Tier, observation.DeathPosition); return; } ZPackage val = new ZPackage(); val.Write((int)observation.Tier); val.Write(observation.BossId); val.Write(observation.DeathPosition); instance2.InvokeRoutedRPC("jg224.worldstagedirector.BossDeath", new object[1] { val }); } } internal static void SubmitTrophyPickup(ProgressionTier tier) { //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Expected O, but got Unknown ZNet instance = ZNet.instance; ZRoutedRpc instance2 = ZRoutedRpc.instance; Player localPlayer = Player.m_localPlayer; if (!((Object)(object)instance == (Object)null) && instance2 != null && !((Object)(object)localPlayer == (Object)null) && BossDefinitionRegistry.TryFromTier(tier, out var _)) { if (instance.IsServer()) { AnnounceTrophyPickup(0L, localPlayer.GetPlayerID(), localPlayer.GetPlayerName(), tier); return; } ZPackage val = new ZPackage(); val.Write((int)tier); instance2.InvokeRoutedRPC("jg224.worldstagedirector.BossTrophyPickup", new object[1] { val }); } } internal static void SubmitOfferingReadinessCheck(int requestId, ZDOID stoneId, ProgressionTier tier) { //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Expected O, but got Unknown //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) ZNet instance = ZNet.instance; ZRoutedRpc instance2 = ZRoutedRpc.instance; Player localPlayer = Player.m_localPlayer; if ((Object)(object)instance == (Object)null || instance2 == null || (Object)(object)localPlayer == (Object)null) { return; } if (instance.IsServer()) { try { int missingPlayers = ProcessOfferingReadiness(stoneId, tier, localPlayer.GetPlayerID(), ((Component)localPlayer).transform.position); SendOfferingReadinessResult(0L, requestId, success: true, missingPlayers, string.Empty); return; } catch (Exception ex) { SendOfferingReadinessResult(0L, requestId, success: false, 0, ex.Message); return; } } ZPackage val = new ZPackage(); val.Write(requestId); val.Write(stoneId); val.Write((int)tier); instance2.InvokeRoutedRPC("jg224.worldstagedirector.BossOfferingReadinessRequest", new object[1] { val }); } internal static void SubmitSacrifice(int requestId, ZDOID bossStoneId, BossDefinition definition, string trophyPrefabName) { //IL_008d: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Expected O, but got Unknown //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_0036: 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) ZNet instance = ZNet.instance; ZRoutedRpc instance2 = ZRoutedRpc.instance; Player localPlayer = Player.m_localPlayer; if ((Object)(object)instance == (Object)null || instance2 == null || (Object)(object)localPlayer == (Object)null || definition == null) { return; } if (instance.IsServer()) { try { ProcessSacrifice(0L, requestId, bossStoneId, definition.Tier, trophyPrefabName, localPlayer.GetPlayerID(), localPlayer.GetPlayerName(), ((Component)localPlayer).transform.position); return; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Rejected local boss trophy sacrifice: " + ex.Message)); SendSacrificeResult(0L, requestId, success: false, trophyPrefabName, ex.Message); return; } } ZPackage val = new ZPackage(); val.Write(requestId); val.Write(bossStoneId); val.Write((int)definition.Tier); val.Write(trophyPrefabName ?? string.Empty); instance2.InvokeRoutedRPC("jg224.worldstagedirector.BossSacrificeRequest", new object[1] { val }); } internal static void Shutdown() { _registeredInstance = null; BossStonePatches.ResetPendingRequests(); ServerBossFightLedger.Clear(); } private static void OnParticipation(long sender, ZPackage package) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0030: 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_0093: 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 { int tier = package.ReadInt(); ZDOID val = package.ReadZDOID(); if (!BossDefinitionRegistry.TryFromTier((ProgressionTier)tier, out var definition) || val == ZDOID.None) { throw new InvalidOperationException("Invalid boss-participation payload."); } if (!PeerCharacterIdentity.TryResolve(instance.GetPeer(sender), out var playerId, out var playerName, out var _)) { throw new InvalidOperationException("The reporting peer has no resolved character."); } ZDOMan instance2 = ZDOMan.instance; ZDO val2 = ((instance2 != null) ? instance2.GetZDO(val) : null); if (val2 == null || !ZdoMatchesBoss(val2, definition)) { throw new InvalidOperationException("The reported boss instance does not match the reported tier."); } RegisterParticipation(val, definition.Tier, playerId, playerName); } catch (Exception ex) { Plugin.Log.LogWarning((object)$"Rejected boss participation from peer {sender}: {ex.Message}"); } } private static bool RegisterParticipation(ZDOID bossId, ProgressionTier tier, long playerId, string playerName) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer() || !BossProgressionRuntime.IsEnabled() || playerId <= 0) { return false; } bool flag = ServerBossFightLedger.Register(bossId, tier, playerId); if (flag) { Plugin.Debug($"Registered {tier} fight participation for '{playerName}' ({playerId}) on {bossId}."); } return flag; } private static void OnBossDeath(long sender, ZPackage package) { //IL_0021: 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_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: 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 { ProgressionTier tier = (ProgressionTier)package.ReadInt(); ZDOID val = package.ReadZDOID(); Vector3 val2 = package.ReadVector3(); if (!BossDefinitionRegistry.TryFromTier(tier, out var definition) || val == ZDOID.None || !IsFinite(val2)) { throw new InvalidOperationException("Invalid boss-death payload."); } ZDOMan instance2 = ZDOMan.instance; ZDO val3 = ((instance2 != null) ? instance2.GetZDO(val) : null); if (val3 == null || !ZdoMatchesBoss(val3, definition)) { throw new InvalidOperationException("The dying boss could not be resolved or did not match its tier."); } if (val3.GetOwner() != sender) { throw new InvalidOperationException("The death report did not come from the boss network owner."); } Vector3 position = val3.GetPosition(); if (BossProgressionRuntime.HorizontalDistance(position, val2) > 10f) { throw new InvalidOperationException("The reported boss-death position was inconsistent."); } ProcessBossDeath(sender, val, tier, position); } catch (Exception ex) { Plugin.Log.LogWarning((object)$"Rejected boss death from peer {sender}: {ex.Message}"); } } private static void ProcessBossDeath(long sender, ZDOID bossId, ProgressionTier tier, Vector3 deathPosition) { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0069: 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) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Unknown result type (might be due to invalid IL or missing references) //IL_017f: Unknown result type (might be due to invalid IL or missing references) ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer() || !BossProgressionRuntime.IsEnabled() || !BossDefinitionRegistry.TryFromTier(tier, out var _)) { return; } HashSet hashSet = ServerBossFightLedger.Consume(bossId, tier); GateSettings settings = BossProgressionRuntime.GetSettings(); EnsureRegistry(instance); int num = 0; int num2 = 0; Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null && hashSet.Contains(localPlayer.GetPlayerID())) { float horizontalDistance = BossProgressionRuntime.HorizontalDistance(((Component)localPlayer).transform.position, deathPosition); if (BossProgressionRules.CanRecordKill(damagedBoss: true, horizontalDistance, settings.BossKillWitnessRadius)) { num++; bool num3 = !ProgressionRegistry.HasBossCredit(localPlayer.GetPlayerID(), tier); if (ProgressionRegistry.RecordKill(localPlayer.GetPlayerID(), localPlayer.GetPlayerName(), tier)) { num2++; } if (num3) { SendPlayerNotice(0L, PlayerGuidanceNotice.KillReceipt, tier, string.Empty); } } } foreach (ZNetPeer connectedPeer in instance.GetConnectedPeers()) { if (!PeerCharacterIdentity.TryResolve(connectedPeer, out var playerId, out var playerName, out var position) || !hashSet.Contains(playerId)) { continue; } float horizontalDistance2 = BossProgressionRuntime.HorizontalDistance(position, deathPosition); if (BossProgressionRules.CanRecordKill(damagedBoss: true, horizontalDistance2, settings.BossKillWitnessRadius)) { num++; bool num4 = !ProgressionRegistry.HasBossCredit(playerId, tier); if (ProgressionRegistry.RecordKill(playerId, playerName, tier)) { num2++; } if (num4) { SendPlayerNotice(connectedPeer.m_uid, PlayerGuidanceNotice.KillReceipt, tier, string.Empty); } } } if (num2 > 0) { ProgressionRegistry.Save(); } ProgressionSync.BroadcastState(); Plugin.Log.LogInfo((object)($"Settled witnessed {tier} death for boss {bossId}: " + $"registered participants={hashSet.Count}, in-range witnesses={num}, " + $"new receipts={num2}, reporter={sender}.")); } private static void OnTrophyPickup(long sender, ZPackage package) { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return; } try { ProgressionTier tier = (ProgressionTier)package.ReadInt(); if (!BossDefinitionRegistry.TryFromTier(tier, out var _)) { throw new InvalidOperationException("Invalid boss-trophy pickup tier."); } if (!PeerCharacterIdentity.TryResolve(instance.GetPeer(sender), out var playerId, out var playerName)) { throw new InvalidOperationException("The trophy carrier has no resolved character."); } AnnounceTrophyPickup(sender, playerId, playerName, tier); } catch (Exception ex) { Plugin.Log.LogWarning((object)$"Rejected boss-trophy pickup notice from peer {sender}: {ex.Message}"); } } private static void OnOfferingReadinessRequest(long sender, ZPackage package) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_004f: 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; } int requestId = 0; try { requestId = package.ReadInt(); ZDOID stoneId = package.ReadZDOID(); ProgressionTier tier = (ProgressionTier)package.ReadInt(); if (!PeerCharacterIdentity.TryResolve(instance.GetPeer(sender), out var playerId, out var _, out var position)) { throw new InvalidOperationException("The offering player has no resolved character."); } int missingPlayers = ProcessOfferingReadiness(stoneId, tier, playerId, position); SendOfferingReadinessResult(sender, requestId, success: true, missingPlayers, string.Empty); } catch (Exception ex) { Plugin.Log.LogWarning((object)$"Rejected altar readiness check from peer {sender}: {ex.Message}"); SendOfferingReadinessResult(sender, requestId, success: false, 0, ex.Message); } } private static int ProcessOfferingReadiness(ZDOID stoneId, ProgressionTier tier, long requesterId, Vector3 requesterPosition) { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0055: 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_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00dc: Unknown result type (might be due to invalid IL or missing references) //IL_011d: Unknown result type (might be due to invalid IL or missing references) //IL_011f: Unknown result type (might be due to invalid IL or missing references) ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer() || !BossProgressionRuntime.IsEnabled() || !BossDefinitionRegistry.TryFromTier(tier, out var definition)) { throw new InvalidOperationException("Boss progression is not enabled for this altar."); } if (!TryResolveStone(stoneId, definition, out var position)) { throw new InvalidOperationException("The Forsaken altar could not be resolved."); } EnsureRegistry(instance); GateSettings settings = BossProgressionRuntime.GetSettings(); float horizontalDistance = BossProgressionRuntime.HorizontalDistance(requesterPosition, position); PlayerProgressRecord playerProgressRecord = ProgressionRegistry.Get(requesterId); if (!BossProgressionRules.HasOpenReceipt(playerProgressRecord?.KillReceipts ?? BossCreditFlags.None, playerProgressRecord?.BossCredits ?? BossCreditFlags.None, tier) || !BossProgressionRules.CanRequestSacrifice(playerProgressRecord.KillReceipts, tier, trophyMatches: true, horizontalDistance, settings.SacrificeTrophyRange)) { throw new InvalidOperationException("You must hold this kill receipt and stand within altar range."); } int num = 0; Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null && NeedsOfferingCredit(localPlayer.GetPlayerID(), tier) && BossProgressionRuntime.HorizontalDistance(((Component)localPlayer).transform.position, position) > settings.SacrificeTrophyRange) { num++; } foreach (ZNetPeer connectedPeer in instance.GetConnectedPeers()) { if (PeerCharacterIdentity.TryResolve(connectedPeer, out var playerId, out var _, out var position2) && NeedsOfferingCredit(playerId, tier) && BossProgressionRuntime.HorizontalDistance(position2, position) > settings.SacrificeTrophyRange) { num++; } } return num; } private static bool NeedsOfferingCredit(long playerId, ProgressionTier tier) { if (ProgressionRegistry.HasKillReceipt(playerId, tier)) { return !ProgressionRegistry.HasBossCredit(playerId, tier); } return false; } private static void AnnounceTrophyPickup(long sender, long carrierId, string carrierName, ProgressionTier tier) { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return; } EnsureRegistry(instance); Player localPlayer = Player.m_localPlayer; if (sender != 0L && (Object)(object)localPlayer != (Object)null && ProgressionRegistry.HasKillReceipt(localPlayer.GetPlayerID(), tier) && !ProgressionRegistry.HasBossCredit(localPlayer.GetPlayerID(), tier)) { SendPlayerNotice(0L, PlayerGuidanceNotice.TrophyCarrier, tier, carrierName); } foreach (ZNetPeer connectedPeer in instance.GetConnectedPeers()) { if (connectedPeer != null && connectedPeer.m_uid != sender && PeerCharacterIdentity.TryResolve(connectedPeer, out var playerId, out var _) && ProgressionRegistry.HasKillReceipt(playerId, tier) && !ProgressionRegistry.HasBossCredit(playerId, tier)) { SendPlayerNotice(connectedPeer.m_uid, PlayerGuidanceNotice.TrophyCarrier, tier, carrierName); } } Plugin.Log.LogInfo((object)$"Announced {tier} trophy carrier '{carrierName}' ({carrierId}) to connected kill witnesses."); } private static void OnSacrificeRequest(long sender, ZPackage package) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0066: 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; } int requestId = 0; string trophyPrefabName = string.Empty; try { requestId = package.ReadInt(); ZDOID stoneId = package.ReadZDOID(); ProgressionTier tier = (ProgressionTier)package.ReadInt(); trophyPrefabName = package.ReadString(); if (!PeerCharacterIdentity.TryResolve(instance.GetPeer(sender), out var playerId, out var playerName, out var position)) { throw new InvalidOperationException("The requesting peer has no resolved character."); } ProcessSacrifice(sender, requestId, stoneId, tier, trophyPrefabName, playerId, playerName, position); } catch (Exception ex) { Plugin.Log.LogWarning((object)$"Rejected boss trophy sacrifice from peer {sender}: {ex.Message}"); SendSacrificeResult(sender, requestId, success: false, trophyPrefabName, ex.Message); } } private static void ProcessSacrifice(long sender, int requestId, ZDOID stoneId, ProgressionTier tier, string trophyPrefabName, long requesterId, string requesterName, Vector3 requesterPosition) { //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Unknown result type (might be due to invalid IL or missing references) //IL_011a: Unknown result type (might be due to invalid IL or missing references) //IL_0169: Unknown result type (might be due to invalid IL or missing references) //IL_016b: Unknown result type (might be due to invalid IL or missing references) //IL_01e6: Unknown result type (might be due to invalid IL or missing references) ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer() || !BossProgressionRuntime.IsEnabled()) { throw new InvalidOperationException("Boss progression is not enabled on the server."); } if (!BossDefinitionRegistry.TryFromTier(tier, out var definition) || !BossDefinitionRegistry.TryFromTrophy(trophyPrefabName, out var definition2) || definition2.Tier != tier) { throw new InvalidOperationException("The trophy does not match the requested boss tier."); } if (!TryResolveStone(stoneId, definition, out var position)) { throw new InvalidOperationException("The Forsaken altar could not be resolved or does not match the trophy."); } EnsureRegistry(instance); GateSettings settings = BossProgressionRuntime.GetSettings(); float horizontalDistance = BossProgressionRuntime.HorizontalDistance(requesterPosition, position); PlayerProgressRecord playerProgressRecord = ProgressionRegistry.Get(requesterId); if (!BossProgressionRules.HasOpenReceipt(playerProgressRecord?.KillReceipts ?? BossCreditFlags.None, playerProgressRecord?.BossCredits ?? BossCreditFlags.None, tier) || !BossProgressionRules.CanRequestSacrifice(playerProgressRecord.KillReceipts, tier, trophyMatches: true, horizontalDistance, settings.SacrificeTrophyRange)) { throw new InvalidOperationException("You must participate in this boss kill and stand within the altar range."); } ZoneSystem instance2 = ZoneSystem.instance; if ((Object)(object)instance2 == (Object)null) { throw new InvalidOperationException("The world progression service is unavailable."); } int num = 0; Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null && TryGrantNearby(localPlayer.GetPlayerID(), localPlayer.GetPlayerName(), ((Component)localPlayer).transform.position, position, tier, settings.SacrificeTrophyRange)) { num++; SendPlayerNotice(0L, PlayerGuidanceNotice.BossCredit, tier, string.Empty); } foreach (ZNetPeer connectedPeer in instance.GetConnectedPeers()) { if (PeerCharacterIdentity.TryResolve(connectedPeer, out var playerId, out var playerName, out var position2) && TryGrantNearby(playerId, playerName, position2, position, tier, settings.SacrificeTrophyRange)) { num++; SendPlayerNotice(connectedPeer.m_uid, PlayerGuidanceNotice.BossCredit, tier, string.Empty); } } if (num < 1) { throw new InvalidOperationException("No open kill receipts were present within altar range."); } bool flag = !instance2.GetGlobalKey(definition.DefeatKey); instance2.SetGlobalKey(definition.DefeatKey); ProgressionRegistry.Save(); ProgressionSync.BroadcastState(); BroadcastSacrifice(stoneId, tier, num); SendSacrificeResult(sender, requestId, success: true, trophyPrefabName, string.Empty); Plugin.Log.LogInfo((object)($"Committed {tier} trophy sacrifice by '{requesterName}' ({requesterId}): " + $"world advanced={flag}, eligible witnesses={num}, " + $"receipts consumed={num}.")); } private static bool TryGrantNearby(long playerId, string playerName, Vector3 playerPosition, Vector3 stonePosition, ProgressionTier tier, float range) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) PlayerProgressRecord playerProgressRecord = ProgressionRegistry.Get(playerId); float horizontalDistance = BossProgressionRuntime.HorizontalDistance(playerPosition, stonePosition); if (!BossProgressionRules.HasOpenReceipt(playerProgressRecord?.KillReceipts ?? BossCreditFlags.None, playerProgressRecord?.BossCredits ?? BossCreditFlags.None, tier) || !BossProgressionRules.CanReceiveCredit(playerProgressRecord.KillReceipts, tier, horizontalDistance, range)) { return false; } return ProgressionRegistry.GrantBossCredit(playerId, playerName, tier); } private static void BroadcastSacrifice(ZDOID stoneId, ProgressionTier tier, int eligiblePlayers) { //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Expected O, but got Unknown //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) ZRoutedRpc instance = ZRoutedRpc.instance; if (instance != null) { if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer() && !ZNet.instance.IsDedicated() && BossDefinitionRegistry.TryFromTier(tier, out var definition)) { BossStonePresentation.PlaySacrifice(stoneId, definition, eligiblePlayers); } ZPackage val = new ZPackage(); val.Write(stoneId); val.Write((int)tier); val.Write(eligiblePlayers); instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "jg224.worldstagedirector.BossSacrificeEvent", new object[1] { val }); } } private static void OnSacrificeEvent(long sender, ZPackage package) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) if (((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) || !IsServerSender(sender)) { return; } try { ZDOID stoneId = package.ReadZDOID(); int tier = package.ReadInt(); int num = package.ReadInt(); if (BossDefinitionRegistry.TryFromTier((ProgressionTier)tier, out var definition) && num >= 1 && num <= 100) { BossStonePresentation.PlaySacrifice(stoneId, definition, num); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not play boss sacrifice event: " + ex.Message)); } } private static void SendSacrificeResult(long peerUid, int requestId, bool success, string trophyPrefabName, string message) { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Expected O, but got Unknown if (peerUid == 0L && (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { BossStonePatches.ResolveRequest(requestId, success, trophyPrefabName, message); return; } ZRoutedRpc instance = ZRoutedRpc.instance; if (instance != null) { ZPackage val = new ZPackage(); val.Write(requestId); val.Write(success); val.Write(trophyPrefabName ?? string.Empty); val.Write(message ?? string.Empty); instance.InvokeRoutedRPC(peerUid, "jg224.worldstagedirector.BossSacrificeResult", new object[1] { val }); } } private static void OnSacrificeResult(long sender, ZPackage package) { if ((!((Object)(object)ZNet.instance != (Object)null) || !ZNet.instance.IsServer()) && IsServerSender(sender)) { BossStonePatches.ResolveRequest(package.ReadInt(), package.ReadBool(), package.ReadString(), package.ReadString()); } } private static void SendPlayerNotice(long peerUid, PlayerGuidanceNotice notice, ProgressionTier tier, string actorName) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Expected O, but got Unknown if (peerUid == 0L && (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { PlayerGuidance.ShowNotice(notice, tier, actorName); return; } ZRoutedRpc instance = ZRoutedRpc.instance; if (instance != null) { ZPackage val = new ZPackage(); val.Write((int)notice); val.Write((int)tier); val.Write(actorName ?? string.Empty); instance.InvokeRoutedRPC(peerUid, "jg224.worldstagedirector.BossPlayerNotice", new object[1] { val }); } } private static void OnPlayerNotice(long sender, ZPackage package) { if (!IsServerSender(sender)) { return; } try { PlayerGuidanceNotice playerGuidanceNotice = (PlayerGuidanceNotice)package.ReadInt(); ProgressionTier tier = (ProgressionTier)package.ReadInt(); string actorName = package.ReadString(); if (playerGuidanceNotice >= PlayerGuidanceNotice.KillReceipt && playerGuidanceNotice <= PlayerGuidanceNotice.BossCredit && BossDefinitionRegistry.TryFromTier(tier, out var _)) { PlayerGuidance.ShowNotice(playerGuidanceNotice, tier, actorName); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not display boss progression notice: " + ex.Message)); } } private static void SendOfferingReadinessResult(long peerUid, int requestId, bool success, int missingPlayers, string message) { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Expected O, but got Unknown if (peerUid == 0L && (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { BossStonePatches.ResolveReadiness(requestId, success, missingPlayers, message); return; } ZRoutedRpc instance = ZRoutedRpc.instance; if (instance != null) { ZPackage val = new ZPackage(); val.Write(requestId); val.Write(success); val.Write(missingPlayers); val.Write(message ?? string.Empty); instance.InvokeRoutedRPC(peerUid, "jg224.worldstagedirector.BossOfferingReadinessResult", new object[1] { val }); } } private static void OnOfferingReadinessResult(long sender, ZPackage package) { if (!IsServerSender(sender)) { return; } try { int requestId = package.ReadInt(); bool success = package.ReadBool(); int num = package.ReadInt(); string message = package.ReadString(); if (num >= 0 && num <= 100) { BossStonePatches.ResolveReadiness(requestId, success, num, message); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not process altar readiness result: " + ex.Message)); } } private static bool TryResolveStone(ZDOID stoneId, BossDefinition definition, out Vector3 position) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Unknown result type (might be due to invalid IL or missing references) position = Vector3.zero; ZDOMan instance = ZDOMan.instance; ZDO val = ((instance != null) ? instance.GetZDO(stoneId) : null); ZNetScene instance2 = ZNetScene.instance; if (val == null || (Object)(object)instance2 == (Object)null) { return false; } GameObject prefab = instance2.GetPrefab(val.GetPrefab()); StatusEffect val2 = (((Object)(object)prefab == (Object)null) ? null : (prefab.GetComponent() ?? prefab.GetComponentInParent() ?? prefab.GetComponentInChildren()))?.m_itemStand?.m_guardianPower; if ((Object)(object)val2 == (Object)null || !BossDefinitionRegistry.TryFromGuardianPower(((Object)val2).name, out var definition2) || definition2.Tier != definition.Tier) { return false; } position = val.GetPosition(); return true; } private static bool ZdoMatchesBoss(ZDO zdo, BossDefinition expected) { ZNetScene instance = ZNetScene.instance; GameObject val = ((instance != null) ? instance.GetPrefab(zdo.GetPrefab()) : null); Character val2 = (((Object)(object)val == (Object)null) ? null : val.GetComponent()); if ((Object)(object)val2 != (Object)null && BossProgressionRuntime.TryGetDefinition(val2, out var definition)) { return definition.Tier == expected.Tier; } return false; } private static bool IsServerSender(long sender) { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { return false; } if (instance.IsServer()) { return false; } ZNetPeer serverPeer = instance.GetServerPeer(); if (serverPeer != null) { return serverPeer.m_uid == sender; } return false; } private static void EnsureRegistry(ZNet znet) { if (ProgressionRegistry.WorldUid == 0L) { ProgressionRegistry.LoadForWorld(znet.GetWorldUID()); } } private static bool IsFinite(Vector3 value) { //IL_0000: 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_001a: Unknown result type (might be due to invalid IL or missing references) if (IsFinite(value.x) && IsFinite(value.y)) { return IsFinite(value.z); } return false; } private static bool IsFinite(float value) { if (!float.IsNaN(value)) { return !float.IsInfinity(value); } return false; } } internal static class BossProgressionRuntime { internal static GateSettings GetSettings() { ZNet instance = ZNet.instance; if ((Object)(object)instance != (Object)null && instance.IsServer()) { return ModConfig.Snapshot(); } if (!ClientProgressionState.HasServerSnapshot) { return ModConfig.Snapshot(); } return ClientProgressionState.GetSettings(); } internal static bool IsEnabled() { GateSettings settings = GetSettings(); if (settings.Enabled) { return settings.BossProgressionEnabled; } return false; } internal static bool TryGetDefinition(Character character, out BossDefinition definition) { definition = null; if ((Object)(object)character == (Object)null || !character.IsBoss()) { return false; } if (BossDefinitionRegistry.TryFromDefeatKey(character.m_defeatSetGlobalKey, out definition)) { return true; } return BossDefinitionRegistry.TryFromBossPrefab(Utils.GetPrefabName(((Component)character).gameObject), out definition); } internal static float HorizontalDistance(Vector3 first, Vector3 second) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: 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_0013: Unknown result type (might be due to invalid IL or missing references) float num = first.x - second.x; float num2 = first.z - second.z; return Mathf.Sqrt(num * num + num2 * num2); } } internal static class BossStonePatches { private sealed class PendingReadiness { internal ZDOID StoneId { get; } internal ProgressionTier Tier { get; } internal string TrophyPrefabName { get; } internal PendingReadiness(ZDOID stoneId, ProgressionTier tier, string trophyPrefabName) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) StoneId = stoneId; Tier = tier; TrophyPrefabName = trophyPrefabName; } } private static readonly Dictionary PendingRequests = new Dictionary(); private static readonly Dictionary PendingReadinessChecks = new Dictionary(); private static readonly Dictionary ArmedOfferings = new Dictionary(); private const float OfferingConfirmationSeconds = 15f; private static int _nextRequestId; internal static bool TryOffer(ItemStand stand, Humanoid user, ItemData item, out bool accepted) { //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_012f: Unknown result type (might be due to invalid IL or missing references) //IL_0171: Unknown result type (might be due to invalid IL or missing references) //IL_018c: Unknown result type (might be due to invalid IL or missing references) //IL_01a6: Unknown result type (might be due to invalid IL or missing references) //IL_014d: Unknown result type (might be due to invalid IL or missing references) //IL_015c: Unknown result type (might be due to invalid IL or missing references) accepted = false; if (!BossProgressionRuntime.IsEnabled() || (Object)(object)user != (Object)(object)Player.m_localPlayer || !BossStonePresentation.TryGetDefinition(stand, out var _, out var definition) || !TrophyRules.TryGetManagedTrophy(item, out var definition2, out var prefabName)) { return false; } accepted = true; Player localPlayer = Player.m_localPlayer; if (!PrivateArea.CheckAccess(((Component)stand).transform.position, 0f, true, false)) { return true; } if (definition2.Tier != definition.Tier) { ((Character)localPlayer).Message((MessageType)2, "$piece_itemstand_cantattach", 0, (Sprite)null); return true; } float horizontalDistance = BossProgressionRuntime.HorizontalDistance(((Component)localPlayer).transform.position, ((Component)stand).transform.position); GateSettings settings = BossProgressionRuntime.GetSettings(); if (BossStonePresentation.HasLocalCredit(definition) || !BossProgressionRules.CanRequestSacrifice(BossStonePresentation.HasLocalKillReceipt(definition) ? BossDefinitionRegistry.FlagFor(definition.Tier) : BossCreditFlags.None, definition.Tier, trophyMatches: true, horizontalDistance, settings.SacrificeTrophyRange)) { ((Character)localPlayer).Message((MessageType)2, "You must help kill this boss and witness its death before offering the trophy.", 0, (Sprite)null); return true; } if ((Object)(object)ZNet.instance == (Object)null || ZRoutedRpc.instance == null) { ((Character)localPlayer).Message((MessageType)2, "The altar is not connected to the server yet.", 0, (Sprite)null); return true; } ZNetView itemStandNetworkView = BossStonePresentation.GetItemStandNetworkView(stand); ZDO val = ((itemStandNetworkView != null) ? itemStandNetworkView.GetZDO() : null); if (val == null) { ((Character)localPlayer).Message((MessageType)2, "The altar is not network-ready.", 0, (Sprite)null); return true; } if (ArmedOfferings.TryGetValue(val.m_uid, out var value) && Time.time <= value) { ArmedOfferings.Remove(val.m_uid); CommitOffering(localPlayer, item, val.m_uid, definition, prefabName); } else { ArmedOfferings.Remove(val.m_uid); int num = NextRequestId(); PendingReadinessChecks[num] = new PendingReadiness(val.m_uid, definition.Tier, prefabName); BossProgressionRpc.SubmitOfferingReadinessCheck(num, val.m_uid, definition.Tier); } return true; } internal static void ResolveReadiness(int requestId, bool success, int missingPlayers, string message) { //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Unknown result type (might be due to invalid IL or missing references) if (!PendingReadinessChecks.TryGetValue(requestId, out var value)) { return; } PendingReadinessChecks.Remove(requestId); Player localPlayer = Player.m_localPlayer; if (!success || (Object)(object)localPlayer == (Object)null || !BossDefinitionRegistry.TryFromTier(value.Tier, out var definition)) { if (!string.IsNullOrWhiteSpace(message) && localPlayer != null) { ((Character)localPlayer).Message((MessageType)2, message, 0, (Sprite)null); } return; } if (missingPlayers > 0) { ArmedOfferings[value.StoneId] = Time.time + 15f; PlayerGuidance.ShowOfferingWarning(definition, BossProgressionRuntime.GetSettings().SacrificeTrophyRange, 15f, missingPlayers); return; } ItemData val = FindTrophyByPrefab(((Humanoid)localPlayer).GetInventory(), value.TrophyPrefabName); if (val == null) { ((Character)localPlayer).Message((MessageType)2, "$piece_itemstand_missingitem", 0, (Sprite)null); } else { CommitOffering(localPlayer, val, value.StoneId, definition, value.TrophyPrefabName); } } internal static void ResolveRequest(int requestId, bool success, string trophyPrefabName, string message) { if (!PendingRequests.TryGetValue(requestId, out var value)) { return; } PendingRequests.Remove(requestId); if (success) { Player localPlayer = Player.m_localPlayer; if (localPlayer != null) { ((Character)localPlayer).Message((MessageType)2, "The offering is accepted. The world advances.", 0, (Sprite)null); } } else { TrophyRules.Refund(string.IsNullOrWhiteSpace(value) ? trophyPrefabName : value); if (!string.IsNullOrWhiteSpace(message)) { Plugin.Log.LogWarning((object)("Trophy offering rejected: " + message)); } } } internal static void ResetPendingRequests() { PendingRequests.Clear(); PendingReadinessChecks.Clear(); ArmedOfferings.Clear(); _nextRequestId = 0; } internal static ItemData FindMatchingTrophy(ItemStand stand, Inventory inventory) { if (!BossStonePresentation.TryGetDefinition(stand, out var _, out var definition) || inventory == null) { return null; } foreach (ItemData allItem in inventory.GetAllItems()) { if (TrophyRules.TryGetManagedTrophy(allItem, out var definition2, out var _) && definition2.Tier == definition.Tier) { return allItem; } } return null; } private static int NextRequestId() { _nextRequestId = ((_nextRequestId == int.MaxValue) ? 1 : (_nextRequestId + 1)); return _nextRequestId; } private static ItemData FindTrophyByPrefab(Inventory inventory, string trophyPrefabName) { if (inventory == null) { return null; } foreach (ItemData allItem in inventory.GetAllItems()) { if (string.Equals(TrophyRules.GetPrefabName(allItem), trophyPrefabName, StringComparison.OrdinalIgnoreCase)) { return allItem; } } return null; } private static void CommitOffering(Player player, ItemData item, ZDOID stoneId, BossDefinition definition, string trophyPrefabName) { //IL_0031: Unknown result type (might be due to invalid IL or missing references) if (!((Humanoid)player).GetInventory().RemoveOneItem(item)) { ((Character)player).Message((MessageType)2, "$piece_itemstand_missingitem", 0, (Sprite)null); return; } int num = NextRequestId(); PendingRequests[num] = trophyPrefabName; BossProgressionRpc.SubmitSacrifice(num, stoneId, definition, trophyPrefabName); } } [HarmonyPatch(typeof(ItemStand), "UseItem", new Type[] { typeof(Humanoid), typeof(ItemData) })] internal static class BossStoneUseItemPatch { [HarmonyPrefix] private static bool Prefix(ItemStand __instance, Humanoid user, ItemData item, ref bool __result) { if (!BossStonePatches.TryOffer(__instance, user, item, out var accepted)) { return true; } __result = accepted; return false; } } [HarmonyPatch(typeof(ItemStand), "GetHoverText")] internal static class BossStoneHoverTextPatch { [HarmonyPostfix] private static void Postfix(ItemStand __instance, ref string __result) { if (BossProgressionRuntime.IsEnabled() && !((Object)(object)Player.m_localPlayer == (Object)null) && BossStonePresentation.TryGetDefinition(__instance, out var _, out var definition)) { bool num = BossStonePresentation.HasLocalCredit(definition); bool flag = BossStonePresentation.HasLocalKillReceipt(definition); if (num) { __result += "\n\nPersonal boss progression unlocked"; } else if (flag) { __result += "\n\nKill witnessed — be within the altar range when the trophy is offered"; } else { __result += "\n\nDamage this boss and witness its death to earn progression"; } if (BossStonePatches.FindMatchingTrophy(__instance, ((Humanoid)Player.m_localPlayer).GetInventory()) != null && flag) { __result += "\nUse the matching trophy on this hook after your group has gathered"; } } } } [HarmonyPatch(typeof(Hud), "UpdateCrosshair", new Type[] { typeof(Player), typeof(float) })] internal static class YagluthBossStoneHoverLayoutPatch { internal const int VerticalOffset = 160; private static RectTransform _trackedHoverText; private static bool _offsetApplied; [HarmonyPostfix] [HarmonyPriority(0)] private static void Postfix(Hud __instance, Player player) { //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0089: 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) RectTransform val = (((Object)(object)__instance == (Object)null || (Object)(object)__instance.m_hoverName == (Object)null) ? null : ((TMP_Text)__instance.m_hoverName).rectTransform); RestorePreviousOffset(val); if (!((Object)(object)val == (Object)null) && !((Object)(object)player == (Object)null)) { GameObject hoverObject = ((Humanoid)player).GetHoverObject(); ItemStand val2 = (((Object)(object)hoverObject == (Object)null) ? null : hoverObject.GetComponentInParent()); if (!((Object)(object)val2 == (Object)null) && BossStonePresentation.TryGetDefinition(val2, out var _, out var definition) && definition.Tier == ProgressionTier.Yagluth) { val.anchoredPosition += Vector2.up * 160f; _trackedHoverText = val; _offsetApplied = true; } } } private static void RestorePreviousOffset(RectTransform currentHoverText) { //IL_001a: 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_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) if (_offsetApplied && (Object)(object)_trackedHoverText != (Object)null) { RectTransform trackedHoverText = _trackedHoverText; trackedHoverText.anchoredPosition -= Vector2.up * 160f; } _trackedHoverText = currentHoverText; _offsetApplied = false; } } [HarmonyPatch(typeof(ItemStand), "HaveAttachment")] internal static class PersonalBossStoneAttachmentPatch { [HarmonyPrefix] private static bool Prefix(ItemStand __instance, ref bool __result) { if (!BossProgressionRuntime.IsEnabled() || !BossStonePresentation.TryGetDefinition(__instance, out var stone, out var definition)) { return true; } __result = BossStonePresentation.IsSacrificeActive(stone) || BossStonePresentation.HasLocalCredit(definition); return false; } } [HarmonyPatch(typeof(ItemStand), "UpdateVisual")] internal static class PersonalBossStoneVisualPatch { [HarmonyPrefix] private static bool Prefix(ItemStand __instance) { if (!BossProgressionRuntime.IsEnabled() || !BossStonePresentation.TryGetDefinition(__instance, out var stone, out var definition)) { return true; } BossStonePresentation.SetPersonalVisual(__instance, BossStonePresentation.IsSacrificeActive(stone) || BossStonePresentation.HasLocalCredit(definition)); return false; } } [HarmonyPatch(typeof(BossStone), "Start")] internal static class BossStoneStartPatch { [HarmonyPostfix] private static void Postfix(BossStone __instance) { BossStonePresentation.Track(__instance); } } [HarmonyPatch(typeof(BossStone), "SetActivated", new Type[] { typeof(bool), typeof(bool) })] internal static class PersonalBossStoneActivationPatch { [HarmonyPrefix] private static void Prefix(BossStone __instance, ref bool active) { if (BossProgressionRuntime.IsEnabled() && BossStonePresentation.TryGetDefinition(__instance.m_itemStand, out var _, out var definition)) { active = BossStonePresentation.IsSacrificeActive(__instance) || BossStonePresentation.HasLocalCredit(definition); } } } internal static class BossStonePresentation { private static readonly HashSet LoadedStones = new HashSet(); private static readonly HashSet ActiveSacrifices = new HashSet(); private static readonly MethodInfo SetActivatedMethod = AccessTools.Method(typeof(BossStone), "SetActivated", new Type[2] { typeof(bool), typeof(bool) }, (Type[])null); private static readonly FieldInfo ActiveField = AccessTools.Field(typeof(BossStone), "m_active"); private static readonly MethodInfo SetVisualItemMethod = AccessTools.Method(typeof(ItemStand), "SetVisualItem", new Type[4] { typeof(string), typeof(int), typeof(int), typeof(int) }, (Type[])null); private static readonly MethodInfo GetOrientationMethod = AccessTools.Method(typeof(ItemStand), "GetOrientation", (Type[])null, (Type[])null); internal static bool TryGetDefinition(ItemStand stand, out BossStone stone, out BossDefinition definition) { definition = null; stone = (((Object)(object)stand == (Object)null) ? null : ((Component)stand).GetComponentInParent()); StatusEffect val = stand?.m_guardianPower; if ((Object)(object)stone != (Object)null && (Object)(object)val != (Object)null) { return BossDefinitionRegistry.TryFromGuardianPower(((Object)val).name, out definition); } return false; } internal static bool HasLocalCredit(BossDefinition definition) { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null || definition == null) { return false; } ZNet instance = ZNet.instance; if (!((Object)(object)instance != (Object)null) || !instance.IsServer()) { return ClientProgressionState.HasLocalBossCredit(definition.Tier); } return ProgressionRegistry.HasBossCredit(localPlayer.GetPlayerID(), definition.Tier); } internal static bool HasLocalKillReceipt(BossDefinition definition) { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null || definition == null) { return false; } ZNet instance = ZNet.instance; if (!((Object)(object)instance != (Object)null) || !instance.IsServer()) { return ClientProgressionState.HasLocalKillReceipt(definition.Tier); } return ProgressionRegistry.HasKillReceipt(localPlayer.GetPlayerID(), definition.Tier); } internal static void Track(BossStone stone) { if (!((Object)(object)stone == (Object)null)) { LoadedStones.Add(stone); Refresh(stone); } } internal static void RefreshLoadedStones() { LoadedStones.RemoveWhere((BossStone stone) => (Object)(object)stone == (Object)null); foreach (BossStone loadedStone in LoadedStones) { Refresh(loadedStone); } } internal static void Refresh(BossStone stone) { ItemStand stand = stone?.m_itemStand; if (TryGetDefinition(stand, out var _, out var definition)) { bool active = HasLocalCredit(definition); SetActivated(stone, active, effect: false); SetPersonalVisual(stand, active); } } internal static void SetPersonalVisual(ItemStand stand, bool active) { if (!((Object)(object)stand == (Object)null)) { string text = ((active && stand.m_supportedItems != null && stand.m_supportedItems.Count > 0) ? ((Object)stand.m_supportedItems[0]).name : string.Empty); int num = ((!(GetOrientationMethod == null)) ? ((int)GetOrientationMethod.Invoke(stand, null)) : 0); SetVisualItemMethod?.Invoke(stand, new object[4] { text, 0, 1, num }); } } internal static ZNetView GetItemStandNetworkView(ItemStand stand) { if ((Object)(object)stand == (Object)null) { return null; } if (!Object.op_Implicit((Object)(object)stand.m_netViewOverride)) { return ((Component)stand).GetComponent(); } return stand.m_netViewOverride; } internal static bool IsSacrificeActive(BossStone stone) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) object obj; if (!((Object)(object)stone?.m_itemStand == (Object)null)) { ZNetView itemStandNetworkView = GetItemStandNetworkView(stone.m_itemStand); obj = ((itemStandNetworkView != null) ? itemStandNetworkView.GetZDO() : null); } else { obj = null; } ZDO val = (ZDO)obj; if (val != null) { return ActiveSacrifices.Contains(val.m_uid); } return false; } internal static void PlaySacrifice(ZDOID stoneId, BossDefinition definition, int eligiblePlayers) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0054: 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) ZNetScene instance = ZNetScene.instance; GameObject val = ((instance != null) ? instance.FindInstance(stoneId) : null); BossStone val2 = (((Object)(object)val == (Object)null) ? null : val.GetComponentInParent()); if ((Object)(object)val2 == (Object)null) { Plugin.Log.LogWarning((object)$"Could not find loaded {definition.Tier} altar for sacrifice effects."); return; } ActiveSacrifices.Add(stoneId); ActiveField?.SetValue(val2, false); SetActivated(val2, active: true, effect: true); SetPersonalVisual(val2.m_itemStand, active: true); ((MonoBehaviour)val2).StartCoroutine(CompleteSacrifice(val2, stoneId, definition, eligiblePlayers)); } internal static void Clear() { LoadedStones.Clear(); ActiveSacrifices.Clear(); } private static IEnumerator CompleteSacrifice(BossStone stone, ZDOID stoneId, BossDefinition definition, int eligiblePlayers) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) GateSettings settings = BossProgressionRuntime.GetSettings(); yield return (object)new WaitForSeconds(settings.SacrificeLootDelaySeconds); ZNetView val = (((Object)(object)stone?.m_itemStand == (Object)null) ? null : GetItemStandNetworkView(stone.m_itemStand)); if ((Object)(object)stone != (Object)null && (Object)(object)val != (Object)null && val.IsOwner()) { BossLootService.SpawnSacrificeLoot(stone, definition, eligiblePlayers); } float num = Mathf.Max(0f, 12f - settings.SacrificeLootDelaySeconds); if (num > 0f) { yield return (object)new WaitForSeconds(num); } ActiveSacrifices.Remove(stoneId); if ((Object)(object)stone != (Object)null) { Refresh(stone); } } private static void SetActivated(BossStone stone, bool active, bool effect) { SetActivatedMethod?.Invoke(stone, new object[2] { active, effect }); } } [HarmonyPatch(typeof(ZoneSystem), "Start")] internal static class PlayerEventsWorldModifierPatch { [HarmonyPostfix] private static void Postfix(ZoneSystem __instance) { ZNet instance = ZNet.instance; if (!((Object)(object)instance == (Object)null) && instance.IsServer() && ModConfig.Enabled.Value && ModConfig.BossProgressionEnabled.Value && ModConfig.AutoEnablePlayerEvents.Value) { __instance.SetGlobalKey((GlobalKeys)13); Plugin.Log.LogInfo((object)"World modifier enabled: PlayerEvents."); } } } internal enum PlayerGuidanceNotice { KillReceipt = 1, TrophyCarrier, BossCredit } internal static class PlayerGuidance { private static readonly Dictionary RecentTrophyPickups = new Dictionary(); internal const int OnboardingDisplaySeconds = 60; internal const int OnboardingVerticalOffset = 80; private static bool _onboardingShown; private static TMP_Text _onboardingText; internal static void ShowOnboarding(Player player) { if (!_onboardingShown && !((Object)(object)player == (Object)null) && !((Object)(object)player != (Object)(object)Player.m_localPlayer) && BossProgressionRuntime.IsEnabled()) { _onboardingShown = true; if (ModConfig.ConsumeOnboardingLogin(((Object)(object)ZNet.instance == (Object)null) ? 0 : ZNet.instance.GetWorldUID(), player.GetPlayerID())) { ((MonoBehaviour)player).StartCoroutine(ShowOnboardingMessages(player)); } } } internal static void OnTrophyPickedUp(BossDefinition definition) { Player localPlayer = Player.m_localPlayer; if (!((Object)(object)localPlayer == (Object)null) && definition != null && BossProgressionRuntime.IsEnabled() && (!RecentTrophyPickups.TryGetValue(definition.Tier, out var value) || !(Time.time - value < 10f))) { RecentTrophyPickups[definition.Tier] = Time.time; ((Character)localPlayer).Message((MessageType)2, "You carry the " + BossName(definition.Tier) + " trophy — wait for the group.", 0, (Sprite)null); ((Character)localPlayer).Message((MessageType)1, "All players who fought this boss must be present for the sacrifice to receive rewards.", 0, (Sprite)null); ((MonoBehaviour)localPlayer).StartCoroutine(SubmitTrophyPickupAfterDelay(localPlayer, definition.Tier)); } } internal static void ShowOfferingWarning(BossDefinition definition, float range, float confirmationSeconds, int missingPlayers) { Player localPlayer = Player.m_localPlayer; if (!((Object)(object)localPlayer == (Object)null) && definition != null) { ((Character)localPlayer).Message((MessageType)2, "WAIT FOR YOUR PARTY", 0, (Sprite)null); string text = ((missingPlayers == 1) ? "1 eligible player is" : $"{missingPlayers} eligible players are"); ((Character)localPlayer).Message((MessageType)1, text + " outside the " + FormatRange(range) + "m altar range. Only nearby " + BossName(definition.Tier) + " kill witnesses receive personal credit. When everyone is gathered, use the trophy on the hook again within " + confirmationSeconds.ToString("0", CultureInfo.InvariantCulture) + " seconds to confirm.", 0, (Sprite)null); } } internal static void ShowNotice(PlayerGuidanceNotice notice, ProgressionTier tier, string actorName) { Player localPlayer = Player.m_localPlayer; if (!((Object)(object)localPlayer == (Object)null) && BossDefinitionRegistry.TryFromTier(tier, out var _)) { float sacrificeTrophyRange = BossProgressionRuntime.GetSettings().SacrificeTrophyRange; string text = BossName(tier); switch (notice) { case PlayerGuidanceNotice.KillReceipt: ((Character)localPlayer).Message((MessageType)2, text + " kill witnessed — receipt earned.", 0, (Sprite)null); ((Character)localPlayer).Message((MessageType)1, "Do not miss the offering: gather within " + FormatRange(sacrificeTrophyRange) + "m of the " + text + " altar before the trophy holder uses the trophy on its hook.", 0, (Sprite)null); break; case PlayerGuidanceNotice.TrophyCarrier: { string text2 = (string.IsNullOrWhiteSpace(actorName) ? "A player" : actorName); ((Character)localPlayer).Message((MessageType)1, text2 + " has the " + text + " trophy. Meet at its altar and stand within " + FormatRange(sacrificeTrophyRange) + "m before it is offered.", 0, (Sprite)null); break; } case PlayerGuidanceNotice.BossCredit: ((Character)localPlayer).Message((MessageType)2, text + " offering witnessed — personal progression unlocked.", 0, (Sprite)null); break; } } } internal static void Reset() { _onboardingShown = false; RecentTrophyPickups.Clear(); DestroyOnboardingText(); } private static IEnumerator ShowOnboardingMessages(Player player) { yield return (object)new WaitForSeconds(3f); if ((Object)(object)player == (Object)null || (Object)(object)player != (Object)(object)Player.m_localPlayer) { yield break; } string text = BuildOnboardingMessage(BossProgressionRuntime.GetSettings()); MessageHud messageHud = MessageHud.instance; TMP_Text val = (((Object)(object)messageHud == (Object)null) ? null : messageHud.m_messageText); if ((Object)(object)val == (Object)null) { ((Character)player).Message((MessageType)1, text, 0, (Sprite)null); yield break; } DestroyOnboardingText(); TMP_Text overlay = (_onboardingText = Object.Instantiate(val, val.transform.parent)); ((Object)overlay).name = "WorldStageDirector onboarding"; overlay.text = text; overlay.overflowMode = (TextOverflowModes)0; ((Graphic)overlay).raycastTarget = false; RectTransform rectTransform = overlay.rectTransform; rectTransform.anchoredPosition += Vector2.down * 80f; ((Component)overlay).gameObject.SetActive(true); overlay.transform.SetAsLastSibling(); ((Graphic)overlay).canvasRenderer.SetAlpha(1f); float hideAt = Time.realtimeSinceStartup + 60f; while ((Object)(object)player != (Object)null && (Object)(object)player == (Object)(object)Player.m_localPlayer && (Object)(object)overlay != (Object)null && (Object)(object)MessageHud.instance == (Object)(object)messageHud && Time.realtimeSinceStartup < hideAt) { ((Behaviour)overlay).enabled = !Hud.IsUserHidden(); yield return null; } if ((Object)(object)_onboardingText == (Object)(object)overlay) { DestroyOnboardingText(); } } internal static string BuildOnboardingMessage(GateSettings settings) { return "WorldStageDirector: To be credited for a boss kill players must damage a boss and stay within " + FormatRange(settings.BossKillWitnessRadius) + "m when it dies to earn a kill receipt.\n\n\nAfter the kill, all witnesses must gather within " + FormatRange(settings.SacrificeTrophyRange) + "m of the altar before the trophy is used on its hook. Press E normally to select powers."; } private static void DestroyOnboardingText() { if ((Object)(object)_onboardingText != (Object)null) { Object.Destroy((Object)(object)((Component)_onboardingText).gameObject); } _onboardingText = null; } private static IEnumerator SubmitTrophyPickupAfterDelay(Player player, ProgressionTier tier) { yield return (object)new WaitForSeconds(1f); if ((Object)(object)player != (Object)null && (Object)(object)player == (Object)(object)Player.m_localPlayer) { BossProgressionRpc.SubmitTrophyPickup(tier); } } private static string BossName(ProgressionTier tier) { return tier switch { ProgressionTier.Elder => "The Elder", ProgressionTier.Queen => "The Queen", _ => tier.ToString(), }; } private static string FormatRange(float range) { return range.ToString("0", CultureInfo.InvariantCulture); } } [HarmonyPatch(typeof(Player), "OnSpawned", new Type[] { typeof(bool) })] internal static class PlayerGuidanceSpawnPatch { [HarmonyPostfix] private static void Postfix(Player __instance) { PlayerGuidance.ShowOnboarding(__instance); } } [HarmonyPatch(typeof(Humanoid), "Pickup", new Type[] { typeof(GameObject), typeof(bool), typeof(bool) })] internal static class BossTrophyPickupPatch { [HarmonyPrefix] private static void Prefix(Humanoid __instance, GameObject go, out BossDefinition __state) { __state = null; if (!((Object)(object)__instance != (Object)(object)Player.m_localPlayer) && !((Object)(object)go == (Object)null)) { ItemDrop component = go.GetComponent(); if ((Object)(object)component != (Object)null) { TrophyRules.TryGetManagedTrophy(component.m_itemData, out __state, out var _); } } } [HarmonyPostfix] private static void Postfix(Humanoid __instance, bool __result, BossDefinition __state) { if (__result && (Object)(object)__instance == (Object)(object)Player.m_localPlayer && __state != null) { PlayerGuidance.OnTrophyPickedUp(__state); } } } internal static class ServerBossFightLedger { private sealed class Fight { internal ProgressionTier Tier { get; } internal HashSet Participants { get; } = new HashSet(); internal Fight(ProgressionTier tier) { Tier = tier; } } private static readonly object Sync = new object(); private static readonly Dictionary Fights = new Dictionary(); internal static bool Register(ZDOID bossId, ProgressionTier tier, long playerId) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) if (bossId == ZDOID.None || playerId <= 0 || !BossDefinitionRegistry.TryFromTier(tier, out var _)) { return false; } lock (Sync) { if (!Fights.TryGetValue(bossId, out var value)) { value = new Fight(tier); Fights[bossId] = value; } else if (value.Tier != tier) { return false; } return value.Participants.Add(playerId); } } internal static HashSet Consume(ZDOID bossId, ProgressionTier tier) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) lock (Sync) { if (!Fights.TryGetValue(bossId, out var value) || value.Tier != tier) { return new HashSet(); } Fights.Remove(bossId); return new HashSet(value.Participants); } } internal static void Clear() { lock (Sync) { Fights.Clear(); } } } internal static class TrophyRules { internal static void ApplyLoadedPrefabs() { ObjectDB instance = ObjectDB.instance; if ((Object)(object)instance == (Object)null) { return; } GateSettings settings = BossProgressionRuntime.GetSettings(); foreach (BossDefinition item in BossDefinitionRegistry.All) { GameObject itemPrefab = instance.GetItemPrefab(item.TrophyPrefabName); ItemDrop val = (((Object)(object)itemPrefab == (Object)null) ? null : itemPrefab.GetComponent()); if (val?.m_itemData?.m_shared != null) { val.m_itemData.m_shared.m_weight = ((item.Tier == ProgressionTier.Eikthyr) ? Mathf.Min(200f, settings.BossTrophyWeight) : settings.BossTrophyWeight); val.m_itemData.m_shared.m_maxStackSize = settings.BossTrophyMaxStackSize; val.m_itemData.m_shared.m_teleportable = settings.BossTrophyPortalRestriction == TrophyPortalRestriction.None; val.m_autoPickup = settings.BossTrophyAutoPickup; } } } internal static bool TryGetManagedTrophy(ItemData item, out BossDefinition definition, out string prefabName) { prefabName = GetPrefabName(item); return BossDefinitionRegistry.TryFromTrophy(prefabName, out definition); } internal static string GetPrefabName(ItemData item) { if (!((Object)(object)item?.m_dropPrefab == (Object)null)) { return ((Object)item.m_dropPrefab).name; } return string.Empty; } internal static bool ContainsManagedTrophy(Inventory inventory) { if (inventory == null) { return false; } foreach (ItemData allItem in inventory.GetAllItems()) { if (TryGetManagedTrophy(allItem, out var _, out var _)) { return true; } } return false; } internal static void Refund(string trophyPrefabName) { //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; ObjectDB instance = ObjectDB.instance; GameObject val = ((instance != null) ? instance.GetItemPrefab(trophyPrefabName) : null); if ((Object)(object)localPlayer == (Object)null || (Object)(object)val == (Object)null) { Plugin.Log.LogError((object)("Could not refund rejected trophy sacrifice '" + trophyPrefabName + "'.")); return; } if (((Humanoid)localPlayer).GetInventory().AddItem(val, 1)) { ((Character)localPlayer).Message((MessageType)2, "The offering was rejected; your trophy was returned.", 0, (Sprite)null); return; } Object.Instantiate(val, ((Component)localPlayer).transform.position + Vector3.up, Quaternion.identity); ((Character)localPlayer).Message((MessageType)2, "The offering was rejected; your trophy was dropped at your feet.", 0, (Sprite)null); Plugin.Log.LogWarning((object)("Refunded rejected trophy '" + trophyPrefabName + "' to the ground because the inventory was full.")); } } [HarmonyPatch(typeof(ObjectDB), "Awake")] internal static class BossTrophyObjectDbPatch { [HarmonyPostfix] private static void Postfix() { TrophyRules.ApplyLoadedPrefabs(); } } [HarmonyPatch(typeof(TeleportWorld), "Teleport", new Type[] { typeof(Player) })] internal static class BossTrophyPortalPatch { [HarmonyPrefix] private static bool Prefix(TeleportWorld __instance, Player player) { if (!BossProgressionRuntime.IsEnabled() || (Object)(object)player == (Object)null || !TrophyRules.ContainsManagedTrophy(((Humanoid)player).GetInventory())) { return true; } TrophyPortalRestriction bossTrophyPortalRestriction = BossProgressionRuntime.GetSettings().BossTrophyPortalRestriction; if (bossTrophyPortalRestriction != TrophyPortalRestriction.WoodAndStone && (bossTrophyPortalRestriction != TrophyPortalRestriction.WoodOnly || __instance.m_allowAllItems)) { return true; } ((Character)player).Message((MessageType)2, "$msg_noteleport", 0, (Sprite)null); return false; } } }