Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of PortalRules v1.0.0
PortalRules.dll
Decompiled 10 hours ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; using System.Threading; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using JetBrains.Annotations; using Jotunn.Configs; using Jotunn.Managers; using Microsoft.CodeAnalysis; using ServerSync; using Splatform; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Events; using UnityEngine.UI; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Core.ObjectPool; using YamlDotNet.Core.Tokens; using YamlDotNet.Helpers; using YamlDotNet.RepresentationModel; using YamlDotNet.Serialization; using YamlDotNet.Serialization.BufferedDeserialization; using YamlDotNet.Serialization.BufferedDeserialization.TypeDiscriminators; using YamlDotNet.Serialization.Callbacks; using YamlDotNet.Serialization.Converters; using YamlDotNet.Serialization.EventEmitters; using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.NodeDeserializers; using YamlDotNet.Serialization.NodeTypeResolvers; using YamlDotNet.Serialization.ObjectFactories; using YamlDotNet.Serialization.ObjectGraphTraversalStrategies; using YamlDotNet.Serialization.ObjectGraphVisitors; using YamlDotNet.Serialization.Schemas; using YamlDotNet.Serialization.TypeInspectors; using YamlDotNet.Serialization.TypeResolvers; using YamlDotNet.Serialization.Utilities; using YamlDotNet.Serialization.ValueDeserializers; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("PortalRules")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("sighsorry")] [assembly: AssemblyProduct("PortalRules")] [assembly: AssemblyCopyright("Copyright © 2026 sighsorry")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("4358610B-F3F4-4843-B7AF-98B7BC60DCDE")] [assembly: AssemblyFileVersion("1.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyVersion("1.0.0.0")] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } } namespace PortalRules { internal static class AdminPortalBiomeDefaults { private sealed class BiomeDefaultsYaml { [YamlMember(Alias = "format_version")] public int FormatVersion { get; set; } [YamlMember(Alias = "biomes")] public Dictionary<string, string?>? Biomes { get; set; } } [HarmonyPatch(typeof(ZoneSystem), "Start")] [HarmonyAfter(new string[] { "expand_world_data" })] [HarmonyPriority(0)] private static class BiomeRegistryReadyPatch { private static void Postfix() { if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { CompleteBiomeRegistryInitialization(); } } } private const int FormatVersion = 1; private const int MaximumBiomeCount = 4096; private const int MaximumBiomeNameLength = 128; private const int MaximumReloadAttempts = 5; private const long MaximumFileBytes = 524288L; private const string DirectoryName = "PortalRules"; private const string FileName = "admin-portal-biome-global-keys.yml"; private const float RegistryReadyFallbackSeconds = 5f; private static readonly KeyValuePair<string, string>[] VanillaTemplate = new KeyValuePair<string, string>[9] { new KeyValuePair<string, string>("Meadows", ""), new KeyValuePair<string, string>("BlackForest", "defeated_eikthyr"), new KeyValuePair<string, string>("Swamp", "defeated_gdking"), new KeyValuePair<string, string>("Ocean", "defeated_bonemass"), new KeyValuePair<string, string>("Mountain", "defeated_bonemass"), new KeyValuePair<string, string>("Plains", "defeated_dragon"), new KeyValuePair<string, string>("Mistlands", "defeated_goblinking"), new KeyValuePair<string, string>("AshLands", "defeated_queen"), new KeyValuePair<string, string>("DeepNorth", "defeated_fader") }; private static readonly HashSet<string> AmbiguousPlainYamlScalars = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "null", "true", "false", "y", "n", "yes", "no", "on", "off" }; private static readonly TimeSpan ReloadDebounce = TimeSpan.FromMilliseconds(500.0); private static readonly TimeSpan ReloadRetryDelay = TimeSpan.FromSeconds(1.0); private static readonly TimeSpan PollingInterval = TimeSpan.FromSeconds(30.0); private static readonly TimeSpan WatcherRestartRetryDelay = TimeSpan.FromSeconds(10.0); private static readonly IDeserializer Deserializer = new DeserializerBuilder().WithDuplicateKeyChecking().Build(); private static Dictionary<string, string> _defaults = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); private static FileSystemWatcher? _watcher; private static string _configurationDirectory = ""; private static string _filePath = ""; private static bool _active; private static bool _hasValidSnapshot; private static bool _templatePending; private static bool _biomeRegistryReady; private static bool _registryFallbackLogged; private static float _registryFallbackNotBefore; private static DateTime _templateRetryNotBeforeUtc; private static DateTime _nextPollUtc; private static int _reloadRequested; private static int _reloadAttempts; private static int _watcherRestartRequested; private static long _reloadNotBeforeUtcTicks; private static long _watcherRestartNotBeforeUtcTicks; internal static bool IsActive => _active; internal static bool IsBiomeRegistryReady => _biomeRegistryReady; internal static bool HasValidSnapshot { get { if (_active) { return _hasValidSnapshot; } return false; } } internal static void BeginServerSession() { if (_active || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } bool biomeRegistryReady = _biomeRegistryReady; EndServerSession(); _active = true; _defaults = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); _hasValidSnapshot = false; _templatePending = false; _biomeRegistryReady = biomeRegistryReady; _registryFallbackLogged = false; _registryFallbackNotBefore = Time.realtimeSinceStartup + 5f; _templateRetryNotBeforeUtc = DateTime.MinValue; _nextPollUtc = DateTime.UtcNow + PollingInterval; Interlocked.Exchange(ref _reloadRequested, 0); Interlocked.Exchange(ref _reloadAttempts, 0); Interlocked.Exchange(ref _watcherRestartRequested, 0); Interlocked.Exchange(ref _reloadNotBeforeUtcTicks, 0L); Interlocked.Exchange(ref _watcherRestartNotBeforeUtcTicks, 0L); _configurationDirectory = Path.Combine(Paths.ConfigPath, "PortalRules"); _filePath = Path.Combine(_configurationDirectory, "admin-portal-biome-global-keys.yml"); try { Directory.CreateDirectory(_configurationDirectory); _templatePending = !File.Exists(_filePath); try { StartWatcher(); } catch (Exception ex) { StopWatcher(); Interlocked.Exchange(ref _watcherRestartRequested, 1); ScheduleWatcherRestart(WatcherRestartRetryDelay); PortalRulesPlugin.PortalRulesLogger.LogError((object)("Failed to watch admin-portal-biome-global-keys.yml; periodic reload remains active: " + ex.Message)); } bool changed; if (_templatePending) { PortalRulesPlugin.PortalRulesLogger.LogInfo((object)"admin-portal-biome-global-keys.yml will be generated after the biome registry is ready."); } else if (!TryReload(logOnlyWhenChanged: false, out changed)) { ScheduleReload(ReloadRetryDelay); } } catch (Exception ex2) { StopWatcher(); _active = false; _templatePending = false; PortalRulesPlugin.PortalRulesLogger.LogError((object)("Failed to initialize admin-portal-biome-global-keys.yml: " + ex2.Message)); } } internal static bool EnsureTemplateReady() { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return false; } BeginServerSession(); if (!_active) { return false; } if (!_templatePending) { if (_hasValidSnapshot || !File.Exists(_filePath)) { return _hasValidSnapshot; } return TryReloadImmediately(); } if (!File.Exists(_filePath) && DateTime.UtcNow < _templateRetryNotBeforeUtc) { return false; } string filePath = _filePath; try { bool flag = false; if (!File.Exists(filePath)) { string content = BuildRegistryTemplate(); flag = TryCreateFileWithoutOverwrite(filePath, content); } if (!_active || !string.Equals(_filePath, filePath, StringComparison.Ordinal) || !File.Exists(filePath)) { return false; } _templatePending = false; _templateRetryNotBeforeUtc = DateTime.MinValue; if (flag) { PortalRulesPlugin.PortalRulesLogger.LogInfo((object)"Generated admin-portal-biome-global-keys.yml from the registered biome names."); } return TryReloadImmediately(); } catch (Exception ex) { _templateRetryNotBeforeUtc = DateTime.UtcNow + ReloadRetryDelay; PortalRulesPlugin.PortalRulesLogger.LogError((object)("Failed to generate admin-portal-biome-global-keys.yml; it will remain pending until the next retry: " + ex.Message)); return false; } } internal static void EndServerSession() { StopWatcher(); _active = false; _defaults = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); _hasValidSnapshot = false; _templatePending = false; _biomeRegistryReady = false; _registryFallbackLogged = false; _registryFallbackNotBefore = 0f; _templateRetryNotBeforeUtc = DateTime.MinValue; _nextPollUtc = DateTime.MinValue; _configurationDirectory = ""; _filePath = ""; Interlocked.Exchange(ref _reloadRequested, 0); Interlocked.Exchange(ref _reloadAttempts, 0); Interlocked.Exchange(ref _watcherRestartRequested, 0); Interlocked.Exchange(ref _reloadNotBeforeUtcTicks, 0L); Interlocked.Exchange(ref _watcherRestartNotBeforeUtcTicks, 0L); } internal static bool Tick() { TryCompleteBiomeRegistryFallback(); if (!_active) { return false; } DateTime utcNow = DateTime.UtcNow; if (Volatile.Read(in _watcherRestartRequested) != 0 && utcNow.Ticks >= Interlocked.Read(in _watcherRestartNotBeforeUtcTicks)) { Interlocked.Exchange(ref _watcherRestartRequested, 0); try { StartWatcher(); RequestReload(); } catch (Exception ex) { PortalRulesPlugin.PortalRulesLogger.LogError((object)("Failed to restart the admin-portal-biome-global-keys.yml watcher: " + ex.Message)); Interlocked.Exchange(ref _watcherRestartRequested, 1); ScheduleWatcherRestart(WatcherRestartRetryDelay); } } if (_templatePending) { if (!File.Exists(_filePath)) { if (_biomeRegistryReady && utcNow >= _templateRetryNotBeforeUtc) { bool hasValidSnapshot = _hasValidSnapshot; if (EnsureTemplateReady()) { return !hasValidSnapshot; } return false; } return false; } _templatePending = false; ScheduleReload(TimeSpan.Zero); } if (utcNow >= _nextPollUtc) { _nextPollUtc = utcNow + PollingInterval; if (Volatile.Read(in _reloadRequested) == 0) { ScheduleReload(TimeSpan.Zero); } } if (Volatile.Read(in _reloadRequested) == 0 || utcNow.Ticks < Interlocked.Read(in _reloadNotBeforeUtcTicks)) { return false; } Interlocked.Exchange(ref _reloadRequested, 0); if (TryReload(logOnlyWhenChanged: true, out var changed)) { Interlocked.Exchange(ref _reloadAttempts, 0); return changed; } if (Interlocked.Increment(ref _reloadAttempts) <= 5) { ScheduleReload(ReloadRetryDelay); } return false; } private static void TryCompleteBiomeRegistryFallback() { if (!_biomeRegistryReady && !((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer() && !((Object)(object)ZoneSystem.instance == (Object)null) && !(Time.realtimeSinceStartup < _registryFallbackNotBefore)) { if (!_registryFallbackLogged) { _registryFallbackLogged = true; PortalRulesPlugin.PortalRulesLogger.LogWarning((object)"The ZoneSystem biome-registry completion hook was not observed; using the currently registered biome names as a fallback."); } CompleteBiomeRegistryInitialization(); } } private static void CompleteBiomeRegistryInitialization() { BeginServerSession(); if (!_biomeRegistryReady) { _biomeRegistryReady = true; if (_active) { EnsureTemplateReady(); } PublicPortalCatalog.RefreshAndBroadcast(); } } internal static bool TryResolve(Vector3 position, out string requiredGlobalKey) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //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_0034: Unknown result type (might be due to invalid IL or missing references) requiredGlobalKey = ""; if (!_active || !_hasValidSnapshot || WorldGenerator.instance == null) { return false; } Biome biome = WorldGenerator.instance.GetBiome(position); string name = Enum.GetName(typeof(Biome), biome); if (!string.IsNullOrWhiteSpace(name)) { return _defaults.TryGetValue(name.Trim(), out requiredGlobalKey); } return false; } private static string BuildRegistryTemplate() { return BuildRegistryTemplate(Enum.GetNames(typeof(Biome))); } private static string BuildRegistryTemplate(string[] registeredNames) { if (registeredNames.Length > 4096) { throw new InvalidDataException($"biome registry exceeds {4096} entries"); } Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < registeredNames.Length; i++) { string text = (registeredNames[i] ?? "").Trim(); if (!string.Equals(text, "None", StringComparison.OrdinalIgnoreCase) && !string.Equals(text, "All", StringComparison.OrdinalIgnoreCase)) { try { ValidateBiomeName(text); } catch (InvalidDataException) { string value = ((text.Length <= 64) ? text : (text.Substring(0, 64) + "...")); PortalRulesPlugin.PortalRulesLogger.LogWarning((object)("Skipped invalid registered biome name " + FormatYamlKey(value) + " while generating admin-portal-biome-global-keys.yml.")); continue; } if (!dictionary.TryGetValue(text, out var value2) || string.CompareOrdinal(text, value2) < 0) { dictionary[text] = text; } } } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("# PortalRules default GlobalKey requirements for newly initialized Admin portals."); stringBuilder.AppendLine("# This file is generated once from the server's registered biome names and is"); stringBuilder.AppendLine("# never automatically edited. Delete it before the next server session to rebuild it."); stringBuilder.AppendLine("# Biome names are case-insensitive. Expand World Data custom biomes use the stable"); stringBuilder.AppendLine("# `biome:` name from its biome YAML, not a translated name or numeric biome value."); stringBuilder.AppendLine("# The biome is resolved from each portal's actual world position."); stringBuilder.AppendLine("# A RequiredGlobalKey already stored in the portal ZDO wins over this default."); stringBuilder.AppendLine("# This includes InfinityHammer blueprint data and EWD objectData/locationObjectData."); stringBuilder.AppendLine("# An explicitly stored empty RequiredGlobalKey suppresses the biome default."); stringBuilder.AppendLine("# With YouAreNotWorthy, the selected key is checked per character; otherwise it"); stringBuilder.AppendLine("# is checked against the world's shared GlobalKeys."); stringBuilder.AppendLine("# An empty value means that biome has no default required GlobalKey."); stringBuilder.AppendLine("# Reloads affect only Admin portals whose initial authority is resolved afterwards."); stringBuilder.Append("format_version: ").AppendLine(1.ToString(CultureInfo.InvariantCulture)); stringBuilder.AppendLine("biomes:"); KeyValuePair<string, string>[] vanillaTemplate = VanillaTemplate; for (int i = 0; i < vanillaTemplate.Length; i++) { KeyValuePair<string, string> keyValuePair = vanillaTemplate[i]; dictionary.Remove(keyValuePair.Key); AppendBiome(stringBuilder, keyValuePair.Key, keyValuePair.Value); } List<string> list = new List<string>(dictionary.Values); list.Sort(CompareBiomeNames); if (VanillaTemplate.Length + list.Count > 4096) { throw new InvalidDataException($"generated biome list exceeds {4096} entries"); } foreach (string item in list) { AppendBiome(stringBuilder, item, ""); } string text2 = stringBuilder.ToString(); if ((long)Encoding.UTF8.GetByteCount(text2) > 524288L) { throw new InvalidDataException($"generated template exceeds {524288L} bytes"); } return text2; } private static int CompareBiomeNames(string left, string right) { int num = StringComparer.OrdinalIgnoreCase.Compare(left, right); if (num == 0) { return StringComparer.Ordinal.Compare(left, right); } return num; } private static void AppendBiome(StringBuilder yaml, string biomeName, string requiredGlobalKey) { yaml.Append(" ").Append(FormatYamlKey(biomeName)).Append(':'); if (requiredGlobalKey.Length != 0) { yaml.Append(' ').Append(requiredGlobalKey); } yaml.AppendLine(); } private static string FormatYamlKey(string value) { if (IsSafePlainYamlKey(value)) { return value; } StringBuilder stringBuilder = new StringBuilder(value.Length + 2); stringBuilder.Append('"'); foreach (char c in value) { switch (c) { case '\\': stringBuilder.Append("\\\\"); continue; case '"': stringBuilder.Append("\\\""); continue; case '\u2028': case '\u2029': { StringBuilder stringBuilder2 = stringBuilder.Append("\\u"); int num = c; stringBuilder2.Append(num.ToString("X4", CultureInfo.InvariantCulture)); continue; } } if (char.IsControl(c)) { StringBuilder stringBuilder3 = stringBuilder.Append("\\u"); int num = c; stringBuilder3.Append(num.ToString("X4", CultureInfo.InvariantCulture)); } else { stringBuilder.Append(c); } } return stringBuilder.Append('"').ToString(); } private static bool IsSafePlainYamlKey(string value) { if (value.Length == 0 || AmbiguousPlainYamlScalars.Contains(value) || (!char.IsLetter(value[0]) && value[0] != '_')) { return false; } foreach (char c in value) { if (!char.IsLetterOrDigit(c) && c != '_' && c != '-') { return false; } } return true; } private static bool TryCreateFileWithoutOverwrite(string path, string content) { string text = Path.Combine(Path.GetDirectoryName(path) ?? throw new InvalidOperationException("Configuration directory is unavailable."), "." + Path.GetFileName(path) + "." + Guid.NewGuid().ToString("N") + ".pending"); try { PortalAccountStore.WriteAtomicFile(text, content, null); try { File.Move(text, path); return true; } catch (IOException) when (File.Exists(path)) { return false; } } finally { if (File.Exists(text)) { File.Delete(text); } } } private static bool TryReloadImmediately() { Interlocked.Exchange(ref _reloadRequested, 0); Interlocked.Exchange(ref _reloadAttempts, 0); if (TryReload(logOnlyWhenChanged: false, out var _)) { _nextPollUtc = DateTime.UtcNow + PollingInterval; return true; } ScheduleReload(ReloadRetryDelay); return false; } private static bool TryReload(bool logOnlyWhenChanged, out bool changed) { changed = false; if (!File.Exists(_filePath)) { PortalRulesPlugin.PortalRulesLogger.LogWarning((object)"admin-portal-biome-global-keys.yml is missing; retaining the last valid biome defaults. Use 'biomes: {}' to clear them."); return false; } try { if (new FileInfo(_filePath).Length > 524288) { throw new InvalidDataException($"file exceeds {524288L} bytes"); } BiomeDefaultsYaml biomeDefaultsYaml = Deserializer.Deserialize<BiomeDefaultsYaml>(File.ReadAllText(_filePath)); if (biomeDefaultsYaml == null || biomeDefaultsYaml.FormatVersion != 1 || biomeDefaultsYaml.Biomes == null) { throw new InvalidDataException($"format_version must be {1} and biomes must be present"); } if (biomeDefaultsYaml.Biomes.Count > 4096) { throw new InvalidDataException($"biomes exceeds {4096} entries"); } Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair<string, string> biome in biomeDefaultsYaml.Biomes) { string text = (biome.Key ?? "").Trim(); ValidateBiomeName(text); if (!PublicPortalData.TryNormalizeRequiredGlobalKey(biome.Value, out string normalized, out RequiredGlobalKeyValidationFailure failure)) { throw new InvalidDataException($"GlobalKey for biome '{text}' is invalid ({failure})"); } if (dictionary.ContainsKey(text)) { throw new InvalidDataException("biome '" + text + "' is duplicated after case and whitespace normalization"); } dictionary.Add(text, normalized); } changed = !_hasValidSnapshot || !DictionariesEqual(_defaults, dictionary); _defaults = dictionary; _hasValidSnapshot = true; if (!logOnlyWhenChanged | changed) { PortalRulesPlugin.PortalRulesLogger.LogInfo((object)$"Loaded {_defaults.Count} Admin portal biome GlobalKey default(s)."); } return true; } catch (Exception ex) { PortalRulesPlugin.PortalRulesLogger.LogError((object)("Failed to reload admin-portal-biome-global-keys.yml; retaining the last valid biome defaults: " + ex.Message)); return false; } } private static void ValidateBiomeName(string biomeName) { if (biomeName.Length == 0) { throw new InvalidDataException("biome names must not be empty"); } if (biomeName.Length > 128) { throw new InvalidDataException($"biome name '{biomeName}' exceeds {128} characters"); } for (int i = 0; i < biomeName.Length; i++) { if (char.IsControl(biomeName[i])) { throw new InvalidDataException("biome name '" + biomeName + "' contains control characters"); } } if (long.TryParse(biomeName, NumberStyles.Integer, CultureInfo.InvariantCulture, out var _)) { throw new InvalidDataException("biome '" + biomeName + "' is numeric; use its stable biome name instead"); } } private static bool DictionariesEqual(Dictionary<string, string> left, Dictionary<string, string> right) { if (left.Count != right.Count) { return false; } foreach (KeyValuePair<string, string> item in left) { if (!right.TryGetValue(item.Key, out string value) || !string.Equals(value, item.Value, StringComparison.Ordinal)) { return false; } } return true; } private static void StartWatcher() { StopWatcher(); _watcher = new FileSystemWatcher(_configurationDirectory, "admin-portal-biome-global-keys.yml") { IncludeSubdirectories = false, NotifyFilter = (NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite) }; _watcher.Changed += OnFileChanged; _watcher.Created += OnFileChanged; _watcher.Deleted += OnFileChanged; _watcher.Renamed += OnFileRenamed; _watcher.Error += OnWatcherError; _watcher.EnableRaisingEvents = true; } private static void StopWatcher() { if (_watcher != null) { _watcher.EnableRaisingEvents = false; _watcher.Changed -= OnFileChanged; _watcher.Created -= OnFileChanged; _watcher.Deleted -= OnFileChanged; _watcher.Renamed -= OnFileRenamed; _watcher.Error -= OnWatcherError; _watcher.Dispose(); _watcher = null; } } private static void OnFileChanged(object sender, FileSystemEventArgs args) { RequestReload(); } private static void OnFileRenamed(object sender, RenamedEventArgs args) { RequestReload(); } private static void OnWatcherError(object sender, ErrorEventArgs args) { PortalRulesPlugin.PortalRulesLogger.LogError((object)("The admin-portal-biome-global-keys.yml watcher failed and will be restarted: " + args.GetException().Message)); Interlocked.Exchange(ref _watcherRestartRequested, 1); ScheduleWatcherRestart(WatcherRestartRetryDelay); RequestReload(); } private static void RequestReload() { Interlocked.Exchange(ref _reloadAttempts, 0); ScheduleReload(ReloadDebounce); } private static void ScheduleReload(TimeSpan delay) { Interlocked.Exchange(ref _reloadNotBeforeUtcTicks, (DateTime.UtcNow + delay).Ticks); Interlocked.Exchange(ref _reloadRequested, 1); } private static void ScheduleWatcherRestart(TimeSpan delay) { Interlocked.Exchange(ref _watcherRestartNotBeforeUtcTicks, (DateTime.UtcNow + delay).Ticks); } } [HarmonyPatch] internal static class AdminPortalPrefabManager { [HarmonyPatch(typeof(Game), "Awake")] private static class GameAwakePatch { private static void Postfix(Game __instance) { RegisterPortalHashes(__instance); } } [HarmonyPatch(typeof(TeleportWorld), "UpdatePortal")] private static class ShowAdminPortalWhirlingEffectNearbyPatch { [HarmonyPriority(0)] private static void Postfix(TeleportWorld __instance) { //IL_0064: Unknown result type (might be due to invalid IL or missing references) ZDO portalZdo = PublicPortalKinds.GetPortalZdo(__instance); if (portalZdo != null && PublicPortalKinds.IsAdminPortalPrefab(portalZdo.GetPrefab())) { EffectFade target_found = __instance.m_target_found; if ((Object)(object)target_found != (Object)null) { Player localPlayer = Player.m_localPlayer; Transform val = (((Object)(object)__instance.m_proximityRoot != (Object)null) ? __instance.m_proximityRoot : ((Component)__instance).transform); float num = Mathf.Max(0f, __instance.m_activationRange); bool active = (Object)(object)localPlayer != (Object)null && (Object)(object)Player.GetClosestPlayer(val.position, num) != (Object)null; target_found.SetActive(active); } } } } [HarmonyPatch(typeof(Piece), "Awake")] private static class ClearAdminPortalPieceCreatorPatch { private static void Postfix(Piece __instance, ref long ___m_creator) { if (IsAdminPortalPiece(__instance)) { ___m_creator = 0L; if (!ZNetView.m_forceDisableInit) { DisablePlacedPortalSolidColliders(((Component)__instance).gameObject); HidePlacedPortalMeshes(((Component)__instance).gameObject); } } } } [HarmonyPatch(typeof(Piece), "SetCreator")] private static class BlockAdminPortalPieceCreatorPatch { private static bool Prefix(Piece __instance) { return !IsAdminPortalPiece(__instance); } } [HarmonyPatch(typeof(PieceTable), "UpdateAvailable")] private static class AdminPortalBuildMenuVisibilityPatch { [HarmonyPriority(0)] private static void Postfix(PieceTable __instance, Player player, List<List<Piece>> ___m_availablePieces) { foreach (List<Piece> ___m_availablePiece in ___m_availablePieces) { ___m_availablePiece.RemoveAll(IsAdminPortalPiece); } if ((Object)(object)player != (Object)(object)Player.m_localPlayer || !PortalRulesPlugin.HasAdminDebugAccess) { return; } int num = 0; if (num < 0 || num >= ___m_availablePieces.Count) { return; } List<Piece> list = ___m_availablePieces[num]; foreach (GameObject piece in __instance.m_pieces) { Piece val = (((Object)(object)piece != (Object)null) ? piece.GetComponent<Piece>() : null); if ((Object)(object)val != (Object)null && IsAdminPortalPiece(val) && !list.Contains(val)) { list.Add(val); } } } } [HarmonyPatch(typeof(Player), "TryPlacePiece")] private static class AdminPortalPlacementAccessPatch { private static bool Prefix(Player __instance, Piece piece, ref bool __result) { if (!IsAdminPortalPiece(piece)) { return true; } if ((Object)(object)__instance == (Object)(object)Player.m_localPlayer && PortalRulesPlugin.HasAdminDebugAccess) { return true; } if (__instance != null) { ((Character)__instance).Message((MessageType)2, new PortalRulesMessage("$sighsorry_portalrules_admin_portal_placement_requires_admin_debug").Localize(), 0, (Sprite)null); } __result = false; return false; } } private readonly struct PortalCloneSpec { internal readonly string SourcePrefabName; internal readonly string TargetPrefabName; internal readonly string DisplayName; internal PortalCloneSpec(string sourcePrefabName, string targetPrefabName, string displayName) { SourcePrefabName = sourcePrefabName; TargetPrefabName = targetPrefabName; DisplayName = displayName; } } private static readonly PortalCloneSpec[] PortalCloneSpecs = new PortalCloneSpec[2] { new PortalCloneSpec("portal_wood", "admin_portal_wood", "$sighsorry_portalrules_admin_wood_portal_name"), new PortalCloneSpec("portal_stone", "admin_portal_stone", "$sighsorry_portalrules_admin_stone_portal_name") }; private static bool _registered; private static Player? _lastBuildMenuPlayer; private static bool _lastBuildMenuAccess; private static bool _buildMenuAccessInitialized; private static readonly MethodInfo? UpdateAvailablePiecesMethod = AccessTools.DeclaredMethod(typeof(Player), "UpdateAvailablePiecesList", (Type[])null, (Type[])null); internal static void Initialize() { PrefabManager.OnVanillaPrefabsAvailable += RegisterAdminPortals; PieceManager.OnPiecesRegistered += RegisterAdminPortalPieces; } internal static void Shutdown() { PrefabManager.OnVanillaPrefabsAvailable -= RegisterAdminPortals; PieceManager.OnPiecesRegistered -= RegisterAdminPortalPieces; _lastBuildMenuPlayer = null; _lastBuildMenuAccess = false; _buildMenuAccessInitialized = false; } internal static void Tick() { Player localPlayer = Player.m_localPlayer; if (!_registered || (Object)(object)localPlayer == (Object)null) { _lastBuildMenuPlayer = null; _buildMenuAccessInitialized = false; return; } bool hasAdminDebugAccess = PortalRulesPlugin.HasAdminDebugAccess; if (_buildMenuAccessInitialized && _lastBuildMenuPlayer == localPlayer && _lastBuildMenuAccess == hasAdminDebugAccess) { return; } _lastBuildMenuPlayer = localPlayer; _lastBuildMenuAccess = hasAdminDebugAccess; _buildMenuAccessInitialized = true; if (!((Character)localPlayer).InPlaceMode()) { return; } if (UpdateAvailablePiecesMethod == null) { PortalRulesPlugin.PortalRulesLogger.LogWarning((object)"Could not refresh the Hammer piece list after admin build access changed."); return; } try { UpdateAvailablePiecesMethod.Invoke(localPlayer, Array.Empty<object>()); } catch (Exception ex) { PortalRulesPlugin.PortalRulesLogger.LogWarning((object)("Could not refresh the Hammer piece list after admin build access changed: " + ex.GetBaseException().Message)); } } private static void RegisterAdminPortals() { if (_registered) { PrefabManager.OnVanillaPrefabsAvailable -= RegisterAdminPortals; return; } int num = 0; PortalCloneSpec[] portalCloneSpecs = PortalCloneSpecs; for (int i = 0; i < portalCloneSpecs.Length; i++) { PortalCloneSpec portalCloneSpec = portalCloneSpecs[i]; GameObject prefab = PrefabManager.Instance.GetPrefab(portalCloneSpec.TargetPrefabName); if ((Object)(object)prefab != (Object)null) { num++; continue; } GameObject prefab2 = PrefabManager.Instance.GetPrefab(portalCloneSpec.SourcePrefabName); Piece sourcePiece = ((prefab2 != null) ? prefab2.GetComponent<Piece>() : null); prefab = PrefabManager.Instance.CreateClonedPrefab(portalCloneSpec.TargetPrefabName, portalCloneSpec.SourcePrefabName); if ((Object)(object)prefab == (Object)null) { PortalRulesPlugin.PortalRulesLogger.LogWarning((object)("Could not clone '" + portalCloneSpec.SourcePrefabName + "' into '" + portalCloneSpec.TargetPrefabName + "'.")); } else { PrepareAdminPortal(prefab, sourcePiece, portalCloneSpec.DisplayName); PrefabManager.Instance.AddPrefab(prefab); num++; } } if (num == PortalCloneSpecs.Length) { _registered = true; PrefabManager.OnVanillaPrefabsAvailable -= RegisterAdminPortals; PortalRulesPlugin.PortalRulesLogger.LogInfo((object)"Registered admin portal prefabs: admin_portal_wood, admin_portal_stone."); } } private static void RegisterAdminPortalPieces() { int num = 0; PortalCloneSpec[] portalCloneSpecs = PortalCloneSpecs; for (int i = 0; i < portalCloneSpecs.Length; i++) { PortalCloneSpec portalCloneSpec = portalCloneSpecs[i]; GameObject prefab = PrefabManager.Instance.GetPrefab(portalCloneSpec.TargetPrefabName); if ((Object)(object)prefab == (Object)null) { PortalRulesPlugin.PortalRulesLogger.LogWarning((object)("Could not find admin portal '" + portalCloneSpec.TargetPrefabName + "' for Hammer registration.")); continue; } try { PieceManager.Instance.RegisterPieceInPieceTable(prefab, PieceTables.Hammer, PieceCategories.Misc); num++; } catch (Exception ex) { PortalRulesPlugin.PortalRulesLogger.LogWarning((object)("Could not add admin portal '" + portalCloneSpec.TargetPrefabName + "' to the Hammer piece table: " + ex.Message)); } } if (num == PortalCloneSpecs.Length) { PortalRulesPlugin.PortalRulesLogger.LogInfo((object)"Registered admin portal pieces in the Hammer Misc category."); } } private static void PrepareAdminPortal(GameObject prefab, Piece? sourcePiece, string displayName) { //IL_007c: Unknown result type (might be due to invalid IL or missing references) PrepareAdminPiece(prefab, sourcePiece, displayName); RemoveRootComponent<WearNTear>(prefab); TeleportWorld component = prefab.GetComponent<TeleportWorld>(); if ((Object)(object)component != (Object)null) { ((Behaviour)component).enabled = true; component.m_allowAllItems = true; component.m_proximityRoot = (((Object)(object)component.m_proximityRoot != (Object)null) ? component.m_proximityRoot : prefab.transform); EnsureTargetFoundEffect(prefab, component); EnsureModelRenderer(prefab, component); } ZNetView component2 = prefab.GetComponent<ZNetView>(); if ((Object)(object)component2 != (Object)null) { component2.m_persistent = true; component2.m_distant = false; component2.m_type = (ObjectType)2; } EnsureInteractionTrigger(prefab); } private static void PrepareAdminPiece(GameObject prefab, Piece? sourcePiece, string displayName) { //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) Piece component = prefab.GetComponent<Piece>(); if ((Object)(object)component == (Object)null) { PortalRulesPlugin.PortalRulesLogger.LogWarning((object)("Admin portal '" + ((Object)prefab).name + "' has no Piece metadata; its icon cannot be displayed.")); return; } if ((Object)(object)sourcePiece?.m_icon != (Object)null) { component.m_icon = sourcePiece.m_icon; } else { PortalRulesPlugin.PortalRulesLogger.LogWarning((object)("Admin portal '" + ((Object)prefab).name + "' could not reuse its source portal icon.")); } component.m_name = displayName; component.m_description = "$sighsorry_portalrules_admin_portal_description"; component.m_category = (PieceCategory)0; component.m_enabled = false; component.m_canBeRemoved = false; component.m_craftingStation = null; component.m_resources = Array.Empty<Requirement>(); component.m_destroyedLootPrefab = null; component.m_comfort = 0; component.m_comfortGroup = (ComfortGroup)0; component.m_comfortObject = null; component.m_harvest = false; ((StaticTarget)component).m_primaryTarget = false; ((StaticTarget)component).m_randomTarget = false; component.m_targetNonPlayerBuilt = false; } private static bool IsAdminPortalPiece(Piece piece) { if ((Object)(object)piece == (Object)null) { return false; } string prefabName = Utils.GetPrefabName(((Component)piece).gameObject); if (!(prefabName == "admin_portal_wood")) { return prefabName == "admin_portal_stone"; } return true; } private static void EnsureInteractionTrigger(GameObject prefab) { //IL_003d: 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) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_008c: 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) //IL_00dd: 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_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)prefab.transform.Find("ADMIN_INTERACT") != (Object)null)) { Collider val = FindTeleportCollider(prefab); Transform val2 = (((Object)(object)val != (Object)null) ? ((Component)val).transform : prefab.transform); GameObject val3 = new GameObject("ADMIN_INTERACT") { layer = LayerOrFallback("piece_nonsolid", prefab.layer) }; val3.transform.SetParent(prefab.transform, false); val3.transform.localPosition = val2.localPosition; val3.transform.localRotation = val2.localRotation; val3.transform.localScale = val2.localScale; BoxCollider val4 = val3.AddComponent<BoxCollider>(); ((Collider)val4).isTrigger = true; BoxCollider val5 = (BoxCollider)(object)((val is BoxCollider) ? val : null); if (val5 != null) { val4.center = val5.center; val4.size = val5.size; } else { val4.center = new Vector3(0f, 1.5f, 0f); val4.size = new Vector3(2f, 3f, 0.5f); } } } private static Collider FindTeleportCollider(GameObject prefab) { Collider[] componentsInChildren = prefab.GetComponentsInChildren<Collider>(true); foreach (Collider val in componentsInChildren) { if ((Object)(object)((Component)val).GetComponent<TeleportWorldTrigger>() != (Object)null) { return val; } } return null; } private static void DisablePlacedPortalSolidColliders(GameObject portal) { Collider[] componentsInChildren = portal.GetComponentsInChildren<Collider>(true); foreach (Collider val in componentsInChildren) { if (!val.isTrigger) { val.enabled = false; } } } private static void HidePlacedPortalMeshes(GameObject portal) { TeleportWorld component = portal.GetComponent<TeleportWorld>(); object obj; if (component == null) { obj = null; } else { EffectFade target_found = component.m_target_found; obj = ((target_found != null) ? ((Component)target_found).transform : null); } Transform val = (Transform)obj; MeshRenderer[] componentsInChildren = portal.GetComponentsInChildren<MeshRenderer>(true); foreach (MeshRenderer val2 in componentsInChildren) { if ((Object)(object)val == (Object)null || !((Component)val2).transform.IsChildOf(val)) { ((Renderer)val2).enabled = false; } } SkinnedMeshRenderer[] componentsInChildren2 = portal.GetComponentsInChildren<SkinnedMeshRenderer>(true); foreach (SkinnedMeshRenderer val3 in componentsInChildren2) { if ((Object)(object)val == (Object)null || !((Component)val3).transform.IsChildOf(val)) { ((Renderer)val3).enabled = false; } } } private static void EnsureTargetFoundEffect(GameObject prefab, TeleportWorld teleportWorld) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Expected O, but got Unknown if (!((Object)(object)teleportWorld.m_target_found != (Object)null)) { GameObject val = new GameObject("ADMIN_TARGET_FOUND_DUMMY"); val.transform.SetParent(prefab.transform, false); teleportWorld.m_target_found = val.AddComponent<EffectFade>(); } } private static void EnsureModelRenderer(GameObject prefab, TeleportWorld teleportWorld) { //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)teleportWorld.m_model != (Object)null)) { MeshRenderer val = prefab.GetComponentInChildren<MeshRenderer>(true); if ((Object)(object)val == (Object)null) { GameObject obj = GameObject.CreatePrimitive((PrimitiveType)3); ((Object)obj).name = "ADMIN_MODEL_DUMMY"; obj.transform.SetParent(prefab.transform, false); obj.transform.localScale = Vector3.one * 0.1f; RemoveRootComponent<Collider>(obj); val = obj.GetComponent<MeshRenderer>(); } teleportWorld.m_model = val; } } private static int LayerOrFallback(string layerName, int fallback) { int num = LayerMask.NameToLayer(layerName); if (num < 0) { return fallback; } return num; } private static void RemoveRootComponent<T>(GameObject gameObject) where T : Component { T component = gameObject.GetComponent<T>(); if ((Object)(object)component != (Object)null) { Object.DestroyImmediate((Object)(object)component); } } internal static void RegisterPortalHashes(Game game) { PortalCloneSpec[] portalCloneSpecs = PortalCloneSpecs; for (int i = 0; i < portalCloneSpecs.Length; i++) { int stableHashCode = StringExtensionMethods.GetStableHashCode(portalCloneSpecs[i].TargetPrefabName); if (!game.PortalPrefabHash.Contains(stableHashCode)) { game.PortalPrefabHash.Add(stableHashCode); } } } } [HarmonyPatch] internal static class AdminPortalOperations { private sealed class RequiredGlobalKeyTextReceiver : TextReceiver { private readonly ZDOID _portalId; private readonly string _currentValue; internal RequiredGlobalKeyTextReceiver(ZDOID portalId, string currentValue) { //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) _portalId = portalId; _currentValue = currentValue ?? ""; } public string GetText() { return _currentValue; } public void SetText(string text) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) RequestRequiredGlobalKeyChange(_portalId, text); } } [HarmonyPatch(typeof(TeleportWorld), "SetText")] private static class AdminPortalSetTextPatch { private static bool Prefix(TeleportWorld __instance, string text) { ZDO portalZdo = PublicPortalKinds.GetPortalZdo(__instance); if (portalZdo == null || !PublicPortalInteraction.IsAdminPortal(portalZdo)) { return true; } RequestTagChange(__instance, text); return false; } } [HarmonyPatch(typeof(TeleportWorld), "RPC_SetTag", new Type[] { typeof(long), typeof(string), typeof(string) })] private static class BlockVanillaAdminPortalTagRpcPatch { private static bool Prefix(TeleportWorld __instance) { ZDO portalZdo = PublicPortalKinds.GetPortalZdo(__instance); if (portalZdo != null) { return !PublicPortalInteraction.IsAdminPortal(portalZdo); } return true; } } [HarmonyPatch(typeof(Player), "RemovePiece")] private static class AdminPortalRemovalPatch { private static bool Prefix(Player __instance, ref bool __result) { if ((Object)(object)__instance == (Object)null || (Object)(object)__instance != (Object)(object)Player.m_localPlayer || !TryGetAimedAdminPortal(__instance, out ZDO zdo)) { return true; } if (!PortalRulesPlugin.HasAdminDebugAccess) { ShowOperationMessage(new PortalRulesMessage("$sighsorry_portalrules_admin_portal_dismantling_requires_admin_debug")); __result = false; return false; } RequestRemoval(zdo); __result = false; return false; } private static bool TryGetAimedAdminPortal(Player player, out ZDO zdo) { //IL_001a: 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_0044: 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) zdo = null; RaycastHit val = default(RaycastHit); if ((Object)(object)GameCamera.instance == (Object)null || !Physics.Raycast(((Component)GameCamera.instance).transform.position, ((Component)GameCamera.instance).transform.forward, ref val, 50f, AdminRemoveRayMask, (QueryTriggerInteraction)2) || Vector3.Distance(((RaycastHit)(ref val)).point, ((Character)player).GetEyePoint()) >= player.m_maxPlaceDistance) { return false; } TeleportWorld componentInParent = ((Component)((RaycastHit)(ref val)).collider).GetComponentInParent<TeleportWorld>(); ZDO val2 = (((Object)(object)componentInParent != (Object)null) ? PublicPortalKinds.GetPortalZdo(componentInParent) : null); if (val2 == null || !PublicPortalInteraction.IsAdminPortal(val2)) { return false; } zdo = val2; return true; } } private const float AdminOperationCooldownSeconds = 0.25f; private const int MaximumPortalTagLength = 10; private static readonly Dictionary<ZRpc, float> LastAdminOperationAt = new Dictionary<ZRpc, float>(); private static readonly int AdminRemoveRayMask = LayerMask.GetMask(new string[7] { "Default", "static_solid", "Default_small", "piece", "piece_nonsolid", "terrain", "vehicle" }); internal static void Shutdown() { LastAdminOperationAt.Clear(); } internal static void RegisterPeer(ZNet znet, ZNetPeer peer) { if (!((Object)(object)znet == (Object)null) && peer?.m_rpc != null && znet.IsServer()) { peer.m_rpc.Register<ZDOID, string, bool>("sighsorry.PortalRules.ChangeAdminPortalTag.v1", (Action<ZRpc, ZDOID, string, bool>)OnRemoteTagChange); peer.m_rpc.Register<ZDOID, string, bool>("sighsorry.PortalRules.ChangeAdminPortalRequiredGlobalKey.v1", (Action<ZRpc, ZDOID, string, bool>)OnRemoteRequiredGlobalKeyChange); peer.m_rpc.Register<ZDOID, bool>("sighsorry.PortalRules.RemoveAdminPortal.v1", (Action<ZRpc, ZDOID, bool>)OnRemoteRemoval); } } internal static void ForgetPeer(ZRpc rpc) { if (rpc != null) { LastAdminOperationAt.Remove(rpc); } } private static void RequestTagChange(TeleportWorld portal, string tag) { //IL_004e: 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) ZDO portalZdo = PublicPortalKinds.GetPortalZdo(portal); if (portalZdo == null) { ShowOperationMessage(new PortalRulesMessage("$sighsorry_portalrules_admin_portal_unavailable")); return; } if ((Object)(object)ZNet.instance == (Object)null) { ShowOperationMessage(new PortalRulesMessage("$sighsorry_portalrules_server_unavailable")); return; } if (ZNet.instance.IsServer()) { TryApplyTagChange(portalZdo.m_uid, tag, null, PortalRulesPlugin.IsAdmin, Player.m_debugMode, out var message); ShowOperationMessage(message); return; } ZRpc serverRPC = ZNet.instance.GetServerRPC(); if (serverRPC == null) { ShowOperationMessage(new PortalRulesMessage("$sighsorry_portalrules_server_connection_unavailable")); return; } serverRPC.Invoke("sighsorry.PortalRules.ChangeAdminPortalTag.v1", new object[3] { portalZdo.m_uid, tag ?? "", Player.m_debugMode }); } private static void RequestRemoval(ZDO zdo) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)ZNet.instance == (Object)null) { ShowOperationMessage(new PortalRulesMessage("$sighsorry_portalrules_server_unavailable")); return; } if (ZNet.instance.IsServer()) { TryApplyRemoval(zdo.m_uid, null, PortalRulesPlugin.IsAdmin, Player.m_debugMode, out var message); ShowOperationMessage(message); return; } ZRpc serverRPC = ZNet.instance.GetServerRPC(); if (serverRPC == null) { ShowOperationMessage(new PortalRulesMessage("$sighsorry_portalrules_server_connection_unavailable")); return; } serverRPC.Invoke("sighsorry.PortalRules.RemoveAdminPortal.v1", new object[2] { zdo.m_uid, Player.m_debugMode }); } internal static void OpenRequiredGlobalKeyInput(TeleportWorld portal) { //IL_0045: Unknown result type (might be due to invalid IL or missing references) ZDO portalZdo = PublicPortalKinds.GetPortalZdo(portal); if (portalZdo == null) { ShowOperationMessage(new PortalRulesMessage("$sighsorry_portalrules_admin_portal_unavailable")); return; } TextInput instance = TextInput.instance; if ((Object)(object)instance == (Object)null) { ShowOperationMessage(new PortalRulesMessage("$sighsorry_portalrules_text_input_unavailable")); } else { instance.RequestText((TextReceiver)(object)new RequiredGlobalKeyTextReceiver(portalZdo.m_uid, PublicPortalCatalog.GetEffectiveRequiredGlobalKey(portalZdo)), new PortalRulesMessage("$sighsorry_portalrules_required_global_key_input_title").Localize(), 128); } } private static void RequestRequiredGlobalKeyChange(ZDOID portalId, string requiredGlobalKey) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)ZNet.instance == (Object)null || ZDOMan.instance == null) { ShowOperationMessage(new PortalRulesMessage("$sighsorry_portalrules_server_unavailable")); return; } if (ZNet.instance.IsServer()) { TryApplyRequiredGlobalKeyChange(portalId, requiredGlobalKey, null, PortalRulesPlugin.IsAdmin, Player.m_debugMode, out var message); ShowOperationMessage(message); return; } ZRpc serverRPC = ZNet.instance.GetServerRPC(); if (serverRPC == null) { ShowOperationMessage(new PortalRulesMessage("$sighsorry_portalrules_server_connection_unavailable")); return; } serverRPC.Invoke("sighsorry.PortalRules.ChangeAdminPortalRequiredGlobalKey.v1", new object[3] { portalId, requiredGlobalKey ?? "", Player.m_debugMode }); } private static void OnRemoteTagChange(ZRpc rpc, ZDOID portalId, string tag, bool debugEnabled) { //IL_0019: 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) if (!TryResolveRemoteAdminOperation(rpc, out ZNetPeer peer, out bool requesterIsAdmin, out PortalRulesMessage rejection)) { if (peer != null) { PublicPortalServerPolicy.SendPolicyMessage(peer, rejection); } } else { LogRejectedOperation(TryApplyTagChange(portalId, tag, peer, requesterIsAdmin, debugEnabled, out var message), peer, portalId, "tag change", message); PublicPortalServerPolicy.SendPolicyMessage(peer, message); } } private static void OnRemoteRemoval(ZRpc rpc, ZDOID portalId, bool debugEnabled) { //IL_0019: 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) if (!TryResolveRemoteAdminOperation(rpc, out ZNetPeer peer, out bool requesterIsAdmin, out PortalRulesMessage rejection)) { if (peer != null) { PublicPortalServerPolicy.SendPolicyMessage(peer, rejection); } } else { LogRejectedOperation(TryApplyRemoval(portalId, peer, requesterIsAdmin, debugEnabled, out var message), peer, portalId, "removal", message); PublicPortalServerPolicy.SendPolicyMessage(peer, message); } } private static void OnRemoteRequiredGlobalKeyChange(ZRpc rpc, ZDOID portalId, string requiredGlobalKey, bool debugEnabled) { //IL_0019: 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) if (!TryResolveRemoteAdminOperation(rpc, out ZNetPeer peer, out bool requesterIsAdmin, out PortalRulesMessage rejection)) { if (peer != null) { PublicPortalServerPolicy.SendPolicyMessage(peer, rejection); } } else { LogRejectedOperation(TryApplyRequiredGlobalKeyChange(portalId, requiredGlobalKey, peer, requesterIsAdmin, debugEnabled, out var message), peer, portalId, "Required GlobalKey change", message); PublicPortalServerPolicy.SendPolicyMessage(peer, message); } } private static bool TryResolveRemoteAdminOperation(ZRpc rpc, out ZNetPeer? peer, out bool requesterIsAdmin, out PortalRulesMessage rejection) { peer = null; requesterIsAdmin = false; rejection = new PortalRulesMessage("$sighsorry_portalrules_admin_request_unauthenticated"); ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return false; } peer = PublicPortalData.FindPeer(instance, rpc); if (peer == null || !peer.IsReady()) { return false; } float realtimeSinceStartup = Time.realtimeSinceStartup; if (LastAdminOperationAt.TryGetValue(rpc, out var value) && realtimeSinceStartup - value < 0.25f) { rejection = new PortalRulesMessage("$sighsorry_portalrules_admin_change_rate_limited"); return false; } LastAdminOperationAt[rpc] = realtimeSinceStartup; requesterIsAdmin = PublicPortalData.IsPeerAdmin(instance, peer); return true; } private static bool TryApplyTagChange(ZDOID portalId, string tag, ZNetPeer? requesterPeer, bool requesterIsAdmin, bool debugEnabled, out PortalRulesMessage message) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) if (!TryGetAuthorizedAdminPortal(portalId, requesterPeer, requesterIsAdmin, debugEnabled, out ZDO zdo, out message)) { return false; } if (tag == null) { tag = ""; } if (tag.Length > 10) { message = new PortalRulesMessage("$sighsorry_portalrules_admin_tag_length_limit", 10.ToString(CultureInfo.InvariantCulture)); return false; } if (!PublicPortalCatalog.SetAuthoritativeAdminPortalTag(zdo, tag)) { message = new PortalRulesMessage("$sighsorry_portalrules_admin_tag_update_failed"); return false; } message = new PortalRulesMessage("$sighsorry_portalrules_admin_tag_updated"); return true; } private static bool TryApplyRemoval(ZDOID portalId, ZNetPeer? requesterPeer, bool requesterIsAdmin, bool debugEnabled, out PortalRulesMessage message) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) if (!TryGetAuthorizedAdminPortal(portalId, requesterPeer, requesterIsAdmin, debugEnabled, out ZDO zdo, out message)) { return false; } if (!PublicPortalServerPolicy.QueueAdminPortalRemoval(zdo)) { message = new PortalRulesMessage("$sighsorry_portalrules_admin_portal_removal_unavailable"); return false; } message = new PortalRulesMessage("$sighsorry_portalrules_admin_portal_dismantling_queued"); return true; } private static bool TryApplyRequiredGlobalKeyChange(ZDOID portalId, string requiredGlobalKey, ZNetPeer? requesterPeer, bool requesterIsAdmin, bool debugEnabled, out PortalRulesMessage message) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) if (!TryGetAuthorizedAdminPortal(portalId, requesterPeer, requesterIsAdmin, debugEnabled, out ZDO zdo, out message)) { return false; } if (!TryNormalizeRequiredGlobalKeyMessage(requiredGlobalKey, out string normalizedRequiredGlobalKey, out message)) { return false; } if (normalizedRequiredGlobalKey.Length != 0 && RequiredGlobalKeyAccess.YouAreNotWorthyInstalled) { switch ((requesterPeer != null) ? RequiredGlobalKeyAccess.QueryPeer(requesterPeer, normalizedRequiredGlobalKey) : RequiredGlobalKeyAccess.QueryLocal(normalizedRequiredGlobalKey)) { case RequiredGlobalKeyQueryResult.Invalid: message = new PortalRulesMessage("$sighsorry_portalrules_ynw_required_global_key_rejected"); return false; case RequiredGlobalKeyQueryResult.Unavailable: message = new PortalRulesMessage("$sighsorry_portalrules_ynw_key_api_unavailable"); return false; } } if (!PublicPortalCatalog.SetAuthoritativeAdminPortalRequiredGlobalKey(zdo, normalizedRequiredGlobalKey)) { message = new PortalRulesMessage("$sighsorry_portalrules_admin_required_global_key_update_failed"); return false; } message = ((normalizedRequiredGlobalKey.Length == 0) ? new PortalRulesMessage("$sighsorry_portalrules_admin_required_global_key_cleared") : new PortalRulesMessage("$sighsorry_portalrules_admin_required_global_key_updated")); return true; } private static bool TryNormalizeRequiredGlobalKeyMessage(string? requiredGlobalKey, out string normalizedRequiredGlobalKey, out PortalRulesMessage message) { if (PublicPortalData.TryNormalizeRequiredGlobalKey(requiredGlobalKey, out normalizedRequiredGlobalKey, out var failure)) { message = PortalRulesMessage.Empty; return true; } switch (failure) { case RequiredGlobalKeyValidationFailure.TooLong: message = new PortalRulesMessage("$sighsorry_portalrules_required_global_key_length_limit", 128.ToString(CultureInfo.InvariantCulture)); break; case RequiredGlobalKeyValidationFailure.ControlCharacters: message = new PortalRulesMessage("$sighsorry_portalrules_required_global_key_control_characters"); break; case RequiredGlobalKeyValidationFailure.NumericKey: message = new PortalRulesMessage("$sighsorry_portalrules_required_global_key_numeric_invalid"); break; case RequiredGlobalKeyValidationFailure.ReservedKey: message = new PortalRulesMessage("$sighsorry_portalrules_required_global_key_not_allowed"); break; default: message = new PortalRulesMessage("$sighsorry_portalrules_admin_required_global_key_update_failed"); break; } return false; } private static bool TryGetAuthorizedAdminPortal(ZDOID portalId, ZNetPeer? requesterPeer, bool requesterIsAdmin, bool debugEnabled, out ZDO zdo, out PortalRulesMessage message) { //IL_005f: Unknown result type (might be due to invalid IL or missing references) zdo = null; if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || ZDOMan.instance == null) { message = new PortalRulesMessage("$sighsorry_portalrules_server_not_ready"); return false; } if (!requesterIsAdmin || !debugEnabled) { message = new PortalRulesMessage("$sighsorry_portalrules_admin_portal_controls_require_admin_debug"); return false; } ZDO zDO = ZDOMan.instance.GetZDO(portalId); if (zDO == null || !zDO.IsValid() || !PublicPortalInteraction.IsAdminPortal(zDO)) { message = new PortalRulesMessage("$sighsorry_portalrules_requested_admin_portal_unavailable"); return false; } if (!PublicPortalInteraction.TryValidatePortalInteraction(zDO, requesterPeer, out message)) { return false; } if (!((requesterPeer != null) ? PublicPortalData.TryGetPeerOwner(requesterPeer, out var _) : PublicPortalData.LocalOwner().IsValid)) { message = new PortalRulesMessage("$sighsorry_portalrules_platform_identity_unverified"); return false; } zdo = zDO; return true; } private static void LogRejectedOperation(bool success, ZNetPeer peer, ZDOID portalId, string operation, PortalRulesMessage message) { //IL_007a: Unknown result type (might be due to invalid IL or missing references) if (!success && PublicPortalData.TryGetPeerOwner(peer, out var owner)) { string text = (message.IsEmpty ? "<empty>" : ((message.Arguments.Length == 0) ? message.Token : (message.Token + " [" + string.Join(", ", message.Arguments) + "]"))); PortalRulesPlugin.PortalRulesLogger.LogWarning((object)$"Rejected admin portal {operation} from {owner.Id} for {portalId}: {text}"); } } private static void ShowOperationMessage(PortalRulesMessage message) { string text = message.Localize(); if (!string.IsNullOrWhiteSpace(text)) { Player localPlayer = Player.m_localPlayer; if (localPlayer != null) { ((Character)localPlayer).Message((MessageType)2, text, 0, (Sprite)null); } } } } internal readonly struct PortalClanMembership { private readonly string? _primaryClanId; private readonly string? _primaryClanName; private readonly string? _guestClanId; public string PrimaryClanId => _primaryClanId ?? string.Empty; public string PrimaryClanName => _primaryClanName ?? string.Empty; public string GuestClanId => _guestClanId ?? string.Empty; public bool HasPrimaryClan => PrimaryClanId.Length != 0; public PortalClanMembership(string primaryClanId, string primaryClanName, string guestClanId) { _primaryClanId = Normalize(primaryClanId); _primaryClanName = ((_primaryClanId.Length == 0) ? string.Empty : Normalize(primaryClanName)); _guestClanId = Normalize(guestClanId); } public bool ContainsClan(string clanId) { string text = Normalize(clanId); if (text.Length != 0) { if (!string.Equals(text, PrimaryClanId, StringComparison.Ordinal)) { return string.Equals(text, GuestClanId, StringComparison.Ordinal); } return true; } return false; } private static string Normalize(string? value) { return (value ?? string.Empty).Trim(); } } internal static class ClanPortalAccess { private const string ClanApiTypeName = "Clan.ClanApi"; private const int MinimumApiVersion = 4; private const string ResolvedResultName = "Resolved"; private static readonly object Sync = new object(); private static MemberInfo? _apiVersionMember; private static MethodInfo? _resolveMembershipsMethod; private static System.Reflection.EventInfo? _registryChangedEventInfo; private static Delegate? _registryChangedHandler; private static bool _initialized; private static bool _apiAvailable; private static bool _registryChangedEventBound; private static bool _bindingFailureLogged; private static bool _invocationFailureLogged; internal static bool IsServerRegistryAvailable { get { lock (Sync) { return IsApiAvailableLocked(); } } } internal static event Action? RegistryChanged; internal static void Initialize() { lock (Sync) { if (_initialized) { return; } _initialized = true; Assembly installedClanAssembly = GetInstalledClanAssembly(); if (!(installedClanAssembly == null)) { BindAssemblyLocked(installedClanAssembly); if (_resolveMembershipsMethod == null || _registryChangedEventInfo == null || !TryReadApiVersion(out var apiVersion) || apiVersion < 4) { LogBindingFailureOnce($"Clan integration requires API version {4} " + "with ResolveMemberships and RegistryChanged; integration is disabled."); ResetBindingLocked(); } else if (!TryBindRegistryChangedEventLocked()) { LogBindingFailureOnce("Could not bind Clan.RegistryChanged; Clan integration is disabled."); ResetBindingLocked(); } else { _apiAvailable = true; } } } } internal static void Shutdown() { lock (Sync) { _initialized = false; ResetBindingLocked(); } } internal static bool TryResolvePeerMembership(ZNetPeer? peer, out PortalClanMembership membership) { membership = default(PortalClanMembership); if (TryGetPeerIdentity(peer, out string platformId, out long playerId)) { return TryResolveMembership(platformId, playerId, out membership); } return false; } internal static bool TryResolveLocalMembership(out PortalClanMembership membership) { membership = default(PortalClanMembership); if (!TryGetLocalIdentity(out string platformId, out long playerId)) { return false; } return TryResolveMembership(platformId, playerId, out membership); } internal static bool TryResolveBuilderMembership(PortalBuilder builder, out PortalClanMembership membership) { membership = default(PortalClanMembership); if (builder.IsValid && builder.CharacterPlayerId != 0L) { return TryResolveMembership(builder.PlatformId, builder.CharacterPlayerId, out membership); } return false; } internal static bool IsRequesterInBuilderPrimaryClan(PortalBuilder builder, ZNetPeer? requesterPeer) { if (!TryResolveBuilderMembership(builder, out var membership) || !membership.HasPrimaryClan) { return false; } if ((requesterPeer != null) ? TryResolvePeerMembership(requesterPeer, out var membership2) : TryResolveLocalMembership(out membership2)) { return membership2.ContainsClan(membership.PrimaryClanId); } return false; } private static bool TryResolveMembership(string platformId, long playerId, out PortalClanMembership membership) { membership = default(PortalClanMembership); if (string.IsNullOrWhiteSpace(platformId) || playerId == 0L) { return false; } MethodInfo resolveMembershipsMethod; lock (Sync) { if (!IsApiAvailableLocked()) { return false; } resolveMembershipsMethod = _resolveMembershipsMethod; } if (resolveMembershipsMethod == null) { return false; } object[] array = new object[6] { platformId.Trim(), playerId, null, null, null, null }; try { if (!string.Equals(resolveMembershipsMethod.Invoke(null, array)?.ToString(), "Resolved", StringComparison.Ordinal)) { return false; } membership = new PortalClanMembership((array[2] as string) ?? string.Empty, (array[3] as string) ?? string.Empty, (array[4] as string) ?? string.Empty); return true; } catch (Exception exception) { LogInvocationFailureOnce(exception); membership = default(PortalClanMembership); return false; } } private static bool TryGetPeerIdentity(ZNetPeer? peer, out string platformId, out long playerId) { platformId = string.Empty; playerId = 0L; if (PublicPortalData.TryGetPeerSteamId64(peer, out platformId)) { return PublicPortalData.TryGetAuthenticatedPeerPlayerId(peer, out playerId); } return false; } private static bool TryGetLocalIdentity(out string platformId, out long playerId) { //IL_0039: 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) platformId = string.Empty; playerId = 0L; try { IDistributionPlatform distributionPlatform = PlatformManager.DistributionPlatform; Player localPlayer = Player.m_localPlayer; if (((distributionPlatform != null) ? distributionPlatform.LocalUser : null) == null || (Object)(object)localPlayer == (Object)null) { return false; } platformId = ((object)((IUser)distributionPlatform.LocalUser).PlatformUserID/*cast due to .constrained prefix*/).ToString().Trim(); playerId = localPlayer.GetPlayerID(); return platformId.Length != 0 && playerId != 0; } catch (Exception) { platformId = string.Empty; playerId = 0L; return false; } } private static bool IsApiAvailableLocked() { if (_initialized && _apiAvailable && _resolveMembershipsMethod != null) { return _registryChangedEventBound; } return false; } private static void BindAssemblyLocked(Assembly assembly) { Type type = assembly.GetType("Clan.ClanApi", throwOnError: false); if (!(type == null)) { _apiVersionMember = (MemberInfo?)(((object)type.GetProperty("ApiVersion", BindingFlags.Static | BindingFlags.Public)) ?? ((object)type.GetField("ApiVersion", BindingFlags.Static | BindingFlags.Public))); _resolveMembershipsMethod = type.GetMethod("ResolveMemberships", BindingFlags.Static | BindingFlags.Public, null, new Type[6] { typeof(string), typeof(long), typeof(string).MakeByRefType(), typeof(string).MakeByRefType(), typeof(string).MakeByRefType(), typeof(string).MakeByRefType() }, null); _registryChangedEventInfo = type.GetEvent("RegistryChanged", BindingFlags.Static | BindingFlags.Public); } } private static Assembly? GetInstalledClanAssembly() { try { PluginInfo value; return (!Chainloader.PluginInfos.TryGetValue("sighsorry.Clan", out value)) ? null : ((object)value.Instance)?.GetType().Assembly; } catch (Exception ex) { LogBindingFailureOnce("Could not inspect the installed Clan plugin; integration is disabled: " + ex.Message); return null; } } private static bool TryReadApiVersion(out int apiVersion) { apiVersion = 0; try { MemberInfo apiVersionMember = _apiVersionMember; object obj = ((apiVersionMember is PropertyInfo propertyInfo) ? propertyInfo.GetValue(null, null) : ((!(apiVersionMember is FieldInfo fieldInfo)) ? null : fieldInfo.GetValue(null))); object obj2 = obj; if (obj2 == null) { return false; } apiVersion = Convert.ToInt32(obj2); return true; } catch (Exception) { return false; } } private static bool TryBindRegistryChangedEventLocked() { if (_registryChangedEventInfo?.EventHandlerType != typeof(Action)) { return false; } try { _registryChangedHandler = new Action(HandleRegistryChanged); _registryChangedEventInfo.AddEventHandler(null, _registryChangedHandler); _registryChangedEventBound = true; return true; } catch (Exception) { _registryChangedHandler = null; _registryChangedEventBound = false; return false; } } private static void HandleRegistryChanged() { Delegate[] invocationList; lock (Sync) { if (!_initialized || ClanPortalAccess.RegistryChanged == null) { return; } invocationList = ClanPortalAccess.RegistryChanged.GetInvocationList(); } Delegate[] array = invocationList; foreach (Delegate obj in array) { try { ((Action)obj)(); } catch (Exception) { } } } private static void ResetBindingLocked() { if (_registryChangedEventBound && _registryChangedEventInfo != null && (object)_registryChangedHandler != null) { try { _registryChangedEventInfo.RemoveEventHandler(null, _registryChangedHandler); } catch (Exception) { } } _apiAvailable = false; _registryChangedEventBound = false; _registryChangedHandler = null; _registryChangedEventInfo = null; _resolveMembershipsMethod = null; _apiVersionMember = null; } private static void LogBindingFailureOnce(string message) { if (!_bindingFailureLogged) { _bindingFailureLogged = true; PortalRulesPlugin.PortalRulesLogger.LogWarning((object)message); } } private static void LogInvocationFailureOnce(Exception exception) { lock (Sync) { if (_invocationFailureLogged) { return; } _invocationFailureLogged = true; } PortalRulesPlugin.PortalRulesLogger.LogWarning((object)("Clan.ResolveMemberships failed; the request was denied: " + exception.Message)); } } internal enum InviteTravelCooldownFailure : byte { None, DataUnavailable, StorageUnavailable, Active, AccountCapacityReached, PortalCapacityReached, FileCapacityReached, SaveFailed, ReservationConflict } internal readonly struct InviteTravelCooldownReservation { internal readonly string ReservationId; internal readonly string AccountId; internal readonly string SourceFavoriteId; internal readonly string DestinationFavoriteId; internal readonly bool RecordsDeparture; internal readonly bool RecordsArrival; internal bool IsActive { get { if (!RecordsDeparture) { return RecordsArrival; } return true; } } internal InviteTravelCooldownReservation(string reservationId, string accountId, string sourceFavoriteId, string destinationFavoriteId, bool recordsDeparture, bool recordsArrival) { ReservationId = reservationId; AccountId = accountId; SourceFavoriteId = sourceFavoriteId; DestinationFavoriteId = destinationFavoriteId; RecordsDeparture = recordsDeparture; RecordsArrival = recordsArrival; } } internal static class InviteTravelCooldownStore { private sealed class PendingReservation { internal readonly InviteTravelCooldownReservation Value; internal readonly long ExpiresAtUtc; internal readonly long SerializedByteEstimate; internal PendingReservation(InviteTravelCooldownReservation value, long expiresAtUtc, long serializedByteEstimate) { Value = value; ExpiresAtUtc = expiresAtUtc; SerializedByteEstimate = serializedByteEstimate; } } private sealed class CooldownFile { [YamlMember(Alias = "format_version")] public int FormatVersion { get; set; } [YamlMember(Alias = "worlds")] public Dictionary<string, Dictionary<string, Dictionary<string, CooldownEntryYaml>>>? Worlds { get; set; } } private sealed class CooldownEntryYaml { [YamlMember(Alias = "departure_last_utc")] public long DepartureLastUtc { get; set; } [YamlMember(Alias = "arrival_last_utc")] public long ArrivalLastUtc { get; set; } } private readonly struct CooldownEntry { public readonly long DepartureLastUtc; public readonly long ArrivalLastUtc; public CooldownEntry(long departureLastUtc, long arrivalLastUtc) { DepartureLastUtc = Math.Max(0L, departureLastUtc); ArrivalLastUtc = Math.Max(0L, arrivalLastUtc); } public CooldownEntry WithDeparture(long value) { return new CooldownEntry(value, ArrivalLastUtc); } public CooldownEntry WithArrival(long value) { return new CooldownEntry(DepartureLastUtc, value); } } private const int FormatVersion = 1; private const int MaximumWorldCount = 1024; private const int MaximumAccountsPerWorld = 32768; private const int MaximumPortalsPerAccount = 100000; private const long MaximumFileBytes = 16777216L; private const int MaximumSerializedPortalMutationBytes = 256; private const int MaximumSerializedAccountHeaderBytes = 64; private const int MaximumPendingReservations = 4096; private const int MaximumReservationIdLength = 128; private const long PendingReservationLifetimeSeconds = 180L; private const long MaximumUtcSeconds = 253402300799L; private const string DirectoryName = "PortalRules"; private const string FileName = "invite-travel-cooldowns.yml"; private static readonly TimeSpan SaveDebounce = TimeSpan.FromSeconds(1.0); private static readonly TimeSpan SaveRetryDelay = TimeSpan.FromSeconds(10.0); private static readonly IDeserializer Deserializer = new DeserializerBuilder().WithDuplicateKeyChecking().Build(); private static readonly Dictionary<string, Dictionary<string, Dictionary<string, CooldownEntry>>> Worlds = new Dictionary<string, Dictionary<string, Dictionary<string, CooldownEntry>>>(StringComparer.Ordinal); private static readonly Dictionary<string, PendingReservation> PendingReservations = new Dictionary<string, PendingReservation>(StringComparer.Ordinal); private static readonly Dictionary<string, string> PendingDirectionOwners = new Dictionary<string, string>(StringComparer.Ordinal); private static string _filePath = ""; private static string _worldId = ""; private static bool _serverActive; private static bool _saveDirty; private static bool _saveFaulted; private static DateTime _nextSaveAttemptUtc; private static long _serializedByteUpperBound; internal static void BeginServerSession() { ClearPendingReservations(); if (!EndServerSession()) { PortalRulesPlugin.PortalRulesLogger.LogError((object)"Invite cooldown storage retained unsaved data from the previous session; the new session will remain unavailable until that data can be saved."); } else { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || ZNet.World == null) { return; } long uid = ZNet.World.m_uid; if (uid != 0L) { _worldId = uid.ToString(CultureInfo.InvariantCulture); string text = Path.Combine(Paths.ConfigPath, "PortalRules"); _filePath = Path.Combine(text, "invite-travel-cooldowns.yml"); try { Directory.CreateDirectory(text); Load(); bool flag = Prune(UtcNowSeconds()); if (!Worlds.ContainsKey(_worldId)) { if (Worlds.Count >= 1024) { throw new InvalidDataException(string.Format("{0} cannot track more than {1} worlds", "invite-travel-cooldowns.yml", 1024)); } Worlds.Add(_worldId, new Dictionary<string, Dictionary<string, CooldownEntry>>(StringComparer.Ordinal)); flag = true; } if (flag) { Save(); } _saveDirty = false; _saveFaulted = false; _nextSaveAttemptUtc = DateTime.UtcNow + SaveDebounce; _serverActive = true; return; } catch (Exception ex) { Worlds.Clear(); _worldId = ""; _filePath = ""; _serverActive = false; _saveDirty = false; _saveFaulted = false; _nextSaveAttemptUtc = DateTime.MinValue; _serializedByteUpperBound = 0L; PortalRulesPlugin.PortalRulesLogger.LogError((object)("Failed to initialize Invite cooldown storage: " + ex.Message)); return; } } PortalRulesPlugin.PortalRulesLogger.LogError((object)"Invite cooldown storage is unavailable because the world UID is invalid."); } } internal static bool EndServerSession() { ClearPendingReservations(); bool num = Flush(); _serverActive = false; if (!num && _saveDirty) { PortalRulesPlugin.PortalRulesLogger.LogError((object)"Invite cooldown storage could not complete its final save; unsaved data is being retained in memory."); return false; } _saveDirty = false; _saveFaulted = false; _nextSaveAttemptUtc = DateTime.MinValue; _serializedByteUpperBound = 0L; _worldId = ""; _filePath = ""; Worlds.Clear(); return true; } internal static void Tick() { PrunePendingReservations(UtcNowSeconds()); if (_saveDirty && !(DateTime.UtcNow < _nextSaveAttemptUtc)) { bool flag = !_serverActive && !string.IsNullOrEmpty(_filePath); if (TryFlushPending(out string _) && flag && !((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer() && ZNet.World != null) { BeginServerSession(); } } } internal static bool Flush() { if (!_saveDirty) { return true; } if (string.IsNullOrEmpty(_filePath)) { return false; } string error; return TryFlushPending(out error); } internal static void GetServerDeadlines(string steamId, string favoriteId, out long departureUntilUtc, out long arrivalUntilUtc) { departureUntilUtc = 0L; arrivalUntilUtc = 0L; if (_serverActive && TryNormalizeKeys(steamId, favoriteId, out string accountId, out string portalId) && TryGetEntry(accountId, portalId, out var entry)) { departureUntilUtc = CalculateDeadline(entry.DepartureLastUtc, PublicPortalConfig.InviteDepartureCooldownHours.Value); arrivalUntilUtc = CalculateDeadline(entry.ArrivalLastUtc, PublicPortalConfig.InviteArrivalCooldownHours.Value); } } internal static void RemovePortal(string favoriteId) { if (!_serverActive || !PublicPortalData.TryNormalizeFavoriteId(favoriteId, out string portalId) || !TryGetCurrentWorldAccounts(out Dictionary<string, Dictionary<string, CooldownEntry>> accounts)) { return; } PendingReservation[] array = PendingReservations.Values.Where((PendingReservation candidate) => (candidate.Value.RecordsDeparture && string.Equals(candidate.Value.SourceFavoriteId, portalId, StringComparison.Ordinal)) || (candidate.Value.RecordsArrival && string.Equals(candidate.Value.DestinationFavoriteId, portalId, StringComparison.Ordinal))).ToArray(); for (int num = 0; num < array.Length; num++) { RemovePendingReservation(array[num]); } bool flag = false; foreach (Dictionary<string, CooldownEntry> value in accounts.Values) { flag |= value.Remove(portalId); } if (flag) { string[] array2 = (from pair in accounts where pair.Value.Count == 0 select pair.Key).ToArray(); foreach (string key in array2) { accounts.Remove(key); } _saveDirty = true; } } internal static bool TryReserve(string reservationId, string steamId, bool usesInviteAsSource, string sourceFavoriteId, bool usesInviteAsDestination, string destinationFavoriteId, out InviteTravelCooldownReservation reservation, out InviteTravelCooldownFailure failure, out long departureRemaining, out long arrivalRemaining) { reservation = default(InviteTravelCooldownReservation); failure = InviteTravelCooldownFailure.None; departureRemaining = 0L; arrivalRemaining = 0L; double num = PublicPortalConfig.InviteDepartureCooldownHours.Value; double num2 = PublicPortalConfig.InviteArrivalCooldownHours.Value; bool flag = usesInviteAsSource && num > 0.0; bool flag2 = usesInviteAsDestination && num2 > 0.0; if (!flag && !flag2) { return true; } string normalizedFavoriteId = ""; string normalizedFavoriteId2 = ""; if (string.IsNullOrWhiteSpace(reservationId) || reservationId.Length > 128 || !_serverActive || !PublicPortalData.TryNormalizeSteamId64(steamId, out string steamId2) || (flag && !PublicPortalData.TryNormalizeFavoriteId(sourceFavoriteId, out normalizedFavoriteId)) || (flag2 && !PublicPortalData.TryNormalizeFavoriteId(destinationFavoriteId, out normalizedFavoriteId2))) { failure = InviteTravelCooldownFailure.DataUnavailable; return false; } if (_saveFaulted && (DateTime.UtcNow < _nextSaveAttemptUtc || !TryFlushPending(out string error))) { failure = InviteTravelCooldownFailure.StorageUnavailable; return false; } long num3 = UtcNowSeconds(); PrunePendingReservations(num3); if (Prune(num3)) { _saveDirty = true; if (!TryFlushPending(out error)) { failure = InviteTravelCooldownFailure.StorageUnavailable; return false; } } CooldownEntry entry = default(CooldownEntry); CooldownEntry entry2 = default(CooldownEntry); if (flag) { TryGetEntry(steamId2, normalizedFavoriteId, out entry); } if (flag2) { TryGetEntry(steamId2, normalizedFavoriteId2, out entry2); } departureRemaining = (flag ? GetRemainingSeconds(CalculateDeadline(entry.DepartureLastUtc, num), num3) : 0); arrivalRemaining = (flag2 ? GetRemainingSeconds(CalculateDeadline(entry2.ArrivalLastUtc, num2), num3) : 0); if (departureRemaining > 0 || arrivalRemaining > 0) { failure = InviteTravelCooldownFailure.Active; return false; } if (PendingReservations.Count >= 4096) { failure = InviteTravelCooldownFailure.StorageUnavailable; return false; } string text = (flag ? CreateDirectionKey(steamId2, normalizedFavoriteId, departure: true) : ""); string text2 = (flag2 ? CreateDirectionKey(steamId2, normalizedFavoriteId2, departure: false) : ""); if ((text.Length > 0 && PendingDirectionOwners.ContainsKey(text)) || (text2.Length > 0 && PendingDirectionOwners.ContainsKey(text2))) { failure = InviteTravelCooldownFailure.ReservationConflict; return false; } if (!TryGetCurrentWorldAccounts(out Dictionary<string, Dictionary<string, CooldownEntry>> accounts)) { failure = InviteTravelCooldownFailure.DataUnavailable; return false; } Dictionary<string, CooldownEntry> portalEntries; bool flag3 = accounts.TryGetValue(steamId2, out portalEntries); if (!flag3) { HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); foreach (PendingReservation value2 in PendingReservations.Values) { if (!accounts.ContainsKey(value2.Value.AccountId)) { hashSet.Add(value2.Value.AccountId); } } if (!hashSet.Contains(steamId2) && accounts.Count > 32768 - hashSet.Count - 1) { failure = InviteTravelCooldownFailure.AccountCapacityReached; return false; } portalEntries = new Dictionary<string, CooldownEntry>(StringComparer.Ordinal); } HashSet<string> hashSet2 = new HashSet<string>(StringComparer.Ordinal); foreach (PendingReservation value3 in PendingReservations.Values) { InviteTravelCooldownReservation value = value3.Value; if (string.Equals(value.AccountId, steamId2, StringComparison.Ordinal)) { if (value.RecordsDeparture) { hashSet2.Add(value.SourceFavoriteId); } if (value.RecordsArrival) { hashSet2.Add(value.DestinationFavoriteId); } } } HashSet<string> hashSet3 = new HashSet<string>(StringComparer.Ordinal); HashSet<string> hashSet4 = new HashSet<string>(StringComparer.Ordinal); if (flag && !portalEntries.ContainsKey(normalizedFavoriteId) && !hashSet2.Contains(normalizedFavoriteId)) { hashSet3.Add(normalizedFavoriteId); } if (flag) { hashSet4.Add(normalizedFavoriteId); } if (flag2 && !portalEntries.ContainsKey(normalizedFavoriteId2) && !hashSet2.Contains(normalizedFavoriteId2)) { hashSet3.Add(normalizedFavoriteId2); } if (flag2) { hashSet4.Add(normalizedFavoriteId2); } int num4 = hashSet2.Count((string favoriteId) => !portalEntries.ContainsKey(favoriteId)); if (portalEntries.Count > 100000 - num4 - hashSet3.Count) { failure = InviteTravelCooldownFailure.PortalCapacityReached; return false; } long num5 = (long)hashSet4.Count * 256L + (flag3 ? 0 : 64); long num6 = PendingReservations.Values.Sum((PendingReservation pending) => pending.SerializedByteEstimate); if (_serializedByteUpperBound > 16777216 - num6 - num5) { if (!_saveDirty || !TryFlushPending(out error)) { failure = InviteTravelCooldownFailure.FileCapacityReached; return false; } if (_serializedByteUpperBound > 16777216 - num6 - num5) { failure = InviteTravelCooldownFailure.FileCapacityReached; return false; } } reservation = new InviteTravelCooldownReservation(reservationId, steamId2, normalizedFavoriteId, normalizedFavoriteId2, flag, flag2); PendingReservations.Add(reservationId, new PendingReservation(reservation, Math.Min(253402300799L, num3 + 180), num5)); if (text.Length > 0) { PendingDirectionOwners.Add(text, reservationId); } if (text2.Length > 0) { PendingDirectionOwners.Add(text2, reservationId); } return true; } internal static bool TryCommitReservation(InviteTravelCooldownReservation reservation, out InviteTravelCooldownFailure failure) { failure = InviteTravelCooldownFailure.None; if (!reservation.IsActive) { return true; } long num = UtcNowSeconds(); if (!PendingReservations.TryGetValue(reservation.ReservationId, out PendingReservation value) || value.ExpiresAtUtc < num || !ReservationMatches(reservation, value.Value)) { CancelReservation(reservation.ReservationId); failure = InviteTravelCooldownFailure.DataUnavailable; return false; } if (!_serverActive) { failure = InviteTravelCooldownFailure.DataUnavailable; return false; } if (_saveFaulted && (DateTime.UtcNow < _nextSaveAttemptUtc || !TryFlushPending(out string error))) { failure = InviteTravelCooldownFailure.StorageUnavailable; return false; } if (!TryGetCurrentWorldAccounts(out Dictionary<string, Dictionary<string, CooldownEntry>> accounts)) { failure = InviteTravelCooldownFailure.DataUnavailable; return false; } Dictionary<string, CooldownEntry> value2; bool flag = accounts.TryGetValue(reservation.AccountId, out value2); if (!flag) { if (accounts.Count >= 32768) { failure = InviteTravelCooldownFailure.AccountCapacityReached; return false; } value2 = new Dictionary<string, CooldownEntry>(StringComparer.Ordinal); } CooldownEntry value3 = default(CooldownEntry); CooldownEntry value4 = default(CooldownEntry); bool flag2 = reservation.RecordsDeparture && value2.TryGetValue(reservation.SourceFavoriteId, out value3); bool flag3 = reservation.RecordsArrival && value2.TryGetValue(reservation.DestinationFavoriteId, out value4); int num2 = 0; if (reservation.RecordsDeparture && !flag2) { num2++; } if (reservation.RecordsArrival && !flag3 && (!reservation.RecordsDeparture || !string.Equals(reservation.SourceFavoriteId, reservation.DestinationFavoriteId, StringComparison.Ordinal))) { num2++; } if (value2.Count > 100000 - num2) { failure = InviteTravelCooldownFailure.PortalCapacityReached; return false; } if (_serializedByteUpperBound > 16777216 - value.SerializedByteEstimate) { failure = InviteTravelCooldownFailure.FileCapacityReached; return false; } if (!flag) { accounts.Add(reservation.AccountId, value2); } bool saveDirty = _saveDirty; long serializedByteUpperBound = _serializedByteUpperBound; if (reservation.RecordsDeparture) { value2[reservation.SourceFavoriteId] = value3.WithDeparture(num); } if (reservation.RecordsArrival) { CooldownEntry value5; CooldownEntry cooldownEntry = ((reservation.RecordsDeparture && string.Equals(reservation.SourceFavoriteId, reservation.DestinationFavoriteId, StringComparison.Ordinal) && value2.TryGetValue(reservation.DestinationFavoriteId, out value5)) ? value5 : value4); value2[reservation.DestinationFavoriteId] = cooldownEntry.WithArrival(num); } _serializedByteUpperBound += value.SerializedByteEstimate; _saveDirty = true; if (TryFlushPending(out error)) { RemovePendingReservation(value); return true; } RestoreEntry(value2, reservation.SourceFavoriteId, flag2, value3); RestoreEntry(value2, reservation.DestinationFavoriteId, flag3, value4); if (value2.Count == 0) { accounts.Remove(reservation.AccountId); } _saveDirty = saveDirty || _saveFaulted; _serializedByteUpperBound = serializedByteUpperBound; failure = InviteTravelCooldownFailure.SaveFailed; return false; } internal static void CancelReservation(string reservationId) { if (!string.IsNullOrEmpty(reservationId) && PendingReservations.TryGetValue(reservationId, out PendingReservation value)) { RemovePendingReservation(value); } } internal static string FormatRemaining(long seconds) { seconds = Math.Max(1L, seconds); long num = Math.Max(1L, (seconds + 59) / 60); if (num < 60) { return PortalRulesLocalization.Translate("$sighsorry_portalrules_duration_minutes", num.ToString(CultureInfo.InvariantCulture)); } long num2 = num / 60; long num3 = num % 60; if (num3 != 0L) { return PortalRulesLocalization.Translate("$sighsorry_portalrules_duration_hours_minutes", num2.ToString(CultureInfo.InvariantCulture), num3.ToString(CultureInfo.InvariantCulture)); } return PortalRulesLocalization.Translate("$sighsorry_portalrules_duration_hours", num2.ToString(CultureInfo.InvariantCulture)); } internal static long GetRemainingSeconds(long deadlineUtc) { return GetRemainingSeconds(deadlineUtc, PublicPortalCatalog.GetEstimatedServerUtcNowSeconds()); } internal static bool TryGetInviteArrivalCooldownRemaining(PublicPortalCatalogEntry portal, out long remainingSeconds) { remainingSeconds = 0L; if (portal.AccessMode != PublicPortalAccessMode.Invite || portal.InviteArrivalCooldownUntilUtc <= 0) { return false; } remainingSeconds = GetRemainingSeconds(portal.InviteArrivalCooldownUntilUtc); return remainingSeconds > 0; } private static bool TryNormalizeKeys(string steamId, string favoriteId, out string accountId, out string portalId) { accountId = ""; portalId = ""; if (PublicPortalData.TryNormalizeSteamId64(steamId, out accountId)) { return PublicPortalData.TryNormalizeFavoriteId(favoriteId, out portalId); } return false; } private static string CreateDirectionKey(string accountId, string favoriteId, bool departure) { return accountId + (departure ? ":D:" : ":A:") + favoriteId; } private static bool ReservationMatches(InviteTravelCooldownReservation first, InviteTravelCooldownReservation second) { if (string.Equals(first.ReservationId, second.ReservationId, StringComparison.Ordinal) && string.Equals(first.AccountId, second.AccountId, StringComparison.Ordinal) && string.Equals(first.SourceFavoriteId, second.SourceFavoriteId, StringComparison.Ordinal) && string.Equals(first.DestinationFavoriteId, second.DestinationFavoriteId, StringComparison.Ordinal) && first.RecordsDeparture == second.RecordsDeparture) { return first.RecordsArrival == second.RecordsArrival; } return false; } private static void PrunePendingReservations(long nowUtc) { PendingReservation[] array = PendingReservations.Values.Where((PendingReservation candidate) => candidate.ExpiresAtUtc < nowUtc).ToArray(); for (int num = 0; num < array.Length; num++) { RemovePendingReservation(array[num]); } } private static void RemovePendingReservation(PendingReservation pending) { InviteTravelCooldownReservation value = pending.Value; PendingReservations.Remove(value.ReservationId); if (value.RecordsDeparture) { PendingDirectionOwners.Remove(CreateDirectionKey(value.AccountId, value.SourceFavoriteId, departure: true)); } if (value.RecordsArrival) { PendingDirectionOwners.Remove(CreateDirectionKey(value.AccountId, value.DestinationFavoriteId, departure: false)); } } private static void ClearPendingReservations() { PendingReservations.Clear(); PendingDirectionOwners.Clear(); } private static bool TryGetEntry(string accountId, string portalId, out CooldownEntry entry) { entry = default(CooldownEntry); if (TryGetCurrentWorldAccounts(out Dictionary<string, Dictionary<string, CooldownEntry>> accounts) && accounts.TryGetValue(accountId, out var value)) { return value.TryGetValue(portalId, out entry); } return false; } private static bool TryGetCurrentWorldAccounts(out Dictionary<string, Dictionary<string, CooldownEntry>> accounts) { accounts = null; if (!string.IsNullOrEmpty(_worldId)) { return Worlds.TryGetValue(_worldId, out accounts); } return false; } private static void RestoreEntry(Dictionary<string, CooldownEntry> entries, string portalId, bool existed, CooldownEntry previous) { if (!string.IsNullOrEmpty(portalId)) { if (existed) { entries[portalId] = previous; } else { entries.Remove(portalId); } } } private static long CalculateDeadline(long lastUtc, double hours) { if (lastUtc <= 0 || hours <= 0.0 || double.IsNaN(hours) || double.IsInfinity(hours)) { return 0L; } long num = (long)Math.Ceiling(Math.Min(hours, 8760.0) * 3600.0); if (lastUtc <= 253402300799L - num) { return lastUtc + num; } return 253402300799L; } private static long GetRemainingSeconds(long deadlineUtc, long nowUtc) { if (deadlineUtc <= nowUtc) { return 0L; } return deadlineUtc - nowUtc; } private static long UtcNowSeconds() { return DateTimeOffset.UtcNow.ToUnixTimeSeconds(); } private static long NormalizeUtcSeconds(long value) { if (value < 0 || value > 253402300799L) { return 0L; } return value; } private static void Load() { Worlds.Clear(); string path = _filePath + ".bak"; if (!File.Exists(_filePath)) { if (File.Exists(path)) { LoadFromPath(path); Save(); PortalRulesPlugin.PortalRulesLogger.LogWarning((object)"Recovered invite-travel-cooldowns.yml from its backup because the primary file was missing."); } return; } try { LoadFromPath(_filePath); } catch (Exception ex) { Worlds.Clear(); if (!File.Exists(path)) { throw; } try { LoadFromPath(path); string text = _filePath + ".invalid-" + DateTime.UtcNow.ToString("yyyyMMddHHmmssfff", CultureInfo.InvariantCulture) + "-" + Guid.NewGuid().ToString("N"); File.Move(_filePath, text); Save(); PortalRulesPlugin.PortalRulesLogger.LogWarning((object)("Recovered invite-travel-cooldowns.yml from its backup. The invalid primary file was preserved as '" + Path.GetFileName(text) + "'.")); } catch (Exception ex2) { Worlds.Clear(); throw new InvalidDataException("Failed to load invite-travel-cooldowns.yml and its backup. Primary error: " + ex.Message + ". Recovery error: " + ex2.Message, ex2); } } } private static void LoadFromPath(string path) { Worlds.Clear(); _serializedByteUpperBound = 0L; if (new FileInfo(path).Length > 16777216) { throw new InvalidDataException($"'{Path.GetFileName(path)}' exceeds {16777216L} bytes"); } string text = File.ReadAllText(path); CooldownFile cooldownFile = Deserializer.Deserialize<CooldownFile>(text); if (cooldownFile == null || cooldownFile.FormatVersion != 1 || cooldownFile.Worlds == null) { throw new InvalidDataException(string.Format("{0} must contain format_version {1} and worlds", "invite-travel-cooldowns.yml", 1)); } if (cooldownFile.Worlds.Count > 1024) { throw new InvalidDataException(string.Format("{0} contains more than {1} worlds", "invite-travel-cooldowns.yml", 1024)); } foreach (KeyValuePair<string, Dictionary<string, Dictionary<string, CooldownEntryYaml>>> world in cooldownFile.Worlds) { if (!long.TryParse(world.Key, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var result) || result == 0L || !string.Equals(world.Key, result.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal) || world.Value == null || world.Value.Count > 32768) { throw new InvalidDataException("invite-travel-cooldowns.yml contains an invalid world entry '" + world.Key + "'"); } Dictionary<string, Dictionary<string, CooldownEntry>> dictionary = new Dictionary<string, Dictionary<string, CooldownEntry>>(StringComparer.Ordinal); foreach (KeyValuePair<string, Dictionary<string, CooldownEntryYaml>> item in world.Value) { if (!PublicPortalData.TryNormalizeSteamId64(item.Key, out string steamId) || !string.Equals(item.Key, steamId, StringComparison.Ordinal) || item.Value == null || item.Value.Count > 100000) { throw new InvalidDataException("invite-travel-cooldowns.yml contains an invalid account entry '" + item.Key + "'"); } Dictionary<string, CooldownEntry> dictionary2 = new Dictionary<string, C