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 TeleportCreatures v0.3.3
TeleportCreatures.dll
Decompiled a month 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.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; 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 LocalizationManager; using Microsoft.CodeAnalysis; using Odinplus.Shared.ConfigurationUi; using Odinplus.Shared.Input; using Odinplus.Shared.Timing; using Odinplus.Shared.YamlEditor; using ServerSync; using TMPro; using UnityEngine; 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("TeleportCreatures")] [assembly: AssemblyDescription("https://valheim.hexium.gg/mods/OdinPlus/TeleportCreatures")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("odinplus")] [assembly: AssemblyProduct("TeleportCreatures")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: AssemblyFileVersion("0.3.3")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.3.3.0")] [module: UnverifiableCode] 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 TeleportCreatures { internal static class AllyModeCycle { internal static readonly AllySelectionMode[] Order = new AllySelectionMode[5] { AllySelectionMode.None, AllySelectionMode.AllTamed, AllySelectionMode.OnlyFollow, AllySelectionMode.OnlyNamed, AllySelectionMode.AllTamedExceptNamed }; public static AllySelectionMode Next(AllySelectionMode mode) { return Cycle(mode, 1); } public static AllySelectionMode Previous(AllySelectionMode mode) { return Cycle(mode, -1); } public static string LocalizationKey(AllySelectionMode mode) { return mode switch { AllySelectionMode.None => "$tc_ally_mode_none", AllySelectionMode.AllTamed => "$tc_ally_mode_all_tamed", AllySelectionMode.OnlyFollow => "$tc_ally_mode_only_follow", AllySelectionMode.OnlyNamed => "$tc_ally_mode_only_named", AllySelectionMode.AllTamedExceptNamed => "$tc_ally_mode_all_tamed_except_named", _ => "$tc_ally_mode_none", }; } private static AllySelectionMode Cycle(AllySelectionMode mode, int direction) { int num = Array.IndexOf(Order, mode); if (num < 0) { num = 0; } return Order[(num + direction + Order.Length) % Order.Length]; } } internal enum AllyTransportMessageMode { NoMessages, TopLeft, TopCenter, Center } internal static class AllyTransportDisplayPolicy { public static bool ShouldShowMessage(AllyTransportMessageMode mode, int allyCount) { if (mode != AllyTransportMessageMode.NoMessages) { return allyCount > 0; } return false; } public static bool ShouldShowEnemyPreview(bool portalReady, int enemyCount) { if (portalReady) { return enemyCount > 0; } return false; } public static AllyTransportMessageMode ParseMessageMode(string? value) { return value?.Trim().ToLowerInvariant() switch { "no messages" => AllyTransportMessageMode.NoMessages, "top left" => AllyTransportMessageMode.TopLeft, "top center" => AllyTransportMessageMode.TopCenter, _ => AllyTransportMessageMode.Center, }; } } [BepInPlugin("odinplus.TeleportCreatures", "TeleportCreatures", "0.3.3")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] public sealed class TeleportCreaturesPlugin : BaseUnityPlugin { private enum Toggle { On = 1, Off = 0 } private static readonly object UiGate = new object(); private static bool _uiCommitting; private static bool _uiFaulted; private NativeConfigTransport? _uiTransport; private ModuleConfigurationService? _uiService; private string _uiObservedServer = ""; private string _uiObservedPlayer = ""; private string _uiObservedCfg = ""; internal const string Author = "odinplus"; internal const string ModGuid = "odinplus.TeleportCreatures"; internal const string ModName = "TeleportCreatures"; internal const string ModVersion = "0.3.3"; private const string ConfigFileName = "odinplus.TeleportCreatures.cfg"; private static readonly ConfigSync Sync = new ConfigSync("odinplus.TeleportCreatures") { DisplayName = "TeleportCreatures", CurrentVersion = "0.3.3", MinimumRequiredVersion = "0.3.3" }; private static readonly CustomSyncedValue<string> Snapshot = new CustomSyncedValue<string>(Sync, "YamlPolicy", string.Empty); private static ConfigEntry<Toggle>? _forceServerConfig; private static ConfigEntry<bool>? _enableMod; private static ConfigEntry<bool>? _enableDiagnostics; private static ConfigEntry<bool>? _logSkippedAnimals; private static ConfigEntry<bool>? _profileCreatureTransport; private static ConfigEntry<string>? _allyTransportMessageMode; private static ConfigEntry<KeyboardShortcut>? _previousAlliesModeHotkey; private static ConfigEntry<KeyboardShortcut>? _nextAlliesModeHotkey; private const float AllyModePersistDelaySeconds = 2f; private static AllySelectionMode? _pendingAllyMode; private static float _allyModePersistAt; private static ConfigEntry<string>? _enemyTransportMessageMode; private readonly Harmony _harmony = new Harmony("odinplus.TeleportCreatures"); private static readonly <cba6ec40-2546-4a46-9577-18d27987fd9e>ISerializer Serializer = new <cba6ec40-2546-4a46-9577-18d27987fd9e>SerializerBuilder().Build(); private FileSystemWatcher? _serverYamlWatcher; private FileSystemWatcher? _playerYamlWatcher; private FileSystemWatcher? _configWatcher; private string _serverYamlPath; private bool _serverYamlMigratedFromV1; private static string _playerYamlPath = null; private GameObject? _yamlEditorHost; public static readonly ManualLogSource TeleportCreaturesLogger = Logger.CreateLogSource("TeleportCreatures"); internal static ManualLogSource Log = TeleportCreaturesLogger; internal static CreatureConfig ServerPolicy { get; private set; } = new CreatureConfig(); internal static PlayerCreaturePreferences PlayerPreferences { get; private set; } = PlayerCreaturePreferences.CreateDefault(); internal static EffectiveCreaturePolicy Policy { get; private set; } = EffectiveCreaturePolicy.Create(ServerPolicy, PlayerPreferences); internal static bool IsEnabled => _enableMod?.Value ?? false; internal static bool DiagnosticsEnabled => _enableDiagnostics?.Value ?? false; internal static bool LogSkippedAnimals => _logSkippedAnimals?.Value ?? false; internal static bool TransportProfilingEnabled { get { if (DiagnosticsEnabled) { return _profileCreatureTransport?.Value ?? false; } return false; } } internal static AllyTransportMessageMode AllyTransportMessageMode => AllyTransportDisplayPolicy.ParseMessageMode(_allyTransportMessageMode?.Value); internal static KeyboardShortcut PreviousAlliesModeHotkey => (KeyboardShortcut)(((??)_previousAlliesModeHotkey?.Value) ?? new KeyboardShortcut((KeyCode)276, (KeyCode[])(object)new KeyCode[1] { (KeyCode)304 })); internal static KeyboardShortcut NextAlliesModeHotkey => (KeyboardShortcut)(((??)_nextAlliesModeHotkey?.Value) ?? new KeyboardShortcut((KeyCode)275, (KeyCode[])(object)new KeyCode[1] { (KeyCode)304 })); internal static bool IsPreviousAlliesModeHotkeyConfigured { get { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Invalid comparison between Unknown and I4 ConfigEntry<KeyboardShortcut>? previousAlliesModeHotkey = _previousAlliesModeHotkey; if (previousAlliesModeHotkey == null) { return true; } KeyboardShortcut value = previousAlliesModeHotkey.Value; return (int)((KeyboardShortcut)(ref value)).MainKey > 0; } } internal static bool IsNextAlliesModeHotkeyConfigured { get { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Invalid comparison between Unknown and I4 ConfigEntry<KeyboardShortcut>? nextAlliesModeHotkey = _nextAlliesModeHotkey; if (nextAlliesModeHotkey == null) { return true; } KeyboardShortcut value = nextAlliesModeHotkey.Value; return (int)((KeyboardShortcut)(ref value)).MainKey > 0; } } internal static AllyTransportMessageMode EnemyTransportMessageMode => AllyTransportDisplayPolicy.ParseMessageMode(_enemyTransportMessageMode?.Value); internal static bool BasementsInstalled { get; private set; } internal static bool IsBlocked { get; private set; } public string HandleUiConfigRequestV1(string request) { EnsureUi(); return _uiTransport.Exchange(request); } private void EnsureUi() { if (_uiService != null) { return; } try { ObserveUiFiles(reload: false, "Server"); ObserveUiFiles(reload: false, "Player"); } catch { } _uiService = new ModuleConfigurationService("odinplus.TeleportCreatures", ReadUi, UiDefaults, ValidateUi, CommitUi, ReadUiCandidate); _uiTransport = new NativeConfigTransport("odinplus.TeleportCreatures", delegate(UiConfigRequest r, string caller, bool admin) { lock (UiGate) { return _uiService.Handle(r, caller, admin, DateTime.UtcNow); } }, delegate { lock (UiGate) { _uiService.NewSession(); } }); } private void Update() { EnsureUi(); _uiTransport.Tick(); } private Dictionary<string, ConfigEntryBase> UiEntries(string scope) { Dictionary<string, ConfigEntryBase> entries = new Dictionary<string, ConfigEntryBase>(); if (scope == "Server") { Add((ConfigEntryBase?)(object)_forceServerConfig); Add((ConfigEntryBase?)(object)_enableMod); } else { Add((ConfigEntryBase?)(object)_enableDiagnostics); Add((ConfigEntryBase?)(object)_logSkippedAnimals); Add((ConfigEntryBase?)(object)_profileCreatureTransport); Add((ConfigEntryBase?)(object)_allyTransportMessageMode); Add((ConfigEntryBase?)(object)_enemyTransportMessageMode); Add((ConfigEntryBase?)(object)_previousAlliesModeHotkey); Add((ConfigEntryBase?)(object)_nextAlliesModeHotkey); } return entries; void Add(ConfigEntryBase? entry) { if (entry != null) { entries[entry.Definition.Section + "/" + entry.Definition.Key] = entry; } } } private object UiPolicy(string scope) { if (!(scope == "Server")) { return PlayerPreferences; } return ServerPolicy; } private object UiDefaultPolicy(string scope) { if (!(scope == "Server")) { return PlayerCreaturePreferences.CreateDefault(); } return new CreatureConfig(); } private ModuleConfigurationCandidate UiDefaults(string scope) { return new ModuleConfigurationCandidate { Yaml = ModuleConfigProtocolV1.SerializePayload(UiDefaultPolicy(scope)), Cfg = UiEntries(scope).ToDictionary<KeyValuePair<string, ConfigEntryBase>, string, string>((KeyValuePair<string, ConfigEntryBase> x) => x.Key, (KeyValuePair<string, ConfigEntryBase> x) => TomlTypeConverter.ConvertToString(x.Value.DefaultValue, x.Value.SettingType)) }; } private void ObserveUiFiles(bool reload, string scope) { string path = ((scope == "Server") ? _serverYamlPath : _playerYamlPath); string text = (File.Exists(path) ? File.ReadAllText(path) : string.Empty); string text2 = (File.Exists(((BaseUnityPlugin)this).Config.ConfigFilePath) ? File.ReadAllText(((BaseUnityPlugin)this).Config.ConfigFilePath) : string.Empty); string text3 = ((scope == "Server") ? _uiObservedServer : _uiObservedPlayer); if (scope == "Server") { _uiObservedServer = text; } else { _uiObservedPlayer = text; } if (reload && text != text3) { if (scope == "Server") { ReloadServerYaml(); } else { ReloadPlayerYaml(); } } if (reload && text2 != _uiObservedCfg) { ReloadBepInExConfig(this, new FileSystemEventArgs(WatcherChangeTypes.Changed, Path.GetDirectoryName(((BaseUnityPlugin)this).Config.ConfigFilePath), Path.GetFileName(((BaseUnityPlugin)this).Config.ConfigFilePath))); } _uiObservedCfg = text2; } private ModuleConfigurationCandidate ReadUiCandidate(string scope) { ObserveUiFiles(reload: true, scope); return new ModuleConfigurationCandidate { Yaml = ModuleConfigProtocolV1.SerializePayload(UiPolicy(scope)), Cfg = UiEntries(scope).ToDictionary<KeyValuePair<string, ConfigEntryBase>, string, string>((KeyValuePair<string, ConfigEntryBase> x) => x.Key, (KeyValuePair<string, ConfigEntryBase> x) => x.Value.GetSerializedValue()) }; } private ModuleConfigurationView ReadUi(string scope) { object obj = UiPolicy(scope); ModuleConfigurationView moduleConfigurationView = new ModuleConfigurationView { Version = "0.3.3", Candidate = new ModuleConfigurationCandidate { Yaml = ModuleConfigProtocolV1.SerializePayload(obj), Cfg = UiEntries(scope).ToDictionary<KeyValuePair<string, ConfigEntryBase>, string, string>((KeyValuePair<string, ConfigEntryBase> x) => x.Key, (KeyValuePair<string, ConfigEntryBase> x) => x.Value.GetSerializedValue()) }, Descriptors = ConfigurationFields.Describe(obj, UiDefaultPolicy(scope), scope) }; if (scope == "Server" && _serverYamlMigratedFromV1) { moduleConfigurationView.Warnings.Add("The server's SchemaVersion 1 settings were automatically migrated to SchemaVersion 2. Review the server log and confirm the migrated settings."); } foreach (KeyValuePair<string, ConfigEntryBase> item in UiEntries(scope)) { ConfigEntryBase value = item.Value; ConfigurationDescriptor configurationDescriptor = new ConfigurationDescriptor { StableId = "cfg:" + item.Key, Scope = scope, Group = value.Definition.Section, Label = value.Definition.Key, Help = value.Description.Description, ValueYaml = value.GetSerializedValue(), DefaultYaml = TomlTypeConverter.ConvertToString(value.DefaultValue, value.SettingType), Kind = ((value.SettingType == typeof(bool)) ? "Toggle" : ((value.SettingType == typeof(KeyboardShortcut)) ? "KeyboardShortcut" : (value.SettingType.IsEnum ? "Enum" : "Text"))) }; if (value.SettingType.IsEnum) { configurationDescriptor.AllowedValues = Enum.GetNames(value.SettingType).ToList(); } if (value.Description.AcceptableValues is AcceptableValueList<string> val) { configurationDescriptor.Kind = "Enum"; configurationDescriptor.AllowedValues = val.AcceptableValues.ToList(); } moduleConfigurationView.Descriptors.Add(configurationDescriptor); } if ((Object)(object)ZNetScene.instance != (Object)null) { moduleConfigurationView.Prefabs = (from x in (from p in ZNetScene.instance.m_prefabs where (Object)(object)p != (Object)null && (Object)(object)p.GetComponent<Character>() != (Object)null select ((Object)p).name).Distinct() orderby x select x).Take(2048).ToList(); } string[] array = new string[2] { "com.kpro.TeleportEverything", "zenox.teleporteverything" }; foreach (string key in array) { if (Chainloader.PluginInfos.TryGetValue(key, out var value2)) { moduleConfigurationView.CompatibilityWarnings.Add(new CompatibilityWarning { ModName = value2.Metadata.Name, DetectedVersion = value2.Metadata.Version.ToString(), Reason = "Overlapping transport rules.", SuggestedAction = "Remove it or disable overlapping features, then restart." }); } } if (!BasementsInstalled) { moduleConfigurationView.Warnings.Add("Basements is not installed. Basement preferences are retained for future use."); } if (scope == "Player") { moduleConfigurationView.Warnings.Add("TE_ENEMY_SERVER_SETTINGS|Enabled=" + ServerPolicy.Enemies.Enabled + "|Mode=" + ServerPolicy.Enemies.Mode.ToString() + "|Portals=" + ServerPolicy.Enemies.Teleport.Portals.Enabled + "|Dungeons=" + ServerPolicy.Enemies.Teleport.Dungeons.Enabled + "|Basements=" + ServerPolicy.Enemies.Teleport.Basements.Enabled); foreach (ConfigurationDescriptor item2 in moduleConfigurationView.Descriptors.Where((ConfigurationDescriptor r) => !r.StableId.StartsWith("cfg:"))) { if (item2.StableId == "Allies.SearchRadius") { item2.EffectiveYaml = ModuleConfigProtocolV1.SerializePayload(Math.Min(PlayerPreferences.Allies.SearchRadius, ServerPolicy.Allies.MaximumSearchRadius)); item2.Minimum = Math.Max(float.Epsilon, Math.Min(0.01f, ServerPolicy.Allies.MaximumSearchRadius / 1000f)); item2.Maximum = ServerPolicy.Allies.MaximumSearchRadius; } if (item2.StableId == "Allies.SearchHeightRadius") { item2.EffectiveYaml = ModuleConfigProtocolV1.SerializePayload(Math.Min(PlayerPreferences.Allies.SearchHeightRadius, ServerPolicy.Allies.MaximumSearchHeightRadius)); item2.Minimum = Math.Max(float.Epsilon, Math.Min(0.01f, ServerPolicy.Allies.MaximumSearchHeightRadius / 1000f)); item2.Maximum = ServerPolicy.Allies.MaximumSearchHeightRadius; } } moduleConfigurationView.Warnings.Add("Player filters only restrict server policy. Placement and enemy transport remain server-owned; Deny wins."); } foreach (ConfigurationDescriptor item3 in moduleConfigurationView.Descriptors.Where((ConfigurationDescriptor r) => r.Kind == "Number")) { item3.Minimum = 0.0; string text = (item3.StableId.EndsWith("MaximumHeightDifference") ? "Enter zero or greater." : "Enter a number greater than zero. There is no fixed upper limit."); item3.Help = (string.IsNullOrWhiteSpace(item3.Help) ? text : (item3.Help + " " + text)); } foreach (ConfigurationDescriptor item4 in moduleConfigurationView.Descriptors.Where((ConfigurationDescriptor r) => r.Kind == "PortalRules")) { item4.Help += " Add at least one portal tag or type. Set priority from 1 to 100, or leave it blank (100)."; } if (scope == "Player") { PlayerDescriptorAvailability.Apply(moduleConfigurationView, IsEnabled, ServerPolicy); } return moduleConfigurationView; } private object ParseUi(string scope, string yaml) { if (scope == "Server") { CreatureConfig creatureConfig = ModuleConfigProtocolV1.DeserializePayload<CreatureConfig>(yaml); CreatureConfigValidator.Validate(creatureConfig); if (ConfigurationFields.HasNonFinite(creatureConfig)) { throw new ArgumentException("Numbers must be finite."); } return creatureConfig; } PlayerCreaturePreferences playerCreaturePreferences = ModuleConfigProtocolV1.DeserializePayload<PlayerCreaturePreferences>(yaml); PlayerCreaturePreferencesValidator.Validate(playerCreaturePreferences); if (ConfigurationFields.HasNonFinite(playerCreaturePreferences)) { throw new ArgumentException("Numbers must be finite."); } return playerCreaturePreferences; } private string? ValidateUi(string scope, ModuleConfigurationCandidate candidate) { try { if (_uiFaulted) { return "Saving is blocked after a rollback failure. Restart after checking the module log and configuration files."; } ParseUi(scope, candidate.Yaml); Dictionary<string, ConfigEntryBase> entries = UiEntries(scope); if (candidate.Cfg == null || candidate.Cfg.Count != entries.Count || candidate.Cfg.Keys.Any((string k) => !entries.ContainsKey(k))) { return "The complete configuration must include only this scope's supported settings."; } foreach (KeyValuePair<string, string> item in candidate.Cfg) { ConfigEntryBase val = entries[item.Key]; object obj = TomlTypeConverter.ConvertToValue(item.Value, val.SettingType); if (val.Description.AcceptableValues != null && !val.Description.AcceptableValues.IsValid(obj)) { return "Invalid value for " + val.Definition.Key + "."; } if (val.SettingType.IsEnum && !Enum.IsDefined(val.SettingType, obj)) { return "Unsupported option."; } } return null; } catch (ArgumentException ex) { return ex.Message; } catch (InvalidDataException ex2) { return ex2.Message; } catch { return "The draft contains malformed or unsupported configuration values."; } } private void CommitUi(string scope, ModuleConfigurationCandidate candidate) { object parsed = ParseUi(scope, candidate.Yaml); CreatureConfig oldServer = ServerPolicy; PlayerCreaturePreferences oldPlayer = PlayerPreferences; string oldSnapshot = Snapshot.Value; Dictionary<string, ConfigEntryBase> entries = UiEntries(scope); Dictionary<string, string> oldCfg = entries.ToDictionary<KeyValuePair<string, ConfigEntryBase>, string, string>((KeyValuePair<string, ConfigEntryBase> x) => x.Key, (KeyValuePair<string, ConfigEntryBase> x) => x.Value.GetSerializedValue()); bool saveOnConfigSet = ((BaseUnityPlugin)this).Config.SaveOnConfigSet; _uiCommitting = true; ((BaseUnityPlugin)this).Config.SaveOnConfigSet = false; try { AtomicModuleCommit.Execute((scope == "Server") ? _serverYamlPath : _playerYamlPath, ModuleConfigProtocolV1.SerializePayload(parsed), ((BaseUnityPlugin)this).Config.ConfigFilePath, delegate { foreach (KeyValuePair<string, string> item in candidate.Cfg) { entries[item.Key].SetSerializedValue(item.Value); } if (scope == "Server") { ServerPolicy = (CreatureConfig)parsed; } else { PlayerPreferences = (PlayerCreaturePreferences)parsed; } RebuildEffectivePolicy(); }, delegate { ((BaseUnityPlugin)this).Config.Save(); }, delegate { if (scope == "Server") { Snapshot.AssignLocalValue(ModuleConfigProtocolV1.SerializePayload(ServerPolicy)); } }, delegate { AtomicModuleCommit.RestoreAll(((IEnumerable<KeyValuePair<string, string>>)oldCfg).Select((Func<KeyValuePair<string, string>, Action>)((KeyValuePair<string, string> pair) => delegate { entries[pair.Key].SetSerializedValue(pair.Value); })).Concat(new Action[2] { delegate { ServerPolicy = oldServer; PlayerPreferences = oldPlayer; RebuildEffectivePolicy(); }, delegate { if (scope == "Server") { Snapshot.AssignLocalValue(oldSnapshot); } } })); }, delegate(Exception error) { _uiFaulted = true; Log.LogError((object)("UI configuration rollback failed: " + error.Message)); }, (scope == "Server") ? _uiObservedServer : _uiObservedPlayer, _uiObservedCfg); if (scope == "Player") { _pendingAllyMode = null; } try { ObserveUiFiles(reload: false, scope); } catch { } Log.LogInfo((object)("UI configuration saved: scope=" + scope + ".")); } finally { ((BaseUnityPlugin)this).Config.SaveOnConfigSet = saveOnConfigSet; _uiCommitting = false; } } private void Awake() { Log = TeleportCreaturesLogger; FindIncompatibleMods(); BindConfiguration(); _serverYamlPath = Path.Combine(Paths.ConfigPath, "odinplus.TeleportCreatures.server.yml"); _playerYamlPath = Path.Combine(Paths.ConfigPath, "odinplus.TeleportCreatures.yml"); Snapshot.ValueChanged += ReadSnapshot; if (Sync.IsSourceOfTruth) { ReloadServerYaml(); SetupServerYamlWatcher(); } ReloadPlayerYaml(); SetupPlayerYamlWatcher(); SetupConfigWatcher(); try { RegisterYamlEditor(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("YAML editor unavailable; creature transport will continue. " + ex)); } Localizer.Load(); _harmony.PatchAll(Assembly.GetExecutingAssembly()); } private void Start() { EnsureUi(); if (!IsBlocked) { BasementsInstalled = Chainloader.PluginInfos.ContainsKey("com.rolopogo.Basement"); ((BaseUnityPlugin)this).Logger.LogInfo((object)string.Format("{0} loaded. enabled={1}, yamlSource={2}.", "TeleportCreatures", IsEnabled, Sync.IsSourceOfTruth)); } } private void OnDestroy() { _uiTransport?.Dispose(); if (!IsBlocked) { _serverYamlWatcher?.Dispose(); _playerYamlWatcher?.Dispose(); _configWatcher?.Dispose(); if ((Object)(object)_yamlEditorHost != (Object)null) { Object.Destroy((Object)(object)_yamlEditorHost); } Snapshot.ValueChanged -= ReadSnapshot; if (!_uiFaulted) { FlushPendingAllyMode(); } AllyTransport.CancelPendingTransports(); PortalAllyDisplay.Dispose(); PortalEnemyDisplay.Dispose(); AllyModeSwitchDisplay.Dispose(); _harmony.UnpatchSelf(); if (!_uiFaulted) { ((BaseUnityPlugin)this).Config.Save(); } } } private void BindConfiguration() { //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Expected O, but got Unknown //IL_0101: Unknown result type (might be due to invalid IL or missing references) //IL_010b: Expected O, but got Unknown //IL_012e: Unknown result type (might be due to invalid IL or missing references) //IL_0160: Unknown result type (might be due to invalid IL or missing references) //IL_01bb: Unknown result type (might be due to invalid IL or missing references) //IL_01c5: Expected O, but got Unknown //IL_01fb: Unknown result type (might be due to invalid IL or missing references) //IL_0205: Expected O, but got Unknown //IL_023b: Unknown result type (might be due to invalid IL or missing references) //IL_0245: Expected O, but got Unknown bool saveOnConfigSet = ((BaseUnityPlugin)this).Config.SaveOnConfigSet; ((BaseUnityPlugin)this).Config.SaveOnConfigSet = false; try { _forceServerConfig = BindSynced("General", "Force Server Config", Toggle.On, "When On, the server's synchronized TeleportCreatures settings are enforced for connected clients."); Sync.AddLockingConfigEntry<Toggle>(_forceServerConfig); _enableMod = BindSynced("General", "Enable Mod", defaultValue: true, "Master switch for TeleportCreatures portal transport."); _allyTransportMessageMode = ((BaseUnityPlugin)this).Config.Bind<string>("Allies Display", "Portal Message Mode", "Top left", new ConfigDescription("Shows a local live count of eligible allies while you stand in an active portal. No messages hides it.", (AcceptableValueBase)(object)new AcceptableValueList<string>(new string[4] { "No messages", "Top left", "Top center", "Center" }), Array.Empty<object>())); _enemyTransportMessageMode = ((BaseUnityPlugin)this).Config.Bind<string>("Enemies Display", "Transport Message Mode", "Center", new ConfigDescription("Local-only layout for the live enemy preview and completion message. Top left places the preview below the allies counter; the completion result uses the Valheim queue.", (AcceptableValueBase)(object)new AcceptableValueList<string>(new string[4] { "No messages", "Top left", "Top center", "Center" }), Array.Empty<object>())); _previousAlliesModeHotkey = BindLocal<KeyboardShortcut>("Controls", "Previous Allies Mode Hotkey", new KeyboardShortcut((KeyCode)276, (KeyCode[])(object)new KeyCode[1] { (KeyCode)304 }), "Hotkey to cycle to the previous allies mode."); _nextAlliesModeHotkey = BindLocal<KeyboardShortcut>("Controls", "Next Allies Mode Hotkey", new KeyboardShortcut((KeyCode)275, (KeyCode[])(object)new KeyCode[1] { (KeyCode)304 }), "Hotkey to cycle to the next allies mode."); YamlEditorButton.Bind(((BaseUnityPlugin)this).Config, "Advanced Configuration", "Teleport Rules"); _enableDiagnostics = ((BaseUnityPlugin)this).Config.Bind<bool>("Diagnostics", "Enable Creature Teleport Diagnostics", false, new ConfigDescription("Writes capture, arrival, and per-animal placement details to this machine's log.", (AcceptableValueBase)null, new object[1] { new Odinplus.Shared.YamlEditor.ConfigurationManagerAttributes { IsAdvanced = true } })); _logSkippedAnimals = ((BaseUnityPlugin)this).Config.Bind<bool>("Diagnostics", "Log Skipped Animals", false, new ConfigDescription("When diagnostics are enabled, writes an individual reason for each animal not transported.", (AcceptableValueBase)null, new object[1] { new Odinplus.Shared.YamlEditor.ConfigurationManagerAttributes { IsAdvanced = true } })); _profileCreatureTransport = ((BaseUnityPlugin)this).Config.Bind<bool>("Diagnostics", "Profile Creature Transport", false, new ConfigDescription("When diagnostics are enabled, writes one timing and work-count summary for each creature transport.", (AcceptableValueBase)null, new object[1] { new Odinplus.Shared.YamlEditor.ConfigurationManagerAttributes { IsAdvanced = true } })); } finally { ((BaseUnityPlugin)this).Config.SaveOnConfigSet = saveOnConfigSet; ((BaseUnityPlugin)this).Config.Save(); } } private ConfigEntry<T> BindSynced<T>(string group, string key, T defaultValue, string description) { ConfigEntry<T> val = ((BaseUnityPlugin)this).Config.Bind<T>(group, key, defaultValue, Describe(description, "Server synchronized")); Sync.AddConfigEntry<T>(val).SynchronizedConfig = true; return val; } private ConfigEntry<T> BindLocal<T>(string group, string key, T defaultValue, string description) { return ((BaseUnityPlugin)this).Config.Bind<T>(group, key, defaultValue, Describe(description, "Local only")); } private static ConfigDescription Describe(string description, string scope) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Expected O, but got Unknown return new ConfigDescription(description + " [" + scope + "]", (AcceptableValueBase)null, Array.Empty<object>()); } private void SetupServerYamlWatcher() { _serverYamlWatcher = new FileSystemWatcher(Paths.ConfigPath, Path.GetFileName(_serverYamlPath)) { IncludeSubdirectories = false, NotifyFilter = (NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime), SynchronizingObject = ThreadingHelper.SynchronizingObject, EnableRaisingEvents = true }; _serverYamlWatcher.Changed += delegate { ReloadServerYaml(); }; _serverYamlWatcher.Created += delegate { ReloadServerYaml(); }; _serverYamlWatcher.Renamed += delegate { ReloadServerYaml(); }; } private void SetupPlayerYamlWatcher() { _playerYamlWatcher = new FileSystemWatcher(Paths.ConfigPath, Path.GetFileName(_playerYamlPath)) { IncludeSubdirectories = false, NotifyFilter = (NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime), SynchronizingObject = ThreadingHelper.SynchronizingObject, EnableRaisingEvents = true }; _playerYamlWatcher.Changed += delegate { ReloadPlayerYaml(); }; _playerYamlWatcher.Created += delegate { ReloadPlayerYaml(); }; _playerYamlWatcher.Renamed += delegate { ReloadPlayerYaml(); }; } private void SetupConfigWatcher() { _configWatcher = new FileSystemWatcher(Paths.ConfigPath, "odinplus.TeleportCreatures.cfg") { IncludeSubdirectories = false, NotifyFilter = (NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.CreationTime), SynchronizingObject = ThreadingHelper.SynchronizingObject, EnableRaisingEvents = true }; _configWatcher.Changed += ReloadBepInExConfig; _configWatcher.Created += ReloadBepInExConfig; _configWatcher.Renamed += ReloadBepInExConfig; } private void RegisterYamlEditor() { //IL_0117: Unknown result type (might be due to invalid IL or missing references) //IL_0121: Expected O, but got Unknown YamlEditorRegistry.Initialize("odinplus.TeleportCreatures", "TeleportCreatures"); YamlEditorRegistry.Register(new Odinplus.Shared.YamlEditor.YamlDocument("server", "Server policy (" + Path.GetFileName(_serverYamlPath) + ")", _serverYamlPath, YamlValidation.FromExceptional(delegate(string text) { CreatureConfigValidator.Validate(CreatureConfigYaml.DeserializeAndMigrate(text)); }), () => Sync.IsSourceOfTruth && !_uiFaulted, () => "only the server host can edit the server policy")); YamlEditorRegistry.Register(new Odinplus.Shared.YamlEditor.YamlDocument("player", "Player preferences (" + Path.GetFileName(_playerYamlPath) + ")", _playerYamlPath, YamlValidation.FromTryPattern((string text) => (!PlayerPreferenceSnapshot.TryReplace(PlayerPreferences, text, out PlayerCreaturePreferences _, out string error)) ? error : null), () => !_uiFaulted)); _yamlEditorHost = new GameObject("TeleportCreaturesYamlEditor"); Object.DontDestroyOnLoad((Object)(object)_yamlEditorHost); _yamlEditorHost.AddComponent<YamlEditorBehaviour>(); } private void ReloadBepInExConfig(object sender, FileSystemEventArgs eventArgs) { lock (UiGate) { if (!_uiCommitting && !_uiFaulted) { ReloadBepInExConfigCore(sender, eventArgs); } } } private void ReloadBepInExConfigCore(object sender, FileSystemEventArgs eventArgs) { if (!File.Exists(((BaseUnityPlugin)this).Config.ConfigFilePath)) { return; } try { ((BaseUnityPlugin)this).Config.Reload(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("Could not reload odinplus.TeleportCreatures.cfg. Check its entries and format. " + ex.Message)); } } private void ReloadServerYaml() { lock (UiGate) { if (!_uiCommitting && !_uiFaulted) { ReloadServerYamlCore(); } } } private void ReloadServerYamlCore() { if (!Sync.IsSourceOfTruth) { return; } if (!File.Exists(_serverYamlPath)) { File.WriteAllText(_serverYamlPath, DefaultServerYamlTemplate()); } try { bool migrated; CreatureConfig creatureConfig = CreatureConfigYaml.DeserializeAndMigrate(File.ReadAllText(_serverYamlPath), out migrated); CreatureConfigValidator.Validate(creatureConfig); if (migrated) { File.WriteAllText(_serverYamlPath, Serializer.Serialize(creatureConfig)); _serverYamlMigratedFromV1 = true; ((BaseUnityPlugin)this).Logger.LogInfo((object)"Migrated TeleportCreatures server YAML from SchemaVersion 1 to 2; placement settings were moved under Allies and Enemies."); } ServerPolicy = creatureConfig; RebuildEffectivePolicy(); Snapshot.AssignLocalValue(Serializer.Serialize(creatureConfig)); LogDiagnostic($"Accepted server YAML: ally allow={creatureConfig.Allies.Allow.Count}, deny={creatureConfig.Allies.Deny.Count}, radius={creatureConfig.Allies.Placement.Radius:0.##}, water={creatureConfig.Allies.Placement.Water}."); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("TeleportCreatures retained the previous valid server YAML: " + ex.Message)); } } private void ReloadPlayerYaml() { lock (UiGate) { if (!_uiCommitting && !_uiFaulted) { ReloadPlayerYamlCore(); } } } private void ReloadPlayerYamlCore() { if (!File.Exists(_playerYamlPath)) { File.WriteAllText(_playerYamlPath, DefaultPlayerYamlTemplate()); } string yaml = File.ReadAllText(_playerYamlPath); if (PlayerPreferenceSnapshot.TryReplace(PlayerPreferences, yaml, out PlayerCreaturePreferences retained, out string error)) { PlayerPreferences = retained; RebuildEffectivePolicy(); LogDiagnostic($"Accepted local player YAML: ally allow={retained.Allies.Allow.Count}, deny={retained.Allies.Deny.Count}."); } else { ((BaseUnityPlugin)this).Logger.LogError((object)("TeleportCreatures retained the previous valid local player YAML: " + error)); } } private static void ReadSnapshot() { if (Sync.IsSourceOfTruth || string.IsNullOrWhiteSpace(Snapshot.Value)) { return; } try { CreatureConfig creatureConfig = CreatureConfigYaml.DeserializeAndMigrate(Snapshot.Value); CreatureConfigValidator.Validate(creatureConfig); ServerPolicy = creatureConfig; RebuildEffectivePolicy(); LogDiagnostic($"Accepted server YAML snapshot: ally allow={creatureConfig.Allies.Allow.Count}, deny={creatureConfig.Allies.Deny.Count}, radius={creatureConfig.Allies.Placement.Radius:0.##}, water={creatureConfig.Allies.Placement.Water}."); } catch (Exception ex) { Log.LogError((object)("Rejected TeleportCreatures server snapshot: " + ex.Message)); } } private static void RebuildEffectivePolicy() { Policy = EffectiveCreaturePolicy.Create(ServerPolicy, PlayerPreferences); } internal static void CycleAllyMode(int direction) { if (!_uiCommitting && !_uiFaulted) { AllySelectionMode allySelectionMode = ((direction > 0) ? AllyModeCycle.Next(PlayerPreferences.Allies.Mode) : AllyModeCycle.Previous(PlayerPreferences.Allies.Mode)); PlayerPreferences.Allies.Mode = allySelectionMode; RebuildEffectivePolicy(); _pendingAllyMode = allySelectionMode; _allyModePersistAt = Time.time + 2f; LogDiagnostic(string.Format("Ally mode switched to {0} ({1}).", allySelectionMode, (direction > 0) ? "next" : "previous")); AllyModeSwitchDisplay.Show(allySelectionMode, direction); } } internal static void TickAllyModePersistence() { if (_pendingAllyMode.HasValue && Time.time >= _allyModePersistAt) { FlushPendingAllyMode(); } } private static void FlushPendingAllyMode() { if (_uiCommitting || _uiFaulted) { return; } AllySelectionMode? pendingAllyMode = _pendingAllyMode; if (!pendingAllyMode.HasValue) { return; } _pendingAllyMode = null; try { string text = File.ReadAllText(_playerYamlPath); if (PlayerYamlModeWriter.TryReplaceModeLine(text, pendingAllyMode.Value.ToString(), out string updated)) { File.WriteAllText(_playerYamlPath, updated); } else { PlayerCreaturePreferences playerCreaturePreferences = new <cba6ec40-2546-4a46-9577-18d27987fd9e>DeserializerBuilder().Build().Deserialize<PlayerCreaturePreferences>(text); playerCreaturePreferences.Allies.Mode = pendingAllyMode.Value; File.WriteAllText(_playerYamlPath, Serializer.Serialize(playerCreaturePreferences)); } LogDiagnostic($"Persisted ally mode {pendingAllyMode.Value} to {Path.GetFileName(_playerYamlPath)}."); } catch (Exception ex) { Log.LogWarning((object)("Could not persist the ally mode to " + Path.GetFileName(_playerYamlPath) + "; the in-memory mode stays active for this session. " + ex.Message)); } } private void FindIncompatibleMods() { string[] array = new string[2] { "com.kpro.TeleportEverything", "zenox.teleporteverything" }; foreach (string text in array) { if (Chainloader.PluginInfos.TryGetValue(text, out var value)) { ((BaseUnityPlugin)this).Logger.LogWarning((object)($"Incompatible mod detected: {value.Metadata.Name} ({value.Metadata.Version}), GUID: {text}.\n" + "This mod can conflict with TeleportCreatures.\nTo avoid issues, please either:\n- Uninstall the detected mod, or\n- Disable the conflicting features in one of the mods.")); } } } internal static void LogDiagnostic(string message) { if (DiagnosticsEnabled) { Log.LogInfo((object)("TeleportCreatures: " + message)); } } internal static void LogSkipped(string message) { if (DiagnosticsEnabled && LogSkippedAnimals) { Log.LogInfo((object)("TeleportCreatures: skipped animal: " + message)); } } private string DefaultServerYamlTemplate() { return "# Server policy. This file is synchronized to clients.\n# Filters accept all/*, none, exact prefab names, and * / ? globs. Deny always wins.\n# Portal rules use the lowest Priority number first (1 is strongest; omitted is 100).\n# Equal priorities keep YAML order. Tags are portal link text; Prefabs are portal prefab names.\n# A rule Allow narrows server Allow. It never expands the server's global creature ceiling.\nSchemaVersion: 2\n\nAllies:\n # Disable all allied-creature transport for every player.\n Enabled: true\n # Hard limits for each player's local SearchRadius and SearchHeightRadius.\n MaximumSearchRadius: 20\n MaximumSearchHeightRadius: 8\n # The server's global creature ceiling. These filters apply before portal rules.\n Allow: [\"all\"]\n Deny: []\n\n Teleport:\n Portals:\n # Disable creature transport through every normal portal.\n Enabled: true\n Rules:\n # These neutral defaults allow every server-allowed ally through Wood and Stone portals.\n # Add a source-specific rule with Priority: 1..99 to override them.\n - Id: \"wood-portals\"\n Source:\n Prefabs: [\"portal_wood\"]\n Allow: [\"all\"]\n Deny: []\n\n - Id: \"stone-portals\"\n Source:\n Prefabs: [\"portal_stone\"]\n Allow: [\"all\"]\n Deny: []\n\n Dungeons:\n # Covers both dungeon entrances and exits.\n Enabled: true\n Basements:\n # Requires the Basements mod on the server.\n Enabled: true\n Placement:\n # Search radius for ally placement in normal portals.\n Radius: 10\n MaximumHeightDifference: 3\n DistanceFromPlayer: 3\n Spacing: 2.5\n Water: Allow\n Dungeons:\n Radius: 12\n RequireNaturalGroundOnExit: true\n Basements:\n Radius: 8\n RequireClearance: true\n\nEnemies:\n # Enemy transport is opt-in and controlled by this server policy.\n Enabled: false\n MaximumSearchRadius: 20\n MaximumSearchHeightRadius: 8\n Allow: [\"all\"]\n Deny: []\n Teleport:\n Portals:\n Enabled: true\n Rules:\n # Neutral defaults allow all server-allowed enemies through Wood and Stone portals.\n - Id: \"wood-enemy-portals\"\n Source:\n Prefabs: [\"portal_wood\"]\n Allow: [\"all\"]\n Deny: []\n - Id: \"stone-enemy-portals\"\n Source:\n Prefabs: [\"portal_stone\"]\n Allow: [\"all\"]\n Deny: []\n Dungeons:\n Enabled: true\n Basements:\n Enabled: false\n\n Placement:\n Radius: 12\n MaximumHeightDifference: 3\n DistanceFromPlayer: 6\n Spacing: 2.5\n Water: Allow\n Dungeons:\n Radius: 12\n RequireNaturalGroundOnExit: true\n Basements:\n Radius: 8\n RequireClearance: true\n"; } private string DefaultPlayerYamlTemplate() { return "# Local player preference. This file is never synchronized and can only restrict the server policy.\n# SearchRadius values are clamped to the server maximums. Player portal rules cannot override server rules.\n# Filters accept all/*, none, exact prefab names, and * / ? globs. Deny always wins.\nSchemaVersion: 1\n\nAllies:\n # Disable allied-creature transport only for this player.\n Enabled: true\n # None, AllTamed, OnlyFollow, OnlyNamed, or AllTamedExceptNamed.\n Mode: AllTamed\n # Personal search limits; the server clamps them to its configured maximums.\n SearchRadius: 10\n SearchHeightRadius: 3\n # Personal restrictions, applied in addition to the server's filters.\n Allow: [\"all\"]\n Deny: []\n\n Teleport:\n Portals:\n # Disable creature transport through normal portals only for this player.\n Enabled: true\n # Optional personal portal rules. They can narrow, never expand, server policy.\n Rules: []\n Dungeons:\n # Disable dungeon entrance and exit transport only for this player.\n Enabled: true\n Basements:\n # Disable Basements transport only for this player.\n Enabled: true\n\n"; } } internal enum CreatureArrivalAction { Skip, MoveInstance, MoveOwnedRecord } internal static class CreatureArrivalDecision { public static CreatureArrivalAction Select(bool instanceAvailable, bool recordAvailable, bool recordIsOwnedLocally) { if (instanceAvailable) { return CreatureArrivalAction.MoveInstance; } if (recordAvailable && recordIsOwnedLocally) { return CreatureArrivalAction.MoveOwnedRecord; } return CreatureArrivalAction.Skip; } public static bool ShouldPersistNetworkRecord(CreatureArrivalAction action) { if ((uint)(action - 1) <= 1u) { return true; } return false; } } internal readonly struct CreatureClearanceShape { private const float UniformScaleTolerance = 0.001f; public float Radius { get; } public float HalfSegment { get; } public float UniformScale { get; } public float Height => Radius * 2f + HalfSegment * 2f; public CreatureClearanceShape(float radius, float halfSegment, float uniformScale = 1f) { Radius = radius; HalfSegment = halfSegment; UniformScale = uniformScale; } public static bool TryCreate(float localRadius, float localHeight, float scaleX, float scaleY, float scaleZ, out CreatureClearanceShape shape) { shape = default(CreatureClearanceShape); if (!IsFinite(localRadius) || !IsFinite(localHeight) || !IsFinite(scaleX) || !IsFinite(scaleY) || !IsFinite(scaleZ) || localRadius <= 0f || localHeight <= 0f || scaleX <= 0f || scaleY <= 0f || scaleZ <= 0f) { return false; } float num = Math.Max(scaleX, Math.Max(scaleY, scaleZ)); float num2 = Math.Min(scaleX, Math.Min(scaleY, scaleZ)); if (num - num2 > num * 0.001f) { return false; } float num3 = localRadius * num; float num4 = Math.Max(0f, localHeight * 0.5f - localRadius) * num; if (!IsFinite(num3) || !IsFinite(num4)) { return false; } shape = new CreatureClearanceShape(num3, num4, num); return true; } public float RootYForFloor(float floorHeight, float groundClearance, float endpointAOffsetY, float endpointBOffsetY) { return floorHeight + groundClearance - (Math.Min(endpointAOffsetY, endpointBOffsetY) - Radius); } private static bool IsFinite(float value) { if (!float.IsNaN(value)) { return !float.IsInfinity(value); } return false; } } public sealed class CreatureConfig { public int SchemaVersion { get; set; } = 2; public CreatureFilterConfig Allies { get; set; } = new CreatureFilterConfig { Allow = new List<string> { "all" }, Deny = new List<string>() }; public EnemyFilterConfig Enemies { get; set; } = new EnemyFilterConfig(); } public sealed class CreatureFilterConfig { public bool Enabled { get; set; } = true; public AllyTeleportConfig Teleport { get; set; } = new AllyTeleportConfig(); public float MaximumSearchRadius { get; set; } = 20f; public float MaximumSearchHeightRadius { get; set; } = 8f; public List<string> Allow { get; set; } = new List<string>(); public List<string> Deny { get; set; } = new List<string>(); public CreaturePlacementConfig Placement { get; set; } = new CreaturePlacementConfig(); } public sealed class EnemyFilterConfig { public bool Enabled { get; set; } public EnemyPortalMode Mode { get; set; } = EnemyPortalMode.TakeThemWithYou; public EnemyTeleportConfig Teleport { get; set; } = new EnemyTeleportConfig(); public float MaximumSearchRadius { get; set; } = 20f; public float MaximumSearchHeightRadius { get; set; } = 8f; public List<string> Allow { get; set; } = new List<string> { "all" }; public List<string> Deny { get; set; } = new List<string>(); public CreaturePlacementConfig Placement { get; set; } = new CreaturePlacementConfig { Radius = 12f, DistanceFromPlayer = 6f }; } public enum EnemyPortalMode { VikingsDontRun, TakeThemWithYou } public sealed class EnemyTeleportConfig { public PortalTransportConfig Portals { get; set; } = new PortalTransportConfig(); public TransitionToggleConfig Dungeons { get; set; } = new TransitionToggleConfig(); public TransitionToggleConfig Basements { get; set; } = new TransitionToggleConfig { Enabled = false }; } public enum AllySelectionMode { AllTamed, OnlyFollow, OnlyNamed, AllTamedExceptNamed, None } public static class AllySelectionPolicy { public static bool IsSelected(AllySelectionMode mode, bool isFollowingPlayer, bool hasCustomName) { return mode switch { AllySelectionMode.AllTamed => true, AllySelectionMode.OnlyFollow => isFollowingPlayer, AllySelectionMode.OnlyNamed => hasCustomName, AllySelectionMode.AllTamedExceptNamed => !hasCustomName, AllySelectionMode.None => false, _ => false, }; } } public sealed class AllyTeleportConfig { public PortalTransportConfig Portals { get; set; } = new PortalTransportConfig(); public TransitionToggleConfig Dungeons { get; set; } = new TransitionToggleConfig(); public TransitionToggleConfig Basements { get; set; } = new TransitionToggleConfig(); } public sealed class TransitionToggleConfig { public bool Enabled { get; set; } = true; } public sealed class PortalTransportConfig { public bool Enabled { get; set; } = true; public List<PortalRuleConfig> Rules { get; set; } = new List<PortalRuleConfig>(); } public sealed class PortalRuleConfig { public string Id { get; set; } = string.Empty; public int? Priority { get; set; } public PortalRuleSourceConfig Source { get; set; } = new PortalRuleSourceConfig(); public List<string>? Allow { get; set; } public List<string> Deny { get; set; } = new List<string>(); } public sealed class PortalRuleSourceConfig { public List<string>? Tags { get; set; } public List<string>? Prefabs { get; set; } } public sealed class DungeonPlacementConfig { public float Radius { get; set; } = 12f; public bool RequireNaturalGroundOnExit { get; set; } = true; } public sealed class BasementPlacementConfig { public float Radius { get; set; } = 8f; public bool RequireClearance { get; set; } = true; } public sealed class CreaturePlacementConfig { public float Radius { get; set; } = 10f; public float MaximumHeightDifference { get; set; } = 3f; public float DistanceFromPlayer { get; set; } = 3f; public float Spacing { get; set; } = 2.5f; public WaterPolicy Water { get; set; } = WaterPolicy.Allow; public DungeonPlacementConfig Dungeons { get; set; } = new DungeonPlacementConfig(); public BasementPlacementConfig Basements { get; set; } = new BasementPlacementConfig(); } internal sealed class LegacyPlacementConfigV1 { public float Radius { get; set; } = 10f; public float MaximumHeightDifference { get; set; } = 3f; public float DistanceFromPlayer { get; set; } = 3f; public float AnimalSpacing { get; set; } = 2.5f; public WaterPolicy Water { get; set; } = WaterPolicy.Allow; public DungeonPlacementConfig Dungeons { get; set; } = new DungeonPlacementConfig(); public BasementPlacementConfig Basements { get; set; } = new BasementPlacementConfig(); } public sealed class EnemyPlacementConfig { public float MaximumRadius { get; set; } = 12f; public float DistanceFromPlayer { get; set; } = 6f; public float MaximumHeightDifference { get; set; } = 3f; public float Spacing { get; set; } = 2.5f; public WaterPolicy Water { get; set; } = WaterPolicy.Allow; } public enum WaterPolicy { Avoid, Allow, Require } public static class CreatureConfigValidator { public static void Validate(CreatureConfig config) { if (config == null) { throw new InvalidDataException("Creature configuration cannot be null."); } if (config.SchemaVersion != 2) { throw new InvalidDataException("Only SchemaVersion 2 is supported. SchemaVersion 1 is migrated when the server YAML is loaded."); } if (config.Allies == null || config.Enemies == null || config.Enemies.Teleport == null || config.Allies.Teleport == null || config.Allies.Placement == null || config.Enemies.Placement == null || config.Allies.Teleport.Portals == null || config.Enemies.Teleport.Portals == null || config.Enemies.Teleport.Dungeons == null || config.Enemies.Teleport.Basements == null || config.Allies.Placement.Dungeons == null || config.Allies.Placement.Basements == null || config.Enemies.Placement.Dungeons == null || config.Enemies.Placement.Basements == null || config.Allies.Teleport.Dungeons == null || config.Allies.Teleport.Basements == null) { throw new InvalidDataException("Allies, Enemies, their Placement sections, and their required Teleport sections cannot be null."); } if (config.Allies.Placement.Dungeons.Radius <= 0f || config.Allies.Placement.Basements.Radius <= 0f || config.Enemies.Placement.Dungeons.Radius <= 0f || config.Enemies.Placement.Basements.Radius <= 0f) { throw new InvalidDataException("Allies.Placement and Enemies.Placement dungeon and basement radii must be positive."); } if (config.Allies.MaximumSearchRadius <= 0f || config.Allies.MaximumSearchHeightRadius <= 0f) { throw new InvalidDataException("Allies.MaximumSearchRadius and Allies.MaximumSearchHeightRadius must be positive."); } if (!IsPlacementValid(config.Allies.Placement) || !IsPlacementValid(config.Enemies.Placement)) { throw new InvalidDataException("Allies.Placement and Enemies.Placement require positive radius, distance, and spacing, a non-negative height difference, and Water set to Avoid, Allow, or Require."); } CreatureMatcher.ValidateList(config.Allies.Allow, "Allies.Allow"); CreatureMatcher.ValidateList(config.Allies.Deny, "Allies.Deny"); ValidatePortalRules(config.Allies.Teleport.Portals.Rules); ValidatePortalRules(config.Enemies.Teleport.Portals.Rules, "Enemies.Teleport.Portals.Rules"); CreatureMatcher.ValidateList(config.Enemies.Allow, "Enemies.Allow"); CreatureMatcher.ValidateList(config.Enemies.Deny, "Enemies.Deny"); if (!Enum.IsDefined(typeof(EnemyPortalMode), config.Enemies.Mode)) { throw new InvalidDataException("Enemies.Mode must be VikingsDontRun or TakeThemWithYou."); } if (config.Enemies.MaximumSearchRadius <= 0f || config.Enemies.MaximumSearchHeightRadius <= 0f || config.Enemies.Placement.DistanceFromPlayer > config.Enemies.Placement.Radius) { throw new InvalidDataException("Enemy search limits must be positive and Enemies.Placement.DistanceFromPlayer cannot exceed its Radius."); } } private static bool IsPlacementValid(CreaturePlacementConfig placement) { if (placement.Radius > 0f && placement.MaximumHeightDifference >= 0f && placement.DistanceFromPlayer > 0f && placement.Spacing > 0f) { return Enum.IsDefined(typeof(WaterPolicy), placement.Water); } return false; } internal static void ValidatePortalRules(IReadOnlyList<PortalRuleConfig> rules, string path = "Allies.Teleport.Portals.Rules") { if (rules == null) { throw new InvalidDataException(path + " cannot be null."); } HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < rules.Count; i++) { string text = $"{path}[{i}]"; PortalRuleConfig portalRuleConfig = rules[i] ?? throw new InvalidDataException(text + " cannot be null."); if (string.IsNullOrWhiteSpace(portalRuleConfig.Id)) { throw new InvalidDataException(text + ".Id is required."); } if (!hashSet.Add(portalRuleConfig.Id.Trim())) { throw new InvalidDataException(text + ".Id '" + portalRuleConfig.Id + "' duplicates another portal rule Id."); } bool flag; switch (portalRuleConfig.Priority) { default: flag = true; break; case null: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: case 9: case 10: case 11: case 12: case 13: case 14: case 15: case 16: case 17: case 18: case 19: case 20: case 21: case 22: case 23: case 24: case 25: case 26: case 27: case 28: case 29: case 30: case 31: case 32: case 33: case 34: case 35: case 36: case 37: case 38: case 39: case 40: case 41: case 42: case 43: case 44: case 45: case 46: case 47: case 48: case 49: case 50: case 51: case 52: case 53: case 54: case 55: case 56: case 57: case 58: case 59: case 60: case 61: case 62: case 63: case 64: case 65: case 66: case 67: case 68: case 69: case 70: case 71: case 72: case 73: case 74: case 75: case 76: case 77: case 78: case 79: case 80: case 81: case 82: case 83: case 84: case 85: case 86: case 87: case 88: case 89: case 90: case 91: case 92: case 93: case 94: case 95: case 96: case 97: case 98: case 99: case 100: flag = false; break; } if (flag) { throw new InvalidDataException(text + ".Priority must be between 1 and 100 when specified."); } if (portalRuleConfig.Source == null) { throw new InvalidDataException(text + ".Source is required."); } List<string> tags = portalRuleConfig.Source.Tags; bool num = tags != null && tags.Count > 0; tags = portalRuleConfig.Source.Prefabs; bool flag2 = tags != null && tags.Count > 0; if (!num && !flag2) { throw new InvalidDataException(text + ".Source requires Tags and/or Prefabs."); } if (portalRuleConfig.Source.Tags != null) { CreatureMatcher.ValidateList(portalRuleConfig.Source.Tags, text + ".Source.Tags"); } if (portalRuleConfig.Source.Prefabs != null) { CreatureMatcher.ValidateList(portalRuleConfig.Source.Prefabs, text + ".Source.Prefabs"); } if (portalRuleConfig.Allow != null) { CreatureMatcher.ValidateList(portalRuleConfig.Allow, text + ".Allow"); } CreatureMatcher.ValidateList(portalRuleConfig.Deny, text + ".Deny"); } } } public static class CreatureConfigYaml { private sealed class CreatureConfigSchemaProbe { public int SchemaVersion { get; set; } } private sealed class LegacyEnemyFilterConfigV1 { public bool Enabled { get; set; } public LegacyEnemyTeleportConfigV1? Teleport { get; set; } public float MaximumSearchRadius { get; set; } = 20f; public float MaximumSearchHeightRadius { get; set; } = 8f; public List<string>? Allow { get; set; } = new List<string> { "all" }; public List<string>? Deny { get; set; } = new List<string>(); public CreaturePlacementConfig? Placement { get; set; } } private sealed class LegacyEnemyTeleportConfigV1 { public bool PortalsEnabled { get; set; } = true; public TransitionToggleConfig? Dungeons { get; set; } = new TransitionToggleConfig(); public TransitionToggleConfig? Basements { get; set; } = new TransitionToggleConfig { Enabled = false }; } private sealed class LegacyCreatureConfigV1 { public int SchemaVersion { get; set; } public CreatureFilterConfig? Allies { get; set; } public LegacyEnemyFilterConfigV1? Enemies { get; set; } public LegacyPlacementConfigV1? Placement { get; set; } public EnemyPlacementConfig? EnemyPlacement { get; set; } } public static CreatureConfig DeserializeAndMigrate(string yaml) { bool migrated; return DeserializeAndMigrate(yaml, out migrated); } public static CreatureConfig DeserializeAndMigrate(string yaml, out bool migrated) { <cba6ec40-2546-4a46-9577-18d27987fd9e>IDeserializer <cba6ec40-2546-4a46-9577-18d27987fd9e>IDeserializer = new <cba6ec40-2546-4a46-9577-18d27987fd9e>DeserializerBuilder().Build(); CreatureConfigSchemaProbe creatureConfigSchemaProbe = new <cba6ec40-2546-4a46-9577-18d27987fd9e>DeserializerBuilder().IgnoreUnmatchedProperties().Build().Deserialize<CreatureConfigSchemaProbe>(yaml) ?? throw new InvalidDataException("Creature configuration document is empty."); if (creatureConfigSchemaProbe.SchemaVersion == 1) { LegacyCreatureConfigV1 legacyCreatureConfigV = <cba6ec40-2546-4a46-9577-18d27987fd9e>IDeserializer.Deserialize<LegacyCreatureConfigV1>(yaml) ?? throw new InvalidDataException("Creature configuration document is empty."); CreatureConfig creatureConfig = new CreatureConfig { SchemaVersion = 2, Allies = (legacyCreatureConfigV.Allies ?? new CreatureFilterConfig()), Enemies = MigrateEnemyConfig(legacyCreatureConfigV.Enemies) }; if (legacyCreatureConfigV.Placement != null) { creatureConfig.Allies.Placement = MigratePlacement(legacyCreatureConfigV.Placement); } if (legacyCreatureConfigV.EnemyPlacement != null) { creatureConfig.Enemies.Placement = MigrateEnemyPlacement(legacyCreatureConfigV.EnemyPlacement, legacyCreatureConfigV.Placement); } migrated = true; return creatureConfig; } if (creatureConfigSchemaProbe.SchemaVersion != 2) { throw new InvalidDataException($"Unsupported SchemaVersion {creatureConfigSchemaProbe.SchemaVersion}; expected 1 or 2."); } migrated = false; return <cba6ec40-2546-4a46-9577-18d27987fd9e>IDeserializer.Deserialize<CreatureConfig>(yaml) ?? throw new InvalidDataException("Creature configuration document is empty."); } private static CreaturePlacementConfig MigratePlacement(LegacyPlacementConfigV1 legacy) { return new CreaturePlacementConfig { Radius = legacy.Radius, MaximumHeightDifference = legacy.MaximumHeightDifference, DistanceFromPlayer = legacy.DistanceFromPlayer, Spacing = legacy.AnimalSpacing, Water = legacy.Water, Dungeons = legacy.Dungeons, Basements = legacy.Basements }; } private static CreaturePlacementConfig MigrateEnemyPlacement(EnemyPlacementConfig legacy, LegacyPlacementConfigV1? previousPlacement) { return new CreaturePlacementConfig { Radius = legacy.MaximumRadius, MaximumHeightDifference = legacy.MaximumHeightDifference, DistanceFromPlayer = legacy.DistanceFromPlayer, Spacing = legacy.Spacing, Water = legacy.Water, Dungeons = (previousPlacement?.Dungeons ?? new DungeonPlacementConfig()), Basements = (previousPlacement?.Basements ?? new BasementPlacementConfig()) }; } private static EnemyFilterConfig MigrateEnemyConfig(LegacyEnemyFilterConfigV1? legacy) { if (legacy == null) { return new EnemyFilterConfig(); } return new EnemyFilterConfig { Enabled = legacy.Enabled, Teleport = new EnemyTeleportConfig { Portals = new PortalTransportConfig { Enabled = (legacy.Teleport?.PortalsEnabled ?? true) }, Dungeons = (legacy.Teleport?.Dungeons ?? new TransitionToggleConfig()), Basements = (legacy.Teleport?.Basements ?? new TransitionToggleConfig { Enabled = false }) }, MaximumSearchRadius = legacy.MaximumSearchRadius, MaximumSearchHeightRadius = legacy.MaximumSearchHeightRadius, Allow = (legacy.Allow ?? new List<string> { "all" }), Deny = (legacy.Deny ?? new List<string>()), Placement = (legacy.Placement ?? new CreaturePlacementConfig { Radius = 12f, DistanceFromPlayer = 6f }) }; } } public static class CreatureEligibility { public static bool IsAllowed(CreatureConfig config, string prefabName, bool isTamed) { if (!isTamed || string.IsNullOrWhiteSpace(prefabName)) { return false; } if (config.Allies.Deny.Any((string pattern) => CreatureMatcher.IsMatch(pattern, prefabName))) { return false; } return config.Allies.Allow.Any((string pattern) => CreatureMatcher.IsMatch(pattern, prefabName)); } } internal static class CreaturePlacementPolicy { public static bool IsNaturalGround(float terrainHeight, float solidHeight, float tolerance) { return Math.Abs(terrainHeight - solidHeight) <= tolerance; } public static bool CanUseConstructedFloor(bool isBasement, bool clearanceIsOpen) { return !isBasement || clearanceIsOpen; } public static bool UsesNativePlayerLevel(bool isBasement) { return isBasement; } public static float SelectLandingHeight(bool hasInteriorFloor, float interiorFloorHeight, float solidHeight) { if (!hasInteriorFloor) { return solidHeight; } return interiorFloorHeight; } public static bool IsOnPlayerLevel(float playerHeight, float floorHeight, float maximumDifference) { return Math.Abs(playerHeight - floorHeight) <= maximumDifference; } public static bool CanUseDungeonFallbackHeight(float playerHeight, float solidHeight, float maximumDifference) { return IsOnPlayerLevel(playerHeight, solidHeight, maximumDifference); } public static int GetCandidateSlot(int animalIndex, int attempt, int positionsPerRing) { return animalIndex + attempt; } public static float GetForwardSearchAngle(int slot, int positionsPerRing, float halfAngleDegrees) { int num = slot % positionsPerRing; if (num == 0) { return 0f; } int num2 = (num + 1) / 2; int num3 = (positionsPerRing - 1) / 2; return ((num % 2 == 0) ? 1f : (-1f)) * halfAngleDegrees * (float)num2 / (float)num3; } public static bool IsClearanceFree(int collisionCount, int bufferCapacity) { if (collisionCount == 0) { return collisionCount < bufferCapacity; } return false; } public static int FilterClearanceLayerMask(int defaultMask, int excludedLayerMask) { return defaultMask & ~excludedLayerMask; } } internal enum CreaturePlacementFailure { SearchRadius, TerrainUnavailable, DungeonFloorLevel, NaturalGround, BasementLevel, DungeonFallbackLevel, SlopeTooSteep, BlockedClearance, WaterPolicy, Spacing } internal struct CreaturePlacementProbe { private int _radius; private int _terrainUnavailable; private int _dungeonFloorLevel; private int _naturalGround; private int _basementLevel; private int _dungeonFallbackLevel; private int _slope; private int _clearance; private int _waterPolicy; private int _spacing; private bool _hasFailure; private CreaturePlacementFailure _firstFailure; private float _candidateX; private float _candidateY; private float _candidateZ; private bool _hasFloorHeight; private float _floorHeight; private bool _hasClearanceCandidate; private float _clearanceCandidateX; private float _clearanceCandidateY; private float _clearanceCandidateZ; private float _clearanceFloorHeight; private float _clearanceCapsuleRadius; private float _clearanceCapsuleHeight; private float _blockerCandidateX; private float _blockerCandidateY; private float _blockerCandidateZ; private float _blockerFloorHeight; public object? FirstClearanceBlocker { get; private set; } public void RecordFailure(CreaturePlacementFailure failure, float candidateX, float candidateY, float candidateZ, float? floorHeight = null) { switch (failure) { case CreaturePlacementFailure.SearchRadius: _radius++; break; case CreaturePlacementFailure.TerrainUnavailable: _terrainUnavailable++; break; case CreaturePlacementFailure.DungeonFloorLevel: _dungeonFloorLevel++; break; case CreaturePlacementFailure.NaturalGround: _naturalGround++; break; case CreaturePlacementFailure.BasementLevel: _basementLevel++; break; case CreaturePlacementFailure.DungeonFallbackLevel: _dungeonFallbackLevel++; break; case CreaturePlacementFailure.SlopeTooSteep: _slope++; break; case CreaturePlacementFailure.BlockedClearance: _clearance++; break; case CreaturePlacementFailure.WaterPolicy: _waterPolicy++; break; case CreaturePlacementFailure.Spacing: _spacing++; break; } if (!_hasFailure) { _hasFailure = true; _firstFailure = failure; _candidateX = candidateX; _candidateY = candidateY; _candidateZ = candidateZ; if (floorHeight.HasValue) { _hasFloorHeight = true; _floorHeight = floorHeight.Value; } } } public void RecordClearanceFailure(float candidateX, float candidateY, float candidateZ, float floorHeight, float capsuleRadius, float capsuleHeight, object? blocker) { RecordFailure(CreaturePlacementFailure.BlockedClearance, candidateX, candidateY, candidateZ, floorHeight); if (!_hasClearanceCandidate) { _hasClearanceCandidate = true; _clearanceCandidateX = candidateX; _clearanceCandidateY = candidateY; _clearanceCandidateZ = candidateZ; _clearanceFloorHeight = floorHeight; _clearanceCapsuleRadius = capsuleRadius; _clearanceCapsuleHeight = capsuleHeight; } if (blocker != null && FirstClearanceBlocker == null) { FirstClearanceBlocker = blocker; _blockerCandidateX = candidateX; _blockerCandidateY = candidateY; _blockerCandidateZ = candidateZ; _blockerFloorHeight = floorHeight; } } public string FormatSummary() { StringBuilder stringBuilder = new StringBuilder("placement candidates rejected:"); AppendCount(stringBuilder, "radius", _radius); AppendCount(stringBuilder, "terrainUnavailable", _terrainUnavailable); AppendCount(stringBuilder, "dungeonFloorLevel", _dungeonFloorLevel); AppendCount(stringBuilder, "naturalGround", _naturalGround); AppendCount(stringBuilder, "basementLevel", _basementLevel); AppendCount(stringBuilder, "dungeonFallbackLevel", _dungeonFallbackLevel); AppendCount(stringBuilder, "slope", _slope); AppendCount(stringBuilder, "clearance", _clearance); AppendCount(stringBuilder, "waterPolicy", _waterPolicy); AppendCount(stringBuilder, "spacing", _spacing); if (_hasFailure) { stringBuilder.Append("; firstFailure=").Append(FailureName(_firstFailure)).Append(" candidate=(") .Append(Format(_candidateX)) .Append(',') .Append(Format(_candidateY)) .Append(',') .Append(Format(_candidateZ)) .Append(')'); } if (_hasFloorHeight) { stringBuilder.Append(" floorHeight=").Append(Format(_floorHeight)); } if (_hasClearanceCandidate) { stringBuilder.Append(" firstClearanceCandidate=(").Append(Format(_clearanceCandidateX)).Append(',') .Append(Format(_clearanceCandidateY)) .Append(',') .Append(Format(_clearanceCandidateZ)) .Append(')') .Append(" firstClearanceFloorHeight=") .Append(Format(_clearanceFloorHeight)) .Append(" capsuleRadius=") .Append(Format(_clearanceCapsuleRadius)) .Append(" capsuleHeight=") .Append(Format(_clearanceCapsuleHeight)); } if (FirstClearanceBlocker != null) { stringBuilder.Append(" firstBlockerCandidate=(").Append(Format(_blockerCandidateX)).Append(',') .Append(Format(_blockerCandidateY)) .Append(',') .Append(Format(_blockerCandidateZ)) .Append(')') .Append(" firstBlockerFloorHeight=") .Append(Format(_blockerFloorHeight)); } return stringBuilder.ToString(); } private static void AppendCount(StringBuilder summary, string name, int count) { if (count > 0) { summary.Append(' ').Append(name).Append('=') .Append(count); } } private static string FailureName(CreaturePlacementFailure failure) { return failure switch { CreaturePlacementFailure.SearchRadius => "radius", CreaturePlacementFailure.TerrainUnavailable => "terrainUnavailable", CreaturePlacementFailure.DungeonFloorLevel => "dungeonFloorLevel", CreaturePlacementFailure.NaturalGround => "naturalGround", CreaturePlacementFailure.BasementLevel => "basementLevel", CreaturePlacementFailure.DungeonFallbackLevel => "dungeonFallbackLevel", CreaturePlacementFailure.SlopeTooSteep => "slope", CreaturePlacementFailure.BlockedClearance => "clearance", CreaturePlacementFailure.WaterPolicy => "waterPolicy", CreaturePlacementFailure.Spacing => "spacing", _ => "unknown", }; } private static string Format(float value) { return value.ToString("0.##", CultureInfo.InvariantCulture); } } public sealed class PlayerCreaturePreferences { public int SchemaVersion { get; set; } = 1; public PlayerAllyPreferences Allies { get; set; } = new PlayerAllyPreferences(); public static PlayerCreaturePreferences CreateDefault() { return new PlayerCreaturePreferences(); } } public sealed class PlayerAllyPreferences { public bool Enabled { get; set; } = true; public AllySelectionMode Mode { get; set; } public PlayerAllyTeleportConfig Teleport { get; set; } = new PlayerAllyTeleportConfig(); public float SearchRadius { get; set; } = 10f; public float SearchHeightRadius { get; set; } = 3f; public List<string> Allow { get; set; } = new List<string> { "all" }; public List<string> Deny { get; set; } = new List<string>(); } public sealed class PlayerAllyTeleportConfig { public PortalTransportConfig Portals { get; set; } = new PortalTransportConfig(); public TransitionToggleConfig Dungeons { get; set; } = new TransitionToggleConfig(); public TransitionToggleConfig Basements { get; set; } = new TransitionToggleConfig(); } public static class PlayerCreaturePreferencesValidator { public static void Validate(PlayerCreaturePreferences preferences) { if (preferences == null) { throw new InvalidDataException("Player creature preferences cannot be null."); } if (preferences.SchemaVersion != 1) { throw new InvalidDataException("Only SchemaVersion 1 is supported."); } if (preferences.Allies == null || preferences.Allies.Teleport == null || preferences.Allies.Teleport.Portals == null || preferences.Allies.Teleport.Dungeons == null || preferences.Allies.Teleport.Basements == null) { throw new InvalidDataException("Allies and Allies.Teleport sections are required."); } if (!Enum.IsDefined(typeof(AllySelectionMode), preferences.Allies.Mode)) { throw new InvalidDataException("Allies.Mode must be valid."); } if (preferences.Allies.SearchRadius <= 0f || preferences.Allies.SearchHeightRadius <= 0f) { throw new InvalidDataException("Allies.SearchRadius and Allies.SearchHeightRadius must be positive."); } CreatureMatcher.ValidateList(preferences.Allies.Allow, "Allies.Allow"); CreatureMatcher.ValidateList(preferences.Allies.Deny, "Allies.Deny"); CreatureConfigValidator.ValidatePortalRules(preferences.Allies.Teleport.Portals.Rules); } } public static class PlayerPreferenceSnapshot { public static bool TryReplace(PlayerCreaturePreferences lastValid, string yaml, out PlayerCreaturePreferences retained, out string error) { try { PlayerCreaturePreferences playerCreaturePreferences = new <cba6ec40-2546-4a46-9577-18d27987fd9e>DeserializerBuilder().Build().Deserialize<PlayerCreaturePreferences>(yaml); PlayerCreaturePreferencesValidator.Validate(playerCreaturePreferences); retained = playerCreaturePreferences; error = string.Empty; return true; } catch (Exception ex) { retained = lastValid; error = ex.Message; return false; } } } public static class CreatureMatcher { private static readonly char[] LegacyRegexCharacters = new char[9] { '^', '$', '[', ']', '(', ')', '|', '+', '\\' }; public static bool IsMatch(string pattern, string prefabName) { ValidatePattern(pattern, "filter"); string text = pattern.Trim(); if (text.Equals("all", StringComparison.OrdinalIgnoreCase) || text == "*") { return true; } if (text.Equals("none", StringComparison.OrdinalIgnoreCase)) { return false; } string pattern2 = "^" + GlobToRegex(text) + "$"; return Regex.IsMatch(prefabName ?? string.Empty, pattern2, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); } public static void ValidateList(IEnumerable<string> patterns, string section) { if (patterns == null) { throw new InvalidDataException(section + " cannot be null."); } foreach (string pattern in patterns) { ValidatePattern(pattern, section); } } private static void ValidatePattern(string pattern, string section) { if (string.IsNullOrWhiteSpace(pattern)) { throw new InvalidDataException(section + " cannot contain an empty filter."); } string text = pattern.Trim(); if (text.StartsWith("regex:", StringComparison.OrdinalIgnoreCase) || text.IndexOfAny(LegacyRegexCharacters) >= 0) { throw new InvalidDataException(section + " uses legacy regular-expression syntax. Use an exact prefab name or * / ? glob instead."); } } private static string GlobToRegex(string pattern) { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < pattern.Length; i++) { char c = pattern[i]; StringBuilder stringBuilder2 = stringBuilder; stringBuilder2.Append(c switch { '*' => ".*", '?' => ".", _ => Regex.Escape(c.ToString()), }); } return stringBuilder.ToString(); } } public sealed class PortalContext { public string PrefabName { get; } public string Tag { get; } public bool IsUsable => !string.IsNullOrWhiteSpace(PrefabName); public PortalContext(string prefabName, string tag) { PrefabName = prefabName ?? string.Empty; Tag = tag ?? string.Empty; } } public static class PortalRuleResolver { public static PortalRuleConfig? Select(PortalTransportConfig portals, PortalContext context) { if (portals == null || context == null || !context.IsUsable) { return null; } PortalRuleConfig portalRuleConfig = null; foreach (PortalRuleConfig rule in portals.Rules) { if (Matches(rule, context) && (portalRuleConfig == null || GetEffectivePriority(rule) < GetEffectivePriority(portalRuleConfig))) { portalRuleConfig = rule; } } return portalRuleConfig; } public static int GetEffectivePriority(PortalRuleConfig rule) { return rule.Priority ?? 100; } public static bool Matches(PortalRuleConfig rule, PortalContext context) { if (rule?.Source == null || context == null || !context.IsUsable) { return false; } List<string> tags = rule.Source.Tags; bool flag = tags != null && tags.Count > 0; tags = rule.Source.Prefabs; bool flag2 = tags != null && tags.Count > 0; if (!flag && !flag2) { return false; } if (!flag || MatchesAny(rule.Source.Tags, context.Tag)) { if (flag2) { return MatchesAny(rule.Source.Prefabs, context.PrefabName); } return true; } return false; } private static bool MatchesAny(IEnumerable<string> patterns, string value) { return patterns.Any((string pattern) => CreatureMatcher.IsMatch(pattern, value)); } } public sealed class EffectiveCreaturePolicy { public CreatureConfig ServerPolicy { get; } public PlayerCreaturePreferences PlayerPreferences { get; } public bool ShouldCountEnemiesForPortalBlock { get { if (ServerPolicy.Enemies.Enabled) { return ServerPolicy.Enemies.Mode == EnemyPortalMode.VikingsDontRun; } return false; } } public bool ShouldTransportEnemiesThroughPortals { get { if (ServerPolicy.Enemies.Enabled) { return ServerPolicy.Enemies.Mode == EnemyPortalMode.TakeThemWithYou; } return false; } } private EffectiveCreaturePolicy(CreatureConfig serverPolicy, PlayerCreaturePreferences playerPreferences) { ServerPolicy = serverPolicy; PlayerPreferences = playerPreferences; } public static EffectiveCreaturePolicy Create(CreatureConfig server, PlayerCreaturePreferences player) { CreatureConfigValidator.Validate(server); PlayerCreaturePreferencesValidator.Validate(player); return new EffectiveCreaturePolicy(server, player); } public bool IsAllowed(string prefabName, bool isTamed, bool isFollowingPlayer, bool hasCustomName, PortalContext? portalContext = null) { if (!isTamed || !ServerPolicy.Allies.Enabled || !PlayerPreferences.Allies.Enabled || string.IsNullOrWhiteSpace(prefabName)) { return false; } if (!AllySelectionPolicy.IsSelected(PlayerPreferences.Allies.Mode, isFollowingPlayer, hasCustomName)) { return false; } PortalRuleConfig portalRuleConfig = ((portalContext == null) ? null : PortalRuleResolver.Select(ServerPolicy.Allies.Teleport.Portals, portalContext)); PortalRuleConfig portalRuleConfig2 = ((portalContext == null) ? null : PortalRuleResolver.Select(PlayerPreferences.Allies.Teleport.Portals, portalContext)); IEnumerable<string> patterns = portalRuleConfig2?.Allow ?? PlayerPreferences.Allies.Allow; if (MatchesAllowedList(ServerPolicy.Allies.Allow, prefabName) && (portalRuleConfig?.Allow == null || MatchesAllowedList(portalRuleConfig.Allow, prefabName)) && MatchesAllowedList(patterns, prefabName) && !MatchesAny(ServerPolicy.Allies.Deny, prefabName) && (portalRuleConfig == null || !MatchesAny(portalRuleConfig.Deny, prefabName)) && !MatchesAny(PlayerPreferences.Allies.Deny, prefabName)) { if (portalRuleConfig2 != null) { return !MatchesAny(portalRuleConfig2.Deny, prefabName); } return true; } return false; } public bool IsTransitionEnabled(TransportTransitionKind transitionKind, bool basementsInstalled) { if (CreatureTransitionPolicy.IsEnabled(ServerPolicy.Allies.Teleport, transitionKind, basementsInstalled)) { return CreatureTransitionPolicy.IsEnabled(PlayerPreferences.Allies.Teleport, transitionKind, basementsInstalled); } return false; } public bool ShouldScanAlliesFor(TransportTransitionKind transitionKind, bool basementsInstalled) { if (ServerPolicy.Allies.Enabled && PlayerPreferences.Allies.Enabled && PlayerPreferences.Allies.Mode != AllySelectionMode.None) { return IsTransitionEnabled(transitionKind, basementsInstalled); } return false; } public bool ShouldScanEnemiesFor(TransportTransitionKind transitionKind, bool basementsInstalled) { if (ServerPolicy.Enemies.Enabled && (transitionKind != TransportTransitionKind.Portal || ShouldTransportEnemiesThroughPortals)) { return IsEnemyTransitionEnabled(transitionKind, basementsInstalled); } return false; } public bool ShouldCountEnemiesForPortal(bool requirePortalTransition, bool basementsInstalled) { if (ServerPolicy.Enemies.Enabled) { if (requirePortalTransition) { return IsEnemyTransitionEnabled(TransportTransitionKind.Portal, basementsInstalled); } return true; } return false; } public bool ShouldCapturePortalTransport(bool basementsInstalled) { if (!ShouldScanAlliesFor(TransportTransitionKind.Portal, basementsInstalled)) { return ShouldScanEnemiesFor(TransportTransitionKind.Portal, basementsInstalled); } return true; } public bool ShouldEvaluatePortalWork(bool basementsInstalled) { if (!ShouldCapturePortalTransport(basementsInstalled)) { return ShouldCountEnemiesForPortalBlock; } return true; } public float GetAllySearchRadius() { return Math.Min(ServerPolicy.Allies.MaximumSearchRadius, PlayerPreferences.Allies.SearchRadius); } public float GetAllySearchHeightRadius() { return Math.Min(ServerPolicy.Allies.MaximumSearchHeightRadius, PlayerPreferences.Allies.SearchHeightRadius); } public bool IsEnemyAllowed(string prefabName) { return IsEnemyAllowed(prefabName, null); } public bool IsEnemyAllowed(string prefabName, PortalContext? portalContext) { if (!ServerPolicy.Enemies.Enabled || string.IsNullOrWhiteSpace(prefabName) || !MatchesAllowedList(ServerPolicy.Enemies.Allow, prefabName) || MatchesAny(ServerPolicy.Enemies.Deny, prefabName)) { return false; } PortalRuleConfig portalRuleConfig = ((portalContext == null) ? null : PortalRuleResolver.Select(ServerPolicy.Enemies.Teleport.Portals, portalContext)); if (portalRuleConfig?.Allow == null || MatchesAllowedList(portalRuleConfig.Allow, prefabName)) { if (portalRuleConfig != null) { return !MatchesAny(portalRuleConfig.Deny, prefabName); } return true; } return false; } public bool IsEnemyTransitionEnabled(TransportTransitionKind transitionKind, bool basementsInstalled) { return CreatureTransitionPolicy.IsEnabled(ServerPolicy.Enemies.Teleport, transitionKind, basementsInstalled); } public bool ShouldBlockPortalForNearbyEnemies(int eligibleEnemyCount) { if (ServerPolicy.Enemies.Enabled && ServerPolicy.Enemies.Mode == EnemyPortalMode.VikingsDontRun) { return eligibleEnemyCount > 0; } return false; } public float GetEnemySearchRadius() { return ServerPolicy.Enemies.MaximumSearchRadius; } public float GetEnemySearchHeightRadius() { return ServerPolicy.Enemies.MaximumSearchHeightRadius; } private static bool MatchesAllowedList(IEnumerable<string> patterns, string prefabName) { return patterns.Any((string pattern) => CreatureMatcher.IsMatch(pattern, prefabName)); } private static bool MatchesAny(IEnumerable<string> patterns, string prefabName) { return patterns.Any((string pattern) => CreatureMatcher.IsMatch(pattern, prefabName)); } } internal readonly struct CreaturePreviewCounts : IEquatable<CreaturePreviewCounts> { public int AllyCount { get; } public int EnemyCount { get; } public CreaturePreviewCounts(int allyCount, int enemyCount) { AllyCount = allyCount; EnemyCount = enemyCount; } public bool Equals(CreaturePreviewCounts other) { if (AllyCount == other.AllyCount) { return EnemyCount == other.EnemyCount; } return false; } public override bool Equals(object? obj) { if (obj is CreaturePreviewCounts other) { return Equals(other); } return false; } public override int GetHashCode() { return (AllyCount * 397) ^ EnemyCount; } } internal readonly struct CreaturePreviewCacheKey : IEquatable<CreaturePreviewCacheKey> { private static class NullIdentity { public static readonly object Value = new object(); } private readonly object? _player; private readonly object? _session; private readonly object? _portal; private readonly object _policy; private readonly string _prefabName; private readonly string _tag; private readonly bool _includeAllies; private readonly AllyTransportMessageMode _allyDisplayMode; private readonly AllySelectionMode _allyMode; private readonly bool _includeEnemies; private readonly AllyTransportMessageMode _enemyDisplayMode; private readonly EnemyPortalMode _enemyMode; public CreaturePreviewCacheKey(object? player, object? session, object? portal, object policy, string prefabName, string tag, bool includeAllies, AllyTransportMessageMode allyDisplayMode, AllySelectionMode allyMode, bool includeEnemies, AllyTransportMessageMode enemyDisplayMode, EnemyPortalMode enemyMode) { _player = player; _session = session; _portal = portal; _policy = policy ?? throw new ArgumentNullException("policy"); _prefabName = prefabName ?? string.Empty; _tag = tag ?? string.Empty; _includeAllies = includeAllies; _allyDisplayMode = allyDisplayMode; _allyMode = allyMode; _includeEnemies = includeEnemies; _enemyDisplayMode = enemyDisplayMode; _enemyMode = enemyMode; } public bool IsForPortal(object? portal) { return _portal == portal; } public bool Equals(CreaturePreviewCacheKey other) { if (_player == other._player && _session == other._session && _portal == other._portal && _policy == other._policy && string.Equals(_prefabName, other._prefabName, StringComparison.Ordinal) && string.Equals(_tag, other._tag, StringComparison.Ordinal) && _includeAllies == other._includeAllies && _allyDisplayMode == other._allyDisplayMode && _allyMode == other._allyMode && _includeEnemies == other._includeEnemies && _enemyDisplayMode == other._enemyDisplayMode) { return _enemyMode == other._enemyMode; } return false; } public override bool Equals(object? obj) { if (obj is CreaturePreviewCacheKey other) { return Equals(other); } return false; } public override int GetHashCode() { return AddHash(AddHash(AddHash(AddHash(AddHash(AddHash(AddHash(AddHash(AddHash(AddHash(AddHash(RuntimeHelpers.GetHashCode(_player ?? NullIdentity.Value), RuntimeHelpers.GetHashCode(_session ?? NullIdentity.Value)), RuntimeHelpers.GetHashCode(_portal ?? NullIdentity.Value)), RuntimeHelpers.GetHashCode(_policy)), StringComparer.Ordinal.GetHashCode(_prefabName)), StringComparer.Ordinal.GetHashCode(_tag)), _includeAllies ? 1 : 0), (int)_allyDisplayMode), (int)_allyMode), _includeEnemies ? 1 : 0), (int)_enemyDisplayMode), (int)_enemyMode); } private static int AddHash(int hash, int value) { return (hash * 397) ^ value; } } internal static class CreaturePreviewCountCalculator { public static CreaturePreviewCounts Count<T>(IReadOnlyList<T> candidates, bool includeAllies, bool includeEnemies, Func<T, bool> isEligibleAlly, Func<T, bool> isEligibleEnemy) { int num = 0; int num2 = 0; for (int i = 0; i < candidates.Count; i++) { T arg = candidates[i]; if (includeAllies && isEligibleAlly(arg)) { num++; } if (includeEnemies && isEligibleEnemy(arg)) { num2++; } } return new CreaturePreviewCounts(num, num2); } } internal static class CreatureTeleportDiagnostics { public static string FormatCapture(TransportTransitionKind transitionKind, string player, string source, string requestedDestination, int allies, int enemies) { return $"TeleportCaptured: transition={transitionKind} player={player} source={source} requestedDestination={requestedDestination} allies={allies} enemies={enemies}"; } public static string FormatMovement(bool isEnemy, TransportTransitionKind transitionKind, string prefabName, string customName, string zdoId, string source, string destination, string method) { return string.Format("{0}: transition={1} prefab={2} name={3} zdo={4} source={5} destination={6} method={7}", isEnemy ? "EnemyMoved" : "AllyMoved", transitionKind, prefabName, customName, zdoId, source, destination, method); } public static string FormatCompletion(TransportTransitionKind transitionKind, string player, string source, string destination, int movedAllies, int movedEnemies, int skippedAllies, int skippedEnemies) { return $"TeleportCompleted: transition={transitionKind} player={player} source={source} destination={destination} movedAllies={movedAllies} movedEnemies={movedEnemies} skippedAllies={skippedAllies} skippedEnemies={skippedEnemies}"; } public static string FormatSkip(bool isEnemy, TransportTransitionKind transitionKind, string prefabName, string customName, string zdoId, string source, string reason) { return string.Format("{0}: transition={1} prefab={2} name={3} zdo={4} source={5} reason={6}", isEnemy ? "EnemySkipped" : "AllySkipped", transitionKind, prefabName, customName, zdoId, source, reason); } public static string FormatFiltered(TransportTransitionKind transitionKind, string player, int eligibleAllies, int eligibleEnemies, int outOfRangeAllies, int outOfRangeEnemies, int transitionDisabledAllies, int transitionDisabledEnemies, int policyDeniedAllies, int policyDeniedEnemies) { return $"CreatureFilterSummary: transition={transitionKind} player={player} eligibleAllies={eligibleAllies} eligibleEnemies={eligibleEnemies} outOfRangeAllies={outOfRangeAllies} outOfRangeEnemies={outOfRangeEnemies} transitionDisabledAllies={transitionDisabledAllies} transitionDisabledEnemies={transitionDisabledEnemies} policyDeniedAllies={policyDeniedAllies} policyDeniedEnemies={policyDeniedEnemies}"; } public static string FormatPosition(float x, float y, float z) { return string.Format(CultureInfo.InvariantCulture, "({0:0.0}, {1:0.0}, {2:0.0})", x, y, z); } } public enum TransportTransitionKind { Portal, DungeonEntry, DungeonExit, Basement } internal static class CreatureTransitionPolicy { public static bool IsEnabled(EnemyTeleportConfig config, TransportTransitionKind kind, bool basementsInstalled) { switch (kind) { default: return config.Dungeons.Enabled; case TransportTransitionKind.Basement: if (basementsInstalled) { return config.Basements.Enabled; } return false; case TransportTransitionKind.Portal: return config.Portals.Enabled; } } public static bool IsEnabled(AllyTeleportConfig config, TransportTransitionKind kind, bool basementsInstalled) { switch (kind) { default: return config.Dungeons.Enabled; case TransportTransitionKind.Basement: if (basementsInstalled) { return config.Basements.Enabled; } return false; case TransportTransitionKind.Portal: return config.Portals.Enabled; } } public static bool IsEnabled(PlayerAllyTeleportConfig config, TransportTransitionKind kind, bool basementsInstalled) { switch (kind) { default: return config.Dungeons.Enabled; case TransportTransitionKind.Basement: if (basementsInstalled) { return config.Basements.Enabled; } return false; case TransportTransitionKind.Portal: return config.Portals.Enabled; } } } internal sealed class CreatureTransportMetrics { public int CharactersScanned { get; private set; } public int CandidatesEvaluated { get; private set; } public int ClearanceChecks { get; private set; } public void RecordCharacterScanned() { CharactersScanned++; } public void RecordCandidateEvaluated() { CandidatesEvaluated++; } public void RecordClearanceCheck() { ClearanceChecks++; } } internal static class DirectTeleportDecision { public static bool ShouldShowVikingsDontRunAlert(bool isLocalPlayer, bool pluginEnabled, bool isAlive, bool messageHudAvailable, bool localizationAvailable, int eligibleEnemyCount, int frame, int lastAlertFrame) { if (isLocalPlayer && pluginEnabled && isAlive && messageHudAvailable && localizationAvailable && eligibleEnemyCount > 0) { return frame != lastAlertFrame; } return false; } public static bool ShouldBlockPortal(bool isLocalPlayer, bool isInterior, bool isBasement, bool hasPortalContext, bool pluginEnabled, bool enemiesEnabled, EnemyPortalMode mode, int eligibleEnemyCount) { if (isLocalPlayer && !isInterior && !isBasement && hasPortalContext && pluginEnabled && enemiesEnabled && mode == EnemyPortalMode.VikingsDontRun) { return eligibleEnemyCount > 0; } return false; } public static bool ShouldCapture(bool teleportSucceeded, bool isLocalPlayer, bool isInterior, bool isBasement, bool nativePortalTransition, bool hasPortalContext, bool transitionEnabled) { return teleportSucceeded && isLocalPlayer && !isInterior && !isBasement && !nativePortalTransition && hasPortalContext && transitionEnabled; } } internal static class PlayerDescriptorAvailability { internal static void Apply(ModuleConfigurationView view, bool moduleEnabled, CreatureConfig server) { if (!moduleEnabled) { Disable(view, (ConfigurationDescriptor _) => true, "The creature module is off. Saved preferences are retained."); return; } if (!server.Allies.Enabled) { Disable(view, (ConfigurationDescriptor row) => Under(row, "Allies") || row.StableId.StartsWith("cfg:Allies Display/", StringComparison.Ordinal) || row.StableId == "cfg:Controls/Previous Allies Mode Hotkey" || row.StableId == "cfg:Controls/Next Allies Mode Hotkey", "Ally transport is off. Saved preferences are retained."); } else { if (!server.Allies.Teleport.Portals.Enabled) { Disable(view, (ConfigurationDescriptor row) => Under(row, "Allies.Teleport.Portals"), "Portal ally transport is off. Saved preferences are retained."); } if (!server.Allies.Teleport.Dungeons.Enabled) { Disable(view, (ConfigurationDescriptor row) => Under(row, "Allies.Teleport.Dungeons"), "Dungeon ally transport is off. Saved preferences are retained."); } if (!server.Allies.Teleport.Basements.Enabled) { Disable(view, (ConfigurationDescriptor row) => Under(row, "Allies.Teleport.Basements"), "Basement ally transport is off. Saved preferences are retained."); } } if (!server.Enemies.Enabled) { Disable(view, (ConfigurationDescriptor row) => row.StableId.StartsWith("cfg:Enemies Display/", StringComparison.Ordinal), "Enemy transport is off. Saved preferences are retained."); } } private static bool Under(ConfigurationDescriptor row, string path) { if (!row.StableId.Equals(path, StringComparison.Ordinal)) { return row.StableId.StartsWith(path + ".", StringComparison.Ordinal); } return true; } private static void Disable(ModuleConfigurationView view, Func<ConfigurationDescriptor, bool> matches, string reason) { foreach (ConfigurationDescriptor descriptor in view.Descriptors) { if (matches(descriptor)) { descriptor.Enabled = false; descriptor.DisabledReason = "Disabled by server: " + reason; } } } } internal static class PlayerYamlModeWriter { private static readonly Regex ModeLine = new Regex("^([ \\t]*)Mode\\s*:.*$", RegexOptions.Multiline | RegexOptions.Compiled); public static bool TryReplaceModeLine(string yaml, string modeName, out string updated) { updated = yaml; if (string.IsNullOrEmpty(yaml) || string.IsNullOrWhiteSpace(modeName)) { return false; } Match match = ModeLine.Match(yaml); if (!match.Success) { return false; } updated = yaml.Substring(0, match.Index) + match.Groups[1].Value + "Mode: " + modeName + yaml.Substring(match.Index + match.Length); return true; } } internal static class AllyModeSwitchDisplay { private const float FadeInSeconds = 0.2f; private const float HoldSeconds = 1.2f; private const float FadeOutSeconds = 0.4f; private const float SwipeDistancePixels = 36f; private const float CursorOffsetPixels = 208f; private const string LabelTranslationKey = "$tc_ally_mode_label"; private static readonly Color ModeColor = new Color(1f, 0.851f, 0.302f); private static GameObject? _root; private static CanvasGroup? _canvasGroup; private static TMP_Text? _labelText; private static TMP_Text? _modeText; private static RectTransform? _canvasRect; private static Vector2 _basePosition; private static float _shownAt; private static int _direction = 1; private static bool _reportedShowError; public static void Show(AllySelectionMode mode, int direction) { try { ShowSafely(mode, direction); } catch (Exception ex) { Hide(); if (!_reportedShowError) { _reportedShowError = true; TeleportCreaturesPlugin.Log.LogWarning((object)("Could not show the allies mode readout: " + ex.Message)); } } } public static void Tick() { //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00d0: Unknown result type (might be due to invalid IL or missing referen