using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.Specialized; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Net; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Bifrost.ConstEnum; using Bifrost.Cooked; using Cysharp.Threading.Tasks; using DLAgent; using DunGen; using FishNet.Object; using FishNet.Object.Synchronizing; using FishySteamworks.Server; using HarmonyLib; using MelonLoader; using MelonLoader.InternalUtils; using MelonLoader.Logging; using MelonLoader.Pastel; using MelonLoader.Preferences; using MelonLoader.Utils; using Microsoft.CodeAnalysis; using MimesisPlayerEnhancement; using MimesisPlayerEnhancement.Config; using MimesisPlayerEnhancement.Config.HostConfigSync; using MimesisPlayerEnhancement.Config.Models; using MimesisPlayerEnhancement.Config.QuickSettings; using MimesisPlayerEnhancement.Features.DungeonRandomizer; using MimesisPlayerEnhancement.Features.DungeonRandomizer.Patches; using MimesisPlayerEnhancement.Features.DungeonTime; using MimesisPlayerEnhancement.Features.Economy; using MimesisPlayerEnhancement.Features.ExtendedSaveSlots; using MimesisPlayerEnhancement.Features.JoinAnytime; using MimesisPlayerEnhancement.Features.LootMultiplicator; using MimesisPlayerEnhancement.Features.LootMultiplicator.Patches; using MimesisPlayerEnhancement.Features.MimicTuning; using MimesisPlayerEnhancement.Features.MimicTuning.MimicEmoteProps; using MimesisPlayerEnhancement.Features.MimicTuning.MimicHorn; using MimesisPlayerEnhancement.Features.MimicTuning.MimicInventoryCopy; using MimesisPlayerEnhancement.Features.MimicTuning.MimicPossession; using MimesisPlayerEnhancement.Features.MimicTuning.MimicSocial; using MimesisPlayerEnhancement.Features.MimicTuning.MimicTrust; using MimesisPlayerEnhancement.Features.MimicTuning.MimicVoiceTuning; using MimesisPlayerEnhancement.Features.MimicTuning.Patches; using MimesisPlayerEnhancement.Features.ModVersionDisplay; using MimesisPlayerEnhancement.Features.MorePlayers; using MimesisPlayerEnhancement.Features.MoreVoices; using MimesisPlayerEnhancement.Features.Persistence; using MimesisPlayerEnhancement.Features.Persistence.Patches; using MimesisPlayerEnhancement.Features.PlayerAnnouncements; using MimesisPlayerEnhancement.Features.PlayerTuning; using MimesisPlayerEnhancement.Features.Privacy; using MimesisPlayerEnhancement.Features.SavegamePreparation; using MimesisPlayerEnhancement.Features.SpawnScaling; using MimesisPlayerEnhancement.Features.Statistics; using MimesisPlayerEnhancement.Features.Statistics.Models; using MimesisPlayerEnhancement.Features.UserInterface; using MimesisPlayerEnhancement.Features.UserInterface.CustomLoadingScreen; using MimesisPlayerEnhancement.Features.UserInterface.DiscoBallSound; using MimesisPlayerEnhancement.Features.UserInterface.FpsUi; using MimesisPlayerEnhancement.Features.UserInterface.InGameMenuPlayerList; using MimesisPlayerEnhancement.Features.UserInterface.InventoryNumberKeys; using MimesisPlayerEnhancement.Features.UserInterface.InventorySlotOptimization; using MimesisPlayerEnhancement.Features.UserInterface.LoadingWaitMovementLock; using MimesisPlayerEnhancement.Features.UserInterface.LoadingWaitPlayerList; using MimesisPlayerEnhancement.Features.UserInterface.RoundStartSound; using MimesisPlayerEnhancement.Features.UserInterface.SpectatorPlayerList; using MimesisPlayerEnhancement.Features.UserInterface.SpectatorVoiceBalance; using MimesisPlayerEnhancement.Features.UserInterface.SurvivalResultPlayerList; using MimesisPlayerEnhancement.Features.UserInterface.VoiceNoiseGate; using MimesisPlayerEnhancement.Features.UserInterface.WorldOverlays; using MimesisPlayerEnhancement.Features.Weather; using MimesisPlayerEnhancement.Features.WebDashboard; using MimesisPlayerEnhancement.Features.WebDashboard.Debug; using MimesisPlayerEnhancement.Features.WebDashboard.Models; using MimesisPlayerEnhancement.Ui; using MimesisPlayerEnhancement.Ui.MenuMirror; using MimesisPlayerEnhancement.Util; using MimesisPlayerEnhancement.Util.Patches; using MimesisPlayerEnhancement.Util.Players; using Mimic; using Mimic.Actors; using Mimic.Animation; using Mimic.Audio; using Mimic.InputSystem; using Mimic.Voice; using Mimic.Voice.SpeechSystem; using ModUtility; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; using ReluProtocol; using ReluProtocol.C2S; using ReluProtocol.Enum; using ReluReplay; using ReluReplay.Data; using ReluReplay.Recorder; using ReluReplay.Serializer; using ReluReplay.Shared; using ReluServerBase.Threading; using Steamworks; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Events; using UnityEngine.Networking; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: MelonInfo(typeof(Mod), "MimesisPlayerEnhancement", "26.8.2", "kalle", null)] [assembly: MelonGame("ReLUGames", "MIMESIS")] [assembly: HarmonyDontPatchAll] [assembly: InternalsVisibleTo("MimesisPlayerEnhancement.Tests")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyVersion("0.0.0.0")] [module: RefSafetyRules(11)] [CompilerGenerated] internal sealed class <>z__ReadOnlyArray : IEnumerable, ICollection, IList, IEnumerable, IReadOnlyCollection, IReadOnlyList, ICollection, IList { int ICollection.Count => _items.Length; bool ICollection.IsSynchronized => false; object ICollection.SyncRoot => this; object IList.this[int index] { get { return _items[index]; } set { throw new NotSupportedException(); } } bool IList.IsFixedSize => true; bool IList.IsReadOnly => true; int IReadOnlyCollection.Count => _items.Length; T IReadOnlyList.this[int index] => _items[index]; int ICollection.Count => _items.Length; bool ICollection.IsReadOnly => true; T IList.this[int index] { get { return _items[index]; } set { throw new NotSupportedException(); } } public <>z__ReadOnlyArray(T[] items) { _items = items; } IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_items).GetEnumerator(); } void ICollection.CopyTo(Array array, int index) { ((ICollection)_items).CopyTo(array, index); } int IList.Add(object value) { throw new NotSupportedException(); } void IList.Clear() { throw new NotSupportedException(); } bool IList.Contains(object value) { return ((IList)_items).Contains(value); } int IList.IndexOf(object value) { return ((IList)_items).IndexOf(value); } void IList.Insert(int index, object value) { throw new NotSupportedException(); } void IList.Remove(object value) { throw new NotSupportedException(); } void IList.RemoveAt(int index) { throw new NotSupportedException(); } IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_items).GetEnumerator(); } void ICollection.Add(T item) { throw new NotSupportedException(); } void ICollection.Clear() { throw new NotSupportedException(); } bool ICollection.Contains(T item) { return ((ICollection)_items).Contains(item); } void ICollection.CopyTo(T[] array, int arrayIndex) { ((ICollection)_items).CopyTo(array, arrayIndex); } bool ICollection.Remove(T item) { throw new NotSupportedException(); } int IList.IndexOf(T item) { return ((IList)_items).IndexOf(item); } void IList.Insert(int index, T item) { throw new NotSupportedException(); } void IList.RemoveAt(int index) { throw new NotSupportedException(); } } [CompilerGenerated] internal sealed class <>z__ReadOnlySingleElementList : IEnumerable, ICollection, IList, IEnumerable, IReadOnlyCollection, IReadOnlyList, ICollection, IList { private sealed class Enumerator : IDisposable, IEnumerator, IEnumerator { object IEnumerator.Current => _item; T IEnumerator.Current => _item; public Enumerator(T item) { _item = item; } bool IEnumerator.MoveNext() { if (!_moveNextCalled) { return _moveNextCalled = true; } return false; } void IEnumerator.Reset() { _moveNextCalled = false; } void IDisposable.Dispose() { } } int ICollection.Count => 1; bool ICollection.IsSynchronized => false; object ICollection.SyncRoot => this; object IList.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } set { throw new NotSupportedException(); } } bool IList.IsFixedSize => true; bool IList.IsReadOnly => true; int IReadOnlyCollection.Count => 1; T IReadOnlyList.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } } int ICollection.Count => 1; bool ICollection.IsReadOnly => true; T IList.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } set { throw new NotSupportedException(); } } public <>z__ReadOnlySingleElementList(T item) { _item = item; } IEnumerator IEnumerable.GetEnumerator() { return new Enumerator(_item); } void ICollection.CopyTo(Array array, int index) { array.SetValue(_item, index); } int IList.Add(object value) { throw new NotSupportedException(); } void IList.Clear() { throw new NotSupportedException(); } bool IList.Contains(object value) { return EqualityComparer.Default.Equals(_item, (T)value); } int IList.IndexOf(object value) { if (!EqualityComparer.Default.Equals(_item, (T)value)) { return -1; } return 0; } void IList.Insert(int index, object value) { throw new NotSupportedException(); } void IList.Remove(object value) { throw new NotSupportedException(); } void IList.RemoveAt(int index) { throw new NotSupportedException(); } IEnumerator IEnumerable.GetEnumerator() { return new Enumerator(_item); } void ICollection.Add(T item) { throw new NotSupportedException(); } void ICollection.Clear() { throw new NotSupportedException(); } bool ICollection.Contains(T item) { return EqualityComparer.Default.Equals(_item, item); } void ICollection.CopyTo(T[] array, int arrayIndex) { array[arrayIndex] = _item; } bool ICollection.Remove(T item) { throw new NotSupportedException(); } int IList.IndexOf(T item) { if (!EqualityComparer.Default.Equals(_item, item)) { return -1; } return 0; } void IList.Insert(int index, T item) { throw new NotSupportedException(); } void IList.RemoveAt(int index) { throw new NotSupportedException(); } } 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; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace MimesisPlayerEnhancement { internal static class GlobalConfigStore { private const string Feature = "GlobalConfig"; private static SparseTomlConfig.Document? _pendingDoc; private static bool _pendingDocLoaded; private static bool _dirty; internal static bool IsDirty => _dirty; internal static bool HasPendingDocument { get { if (_pendingDocLoaded) { return _pendingDoc != null; } return false; } } internal static SparseTomlConfig.Document Load() { if (string.IsNullOrEmpty(ModConfig.FilePath)) { return new SparseTomlConfig.Document(); } return SparseTomlConfig.Load(AtomicFileIO.ReadText(ModConfig.FilePath, "GlobalConfig")); } internal static bool TryWriteValue(string sectionId, string key, string normalized, out string? error, bool waitForCompletion = false) { error = null; if (!ModConfig.IsInitialized) { error = "Configuration is not initialized."; return false; } if (string.IsNullOrEmpty(ModConfig.FilePath)) { error = "Global config path unavailable."; return false; } if (!ModConfigRegistry.TryGetEntry(sectionId, key, out MelonPreferences_Entry entry) || entry == null) { error = "Unknown setting."; return false; } string rawB = ModConfigRegistry.FormatEntryDefaultValue(entry); EnsurePendingDoc(); SparseTomlConfig.Document pendingDoc = _pendingDoc; if (ModConfigRegistry.RawValuesEqual(sectionId, key, normalized, rawB)) { RemoveKey(pendingDoc, sectionId, key); } else { EnsureSection(pendingDoc, sectionId); pendingDoc.Sections[sectionId][key] = normalized; } _dirty = true; return true; } internal static void RebuildPendingFromMemory() { if (!ModConfig.IsInitialized || SaveSlotConfigStore.IsApplyingOverrides || HostConfigMirror.BlocksGlobalConfigPersistence || _dirty) { return; } SparseTomlConfig.Document document = new SparseTomlConfig.Document(); foreach (string item in ModConfigRegistry.GetSectionOrder()) { foreach (string item2 in ModConfigRegistry.GetEntryOrder(item)) { if (ModConfigRegistry.TryGetEntry(item, item2, out MelonPreferences_Entry entry) && entry != null) { string text = ModConfigRegistry.FormatEntryValue(entry); string rawB = ModConfigRegistry.FormatEntryDefaultValue(entry); if (!ModConfigRegistry.RawValuesEqual(item, item2, text, rawB)) { EnsureSection(document, item); document.Sections[item][item2] = text; } } } } _pendingDoc = document; _pendingDocLoaded = true; _dirty = true; } internal static void FlushIfDirty() { if (_dirty) { ModLog.Debug("GlobalConfig", "Flushing pending global config before reload."); FlushToDisk(); } } internal static void RestorePendingToDisk() { if (HasPendingDocument) { ModLog.Debug("GlobalConfig", "Restoring pending global config after MelonLoader save."); FlushToDisk(waitForCompletion: true, force: true); } } internal static void FlushToDisk(bool waitForCompletion = true, bool force = false) { if (!ModConfig.IsInitialized || string.IsNullOrEmpty(ModConfig.FilePath) || (!force && !_dirty)) { return; } if (!_pendingDocLoaded || _pendingDoc == null) { if (!force) { _dirty = false; } return; } try { string directoryName = Path.GetDirectoryName(ModConfig.FilePath); if (!string.IsNullOrEmpty(directoryName)) { Directory.CreateDirectory(directoryName); } if (SparseTomlConfig.IsEmpty(_pendingDoc) && File.Exists(ModConfig.FilePath)) { AtomicFileIO.Delete(ModConfig.FilePath, "GlobalConfig"); } else if (!SparseTomlConfig.IsEmpty(_pendingDoc)) { AtomicFileIO.WriteText(ModConfig.FilePath, SparseTomlConfig.Serialize(_pendingDoc), "GlobalConfig"); } _dirty = false; } catch (Exception ex) { ModLog.Error("GlobalConfig", "FlushToDisk: " + ex.Message); } } private static void EnsurePendingDoc() { if (!_pendingDocLoaded) { _pendingDoc = Load(); _pendingDocLoaded = true; } } private static void EnsureSection(SparseTomlConfig.Document doc, string sectionId) { if (!doc.Sections.ContainsKey(sectionId)) { doc.SectionOrder.Add(sectionId); doc.Sections[sectionId] = new Dictionary(StringComparer.OrdinalIgnoreCase); } } private static void RemoveKey(SparseTomlConfig.Document doc, string sectionId, string key) { if (doc.Sections.TryGetValue(sectionId, out Dictionary value)) { value.Remove(key); if (value.Count == 0) { doc.Sections.Remove(sectionId); doc.SectionOrder.Remove(sectionId); } } } } public static class ModConfig { private const string MainCategoryId = "MimesisPlayerEnhancement"; private static readonly List> FloatEntries = new List>(); public static int Version => ModConfigRegistry.Version; public static bool IsInitialized { get; private set; } public static string FilePath { get; private set; } = ""; public static MelonPreferences_Category MainCategory { get; private set; } = null; public static MelonPreferences_Entry EnableMorePlayers { get; internal set; } = null; public static MelonPreferences_Entry MaxPlayers { get; internal set; } = null; public static MelonPreferences_Entry EnableScalingRoundGoals { get; internal set; } = null; public static MelonPreferences_Entry OverrideMaxStageCount { get; internal set; } = null; public static MelonPreferences_Entry RoundGoalBasePerZone { get; internal set; } = null; public static MelonPreferences_Entry RoundGoalMoneyMultiplier { get; internal set; } = null; public static MelonPreferences_Entry RoundGoalRandomSpreadPercent { get; internal set; } = null; public static MelonPreferences_Entry RoundGoalCurveExponent { get; internal set; } = null; public static MelonPreferences_Entry EnableMoreVoices { get; internal set; } = null; public static MelonPreferences_Entry UnifyIndoorOutdoorVoices { get; internal set; } = null; public static MelonPreferences_Entry MaxIndoorVoiceEvents { get; internal set; } = null; public static MelonPreferences_Entry MaxDeathMatchVoiceEvents { get; internal set; } = null; public static MelonPreferences_Entry MaxOutdoorVoiceEvents { get; internal set; } = null; public static MelonPreferences_Entry RecordVoiceInMaintenance { get; internal set; } = null; public static MelonPreferences_Entry RecordVoiceInTram { get; internal set; } = null; public static MelonPreferences_Entry RecordVoiceDuringMimicPossession { get; internal set; } = null; public static MelonPreferences_Entry EnableVoicePerformanceCache { get; internal set; } = null; public static MelonPreferences_Entry VoiceClipCacheMaxEntries { get; internal set; } = null; public static MelonPreferences_Entry EnablePersistence { get; internal set; } = null; public static MelonPreferences_Entry EnableStatistics { get; internal set; } = null; public static MelonPreferences_Entry SessionReconnectGraceMinutes { get; internal set; } = null; public static MelonPreferences_Entry ShowStatisticsToasts { get; internal set; } = null; public static MelonPreferences_Entry ShowPlayerAnnouncements { get; internal set; } = null; public static MelonPreferences_Entry EnableJoinAnytime { get; internal set; } = null; public static MelonPreferences_Entry JoinConnectionGraceSeconds { get; internal set; } = null; public static MelonPreferences_Entry EnableSpawnScaling { get; internal set; } = null; public static MelonPreferences_Entry SpawnScalingBaselinePlayerCount { get; internal set; } = null; public static MelonPreferences_Entry MimicSpawnMultiplier { get; internal set; } = null; public static MelonPreferences_Entry MimicSpawnPerPlayerMultiplier { get; internal set; } = null; public static MelonPreferences_Entry BossSpawnMultiplier { get; internal set; } = null; public static MelonPreferences_Entry BossSpawnPerPlayerMultiplier { get; internal set; } = null; public static MelonPreferences_Entry GruntSpawnMultiplier { get; internal set; } = null; public static MelonPreferences_Entry GruntSpawnPerPlayerMultiplier { get; internal set; } = null; public static MelonPreferences_Entry SpecialSpawnMultiplier { get; internal set; } = null; public static MelonPreferences_Entry SpecialSpawnPerPlayerMultiplier { get; internal set; } = null; public static MelonPreferences_Entry TrapSpawnMultiplier { get; internal set; } = null; public static MelonPreferences_Entry TrapSpawnPerPlayerMultiplier { get; internal set; } = null; public static MelonPreferences_Entry TrapRespawnMode { get; internal set; } = null; public static MelonPreferences_Entry TrapRespawnDelaySeconds { get; internal set; } = null; public static MelonPreferences_Entry TrapRespawnDelayMinSeconds { get; internal set; } = null; public static MelonPreferences_Entry TrapRespawnDelayMaxSeconds { get; internal set; } = null; public static MelonPreferences_Entry TrapRespawnMinPlayerDistanceMeters { get; internal set; } = null; public static MelonPreferences_Entry MimicWaveMode { get; internal set; } = null; public static MelonPreferences_Entry MimicWaveInitialDelaySeconds { get; internal set; } = null; public static MelonPreferences_Entry MimicWaveInitialDelayMinSeconds { get; internal set; } = null; public static MelonPreferences_Entry MimicWaveInitialDelayMaxSeconds { get; internal set; } = null; public static MelonPreferences_Entry MimicWaveIntervalSeconds { get; internal set; } = null; public static MelonPreferences_Entry MimicWaveIntervalMinSeconds { get; internal set; } = null; public static MelonPreferences_Entry MimicWaveIntervalMaxSeconds { get; internal set; } = null; public static MelonPreferences_Entry GruntWaveMode { get; internal set; } = null; public static MelonPreferences_Entry GruntWaveInitialDelaySeconds { get; internal set; } = null; public static MelonPreferences_Entry GruntWaveInitialDelayMinSeconds { get; internal set; } = null; public static MelonPreferences_Entry GruntWaveInitialDelayMaxSeconds { get; internal set; } = null; public static MelonPreferences_Entry GruntWaveIntervalSeconds { get; internal set; } = null; public static MelonPreferences_Entry GruntWaveIntervalMinSeconds { get; internal set; } = null; public static MelonPreferences_Entry GruntWaveIntervalMaxSeconds { get; internal set; } = null; public static MelonPreferences_Entry BonusEncounterDelayMinSeconds { get; internal set; } = null; public static MelonPreferences_Entry BonusEncounterDelayMaxSeconds { get; internal set; } = null; public static MelonPreferences_Entry BonusEncounterMinPlayerDistanceMeters { get; internal set; } = null; public static MelonPreferences_Entry OtherSpawnMultiplier { get; internal set; } = null; public static MelonPreferences_Entry OtherSpawnPerPlayerMultiplier { get; internal set; } = null; public static MelonPreferences_Entry EnableLootMultiplicator { get; internal set; } = null; public static MelonPreferences_Entry LootMultiplicatorBaselinePlayerCount { get; internal set; } = null; public static MelonPreferences_Entry MapLootMultiplier { get; internal set; } = null; public static MelonPreferences_Entry MapLootPerPlayerMultiplier { get; internal set; } = null; public static MelonPreferences_Entry DropLootMultiplier { get; internal set; } = null; public static MelonPreferences_Entry DropLootPerPlayerMultiplier { get; internal set; } = null; public static MelonPreferences_Entry LootItemFilterMode { get; internal set; } = null; public static MelonPreferences_Entry LootAllowlist { get; internal set; } = null; public static MelonPreferences_Entry LootBlocklist { get; internal set; } = null; public static MelonPreferences_Entry AutoScaleMapLootBudgetForFilter { get; internal set; } = null; public static MelonPreferences_Entry ConvertFakeActorDyingDropChancePercent { get; internal set; } = null; public static MelonPreferences_Entry EnableEconomy { get; internal set; } = null; public static MelonPreferences_Entry EconomyBaselinePlayerCount { get; internal set; } = null; public static MelonPreferences_Entry StartupMoney { get; internal set; } = null; public static MelonPreferences_Entry StartingZone { get; internal set; } = null; public static MelonPreferences_Entry EnableUpgradeTramHorn { get; internal set; } = null; public static MelonPreferences_Entry EnableUpgradeScrapScanner { get; internal set; } = null; public static MelonPreferences_Entry EnableUpgradeTramBooster { get; internal set; } = null; public static MelonPreferences_Entry EnableUpgradeTramLight { get; internal set; } = null; public static MelonPreferences_Entry ScrapSellValueMultiplier { get; internal set; } = null; public static MelonPreferences_Entry ScrapSellValuePerPlayerMultiplier { get; internal set; } = null; public static MelonPreferences_Entry ShopBuyPriceMultiplier { get; internal set; } = null; public static MelonPreferences_Entry ShopBuyPricePerPlayerMultiplier { get; internal set; } = null; public static MelonPreferences_Entry ShopDiscountMinPercent { get; internal set; } = null; public static MelonPreferences_Entry ShopDiscountMaxPercent { get; internal set; } = null; public static MelonPreferences_Entry ShopDiscountChancePercent { get; internal set; } = null; public static MelonPreferences_Entry ReinforcePriceMultiplier { get; internal set; } = null; public static MelonPreferences_Entry ReinforcePricePerPlayerMultiplier { get; internal set; } = null; public static MelonPreferences_Entry RetainUnspentCurrencyBetweenCycles { get; internal set; } = null; public static MelonPreferences_Entry EnableDungeonTime { get; internal set; } = null; public static MelonPreferences_Entry DungeonTimeBaselinePlayerCount { get; internal set; } = null; public static MelonPreferences_Entry ExtraShiftSecondsPerPlayerAboveBaseline { get; internal set; } = null; public static MelonPreferences_Entry StartTimePreset { get; internal set; } = null; public static MelonPreferences_Entry TimeMultiplier { get; internal set; } = null; public static MelonPreferences_Entry EnableRealtimeTramClock { get; internal set; } = null; public static MelonPreferences_Entry EnableMimicPossessionTuning { get; internal set; } = null; public static MelonPreferences_Entry RandomizeMimicPossessionDuration { get; internal set; } = null; public static MelonPreferences_Entry MimicPossessionMinTimeSeconds { get; internal set; } = null; public static MelonPreferences_Entry MimicPossessionMaxTimeSeconds { get; internal set; } = null; public static MelonPreferences_Entry MimicPossessionCooltimeMultiplier { get; internal set; } = null; public static MelonPreferences_Entry EnableMimicTuning { get; internal set; } = null; public static MelonPreferences_Entry MimicVoiceTuningMode { get; internal set; } = null; public static MelonPreferences_Entry PeriodicVoiceIntervalMultiplier { get; internal set; } = null; public static MelonPreferences_Entry PlayerVoiceResponseChancePercent { get; internal set; } = null; public static MelonPreferences_Entry PlayerVoiceResponseCooldownSeconds { get; internal set; } = null; public static MelonPreferences_Entry PlayerVoiceResponseDelayMinSeconds { get; internal set; } = null; public static MelonPreferences_Entry PlayerVoiceResponseDelayMaxSeconds { get; internal set; } = null; public static MelonPreferences_Entry PlayerVoiceResponseMaxDistance { get; internal set; } = null; public static MelonPreferences_Entry MimicInventoryCopyMode { get; internal set; } = null; public static MelonPreferences_Entry MimicInventoryCopyPickRule { get; internal set; } = null; public static MelonPreferences_Entry ClipReuseCooldownSeconds { get; internal set; } = null; public static MelonPreferences_Entry DeathMatchClipReuseCooldownSeconds { get; internal set; } = null; public static MelonPreferences_Entry SpeakAudienceRangeMeters { get; internal set; } = null; public static MelonPreferences_Entry PostReplyIntervalMode { get; internal set; } = null; public static MelonPreferences_Entry PostReplyIntervalFixedSeconds { get; internal set; } = null; public static MelonPreferences_Entry PostReplyIntervalMinSeconds { get; internal set; } = null; public static MelonPreferences_Entry PostReplyIntervalMaxSeconds { get; internal set; } = null; public static MelonPreferences_Entry MinRequiredSpeechClips { get; internal set; } = null; public static MelonPreferences_Entry HearOwnVoiceFromMimic { get; internal set; } = null; public static MelonPreferences_Entry VoiceInitIntervalMode { get; internal set; } = null; public static MelonPreferences_Entry VoiceInitIntervalMin { get; internal set; } = null; public static MelonPreferences_Entry VoiceInitIntervalMax { get; internal set; } = null; public static MelonPreferences_Entry VoicePeriodicIntervalMode { get; internal set; } = null; public static MelonPreferences_Entry VoicePeriodicIntervalMin { get; internal set; } = null; public static MelonPreferences_Entry VoicePeriodicIntervalMax { get; internal set; } = null; public static MelonPreferences_Entry VoiceDeathMatchIntervalMode { get; internal set; } = null; public static MelonPreferences_Entry VoiceDeathMatchIntervalMin { get; internal set; } = null; public static MelonPreferences_Entry VoiceDeathMatchIntervalMax { get; internal set; } = null; public static MelonPreferences_Entry MimicTrustMode { get; internal set; } = null; public static MelonPreferences_Entry TrustOutdoorMultiplier { get; internal set; } = null; public static MelonPreferences_Entry TrustLookingDelta { get; internal set; } = null; public static MelonPreferences_Entry TrustNotLookingDelta { get; internal set; } = null; public static MelonPreferences_Entry TrustApproachDelta { get; internal set; } = null; public static MelonPreferences_Entry TrustMaintainDelta { get; internal set; } = null; public static MelonPreferences_Entry TrustWalkAwayDelta { get; internal set; } = null; public static MelonPreferences_Entry TrustSprintAwayDelta { get; internal set; } = null; public static MelonPreferences_Entry TrustHitDamageMultiplier { get; internal set; } = null; public static MelonPreferences_Entry TrustFriendlyThreshold { get; internal set; } = null; public static MelonPreferences_Entry TrustDistrustThreshold { get; internal set; } = null; public static MelonPreferences_Entry TrustScoreValueMode { get; internal set; } = null; public static MelonPreferences_Entry TrustInitialFixed { get; internal set; } = null; public static MelonPreferences_Entry TrustInitialRandomMin { get; internal set; } = null; public static MelonPreferences_Entry TrustInitialRandomMax { get; internal set; } = null; public static MelonPreferences_Entry TrustBehaviorFixed { get; internal set; } = null; public static MelonPreferences_Entry TrustBehaviorRandomMin { get; internal set; } = null; public static MelonPreferences_Entry TrustBehaviorRandomMax { get; internal set; } = null; public static MelonPreferences_Entry ChaseActivationDistanceMeters { get; internal set; } = null; public static MelonPreferences_Entry ChaseForceRunDistanceMeters { get; internal set; } = null; public static MelonPreferences_Entry MimicSocialMode { get; internal set; } = null; public static MelonPreferences_Entry MimicRunawayChance { get; internal set; } = null; public static MelonPreferences_Entry JumpCopyChancePercent { get; internal set; } = null; public static MelonPreferences_Entry SlotFollowChangeChancePercent { get; internal set; } = null; public static MelonPreferences_Entry PossessionRangeMeters { get; internal set; } = null; public static MelonPreferences_Entry PossessionBtGateMode { get; internal set; } = null; public static MelonPreferences_Entry MimicEmotePropsMode { get; internal set; } = null; public static MelonPreferences_Entry EmoteRespondChancePercent { get; internal set; } = null; public static MelonPreferences_Entry EmoteSuggestChancePercent { get; internal set; } = null; public static MelonPreferences_Entry ReactToSprinklerChancePercent { get; internal set; } = null; public static MelonPreferences_Entry UseTrapSwitchChancePercent { get; internal set; } = null; public static MelonPreferences_Entry UseChargerChancePercent { get; internal set; } = null; public static MelonPreferences_Entry UseTransmitterChancePercent { get; internal set; } = null; public static MelonPreferences_Entry UseShutterSwitchChancePercent { get; internal set; } = null; public static MelonPreferences_Entry HornImitationMode { get; internal set; } = null; public static MelonPreferences_Entry AllowHornImitation { get; internal set; } = null; public static MelonPreferences_Entry HornMaxRecordSeconds { get; internal set; } = null; public static MelonPreferences_Entry HornRecordingGapSeconds { get; internal set; } = null; public static MelonPreferences_Entry HornMaxStoredRecords { get; internal set; } = null; public static MelonPreferences_Entry EnablePlayerTuning { get; internal set; } = null; public static MelonPreferences_Entry MoveSpeedMultiplier { get; internal set; } = null; public static MelonPreferences_Entry NoClipSpeedMultiplier { get; internal set; } = null; public static MelonPreferences_Entry MaxStaminaMultiplier { get; internal set; } = null; public static MelonPreferences_Entry StaminaDrainMultiplier { get; internal set; } = null; public static MelonPreferences_Entry StaminaRegenMultiplier { get; internal set; } = null; public static MelonPreferences_Entry StaminaRegenDelayMultiplier { get; internal set; } = null; public static MelonPreferences_Entry MaxCarryWeightMultiplier { get; internal set; } = null; public static MelonPreferences_Entry DisablePlayerCollision { get; internal set; } = null; public static MelonPreferences_Entry EnableDungeonRandomizer { get; internal set; } = null; public static MelonPreferences_Entry RandomizeDungeonPick { get; internal set; } = null; public static MelonPreferences_Entry DungeonPickPoolMode { get; internal set; } = null; public static MelonPreferences_Entry DungeonAllowlist { get; internal set; } = null; public static MelonPreferences_Entry DungeonBlocklist { get; internal set; } = null; public static MelonPreferences_Entry IgnoreDungeonExcludeList { get; internal set; } = null; public static MelonPreferences_Entry RandomizeMapVariant { get; internal set; } = null; public static MelonPreferences_Entry DungeonSeedFlavor { get; internal set; } = null; public static MelonPreferences_Entry EnableWeather { get; internal set; } = null; public static MelonPreferences_Entry WeatherMode { get; internal set; } = null; public static MelonPreferences_Entry FixedWeatherPreset { get; internal set; } = null; public static MelonPreferences_Entry DisableRandomWeather { get; internal set; } = null; public static MelonPreferences_Entry WeatherCyclePresets { get; internal set; } = null; public static MelonPreferences_Entry WeatherCycleMinDelaySeconds { get; internal set; } = null; public static MelonPreferences_Entry WeatherCycleMaxDelaySeconds { get; internal set; } = null; public static MelonPreferences_Entry WebDashboardListenAddress { get; internal set; } = null; public static MelonPreferences_Entry WebDashboardListenPort { get; internal set; } = null; public static MelonPreferences_Entry EnableDebugLogging { get; private set; } = null; public static MelonPreferences_Entry LastSeenModVersion { get; private set; } = null; public static MelonPreferences_Entry ModToastDurationSeconds { get; internal set; } = null; public static MelonPreferences_Entry EnableExtendedSaveSlots { get; internal set; } = null; public static MelonPreferences_Entry EnableExtendedSpectatorPlayerList { get; internal set; } = null; public static MelonPreferences_Entry EnableLoadingWaitPlayerList { get; internal set; } = null; public static MelonPreferences_Entry EnableExtendedInGameMenuPlayerList { get; internal set; } = null; public static MelonPreferences_Entry EnableDamageHealthGlow { get; internal set; } = null; public static MelonPreferences_Entry EnableFloatingDamageNumbers { get; internal set; } = null; public static MelonPreferences_Entry FloatingDamageDurationSeconds { get; internal set; } = null; public static MelonPreferences_Entry EnableFpsUi { get; internal set; } = null; public static MelonPreferences_Entry EnableFpsUiInventoryNetWorth { get; internal set; } = null; public static MelonPreferences_Entry EnableInventorySlotOptimization { get; internal set; } = null; public static MelonPreferences_Entry InventoryPickupSelectMode { get; internal set; } = null; public static MelonPreferences_Entry EnableInventorySelectNextOnRemove { get; internal set; } = null; public static MelonPreferences_Entry EnableInventoryNumberKeySelection { get; internal set; } = null; public static MelonPreferences_Entry RoundStartSoundMode { get; internal set; } = null; public static MelonPreferences_Entry RoundStartSoundVariant { get; internal set; } = null; public static MelonPreferences_Entry RoundStartSoundRandomPool { get; internal set; } = null; public static MelonPreferences_Entry RoundStartSoundVolume { get; internal set; } = null; public static MelonPreferences_Entry DiscoBallSoundMode { get; internal set; } = null; public static MelonPreferences_Entry DiscoBallSoundVariant { get; internal set; } = null; public static MelonPreferences_Entry DiscoBallSoundRandomPool { get; internal set; } = null; public static MelonPreferences_Entry DiscoBallSoundVolume { get; internal set; } = null; public static MelonPreferences_Entry CustomLoadingScreenMode { get; internal set; } = null; public static MelonPreferences_Entry CustomLoadingScreenVariant { get; internal set; } = null; public static MelonPreferences_Entry CustomLoadingScreenRandomPool { get; internal set; } = null; public static MelonPreferences_Entry CustomLoadingScreenMotion { get; internal set; } = null; public static MelonPreferences_Entry SpectatorVoiceBalanceMode { get; internal set; } = null; public static MelonPreferences_Entry SpectatorVoiceAttenuation { get; internal set; } = null; public static MelonPreferences_Entry SpectatorVoiceDuckLevel { get; internal set; } = null; public static MelonPreferences_Entry EnableVoiceNoiseGate { get; internal set; } = null; public static MelonPreferences_Entry VoiceNoiseGateStrength { get; internal set; } = null; public static MelonPreferences_Entry EnablePrivacy { get; internal set; } = null; public static MelonPreferences_Entry BlockReluTelemetry { get; internal set; } = null; public static MelonPreferences_Entry BlockReplayUpload { get; internal set; } = null; public static MelonPreferences_Entry BlockReplayRecording { get; internal set; } = null; public static MelonPreferences_Entry BlockCrashReports { get; internal set; } = null; public static MelonPreferences_Entry StripCrashReportMetadata { get; internal set; } = null; public static MelonPreferences_Entry BlockKraftonGppSdk { get; internal set; } = null; public static event Action? Changed { add { ModConfigChangeTracker.Changed += value; } remove { ModConfigChangeTracker.Changed -= value; } } internal static void TrackFloatEntry(MelonPreferences_Entry entry) { FloatEntries.Add(entry); } public static void Initialize(Instance logger) { ModConfigRegistry.ClearRegistrationOrder(); FilePath = Path.Combine(MelonEnvironment.UserDataDirectory, "MimesisPlayerEnhancement.cfg"); SparseTomlConfig.RepairTomletCompatibility(FilePath); MainCategory = CreateCategory("MimesisPlayerEnhancement"); PrivacyConfig.CreateCategory(); UiConfig.CreateCategory(); SavegamePreparationConfig.CreateCategory(); JoinAnytimeConfig.CreateCategory(); MorePlayersConfig.CreateCategory(); MoreVoicesConfig.CreateCategory(); PersistenceConfig.CreateCategory(); PlayerAnnouncementsConfig.CreateCategory(); StatisticsConfig.CreateCategory(); DungeonTimeConfig.CreateCategory(); EconomyConfig.CreateCategory(); LootMultiplicatorConfig.CreateCategory(); SpawnScalingConfig.CreateCategory(); DungeonRandomizerConfig.CreateCategory(); MimicTuningConfig.CreateCategory(); PlayerTuningConfig.CreateCategory(); WeatherConfig.CreateCategory(); WebDashboardConfig.CreateCategory(); EnableDebugLogging = CreateTrackedEntry(MainCategory, "EnableDebugLogging", defaultValue: false); LastSeenModVersion = CreateHiddenTrackedEntry(MainCategory, "LastSeenModVersion", string.Empty); PrivacyConfig.CreateEntries(); UiConfig.CreateEntries(); SavegamePreparationConfig.CreateEntries(); JoinAnytimeConfig.CreateEntries(); MorePlayersConfig.CreateEntries(); MoreVoicesConfig.CreateEntries(); PersistenceConfig.CreateEntries(); PlayerAnnouncementsConfig.CreateEntries(); StatisticsConfig.CreateEntries(); DungeonTimeConfig.CreateEntries(); EconomyConfig.CreateEntries(); LootMultiplicatorConfig.CreateEntries(); SpawnScalingConfig.CreateEntries(); DungeonRandomizerConfig.CreateEntries(); MimicTuningConfig.CreateEntries(); PlayerTuningConfig.CreateEntries(); WeatherConfig.CreateEntries(); WebDashboardConfig.CreateEntries(); MorePlayersConfig.SanitizeInitialValues(logger); JoinAnytimeConfig.SanitizeInitialValues(logger); EconomyConfig.SanitizeInitialValues(logger); DungeonRandomizerConfig.SanitizeInitialValues(logger); DungeonTimeConfig.SanitizeInitialValues(logger); WeatherConfig.SanitizeInitialValues(logger); MorePlayersConfig.WireValidation(logger); MoreVoicesConfig.WireValidation(logger); PersistenceConfig.WireValidation(); StatisticsConfig.WireValidation(logger); PlayerAnnouncementsConfig.WireValidation(); JoinAnytimeConfig.WireValidation(logger); SpawnScalingConfig.WireValidation(logger); LootMultiplicatorConfig.WireValidation(logger); EconomyConfig.WireValidation(logger); SavegamePreparationConfig.WireValidation(logger); DungeonTimeConfig.WireValidation(logger); MimicTuningConfig.WireValidation(); PlayerTuningConfig.WireValidation(logger); DungeonRandomizerConfig.WireValidation(logger); WeatherConfig.WireValidation(logger); WebDashboardConfig.WireValidation(logger); UiConfig.WireValidation(logger); PrivacyConfig.WireValidation(); ((MelonEventBase>)(object)EnableDebugLogging.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged((MelonPreferences_Entry)(object)EnableDebugLogging); }, 0, false); RegisterFloatEntries(); ModConfigFloatHelper.SanitizeAll(FloatEntries); NormalizeSavedFloats(); ModConfigRegistry.Rebuild(); ModConfigRegistry.PurgeObsoleteFromFile(FilePath); MainCategory.LoadFromFile(false); SanitizeFloatEntries(); IsInitialized = true; ModConfigLocalization.Apply(); } public static void SaveToFile() { ModConfigRegistry.SaveToFile(); } internal static void FlushGlobalToDisk() { GlobalConfigStore.FlushToDisk(); } internal static void ReloadGlobalFromFile() { if (IsInitialized) { GlobalConfigStore.FlushIfDirty(); MainCategory.LoadFromFile(false); SanitizeFloatEntries(); } } public static bool TrySetEntryValue(string sectionId, string key, string value, out string? error) { return ModConfigRegistry.TrySetEntryValue(sectionId, key, value, out error); } internal static void NotifyFileReloaded() { ModConfigChangeTracker.NotifyFullReload(); } public static void NormalizeSavedFloats() { ModConfigFloatHelper.NormalizeSavedFloats(FilePath, FloatEntries); } internal static void SanitizeFloatEntries() { ModConfigFloatHelper.SanitizeAll(FloatEntries); } private static void RegisterFloatEntries() { MorePlayersConfig.RegisterFloatEntries(); SpawnScalingConfig.RegisterFloatEntries(); LootMultiplicatorConfig.RegisterFloatEntries(); EconomyConfig.RegisterFloatEntries(); DungeonTimeConfig.RegisterFloatEntries(); MimicTuningConfig.RegisterFloatEntries(); PlayerTuningConfig.RegisterFloatEntries(); WeatherConfig.RegisterFloatEntries(); UiConfig.RegisterFloatEntries(); } internal static void OnSpawnMultiplierChanged(Instance logger, float value, MelonPreferences_Entry entry) { if (value < 0f) { logger.Warning(((MelonPreferences_Entry)entry).Identifier + " must be >= 0; resetting to 0."); entry.Value = 0f; } else { ModConfigFloatHelper.SanitizeEntry(entry); NotifyChanged((MelonPreferences_Entry)(object)entry); } } internal static void OnBaselinePlayerCountChanged(Instance logger, int value, MelonPreferences_Entry entry) { if (value < 1) { logger.Warning(((MelonPreferences_Entry)entry).Identifier + " must be at least 1; resetting to 1."); entry.Value = 1; } else { NotifyChanged((MelonPreferences_Entry)(object)entry); } } internal static MelonPreferences_Category CreateCategory(string id) { string text = ModL10n.GetConfigSectionTitle(id) ?? id; MelonPreferences_Category obj = MelonPreferences.CreateCategory(id, text); obj.SetFilePath(FilePath); ModConfigRegistry.TrackCategory(obj); return obj; } internal static MelonPreferences_Entry CreateTrackedEntry(MelonPreferences_Category category, string identifier, T defaultValue) { string identifier2 = category.Identifier; string text = ModL10n.GetConfigEntryTitle(identifier2, identifier) ?? identifier; string text2 = ModL10n.GetConfigEntryDescription(identifier2, identifier) ?? string.Empty; MelonPreferences_Entry obj = category.CreateEntry(identifier, defaultValue, text, text2, false, false, (ValueValidator)null, (string)null); ModConfigRegistry.TrackEntry((MelonPreferences_Entry)(object)obj); return obj; } internal static MelonPreferences_Entry CreateHiddenTrackedEntry(MelonPreferences_Category category, string identifier, T defaultValue) { MelonPreferences_Entry obj = CreateTrackedEntry(category, identifier, defaultValue); ((MelonPreferences_Entry)obj).IsHidden = true; return obj; } internal static void NotifyChanged(MelonPreferences_Entry entry) { ModConfigChangeTracker.NotifyEntryChanged(entry); } } public sealed class ModConfigKeyChange { public string SectionId { get; set; } = ""; public string Key { get; set; } = ""; } public sealed class ModConfigChangeInfo { public static ModConfigChangeInfo FullReload { get; } = new ModConfigChangeInfo { IsFullReload = true }; public bool IsFullReload { get; set; } public IReadOnlyList ChangedKeys { get; set; } = Array.Empty(); public IEnumerable ChangedSections => ChangedKeys.Select((ModConfigKeyChange change) => change.SectionId).Distinct(StringComparer.OrdinalIgnoreCase); public bool AffectsSection(string sectionId) { if (IsFullReload) { return true; } return ChangedKeys.Any((ModConfigKeyChange change) => string.Equals(change.SectionId, sectionId, StringComparison.OrdinalIgnoreCase)); } } internal static class ModConfigChangeTracker { private sealed class ChangeKeyComparer : IEqualityComparer<(string SectionId, string Key)> { internal static ChangeKeyComparer Instance { get; } = new ChangeKeyComparer(); public bool Equals((string SectionId, string Key) x, (string SectionId, string Key) y) { if (string.Equals(x.SectionId, y.SectionId, StringComparison.OrdinalIgnoreCase)) { return string.Equals(x.Key, y.Key, StringComparison.OrdinalIgnoreCase); } return false; } public int GetHashCode((string SectionId, string Key) obj) { return HashCode.Combine(StringComparer.OrdinalIgnoreCase.GetHashCode(obj.SectionId), StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Key)); } } private static int _batchDepth; private static readonly HashSet<(string SectionId, string Key)> PendingChanges = new HashSet<(string, string)>(ChangeKeyComparer.Instance); internal static event Action? Changed; internal static void BeginBatch() { _batchDepth++; } internal static void EndBatch() { if (_batchDepth > 0) { _batchDepth--; if (_batchDepth <= 0 && PendingChanges.Count != 0) { FlushPending(); } } } internal static void CancelBatch() { PendingChanges.Clear(); _batchDepth = 0; } internal static void NotifyChange(string sectionId, string key) { if (!string.IsNullOrWhiteSpace(sectionId) && !string.IsNullOrWhiteSpace(key)) { if (_batchDepth > 0) { PendingChanges.Add((sectionId, key)); } else { Dispatch(BuildInfo(new <>z__ReadOnlySingleElementList<(string, string)>((sectionId, key)))); } } } internal static void NotifyEntryChanged(MelonPreferences_Entry entry) { NotifyChange(entry.Category.Identifier, entry.Identifier); } internal static void NotifyFullReload() { PendingChanges.Clear(); _batchDepth = 0; Dispatch(ModConfigChangeInfo.FullReload); } private static void FlushPending() { if (PendingChanges.Count != 0) { List<(string SectionId, string Key)> pendingChanges = PendingChanges.ToList(); PendingChanges.Clear(); Dispatch(BuildInfo(pendingChanges)); } } private static ModConfigChangeInfo BuildInfo(IReadOnlyList<(string SectionId, string Key)> pendingChanges) { ModConfigKeyChange[] changedKeys = pendingChanges.Select<(string, string), ModConfigKeyChange>(delegate((string SectionId, string Key) pair) { ModConfigKeyChange modConfigKeyChange = new ModConfigKeyChange(); (modConfigKeyChange.SectionId, modConfigKeyChange.Key) = pair; return modConfigKeyChange; }).ToArray(); return new ModConfigChangeInfo { ChangedKeys = changedKeys }; } private static void Dispatch(ModConfigChangeInfo info) { ModConfigRegistry.NotifyRuntimeChange(); ModConfigChangeTracker.Changed?.Invoke(info); } } internal readonly struct ModConfigEntryBound { internal string? MinValue { get; } internal string? MaxValue { get; } internal ModConfigEntryBound(string? minValue, string? maxValue) { MinValue = minValue; MaxValue = maxValue; } } internal static class ModConfigEntryBounds { private sealed class SectionKeyComparer : IEqualityComparer<(string SectionId, string Key)> { internal static readonly SectionKeyComparer Instance = new SectionKeyComparer(); public bool Equals((string SectionId, string Key) x, (string SectionId, string Key) y) { if (string.Equals(x.SectionId, y.SectionId, StringComparison.OrdinalIgnoreCase)) { return string.Equals(x.Key, y.Key, StringComparison.OrdinalIgnoreCase); } return false; } public int GetHashCode((string SectionId, string Key) obj) { return HashCode.Combine(StringComparer.OrdinalIgnoreCase.GetHashCode(obj.SectionId), StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Key)); } } private static readonly Dictionary<(string SectionId, string Key), ModConfigEntryBound> Bounds; static ModConfigEntryBounds() { Bounds = new Dictionary<(string, string), ModConfigEntryBound>(SectionKeyComparer.Instance); Float("MimesisPlayerEnhancement_Ui", "ModToastDurationSeconds", 1f); FloatRange("MimesisPlayerEnhancement_Ui", "FloatingDamageDurationSeconds", 1f, 3f); FloatRange("MimesisPlayerEnhancement_Ui", "RoundStartSoundVolume", 0f, 1f); FloatRange("MimesisPlayerEnhancement_Ui", "DiscoBallSoundVolume", 0f, 1f); FloatRange("MimesisPlayerEnhancement_Ui", "SpectatorVoiceAttenuation", 0f, 1f); FloatRange("MimesisPlayerEnhancement_Ui", "SpectatorVoiceDuckLevel", 0f, 1f); FloatRange("MimesisPlayerEnhancement_Ui", "VoiceNoiseGateStrength", 0f, 1f); Int("MimesisPlayerEnhancement_MorePlayers", "MaxPlayers", 1); MinZeroFloat("MimesisPlayerEnhancement_MorePlayers", "RoundGoalBasePerZone", "RoundGoalMoneyMultiplier"); FloatRange("MimesisPlayerEnhancement_MorePlayers", "RoundGoalCurveExponent", 0.1f, 2f); IntRange("MimesisPlayerEnhancement_MorePlayers", "RoundGoalRandomSpreadPercent", 0, 100); Int("MimesisPlayerEnhancement_MoreVoices", "MaxIndoorVoiceEvents", 1); Int("MimesisPlayerEnhancement_MoreVoices", "MaxDeathMatchVoiceEvents", 1); Int("MimesisPlayerEnhancement_MoreVoices", "MaxOutdoorVoiceEvents", 1); Int("MimesisPlayerEnhancement_MoreVoices", "VoiceClipCacheMaxEntries", 1); Int("MimesisPlayerEnhancement_Statistics", "SessionReconnectGraceMinutes", 1); FloatRange("MimesisPlayerEnhancement_PlayerTuning", "MoveSpeedMultiplier"); FloatRange("MimesisPlayerEnhancement_PlayerTuning", "NoClipSpeedMultiplier"); FloatRange("MimesisPlayerEnhancement_PlayerTuning", "MaxStaminaMultiplier"); FloatRange("MimesisPlayerEnhancement_PlayerTuning", "StaminaDrainMultiplier"); FloatRange("MimesisPlayerEnhancement_PlayerTuning", "StaminaRegenMultiplier"); FloatRange("MimesisPlayerEnhancement_PlayerTuning", "StaminaRegenDelayMultiplier"); FloatRange("MimesisPlayerEnhancement_PlayerTuning", "MaxCarryWeightMultiplier"); MinZeroFloat("MimesisPlayerEnhancement_Economy", "ScrapSellValueMultiplier", "ScrapSellValuePerPlayerMultiplier", "ShopBuyPriceMultiplier", "ShopBuyPricePerPlayerMultiplier", "ReinforcePriceMultiplier", "ReinforcePricePerPlayerMultiplier"); Int("MimesisPlayerEnhancement_Economy", "EconomyBaselinePlayerCount", 1); Int("MimesisPlayerEnhancement_SavegamePreparation", "StartupMoney", 0); IntRange("MimesisPlayerEnhancement_SavegamePreparation", "StartingZone", 1, 99); IntRange("MimesisPlayerEnhancement_Economy", "ShopDiscountMinPercent", 0, 100); IntRange("MimesisPlayerEnhancement_Economy", "ShopDiscountMaxPercent", 0, 100); IntRange("MimesisPlayerEnhancement_Economy", "ShopDiscountChancePercent", 0, 100); MinZeroFloat("MimesisPlayerEnhancement_LootMultiplicator", "MapLootMultiplier", "MapLootPerPlayerMultiplier", "DropLootMultiplier", "DropLootPerPlayerMultiplier"); Int("MimesisPlayerEnhancement_LootMultiplicator", "LootMultiplicatorBaselinePlayerCount", 1); IntRange("MimesisPlayerEnhancement_LootMultiplicator", "ConvertFakeActorDyingDropChancePercent", 0, 100); MinZeroFloat("MimesisPlayerEnhancement_SpawnScaling", "MimicSpawnMultiplier", "MimicSpawnPerPlayerMultiplier", "MimicWaveInitialDelaySeconds", "MimicWaveInitialDelayMinSeconds", "MimicWaveInitialDelayMaxSeconds", "MimicWaveIntervalSeconds", "MimicWaveIntervalMinSeconds", "MimicWaveIntervalMaxSeconds", "BossSpawnMultiplier", "BossSpawnPerPlayerMultiplier", "GruntSpawnMultiplier", "GruntSpawnPerPlayerMultiplier", "GruntWaveInitialDelaySeconds", "GruntWaveInitialDelayMinSeconds", "GruntWaveInitialDelayMaxSeconds", "GruntWaveIntervalSeconds", "GruntWaveIntervalMinSeconds", "GruntWaveIntervalMaxSeconds", "SpecialSpawnMultiplier", "SpecialSpawnPerPlayerMultiplier", "TrapSpawnMultiplier", "TrapSpawnPerPlayerMultiplier", "TrapRespawnDelaySeconds", "TrapRespawnDelayMinSeconds", "TrapRespawnDelayMaxSeconds", "TrapRespawnMinPlayerDistanceMeters", "OtherSpawnMultiplier", "OtherSpawnPerPlayerMultiplier", "BonusEncounterDelayMinSeconds", "BonusEncounterDelayMaxSeconds", "BonusEncounterMinPlayerDistanceMeters"); Int("MimesisPlayerEnhancement_SpawnScaling", "SpawnScalingBaselinePlayerCount", 1); Int("MimesisPlayerEnhancement_JoinAnytime", "JoinConnectionGraceSeconds", 1); Int("MimesisPlayerEnhancement_DungeonTime", "DungeonTimeBaselinePlayerCount", 1); Float("MimesisPlayerEnhancement_DungeonTime", "ExtraShiftSecondsPerPlayerAboveBaseline", 0f); FloatRange("MimesisPlayerEnhancement_DungeonTime", "TimeMultiplier", -5f, 5f); Float("MimesisPlayerEnhancement_Weather", "WeatherCycleMinDelaySeconds", 0f); Float("MimesisPlayerEnhancement_Weather", "WeatherCycleMaxDelaySeconds", 0f); FloatRange("MimesisPlayerEnhancement_MimicTuning", "MimicPossessionMinTimeSeconds", 0.1f, 120f); FloatRange("MimesisPlayerEnhancement_MimicTuning", "MimicPossessionMaxTimeSeconds", 0.1f, 120f); FloatRange("MimesisPlayerEnhancement_MimicTuning", "MimicPossessionCooltimeMultiplier", 0.1f, 10f); MinZeroFloat("MimesisPlayerEnhancement_MimicTuning", "PeriodicVoiceIntervalMultiplier", "PlayerVoiceResponseCooldownSeconds", "PlayerVoiceResponseDelayMinSeconds", "PlayerVoiceResponseDelayMaxSeconds", "ClipReuseCooldownSeconds", "DeathMatchClipReuseCooldownSeconds", "SpeakAudienceRangeMeters", "PostReplyIntervalFixedSeconds", "PostReplyIntervalMinSeconds", "PostReplyIntervalMaxSeconds", "VoiceInitIntervalMin", "VoiceInitIntervalMax", "VoicePeriodicIntervalMin", "VoicePeriodicIntervalMax", "VoiceDeathMatchIntervalMin", "VoiceDeathMatchIntervalMax"); Float("MimesisPlayerEnhancement_MimicTuning", "PlayerVoiceResponseMaxDistance", 1f); IntRange("MimesisPlayerEnhancement_MimicTuning", "PlayerVoiceResponseChancePercent", 0, 100); IntRange("MimesisPlayerEnhancement_MimicTuning", "MinRequiredSpeechClips", 0, 50); FloatRange("MimesisPlayerEnhancement_MimicTuning", "TrustOutdoorMultiplier", 0f, 10f); FloatRange("MimesisPlayerEnhancement_MimicTuning", "TrustLookingDelta", -100f, 100f); FloatRange("MimesisPlayerEnhancement_MimicTuning", "TrustNotLookingDelta", -100f, 100f); FloatRange("MimesisPlayerEnhancement_MimicTuning", "TrustApproachDelta", -100f, 100f); FloatRange("MimesisPlayerEnhancement_MimicTuning", "TrustMaintainDelta", -100f, 100f); FloatRange("MimesisPlayerEnhancement_MimicTuning", "TrustWalkAwayDelta", -100f, 100f); FloatRange("MimesisPlayerEnhancement_MimicTuning", "TrustSprintAwayDelta", -100f, 100f); FloatRange("MimesisPlayerEnhancement_MimicTuning", "TrustHitDamageMultiplier", -100f, 100f); FloatRange("MimesisPlayerEnhancement_MimicTuning", "TrustFriendlyThreshold", 0f, 100f); FloatRange("MimesisPlayerEnhancement_MimicTuning", "TrustDistrustThreshold", 0f, 100f); FloatRange("MimesisPlayerEnhancement_MimicTuning", "TrustInitialFixed", 0f, 100f); FloatRange("MimesisPlayerEnhancement_MimicTuning", "TrustInitialRandomMin", 0f, 100f); FloatRange("MimesisPlayerEnhancement_MimicTuning", "TrustInitialRandomMax", 0f, 100f); FloatRange("MimesisPlayerEnhancement_MimicTuning", "TrustBehaviorFixed", 0f, 100f); FloatRange("MimesisPlayerEnhancement_MimicTuning", "TrustBehaviorRandomMin", 0f, 100f); FloatRange("MimesisPlayerEnhancement_MimicTuning", "TrustBehaviorRandomMax", 0f, 100f); FloatRange("MimesisPlayerEnhancement_MimicTuning", "ChaseActivationDistanceMeters", 0.1f, 200f); FloatRange("MimesisPlayerEnhancement_MimicTuning", "ChaseForceRunDistanceMeters", 0.1f, 200f); FloatRange("MimesisPlayerEnhancement_MimicTuning", "MimicRunawayChance", 0f, 1f); IntRange("MimesisPlayerEnhancement_MimicTuning", "JumpCopyChancePercent", 0, 100); IntRange("MimesisPlayerEnhancement_MimicTuning", "SlotFollowChangeChancePercent", 0, 100); FloatRange("MimesisPlayerEnhancement_MimicTuning", "PossessionRangeMeters", 1f, 200f); IntRange("MimesisPlayerEnhancement_MimicTuning", "EmoteRespondChancePercent", 0, 100); IntRange("MimesisPlayerEnhancement_MimicTuning", "EmoteSuggestChancePercent", 0, 100); IntRange("MimesisPlayerEnhancement_MimicTuning", "ReactToSprinklerChancePercent", 0, 100); IntRange("MimesisPlayerEnhancement_MimicTuning", "UseTrapSwitchChancePercent", 0, 100); IntRange("MimesisPlayerEnhancement_MimicTuning", "UseChargerChancePercent", 0, 100); IntRange("MimesisPlayerEnhancement_MimicTuning", "UseTransmitterChancePercent", 0, 100); IntRange("MimesisPlayerEnhancement_MimicTuning", "UseShutterSwitchChancePercent", 0, 100); FloatRange("MimesisPlayerEnhancement_MimicTuning", "HornMaxRecordSeconds", 0.1f, 60f); FloatRange("MimesisPlayerEnhancement_MimicTuning", "HornRecordingGapSeconds", 0.1f, 30f); IntRange("MimesisPlayerEnhancement_MimicTuning", "HornMaxStoredRecords", 1, 100); IntRange("MimesisPlayerEnhancement_WebDashboard", "WebDashboardListenPort", 1, 65535); } internal static bool TryGet(string sectionId, string key, out ModConfigEntryBound bound) { return Bounds.TryGetValue((sectionId, key), out bound); } private static void Float(string sectionId, string key, float min, float? max = null) { Bounds[(sectionId, key)] = new ModConfigEntryBound(ModConfigFloatHelper.Format(min), max.HasValue ? ModConfigFloatHelper.Format(max.Value) : null); } private static void FloatRange(string sectionId, string key, float min, float max) { Float(sectionId, key, min, max); } private static void FloatRange(string sectionId, string key) { FloatRange(sectionId, key, 0.1f, 5f); } private static void Int(string sectionId, string key, int min, int? max = null) { Bounds[(sectionId, key)] = new ModConfigEntryBound(min.ToString(CultureInfo.InvariantCulture), max?.ToString(CultureInfo.InvariantCulture)); } private static void IntRange(string sectionId, string key, int min, int max) { Int(sectionId, key, min, max); } private static void MinZeroFloat(string sectionId, params string[] keys) { for (int i = 0; i < keys.Length; i++) { Float(sectionId, keys[i], 0f); } } } internal readonly struct ModConfigEntryDependency { internal string DependsOnKey { get; } internal string? DependsOnValue { get; } internal ModConfigEntryDependency(string dependsOnKey, string? dependsOnValue = null) { DependsOnKey = dependsOnKey; DependsOnValue = dependsOnValue; } } internal static class ModConfigEntryDependencies { internal static void ApplyToEntry(WebDashboardConfigSectionDto section, WebDashboardConfigEntryDto entry) { if (TryGetDependency(section.Id, entry.Key, out var dependency)) { entry.DependsOnKey = dependency.DependsOnKey; entry.DependsOnValue = dependency.DependsOnValue; } } private static bool TryGetDependency(string sectionId, string key, out ModConfigEntryDependency dependency) { dependency = default(ModConfigEntryDependency); if (string.IsNullOrWhiteSpace(sectionId) || string.IsNullOrWhiteSpace(key)) { return false; } return sectionId switch { "MimesisPlayerEnhancement_MimicTuning" => TryGetMimicTuningDependency(key, out dependency), "MimesisPlayerEnhancement_SpawnScaling" => TryGetSpawnScalingDependency(key, out dependency), "MimesisPlayerEnhancement_LootMultiplicator" => TryGetLootDependency(key, out dependency), "MimesisPlayerEnhancement_Economy" => TryGetEconomyDependency(key, out dependency), "MimesisPlayerEnhancement_DungeonRandomizer" => TryGetDungeonRandomizerDependency(key, out dependency), "MimesisPlayerEnhancement_Weather" => TryGetWeatherDependency(key, out dependency), "MimesisPlayerEnhancement_DungeonTime" => TryGetDungeonTimeDependency(key, out dependency), "MimesisPlayerEnhancement_Ui" => TryGetUiDependency(key, out dependency), _ => false, }; } private static bool TryGetUiDependency(string key, out ModConfigEntryDependency dependency) { dependency = default(ModConfigEntryDependency); if (string.Equals(key, "RoundStartSoundVariant", StringComparison.Ordinal)) { dependency = new ModConfigEntryDependency("RoundStartSoundMode", "Specific"); return true; } if (string.Equals(key, "RoundStartSoundRandomPool", StringComparison.Ordinal)) { dependency = new ModConfigEntryDependency("RoundStartSoundMode", "Random"); return true; } if (string.Equals(key, "DiscoBallSoundVariant", StringComparison.Ordinal)) { dependency = new ModConfigEntryDependency("DiscoBallSoundMode", "Specific"); return true; } if (string.Equals(key, "DiscoBallSoundRandomPool", StringComparison.Ordinal)) { dependency = new ModConfigEntryDependency("DiscoBallSoundMode", "Random"); return true; } if (string.Equals(key, "CustomLoadingScreenVariant", StringComparison.Ordinal)) { dependency = new ModConfigEntryDependency("CustomLoadingScreenMode", "Specific"); return true; } if (string.Equals(key, "CustomLoadingScreenRandomPool", StringComparison.Ordinal)) { dependency = new ModConfigEntryDependency("CustomLoadingScreenMode", "Random"); return true; } if (string.Equals(key, "SpectatorVoiceAttenuation", StringComparison.Ordinal)) { dependency = new ModConfigEntryDependency("SpectatorVoiceBalanceMode", "StaticAttenuation"); return true; } if (string.Equals(key, "SpectatorVoiceDuckLevel", StringComparison.Ordinal)) { dependency = new ModConfigEntryDependency("SpectatorVoiceBalanceMode", "SpeechDucking"); return true; } if (string.Equals(key, "VoiceNoiseGateStrength", StringComparison.Ordinal)) { dependency = new ModConfigEntryDependency("EnableVoiceNoiseGate"); return true; } return false; } private static bool TryGetMimicTuningDependency(string key, out ModConfigEntryDependency dependency) { dependency = default(ModConfigEntryDependency); switch (key) { case "PlayerVoiceResponseChancePercent": case "PlayerVoiceResponseDelayMaxSeconds": case "PlayerVoiceResponseDelayMinSeconds": case "PlayerVoiceResponseCooldownSeconds": case "DeathMatchClipReuseCooldownSeconds": case "ClipReuseCooldownSeconds": case "SpeakAudienceRangeMeters": case "HearOwnVoiceFromMimic": case "PostReplyIntervalMode": case "VoiceInitIntervalMode": case "MinRequiredSpeechClips": case "VoicePeriodicIntervalMode": case "VoiceDeathMatchIntervalMode": case "PeriodicVoiceIntervalMultiplier": case "PlayerVoiceResponseMaxDistance": dependency = new ModConfigEntryDependency("MimicVoiceTuningMode", "Custom"); return true; case "PostReplyIntervalFixedSeconds": dependency = new ModConfigEntryDependency("PostReplyIntervalMode", "Fixed"); return true; case "PostReplyIntervalMinSeconds": case "PostReplyIntervalMaxSeconds": dependency = new ModConfigEntryDependency("PostReplyIntervalMode", "Random"); return true; case "VoiceInitIntervalMin": case "VoiceInitIntervalMax": dependency = new ModConfigEntryDependency("VoiceInitIntervalMode", "Random"); return true; case "VoicePeriodicIntervalMin": case "VoicePeriodicIntervalMax": dependency = new ModConfigEntryDependency("VoicePeriodicIntervalMode", "Random"); return true; case "VoiceDeathMatchIntervalMax": case "VoiceDeathMatchIntervalMin": dependency = new ModConfigEntryDependency("VoiceDeathMatchIntervalMode", "Random"); return true; case "TrustHitDamageMultiplier": case "TrustOutdoorMultiplier": case "TrustFriendlyThreshold": case "TrustDistrustThreshold": case "ChaseForceRunDistanceMeters": case "ChaseActivationDistanceMeters": case "TrustSprintAwayDelta": case "TrustNotLookingDelta": case "TrustLookingDelta": case "TrustMaintainDelta": case "TrustWalkAwayDelta": case "TrustApproachDelta": case "TrustScoreValueMode": dependency = new ModConfigEntryDependency("MimicTrustMode", "Custom"); return true; case "TrustInitialFixed": dependency = new ModConfigEntryDependency("TrustScoreValueMode", "Fixed"); return true; case "TrustInitialRandomMin": case "TrustInitialRandomMax": dependency = new ModConfigEntryDependency("TrustScoreValueMode", "Random"); return true; case "TrustBehaviorFixed": dependency = new ModConfigEntryDependency("TrustScoreValueMode", "Fixed"); return true; case "TrustBehaviorRandomMin": case "TrustBehaviorRandomMax": dependency = new ModConfigEntryDependency("TrustScoreValueMode", "Random"); return true; case "MimicInventoryCopyPickRule": dependency = new ModConfigEntryDependency("MimicInventoryCopyMode", "Custom"); return true; case "RandomizeMimicPossessionDuration": dependency = new ModConfigEntryDependency("EnableMimicPossessionTuning"); return true; case "MimicPossessionMinTimeSeconds": case "MimicPossessionMaxTimeSeconds": dependency = new ModConfigEntryDependency("RandomizeMimicPossessionDuration"); return true; case "PossessionRangeMeters": case "PossessionBtGateMode": case "MimicPossessionCooltimeMultiplier": dependency = new ModConfigEntryDependency("EnableMimicPossessionTuning"); return true; case "EmoteRespondChancePercent": case "EmoteSuggestChancePercent": case "UseTransmitterChancePercent": case "ReactToSprinklerChancePercent": case "UseShutterSwitchChancePercent": case "UseTrapSwitchChancePercent": case "UseChargerChancePercent": dependency = new ModConfigEntryDependency("MimicEmotePropsMode", "Custom"); return true; case "HornMaxRecordSeconds": case "HornMaxStoredRecords": case "AllowHornImitation": case "HornRecordingGapSeconds": dependency = new ModConfigEntryDependency("HornImitationMode", "Custom"); return true; default: return false; } } private static bool TryGetSpawnScalingDependency(string key, out ModConfigEntryDependency dependency) { dependency = default(ModConfigEntryDependency); switch (key) { case "MimicWaveInitialDelaySeconds": case "MimicWaveIntervalSeconds": dependency = new ModConfigEntryDependency("MimicWaveMode", "Fixed"); return true; case "MimicWaveInitialDelayMinSeconds": case "MimicWaveInitialDelayMaxSeconds": case "MimicWaveIntervalMinSeconds": case "MimicWaveIntervalMaxSeconds": dependency = new ModConfigEntryDependency("MimicWaveMode", "Random"); return true; case "GruntWaveInitialDelaySeconds": case "GruntWaveIntervalSeconds": dependency = new ModConfigEntryDependency("GruntWaveMode", "Fixed"); return true; case "GruntWaveInitialDelayMinSeconds": case "GruntWaveInitialDelayMaxSeconds": case "GruntWaveIntervalMinSeconds": case "GruntWaveIntervalMaxSeconds": dependency = new ModConfigEntryDependency("GruntWaveMode", "Random"); return true; case "TrapRespawnDelaySeconds": dependency = new ModConfigEntryDependency("TrapRespawnMode", "Fixed"); return true; case "TrapRespawnDelayMaxSeconds": case "TrapRespawnDelayMinSeconds": dependency = new ModConfigEntryDependency("TrapRespawnMode", "Random"); return true; case "TrapRespawnMinPlayerDistanceMeters": dependency = new ModConfigEntryDependency("TrapRespawnMode", "!=Vanilla"); return true; default: return false; } } private static bool TryGetLootDependency(string key, out ModConfigEntryDependency dependency) { dependency = default(ModConfigEntryDependency); if (!(key == "LootAllowlist")) { if (key == "LootBlocklist") { dependency = new ModConfigEntryDependency("LootItemFilterMode", "BlocklistOnly"); return true; } return false; } dependency = new ModConfigEntryDependency("LootItemFilterMode", "AllowlistOnly"); return true; } private static bool TryGetEconomyDependency(string key, out ModConfigEntryDependency dependency) { dependency = default(ModConfigEntryDependency); if (key == "ShopDiscountMinPercent" || key == "ShopDiscountMaxPercent") { dependency = new ModConfigEntryDependency("ShopDiscountChancePercent", ">0"); return true; } return false; } private static bool TryGetDungeonRandomizerDependency(string key, out ModConfigEntryDependency dependency) { dependency = default(ModConfigEntryDependency); switch (key) { case "DungeonPickPoolMode": case "DungeonAllowlist": case "DungeonBlocklist": case "IgnoreDungeonExcludeList": dependency = new ModConfigEntryDependency("RandomizeDungeonPick"); return true; case "DungeonSeedFlavor": dependency = new ModConfigEntryDependency("EnableDungeonRandomizer"); return true; default: return false; } } private static bool TryGetWeatherDependency(string key, out ModConfigEntryDependency dependency) { dependency = default(ModConfigEntryDependency); switch (key) { case "FixedWeatherPreset": dependency = new ModConfigEntryDependency("WeatherMode", "Fixed"); return true; case "DisableRandomWeather": dependency = new ModConfigEntryDependency("WeatherMode", "Vanilla"); return true; case "WeatherCyclePresets": case "WeatherCycleMinDelaySeconds": case "WeatherCycleMaxDelaySeconds": dependency = new ModConfigEntryDependency("WeatherMode", "Cycle"); return true; default: return false; } } private static bool TryGetDungeonTimeDependency(string key, out ModConfigEntryDependency dependency) { dependency = default(ModConfigEntryDependency); if (key == "EnableRealtimeTramClock" || key == "TimeMultiplier") { dependency = new ModConfigEntryDependency("EnableDungeonTime"); return true; } return false; } } internal static class ModConfigEntryLocalEffect { private sealed class EntryKeyComparer : IEqualityComparer<(string SectionId, string Key)> { internal static readonly EntryKeyComparer Instance = new EntryKeyComparer(); public bool Equals((string SectionId, string Key) x, (string SectionId, string Key) y) { if (string.Equals(x.SectionId, y.SectionId, StringComparison.OrdinalIgnoreCase)) { return string.Equals(x.Key, y.Key, StringComparison.OrdinalIgnoreCase); } return false; } public int GetHashCode((string SectionId, string Key) obj) { return HashCode.Combine(StringComparer.OrdinalIgnoreCase.GetHashCode(obj.SectionId), StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Key)); } } private const string MainSectionId = "MimesisPlayerEnhancement"; private const string PlayerTuningSectionId = "MimesisPlayerEnhancement_PlayerTuning"; private static readonly HashSet<(string SectionId, string Key)> LocalEntries = new HashSet<(string, string)>(EntryKeyComparer.Instance) { ("MimesisPlayerEnhancement", "EnableDebugLogging"), ("MimesisPlayerEnhancement", "LastSeenModVersion"), ("MimesisPlayerEnhancement_Ui", "ModToastDurationSeconds"), ("MimesisPlayerEnhancement_Ui", "EnableExtendedSaveSlots"), ("MimesisPlayerEnhancement_Ui", "EnableExtendedSpectatorPlayerList"), ("MimesisPlayerEnhancement_Ui", "EnableLoadingWaitPlayerList"), ("MimesisPlayerEnhancement_Ui", "EnableExtendedInGameMenuPlayerList"), ("MimesisPlayerEnhancement_Ui", "EnableDamageHealthGlow"), ("MimesisPlayerEnhancement_Ui", "EnableFloatingDamageNumbers"), ("MimesisPlayerEnhancement_Ui", "FloatingDamageDurationSeconds"), ("MimesisPlayerEnhancement_Ui", "EnableFpsUi"), ("MimesisPlayerEnhancement_Ui", "EnableFpsUiInventoryNetWorth"), ("MimesisPlayerEnhancement_Ui", "EnableInventorySlotOptimization"), ("MimesisPlayerEnhancement_Ui", "InventoryPickupSelectMode"), ("MimesisPlayerEnhancement_Ui", "EnableInventorySelectNextOnRemove"), ("MimesisPlayerEnhancement_Ui", "EnableInventoryNumberKeySelection"), ("MimesisPlayerEnhancement_Ui", "RoundStartSoundMode"), ("MimesisPlayerEnhancement_Ui", "RoundStartSoundVariant"), ("MimesisPlayerEnhancement_Ui", "RoundStartSoundRandomPool"), ("MimesisPlayerEnhancement_Ui", "RoundStartSoundVolume"), ("MimesisPlayerEnhancement_Ui", "DiscoBallSoundMode"), ("MimesisPlayerEnhancement_Ui", "DiscoBallSoundVariant"), ("MimesisPlayerEnhancement_Ui", "DiscoBallSoundRandomPool"), ("MimesisPlayerEnhancement_Ui", "DiscoBallSoundVolume"), ("MimesisPlayerEnhancement_Ui", "CustomLoadingScreenMode"), ("MimesisPlayerEnhancement_Ui", "CustomLoadingScreenVariant"), ("MimesisPlayerEnhancement_Ui", "CustomLoadingScreenRandomPool"), ("MimesisPlayerEnhancement_Ui", "CustomLoadingScreenMotion"), ("MimesisPlayerEnhancement_Ui", "SpectatorVoiceBalanceMode"), ("MimesisPlayerEnhancement_Ui", "SpectatorVoiceAttenuation"), ("MimesisPlayerEnhancement_Ui", "SpectatorVoiceDuckLevel"), ("MimesisPlayerEnhancement_Ui", "EnableVoiceNoiseGate"), ("MimesisPlayerEnhancement_Ui", "VoiceNoiseGateStrength"), ("MimesisPlayerEnhancement_Privacy", "EnablePrivacy"), ("MimesisPlayerEnhancement_Privacy", "BlockReluTelemetry"), ("MimesisPlayerEnhancement_Privacy", "BlockReplayUpload"), ("MimesisPlayerEnhancement_Privacy", "BlockReplayRecording"), ("MimesisPlayerEnhancement_Privacy", "BlockCrashReports"), ("MimesisPlayerEnhancement_Privacy", "StripCrashReportMetadata"), ("MimesisPlayerEnhancement_Privacy", "BlockKraftonGppSdk"), ("MimesisPlayerEnhancement_PlayerTuning", "DisablePlayerCollision") }; internal static bool HasLocalEffect(string sectionId, string key) { return LocalEntries.Contains((sectionId, key)); } } internal static class ModConfigEntryUiHints { private sealed class EntryKeyComparer : IEqualityComparer<(string SectionId, string Key)> { internal static readonly EntryKeyComparer Instance = new EntryKeyComparer(); public bool Equals((string SectionId, string Key) x, (string SectionId, string Key) y) { if (string.Equals(x.SectionId, y.SectionId, StringComparison.OrdinalIgnoreCase)) { return string.Equals(x.Key, y.Key, StringComparison.OrdinalIgnoreCase); } return false; } public int GetHashCode((string SectionId, string Key) obj) { return HashCode.Combine(StringComparer.OrdinalIgnoreCase.GetHashCode(obj.SectionId), StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Key)); } } private const string LootSectionId = "MimesisPlayerEnhancement_LootMultiplicator"; private const string DungeonSectionId = "MimesisPlayerEnhancement_DungeonRandomizer"; private const string WeatherSectionId = "MimesisPlayerEnhancement_Weather"; private const string DungeonTimeSectionId = "MimesisPlayerEnhancement_DungeonTime"; private const string SpawnScalingSectionId = "MimesisPlayerEnhancement_SpawnScaling"; private const string EconomySectionId = "MimesisPlayerEnhancement_Economy"; private const string MimicTuningSectionId = "MimesisPlayerEnhancement_MimicTuning"; private const string SavegamePreparationSectionId = "MimesisPlayerEnhancement_SavegamePreparation"; private const string MoreVoicesSectionId = "MimesisPlayerEnhancement_MoreVoices"; private const string UiSectionId = "MimesisPlayerEnhancement_Ui"; private static readonly Dictionary<(string SectionId, string Key), string[]> SelectValuesByEntry = new Dictionary<(string, string), string[]>(EntryKeyComparer.Instance) { [("MimesisPlayerEnhancement_LootMultiplicator", "LootItemFilterMode")] = new string[3] { "All", "AllowlistOnly", "BlocklistOnly" }, [("MimesisPlayerEnhancement_DungeonRandomizer", "DungeonPickPoolMode")] = new string[2] { "WidenVanilla", "AllActiveUniform" }, [("MimesisPlayerEnhancement_DungeonRandomizer", "DungeonSeedFlavor")] = DungeonSeedFlavorUtil.AllNames, [("MimesisPlayerEnhancement_Weather", "WeatherMode")] = new string[3] { "Vanilla", "Fixed", "Cycle" }, [("MimesisPlayerEnhancement_Weather", "FixedWeatherPreset")] = new string[4] { "Sunny", "Rain", "HeavyRain", "Squall" }, [("MimesisPlayerEnhancement_DungeonTime", "StartTimePreset")] = new string[6] { "Vanilla", "Morning", "Noon", "Dusk", "Night", "Midnight" }, [("MimesisPlayerEnhancement_SpawnScaling", "MimicWaveMode")] = new string[3] { "Vanilla", "Fixed", "Random" }, [("MimesisPlayerEnhancement_SpawnScaling", "GruntWaveMode")] = new string[3] { "Vanilla", "Fixed", "Random" }, [("MimesisPlayerEnhancement_SpawnScaling", "TrapRespawnMode")] = new string[3] { "Vanilla", "Fixed", "Random" }, [("MimesisPlayerEnhancement_MimicTuning", "MimicVoiceTuningMode")] = new string[2] { "Vanilla", "Custom" }, [("MimesisPlayerEnhancement_MimicTuning", "PostReplyIntervalMode")] = new string[3] { "Vanilla", "Fixed", "Random" }, [("MimesisPlayerEnhancement_MimicTuning", "HearOwnVoiceFromMimic")] = new string[3] { "Vanilla", "AlwaysHear", "OnlyWhenSingleplayer" }, [("MimesisPlayerEnhancement_MimicTuning", "VoiceInitIntervalMode")] = new string[2] { "Vanilla", "Random" }, [("MimesisPlayerEnhancement_MimicTuning", "VoicePeriodicIntervalMode")] = new string[2] { "Vanilla", "Random" }, [("MimesisPlayerEnhancement_MimicTuning", "VoiceDeathMatchIntervalMode")] = new string[2] { "Vanilla", "Random" }, [("MimesisPlayerEnhancement_MimicTuning", "MimicTrustMode")] = new string[2] { "Vanilla", "Custom" }, [("MimesisPlayerEnhancement_MimicTuning", "TrustScoreValueMode")] = new string[3] { "Vanilla", "Fixed", "Random" }, [("MimesisPlayerEnhancement_MimicTuning", "MimicSocialMode")] = new string[2] { "Vanilla", "Custom" }, [("MimesisPlayerEnhancement_MimicTuning", "MimicInventoryCopyMode")] = new string[2] { "Vanilla", "Custom" }, [("MimesisPlayerEnhancement_MimicTuning", "MimicInventoryCopyPickRule")] = new string[3] { "MinDistance", "MaxDistance", "Random" }, [("MimesisPlayerEnhancement_MimicTuning", "PossessionBtGateMode")] = new string[2] { "Vanilla", "Always" }, [("MimesisPlayerEnhancement_MimicTuning", "MimicEmotePropsMode")] = new string[2] { "Vanilla", "Custom" }, [("MimesisPlayerEnhancement_MimicTuning", "HornImitationMode")] = new string[2] { "Vanilla", "Custom" }, [("MimesisPlayerEnhancement_Ui", "RoundStartSoundMode")] = new string[3] { "Vanilla", "Random", "Specific" }, [("MimesisPlayerEnhancement_Ui", "DiscoBallSoundMode")] = new string[3] { "Vanilla", "Random", "Specific" }, [("MimesisPlayerEnhancement_Ui", "CustomLoadingScreenMode")] = new string[3] { "Vanilla", "Random", "Specific" }, [("MimesisPlayerEnhancement_Ui", "InventoryPickupSelectMode")] = new string[3] { "Vanilla", "WeaponsOnly", "Always" }, [("MimesisPlayerEnhancement_Ui", "SpectatorVoiceBalanceMode")] = new string[3] { "Vanilla", "SpeechDucking", "StaticAttenuation" } }; internal static string ResolveInputKind(string sectionId, string key) { if (sectionId == "MimesisPlayerEnhancement_LootMultiplicator" && (string.Equals(key, "LootAllowlist", StringComparison.Ordinal) || string.Equals(key, "LootBlocklist", StringComparison.Ordinal))) { return "ItemIdList"; } if (sectionId == "MimesisPlayerEnhancement_DungeonRandomizer" && (string.Equals(key, "DungeonAllowlist", StringComparison.Ordinal) || string.Equals(key, "DungeonBlocklist", StringComparison.Ordinal))) { return "DungeonIdList"; } if (sectionId == "MimesisPlayerEnhancement_Weather" && string.Equals(key, "WeatherCyclePresets", StringComparison.Ordinal)) { return "WeatherPresetList"; } if (sectionId == "MimesisPlayerEnhancement_Ui" && (string.Equals(key, "RoundStartSoundRandomPool", StringComparison.Ordinal) || string.Equals(key, "DiscoBallSoundRandomPool", StringComparison.Ordinal) || string.Equals(key, "CustomLoadingScreenRandomPool", StringComparison.Ordinal))) { return "VariantIdList"; } if (sectionId == "MimesisPlayerEnhancement_Ui" && string.Equals(key, "RoundStartSoundVariant", StringComparison.Ordinal)) { return "Select"; } if (sectionId == "MimesisPlayerEnhancement_Ui" && string.Equals(key, "DiscoBallSoundVariant", StringComparison.Ordinal)) { return "Select"; } if (sectionId == "MimesisPlayerEnhancement_Ui" && string.Equals(key, "CustomLoadingScreenVariant", StringComparison.Ordinal)) { return "Select"; } if (SelectValuesByEntry.ContainsKey((sectionId, key))) { return "Select"; } return "Default"; } internal static void ApplyToEntry(WebDashboardConfigEntryDto entry, string sectionId, string key) { entry.InputKind = ResolveInputKind(sectionId, key); if (string.Equals(entry.InputKind, "VariantIdList", StringComparison.Ordinal)) { if (sectionId == "MimesisPlayerEnhancement_Ui" && string.Equals(key, "RoundStartSoundRandomPool", StringComparison.Ordinal)) { entry.SelectOptions = BuildRoundStartSoundVariantOptions(); } else if (sectionId == "MimesisPlayerEnhancement_Ui" && string.Equals(key, "DiscoBallSoundRandomPool", StringComparison.Ordinal)) { entry.SelectOptions = BuildDiscoBallSoundVariantOptions(); } else if (sectionId == "MimesisPlayerEnhancement_Ui" && string.Equals(key, "CustomLoadingScreenRandomPool", StringComparison.Ordinal)) { entry.SelectOptions = BuildCustomLoadingScreenVariantOptions(); } } else { if (!string.Equals(entry.InputKind, "Select", StringComparison.Ordinal)) { return; } string[] value; if (sectionId == "MimesisPlayerEnhancement_Ui" && string.Equals(key, "RoundStartSoundVariant", StringComparison.Ordinal)) { entry.SelectOptions = BuildRoundStartSoundVariantOptions(); } else if (sectionId == "MimesisPlayerEnhancement_Ui" && string.Equals(key, "DiscoBallSoundVariant", StringComparison.Ordinal)) { entry.SelectOptions = BuildDiscoBallSoundVariantOptions(); } else if (sectionId == "MimesisPlayerEnhancement_Ui" && string.Equals(key, "CustomLoadingScreenVariant", StringComparison.Ordinal)) { entry.SelectOptions = BuildCustomLoadingScreenVariantOptions(); } else if (SelectValuesByEntry.TryGetValue((sectionId, key), out value)) { List list = new List(); string[] array = value; foreach (string text in array) { string configSelectOptionLabel = WebDashboardL10n.GetConfigSelectOptionLabel(sectionId, key, text); list.Add(new WebDashboardConfigSelectOptionDto { Value = text, Label = (string.IsNullOrWhiteSpace(configSelectOptionLabel) ? text : configSelectOptionLabel) }); } entry.SelectOptions = list; } } } internal static void AssignEntryGroups(WebDashboardConfigSectionDto section) { string id = section.Id; string b = section.FeatureToggle?.Key; List entries = section.Entries; Dictionary explicitGroups = GetExplicitGroups(id); foreach (WebDashboardConfigEntryDto item in entries) { if (explicitGroups.TryGetValue(item.Key, out var value)) { item.EntryGroup = id + "::" + value; } } for (int i = 0; i < entries.Count; i++) { WebDashboardConfigEntryDto webDashboardConfigEntryDto = entries[i]; if (!webDashboardConfigEntryDto.Key.StartsWith("Enable", StringComparison.Ordinal) || string.Equals(webDashboardConfigEntryDto.Key, b, StringComparison.Ordinal) || !string.IsNullOrEmpty(webDashboardConfigEntryDto.EntryGroup)) { continue; } string entryGroup = (webDashboardConfigEntryDto.EntryGroup = id + "::" + webDashboardConfigEntryDto.Key); for (int j = i + 1; j < entries.Count; j++) { WebDashboardConfigEntryDto webDashboardConfigEntryDto2 = entries[j]; if (webDashboardConfigEntryDto2.Key.StartsWith("Enable", StringComparison.Ordinal) || !string.IsNullOrEmpty(webDashboardConfigEntryDto2.EntryGroup)) { break; } webDashboardConfigEntryDto2.EntryGroup = entryGroup; } } } private static Dictionary GetExplicitGroups(string sectionId) { return sectionId switch { "MimesisPlayerEnhancement_LootMultiplicator" => new Dictionary(StringComparer.Ordinal) { ["LootMultiplicatorBaselinePlayerCount"] = "playerScaling", ["MapLootMultiplier"] = "mapLoot", ["MapLootPerPlayerMultiplier"] = "mapLoot", ["DropLootMultiplier"] = "dropLoot", ["DropLootPerPlayerMultiplier"] = "dropLoot", ["LootItemFilterMode"] = "lootFilter", ["LootAllowlist"] = "lootFilter", ["LootBlocklist"] = "lootFilter", ["AutoScaleMapLootBudgetForFilter"] = "lootFilter" }, "MimesisPlayerEnhancement_Economy" => new Dictionary(StringComparer.Ordinal) { ["EconomyBaselinePlayerCount"] = "playerScaling", ["ScrapSellValueMultiplier"] = "scrapSellValue", ["ScrapSellValuePerPlayerMultiplier"] = "scrapSellValue", ["ShopBuyPriceMultiplier"] = "shopBuyPrice", ["ShopBuyPricePerPlayerMultiplier"] = "shopBuyPrice", ["ReinforcePriceMultiplier"] = "reinforcePrice", ["ReinforcePricePerPlayerMultiplier"] = "reinforcePrice" }, "MimesisPlayerEnhancement_DungeonRandomizer" => new Dictionary(StringComparer.Ordinal) { ["RandomizeDungeonPick"] = "dungeonPick", ["DungeonPickPoolMode"] = "dungeonPick", ["DungeonAllowlist"] = "dungeonPick", ["DungeonBlocklist"] = "dungeonPick", ["IgnoreDungeonExcludeList"] = "dungeonPick", ["DungeonSeedFlavor"] = "dungeonSeed" }, "MimesisPlayerEnhancement_SpawnScaling" => new Dictionary(StringComparer.Ordinal) { ["SpawnScalingBaselinePlayerCount"] = "playerScaling", ["MimicSpawnMultiplier"] = "mimicSpawn", ["MimicSpawnPerPlayerMultiplier"] = "mimicSpawn", ["MimicWaveMode"] = "mimicWaveTiming", ["MimicWaveInitialDelaySeconds"] = "mimicWaveTiming", ["MimicWaveInitialDelayMinSeconds"] = "mimicWaveTiming", ["MimicWaveInitialDelayMaxSeconds"] = "mimicWaveTiming", ["MimicWaveIntervalSeconds"] = "mimicWaveTiming", ["MimicWaveIntervalMinSeconds"] = "mimicWaveTiming", ["MimicWaveIntervalMaxSeconds"] = "mimicWaveTiming", ["BossSpawnMultiplier"] = "bossSpawn", ["BossSpawnPerPlayerMultiplier"] = "bossSpawn", ["GruntSpawnMultiplier"] = "gruntSpawn", ["GruntSpawnPerPlayerMultiplier"] = "gruntSpawn", ["GruntWaveMode"] = "gruntWaveTiming", ["GruntWaveInitialDelaySeconds"] = "gruntWaveTiming", ["GruntWaveInitialDelayMinSeconds"] = "gruntWaveTiming", ["GruntWaveInitialDelayMaxSeconds"] = "gruntWaveTiming", ["GruntWaveIntervalSeconds"] = "gruntWaveTiming", ["GruntWaveIntervalMinSeconds"] = "gruntWaveTiming", ["GruntWaveIntervalMaxSeconds"] = "gruntWaveTiming", ["SpecialSpawnMultiplier"] = "specialSpawn", ["SpecialSpawnPerPlayerMultiplier"] = "specialSpawn", ["TrapSpawnMultiplier"] = "trapSpawn", ["TrapSpawnPerPlayerMultiplier"] = "trapSpawn", ["OtherSpawnMultiplier"] = "otherSpawn", ["OtherSpawnPerPlayerMultiplier"] = "otherSpawn", ["TrapRespawnMode"] = "trapRespawn", ["TrapRespawnDelaySeconds"] = "trapRespawn", ["TrapRespawnDelayMinSeconds"] = "trapRespawn", ["TrapRespawnDelayMaxSeconds"] = "trapRespawn", ["TrapRespawnMinPlayerDistanceMeters"] = "trapRespawn", ["BonusEncounterDelayMinSeconds"] = "bonusEncounterTiming", ["BonusEncounterDelayMaxSeconds"] = "bonusEncounterTiming", ["BonusEncounterMinPlayerDistanceMeters"] = "bonusEncounterTiming" }, "MimesisPlayerEnhancement_MimicTuning" => new Dictionary(StringComparer.Ordinal) { ["MimicVoiceTuningMode"] = "mimicVoice", ["PeriodicVoiceIntervalMultiplier"] = "mimicVoice", ["PlayerVoiceResponseChancePercent"] = "mimicVoice", ["PlayerVoiceResponseCooldownSeconds"] = "mimicVoice", ["PlayerVoiceResponseDelayMinSeconds"] = "mimicVoice", ["PlayerVoiceResponseDelayMaxSeconds"] = "mimicVoice", ["PlayerVoiceResponseMaxDistance"] = "mimicVoice", ["ClipReuseCooldownSeconds"] = "mimicVoice", ["DeathMatchClipReuseCooldownSeconds"] = "mimicVoice", ["SpeakAudienceRangeMeters"] = "mimicVoice", ["PostReplyIntervalMode"] = "mimicVoice", ["PostReplyIntervalFixedSeconds"] = "mimicVoice", ["PostReplyIntervalMinSeconds"] = "mimicVoice", ["PostReplyIntervalMaxSeconds"] = "mimicVoice", ["MinRequiredSpeechClips"] = "mimicVoice", ["HearOwnVoiceFromMimic"] = "mimicVoice", ["VoiceInitIntervalMode"] = "mimicVoice", ["VoiceInitIntervalMin"] = "mimicVoice", ["VoiceInitIntervalMax"] = "mimicVoice", ["VoicePeriodicIntervalMode"] = "mimicVoice", ["VoicePeriodicIntervalMin"] = "mimicVoice", ["VoicePeriodicIntervalMax"] = "mimicVoice", ["VoiceDeathMatchIntervalMode"] = "mimicVoice", ["VoiceDeathMatchIntervalMin"] = "mimicVoice", ["VoiceDeathMatchIntervalMax"] = "mimicVoice", ["MimicTrustMode"] = "mimicTrust", ["TrustOutdoorMultiplier"] = "mimicTrust", ["TrustLookingDelta"] = "mimicTrust", ["TrustNotLookingDelta"] = "mimicTrust", ["TrustApproachDelta"] = "mimicTrust", ["TrustMaintainDelta"] = "mimicTrust", ["TrustWalkAwayDelta"] = "mimicTrust", ["TrustSprintAwayDelta"] = "mimicTrust", ["TrustHitDamageMultiplier"] = "mimicTrust", ["TrustFriendlyThreshold"] = "mimicTrust", ["TrustDistrustThreshold"] = "mimicTrust", ["TrustScoreValueMode"] = "mimicTrust", ["TrustInitialFixed"] = "mimicTrust", ["TrustInitialRandomMin"] = "mimicTrust", ["TrustInitialRandomMax"] = "mimicTrust", ["TrustBehaviorFixed"] = "mimicTrust", ["TrustBehaviorRandomMin"] = "mimicTrust", ["TrustBehaviorRandomMax"] = "mimicTrust", ["ChaseActivationDistanceMeters"] = "mimicTrust", ["ChaseForceRunDistanceMeters"] = "mimicTrust", ["MimicSocialMode"] = "mimicSocial", ["MimicRunawayChance"] = "mimicSocial", ["JumpCopyChancePercent"] = "mimicSocial", ["SlotFollowChangeChancePercent"] = "mimicSocial", ["EnableMimicPossessionTuning"] = "mimicPossession", ["RandomizeMimicPossessionDuration"] = "mimicPossession", ["MimicPossessionMinTimeSeconds"] = "mimicPossession", ["MimicPossessionMaxTimeSeconds"] = "mimicPossession", ["MimicPossessionCooltimeMultiplier"] = "mimicPossession", ["PossessionRangeMeters"] = "mimicPossession", ["PossessionBtGateMode"] = "mimicPossession", ["MimicEmotePropsMode"] = "mimicEmoteProps", ["EmoteRespondChancePercent"] = "mimicEmoteProps", ["EmoteSuggestChancePercent"] = "mimicEmoteProps", ["ReactToSprinklerChancePercent"] = "mimicEmoteProps", ["UseTrapSwitchChancePercent"] = "mimicEmoteProps", ["UseChargerChancePercent"] = "mimicEmoteProps", ["UseTransmitterChancePercent"] = "mimicEmoteProps", ["UseShutterSwitchChancePercent"] = "mimicEmoteProps", ["HornImitationMode"] = "mimicHorn", ["AllowHornImitation"] = "mimicHorn", ["HornMaxRecordSeconds"] = "mimicHorn", ["HornRecordingGapSeconds"] = "mimicHorn", ["HornMaxStoredRecords"] = "mimicHorn" }, "MimesisPlayerEnhancement_DungeonTime" => new Dictionary(StringComparer.Ordinal) { ["StartTimePreset"] = "startTime", ["TimeMultiplier"] = "startTime", ["EnableRealtimeTramClock"] = "startTime" }, "MimesisPlayerEnhancement_Weather" => new Dictionary(StringComparer.Ordinal) { ["WeatherMode"] = "weatherMode", ["FixedWeatherPreset"] = "weatherMode", ["DisableRandomWeather"] = "weatherMode", ["WeatherCyclePresets"] = "weatherCycle", ["WeatherCycleMinDelaySeconds"] = "weatherCycle", ["WeatherCycleMaxDelaySeconds"] = "weatherCycle" }, "MimesisPlayerEnhancement_Ui" => new Dictionary(StringComparer.Ordinal) { ["ModToastDurationSeconds"] = "modToasts", ["EnableExtendedSaveSlots"] = "extendedSaveSlots", ["EnableExtendedSpectatorPlayerList"] = "extendedSpectatorList", ["EnableLoadingWaitPlayerList"] = "customLoadingScreen", ["EnableExtendedInGameMenuPlayerList"] = "extendedInGameMenuPlayerList", ["EnableDamageHealthGlow"] = "damageHealthGlow", ["EnableFloatingDamageNumbers"] = "floatingDamage", ["FloatingDamageDurationSeconds"] = "floatingDamage", ["EnableFpsUi"] = "fpsUi", ["EnableFpsUiInventoryNetWorth"] = "fpsUi", ["EnableInventorySlotOptimization"] = "inventorySlotOptimization", ["InventoryPickupSelectMode"] = "inventorySlotOptimization", ["EnableInventorySelectNextOnRemove"] = "inventorySlotOptimization", ["EnableInventoryNumberKeySelection"] = "inventoryNumberKeys", ["RoundStartSoundMode"] = "roundStartSound", ["RoundStartSoundRandomPool"] = "roundStartSound", ["RoundStartSoundVariant"] = "roundStartSound", ["RoundStartSoundVolume"] = "roundStartSound", ["DiscoBallSoundMode"] = "discoBallSound", ["DiscoBallSoundRandomPool"] = "discoBallSound", ["DiscoBallSoundVariant"] = "discoBallSound", ["DiscoBallSoundVolume"] = "discoBallSound", ["CustomLoadingScreenMode"] = "customLoadingScreen", ["CustomLoadingScreenRandomPool"] = "customLoadingScreen", ["CustomLoadingScreenVariant"] = "customLoadingScreen", ["CustomLoadingScreenMotion"] = "customLoadingScreen", ["SpectatorVoiceBalanceMode"] = "spectatorVoiceBalance", ["SpectatorVoiceAttenuation"] = "spectatorVoiceBalance", ["SpectatorVoiceDuckLevel"] = "spectatorVoiceBalance", ["EnableVoiceNoiseGate"] = "voiceNoiseGate", ["VoiceNoiseGateStrength"] = "voiceNoiseGate" }, "MimesisPlayerEnhancement_Privacy" => new Dictionary(StringComparer.Ordinal) { ["BlockReluTelemetry"] = "telemetry", ["BlockReplayUpload"] = "replay", ["BlockReplayRecording"] = "replay", ["BlockCrashReports"] = "crashReports", ["StripCrashReportMetadata"] = "crashReports", ["BlockKraftonGppSdk"] = "thirdParty" }, "MimesisPlayerEnhancement_SavegamePreparation" => new Dictionary(StringComparer.Ordinal) { ["StartupMoney"] = "startupMoney", ["StartingZone"] = "startingZone", ["EnableUpgradeTramHorn"] = "upgrades", ["EnableUpgradeScrapScanner"] = "upgrades", ["EnableUpgradeTramBooster"] = "upgrades", ["EnableUpgradeTramLight"] = "upgrades" }, "MimesisPlayerEnhancement_MoreVoices" => new Dictionary(StringComparer.Ordinal) { ["UnifyIndoorOutdoorVoices"] = "voiceLimits", ["MaxIndoorVoiceEvents"] = "voiceLimits", ["MaxDeathMatchVoiceEvents"] = "voiceLimits", ["MaxOutdoorVoiceEvents"] = "voiceLimits", ["RecordVoiceInMaintenance"] = "voiceRecording", ["RecordVoiceInTram"] = "voiceRecording", ["RecordVoiceDuringMimicPossession"] = "voiceRecording" }, _ => new Dictionary(StringComparer.Ordinal), }; } private static List BuildRoundStartSoundVariantOptions() { List list = new List(); foreach (string item in RoundStartSoundResolver.ListVariantOptionValues()) { string configSelectOptionLabel = WebDashboardL10n.GetConfigSelectOptionLabel("MimesisPlayerEnhancement_Ui", "RoundStartSoundVariant", item); list.Add(new WebDashboardConfigSelectOptionDto { Value = item, Label = (string.IsNullOrWhiteSpace(configSelectOptionLabel) ? RoundStartSoundResolver.FormatVariantDisplayName(item) : configSelectOptionLabel) }); } return list; } private static List BuildDiscoBallSoundVariantOptions() { List list = new List(); foreach (string item in DiscoBallSoundResolver.ListVariantOptionValues()) { string configSelectOptionLabel = WebDashboardL10n.GetConfigSelectOptionLabel("MimesisPlayerEnhancement_Ui", "DiscoBallSoundVariant", item); list.Add(new WebDashboardConfigSelectOptionDto { Value = item, Label = (string.IsNullOrWhiteSpace(configSelectOptionLabel) ? DiscoBallSoundResolver.FormatVariantDisplayName(item) : configSelectOptionLabel) }); } return list; } private static List BuildCustomLoadingScreenVariantOptions() { List list = new List(); foreach (string item in CustomLoadingScreenResolver.ListVariantOptionValues()) { string configSelectOptionLabel = WebDashboardL10n.GetConfigSelectOptionLabel("MimesisPlayerEnhancement_Ui", "CustomLoadingScreenVariant", item); list.Add(new WebDashboardConfigSelectOptionDto { Value = item, Label = (string.IsNullOrWhiteSpace(configSelectOptionLabel) ? CustomLoadingScreenResolver.FormatVariantDisplayName(item) : configSelectOptionLabel) }); } return list; } } internal static class ModConfigFloatHelper { internal const int DecimalPlaces = 2; internal static float Round(float value) { return (float)Math.Round(value, 2, MidpointRounding.AwayFromZero); } internal static string Format(float value) { return Round(value).ToString("0.0#", CultureInfo.InvariantCulture); } internal static void SanitizeEntry(MelonPreferences_Entry entry) { float num = Round(entry.Value); if (!entry.Value.Equals(num)) { entry.Value = num; } } internal static void SanitizeAll(IReadOnlyList> entries) { for (int i = 0; i < entries.Count; i++) { SanitizeEntry(entries[i]); } } internal static void NormalizeSavedFloats(string filePath, IReadOnlyList> entries) { } } internal static class ModConfigLocalization { private static string _appliedLocale = ""; internal static void Apply() { Apply(GameLocaleAccess.GetCurrentLanguage()); } internal static void Apply(string locale) { if (!ModConfig.IsInitialized) { return; } locale = GameLocaleAccess.NormalizeLanguageCode(locale); foreach (string item in ModConfigRegistry.GetSectionOrder()) { string text = ModL10n.GetConfigSectionTitle(item, locale) ?? item; ModConfigRegistry.SetSectionTitle(item, text); MelonPreferences_Category val = null; foreach (string item2 in ModConfigRegistry.GetEntryOrder(item)) { if (ModConfigRegistry.TryGetEntry(item, item2, out MelonPreferences_Entry entry) && entry != null) { if (val == null) { val = entry.Category; } entry.DisplayName = ModL10n.GetConfigEntryTitle(item, item2, locale) ?? item2; entry.Description = ModL10n.GetConfigEntryDescription(item, item2, locale) ?? string.Empty; } } if (val != null) { val.DisplayName = text; } } _appliedLocale = locale; } internal static void RefreshIfLanguageChanged() { string currentLanguage = GameLocaleAccess.GetCurrentLanguage(); if (!string.Equals(currentLanguage, _appliedLocale, StringComparison.OrdinalIgnoreCase)) { Apply(currentLanguage); ModConfig.SaveToFile(); } } } internal static class ModConfigRegistry { internal const string MainSectionId = "MimesisPlayerEnhancement"; internal const string WebDashboardSectionId = "MimesisPlayerEnhancement_WebDashboard"; private const string SectionPrefix = "MimesisPlayerEnhancement_"; private static readonly Dictionary> EntriesBySection = new Dictionary>(StringComparer.OrdinalIgnoreCase); private static readonly List SectionOrder = new List(); private static readonly Dictionary> EntryOrderBySection = new Dictionary>(StringComparer.Ordinal); private static readonly Dictionary SectionTitles = new Dictionary(StringComparer.Ordinal); internal static int Version { get; private set; } internal static IReadOnlyList GetSectionOrder() { return SectionOrder; } internal static IReadOnlyList GetEntryOrder(string sectionId) { if (!EntryOrderBySection.TryGetValue(sectionId, out List value)) { return new List(); } return value; } internal static bool TryGetSectionTitle(string sectionId, out string title) { return SectionTitles.TryGetValue(sectionId, out title); } internal static void SetSectionTitle(string sectionId, string title) { if (SectionTitles.ContainsKey(sectionId)) { SectionTitles[sectionId] = title; } } internal static void ClearRegistrationOrder() { SectionOrder.Clear(); EntryOrderBySection.Clear(); SectionTitles.Clear(); } internal static void TrackCategory(MelonPreferences_Category category) { string identifier = category.Identifier; if (!EntryOrderBySection.ContainsKey(identifier)) { SectionOrder.Add(identifier); EntryOrderBySection[identifier] = new List(); SectionTitles[identifier] = category.DisplayName ?? identifier; } } internal static void TrackEntry(MelonPreferences_Entry entry) { string identifier = entry.Category.Identifier; if (!EntryOrderBySection.TryGetValue(identifier, out List value)) { TrackCategory(entry.Category); value = EntryOrderBySection[identifier]; } value.Add(entry.Identifier); } internal static string FormatEntryValue(MelonPreferences_Entry entry) { Type reflectedType = entry.GetReflectedType(); if (reflectedType == typeof(float) && entry.BoxedValue is float value) { return ModConfigFloatHelper.Format(value); } if (reflectedType == typeof(bool)) { object boxedValue = entry.BoxedValue; if (boxedValue is bool) { if (!(bool)boxedValue) { return "false"; } return "true"; } } return SafeGetString((Func)entry.GetValueAsString); } internal static string FormatEntryDefaultValue(MelonPreferences_Entry entry) { Type reflectedType = entry.GetReflectedType(); string text = SafeGetString((Func)entry.GetDefaultValueAsString); if (reflectedType == typeof(float) && float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { return ModConfigFloatHelper.Format(result); } if (reflectedType == typeof(bool) && bool.TryParse(text, out var result2)) { if (!result2) { return "false"; } return "true"; } return text; } private static string SafeGetString(Func getter) { try { return getter() ?? ""; } catch { return ""; } } internal static void Rebuild() { EntriesBySection.Clear(); PropertyInfo[] properties = typeof(ModConfig).GetProperties(BindingFlags.Static | BindingFlags.Public); foreach (PropertyInfo propertyInfo in properties) { Type propertyType = propertyInfo.PropertyType; if (propertyType.IsGenericType && !(propertyType.GetGenericTypeDefinition() != typeof(MelonPreferences_Entry<>))) { object? value = propertyInfo.GetValue(null); MelonPreferences_Entry val = (MelonPreferences_Entry)((value is MelonPreferences_Entry) ? value : null); if (val != null) { Register(val); } } } } internal static void PurgeObsoleteFromFile(string filePath, bool allowProfileSection = false) { if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath)) { SparseTomlConfig.Document doc = SparseTomlConfig.Load(AtomicFileIO.ReadText(filePath, "ModConfigRegistry")); if (SparseTomlConfig.PurgeUnregisteredEntries(doc, allowProfileSection)) { AtomicFileIO.WriteText(filePath, SparseTomlConfig.SerializeRaw(doc), "ModConfigRegistry"); } } } internal static bool TryGetEntry(string sectionId, string key, out MelonPreferences_Entry? entry) { entry = null; if (!string.IsNullOrWhiteSpace(sectionId) && !string.IsNullOrWhiteSpace(key) && EntriesBySection.TryGetValue(sectionId, out Dictionary value)) { return value.TryGetValue(key, out entry); } return false; } internal static bool TrySetEntryValue(string sectionId, string key, string rawValue, out string? error) { error = null; if (!ModConfig.IsInitialized) { error = "Configuration is not initialized."; return false; } if (!TryGetEntry(sectionId, key, out MelonPreferences_Entry entry) || entry == null) { error = "Unknown setting."; return false; } if (!TryParseValue(entry.GetReflectedType() ?? throw new InvalidOperationException("Setting " + sectionId + "/" + key + " has no value type."), rawValue, out object value, out error)) { return false; } if (BoxedValuesEqual(entry, value)) { return true; } try { if (!TrySetBoxedValue(entry, value)) { error = "Failed to apply setting value."; return false; } } catch (Exception ex) { error = ex.Message; return false; } return true; } internal static bool TryApplyNormalizedEntry(string sectionId, string key, string normalized, out string effectiveValue, out string? error) { effectiveValue = normalized; error = null; if (!TrySetEntryValue(sectionId, key, normalized, out error)) { return false; } ModConfig.SanitizeFloatEntries(); if (!TryGetEntry(sectionId, key, out MelonPreferences_Entry entry) || entry == null) { return true; } effectiveValue = FormatEntryValue(entry); return true; } internal static void SaveToFile() { if (ModConfig.IsInitialized && !SaveSlotConfigStore.IsApplyingOverrides && !HostConfigMirror.BlocksGlobalConfigPersistence) { GlobalConfigStore.RebuildPendingFromMemory(); } } internal static void NotifyRuntimeChange() { Version++; } internal static bool TryGetFeatureModuleName(string sectionId, out string moduleName) { moduleName = ""; if (string.Equals(sectionId, "MimesisPlayerEnhancement", StringComparison.OrdinalIgnoreCase)) { return false; } if (!sectionId.StartsWith("MimesisPlayerEnhancement_", StringComparison.OrdinalIgnoreCase)) { return false; } moduleName = sectionId.Substring("MimesisPlayerEnhancement_".Length); return moduleName.Length > 0; } internal static HashSet GetAffectedModuleNames(ModConfigChangeInfo info) { HashSet hashSet = new HashSet(StringComparer.Ordinal); foreach (ModConfigKeyChange changedKey in info.ChangedKeys) { if (TryGetFeatureModuleName(changedKey.SectionId, out string moduleName)) { hashSet.Add(moduleName); if (string.Equals(moduleName, "MorePlayers", StringComparison.Ordinal)) { hashSet.Add("JoinAnytime"); } } } return hashSet; } internal static bool TryGetFeatureToggleKey(string sectionId, out string key) { key = ""; if (string.Equals(sectionId, "MimesisPlayerEnhancement", StringComparison.OrdinalIgnoreCase)) { return false; } if (string.Equals(sectionId, "MimesisPlayerEnhancement_Ui", StringComparison.OrdinalIgnoreCase)) { return false; } if (string.Equals(sectionId, "MimesisPlayerEnhancement_SavegamePreparation", StringComparison.OrdinalIgnoreCase)) { return false; } foreach (string item in GetEntryOrder(sectionId)) { if (TryGetEntry(sectionId, item, out MelonPreferences_Entry entry) && entry != null && !(entry.GetReflectedType() != typeof(bool)) && (item.StartsWith("Enable", StringComparison.Ordinal) || string.Equals(item, "ShowPlayerAnnouncements", StringComparison.Ordinal))) { key = item; return true; } } return false; } internal static bool IsWebDashboardSection(string sectionId) { return string.Equals(sectionId, "MimesisPlayerEnhancement_WebDashboard", StringComparison.OrdinalIgnoreCase); } internal static bool IsGlobalOnlySection(string sectionId) { if (string.IsNullOrWhiteSpace(sectionId)) { return true; } if (string.Equals(sectionId, "MimesisPlayerEnhancement", StringComparison.OrdinalIgnoreCase)) { return true; } if (IsWebDashboardSection(sectionId)) { return true; } if (!string.Equals(sectionId, "MimesisPlayerEnhancement_Ui", StringComparison.OrdinalIgnoreCase) && !string.Equals(sectionId, "MimesisPlayerEnhancement_Privacy", StringComparison.OrdinalIgnoreCase)) { return string.Equals(sectionId, "MimesisPlayerEnhancement_SavegamePreparation", StringComparison.OrdinalIgnoreCase); } return true; } internal static bool IsSaveOverrideAllowed(string sectionId, string key) { if (string.IsNullOrWhiteSpace(sectionId) || string.IsNullOrWhiteSpace(key)) { return false; } if (SaveSlotConfigProfile.IsProfileSection(sectionId)) { return false; } if (IsGlobalOnlySection(sectionId)) { return false; } MelonPreferences_Entry entry; return TryGetEntry(sectionId, key, out entry); } internal static IEnumerable<(string SectionId, string Key)> EnumerateSaveOverrideableKeys() { foreach (string sectionId in GetSectionOrder()) { foreach (string item in GetEntryOrder(sectionId)) { if (IsSaveOverrideAllowed(sectionId, item)) { yield return (SectionId: sectionId, Key: item); } } } } internal static bool TryNormalizeRawValue(string sectionId, string key, string rawValue, out string normalized, out string? error) { normalized = ""; error = null; if (!TryGetEntry(sectionId, key, out MelonPreferences_Entry entry) || entry == null) { error = "Unknown setting."; return false; } if (!TryParseValue(entry.GetReflectedType() ?? throw new InvalidOperationException("Setting " + sectionId + "/" + key + " has no value type."), rawValue, out object value, out error)) { return false; } normalized = FormatParsedValue(entry, value); return true; } internal static bool RawValuesEqual(string sectionId, string key, string rawA, string rawB) { if (string.Equals(rawA, rawB, StringComparison.Ordinal)) { return true; } if (!TryNormalizeRawValue(sectionId, key, rawA, out string normalized, out string error) || !TryNormalizeRawValue(sectionId, key, rawB, out string normalized2, out error)) { return string.Equals(rawA?.Trim(), rawB?.Trim(), StringComparison.OrdinalIgnoreCase); } return string.Equals(normalized, normalized2, StringComparison.Ordinal); } internal static bool TryGetGlobalRawValue(string sectionId, string key, out string globalRaw) { globalRaw = ""; if (!TryGetEntry(sectionId, key, out MelonPreferences_Entry entry) || entry == null) { return false; } if (string.IsNullOrEmpty(ModConfig.FilePath) || !File.Exists(ModConfig.FilePath)) { globalRaw = FormatEntryDefaultValue(entry); return true; } if (SparseTomlConfig.Load(File.ReadAllText(ModConfig.FilePath)).Sections.TryGetValue(sectionId, out Dictionary value) && value.TryGetValue(key, out var value2)) { if (TryNormalizeRawValue(sectionId, key, value2, out string normalized, out string _)) { globalRaw = normalized; return true; } globalRaw = value2; return true; } globalRaw = FormatEntryDefaultValue(entry); return true; } private static string FormatParsedValue(MelonPreferences_Entry entry, object? parsed) { Type reflectedType = entry.GetReflectedType(); if (reflectedType == typeof(float) && parsed is float value) { return ModConfigFloatHelper.Format(value); } if (reflectedType == typeof(bool) && parsed is bool) { if (!(bool)parsed) { return "false"; } return "true"; } return Convert.ToString(parsed, CultureInfo.InvariantCulture) ?? ""; } private static bool BoxedValuesEqual(MelonPreferences_Entry entry, object? parsed) { object boxedValue = entry.BoxedValue; if (boxedValue == null && parsed == null) { return true; } if (boxedValue == null || parsed == null) { return false; } if (boxedValue.Equals(parsed)) { return true; } if (boxedValue is float num && parsed is float num2) { return Math.Abs(num - num2) < 0.0001f; } if (boxedValue is double num3 && parsed is double num4) { return Math.Abs(num3 - num4) < 0.0001; } return false; } private static bool TrySetBoxedValue(MelonPreferences_Entry entry, object? parsed) { PropertyInfo property = ((object)entry).GetType().GetProperty("Value"); if (property?.GetSetMethod() != null) { property.SetValue(entry, parsed); return true; } entry.BoxedValue = parsed; return true; } private static void Register(MelonPreferences_Entry entry) { string identifier = entry.Category.Identifier; if (!EntriesBySection.TryGetValue(identifier, out Dictionary value)) { value = new Dictionary(StringComparer.OrdinalIgnoreCase); EntriesBySection[identifier] = value; } value[entry.Identifier] = entry; } private static bool TryParseValue(Type type, string rawValue, out object? value, out string? error) { value = null; error = null; rawValue = rawValue?.Trim() ?? ""; if (type == typeof(bool)) { if (bool.TryParse(rawValue, out var result)) { value = result; return true; } if (string.Equals(rawValue, "1", StringComparison.Ordinal) || string.Equals(rawValue, "on", StringComparison.OrdinalIgnoreCase) || string.Equals(rawValue, "yes", StringComparison.OrdinalIgnoreCase)) { value = true; return true; } if (string.Equals(rawValue, "0", StringComparison.Ordinal) || string.Equals(rawValue, "off", StringComparison.OrdinalIgnoreCase) || string.Equals(rawValue, "no", StringComparison.OrdinalIgnoreCase)) { value = false; return true; } error = "Invalid boolean value."; return false; } if (type == typeof(int)) { if (int.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result2)) { value = result2; return true; } error = "Invalid integer value."; return false; } if (type == typeof(float)) { if (float.TryParse(rawValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var result3)) { value = result3; return true; } error = "Invalid number value."; return false; } if (type == typeof(string)) { value = rawValue; return true; } error = "Unsupported setting type: " + type.Name + "."; return false; } } internal static class ModConfigSectionGroups { internal const string Client = "client"; internal const string Session = "session"; internal const string Balance = "balance"; internal const string World = "world"; private static readonly string[] GroupOrder = new string[4] { "client", "session", "balance", "world" }; private static readonly Dictionary SectionToGroup = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["MimesisPlayerEnhancement"] = "client", ["MimesisPlayerEnhancement_Ui"] = "client", ["MimesisPlayerEnhancement_Privacy"] = "client", ["MimesisPlayerEnhancement_SavegamePreparation"] = "session", ["MimesisPlayerEnhancement_MorePlayers"] = "session", ["MimesisPlayerEnhancement_JoinAnytime"] = "session", ["MimesisPlayerEnhancement_MoreVoices"] = "session", ["MimesisPlayerEnhancement_Persistence"] = "session", ["MimesisPlayerEnhancement_PlayerAnnouncements"] = "session", ["MimesisPlayerEnhancement_Statistics"] = "session", ["MimesisPlayerEnhancement_SpawnScaling"] = "balance", ["MimesisPlayerEnhancement_LootMultiplicator"] = "balance", ["MimesisPlayerEnhancement_Economy"] = "balance", ["MimesisPlayerEnhancement_DungeonTime"] = "balance", ["MimesisPlayerEnhancement_MimicTuning"] = "world", ["MimesisPlayerEnhancement_PlayerTuning"] = "world", ["MimesisPlayerEnhancement_DungeonRandomizer"] = "world", ["MimesisPlayerEnhancement_Weather"] = "world" }; internal static readonly string[] PreferredSectionOrder = new string[18] { "MimesisPlayerEnhancement", "MimesisPlayerEnhancement_Privacy", "MimesisPlayerEnhancement_Ui", "MimesisPlayerEnhancement_SavegamePreparation", "MimesisPlayerEnhancement_JoinAnytime", "MimesisPlayerEnhancement_MorePlayers", "MimesisPlayerEnhancement_MoreVoices", "MimesisPlayerEnhancement_Persistence", "MimesisPlayerEnhancement_PlayerAnnouncements", "MimesisPlayerEnhancement_Statistics", "MimesisPlayerEnhancement_DungeonTime", "MimesisPlayerEnhancement_Economy", "MimesisPlayerEnhancement_LootMultiplicator", "MimesisPlayerEnhancement_SpawnScaling", "MimesisPlayerEnhancement_DungeonRandomizer", "MimesisPlayerEnhancement_MimicTuning", "MimesisPlayerEnhancement_PlayerTuning", "MimesisPlayerEnhancement_Weather" }; internal static IReadOnlyList GetGroupOrder() { return GroupOrder; } internal static bool TryGetGroupId(string sectionId, out string groupId) { return SectionToGroup.TryGetValue(sectionId, out groupId); } } internal static class QuickSettingsCatalog { internal static string GetDisplayName(string presetId, string? locale = null) { if (string.IsNullOrWhiteSpace(presetId)) { if (locale != null) { return ModL10n.GetForLocale(locale, "quicksettings.profile.custom"); } return ModL10n.Get("quicksettings.profile.custom"); } string localeKeyForPreset = GetLocaleKeyForPreset(presetId); if (!string.IsNullOrEmpty(localeKeyForPreset)) { string text = ((locale == null) ? ModL10n.Get(localeKeyForPreset) : ModL10n.GetForLocale(locale, localeKeyForPreset)); if (!string.Equals(text, localeKeyForPreset, StringComparison.Ordinal)) { return text; } } if (UserQuickSettingsStore.TryGet(presetId, out QuickSettingPreset preset) && preset != null) { if (!string.IsNullOrWhiteSpace(preset.Name)) { return preset.Name; } return presetId; } return presetId; } internal static string? GetDescription(string presetId, string? locale = null) { string localeDescriptionKeyForPreset = GetLocaleDescriptionKeyForPreset(presetId); if (string.IsNullOrEmpty(localeDescriptionKeyForPreset)) { return null; } string text = ((locale == null) ? ModL10n.Get(localeDescriptionKeyForPreset) : ModL10n.GetForLocale(locale, localeDescriptionKeyForPreset)); if (!string.Equals(text, localeDescriptionKeyForPreset, StringComparison.Ordinal)) { return text; } return null; } internal static bool TryResolvePreset(string presetId, out QuickSettingPreset preset) { preset = null; if (string.IsNullOrWhiteSpace(presetId)) { return false; } if (BuiltinQuickSettings.TryGet(presetId, out QuickSettingPreset preset2)) { preset = preset2; return true; } if (UserQuickSettingsStore.TryGet(presetId, out QuickSettingPreset preset3)) { preset = preset3; return true; } return false; } internal static List ListAllPresets() { List list = new List(); foreach (QuickSettingPreset item in BuiltinQuickSettings.GetAll()) { list.Add(item); } foreach (QuickSettingPreset item2 in UserQuickSettingsStore.GetAll()) { list.Add(item2); } return list; } internal static bool IsBuiltin(string presetId) { return BuiltinQuickSettings.IsBuiltin(presetId); } private static string GetLocaleKeyForPreset(string presetId) { return presetId.ToLowerInvariant() switch { "builtin:loot_picnic" => "quicksettings.presets.loot_picnic.name", "builtin:as_intended" => "quicksettings.presets.as_intended.name", "builtin:full_house" => "quicksettings.presets.full_house.name", "builtin:abandon_hope" => "quicksettings.presets.abandon_hope.name", _ => "", }; } private static string? GetLocaleDescriptionKeyForPreset(string presetId) { return presetId.ToLowerInvariant() switch { "builtin:loot_picnic" => "quicksettings.presets.loot_picnic.description", "builtin:as_intended" => "quicksettings.presets.as_intended.description", "builtin:full_house" => "quicksettings.presets.full_house.description", "builtin:abandon_hope" => "quicksettings.presets.abandon_hope.description", _ => null, }; } } internal static class QuickSettingsResolver { private const string Feature = "QuickSettings"; internal static void ResetSaveOverrideableToDefaults() { if (!ModConfig.IsInitialized) { return; } foreach (var (sectionId, key) in ModConfigRegistry.EnumerateSaveOverrideableKeys()) { if (ModConfigRegistry.TryGetEntry(sectionId, key, out MelonPreferences_Entry entry) && entry != null) { ModConfigRegistry.TrySetEntryValue(sectionId, key, ModConfigRegistry.FormatEntryDefaultValue(entry), out string _); } } } internal static void ApplyPresetValues(Dictionary> values) { if (ModConfig.IsInitialized) { ModConfig.ReloadGlobalFromFile(); } ResetSaveOverrideableToDefaults(); ApplyValues(values); } internal static void ApplyValues(Dictionary> values) { foreach (KeyValuePair> value in values) { foreach (KeyValuePair item in value.Value) { if (ModConfigRegistry.IsSaveOverrideAllowed(value.Key, item.Key) && !ModConfigRegistry.TrySetEntryValue(value.Key, item.Key, item.Value, out string error)) { ModLog.Warn("QuickSettings", "Skipped preset value " + value.Key + "/" + item.Key + " — " + error); } } } } } internal static class QuickSettingsShareCodec { internal sealed class SharePayload { internal string Name = ""; internal Dictionary> Values = new Dictionary>(StringComparer.OrdinalIgnoreCase); } private const string Feature = "QuickSettingsShare"; private const string Prefix = "MPE1:"; internal static string Encode(string name, Dictionary> values) { return EncodePayload(name, FilterAllowedValues(values)); } internal static string EncodePayload(string name, Dictionary> values) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Expected O, but got Unknown JObject val = new JObject { ["v"] = JToken.op_Implicit(1), ["name"] = JToken.op_Implicit(name ?? ""), ["values"] = (JToken)(object)SerializeValues(values) }; byte[] data = Compress(Encoding.UTF8.GetBytes(((JToken)val).ToString((Formatting)0, Array.Empty()))); return "MPE1:" + Base64UrlEncode(data); } internal static bool TryDecode(string shareString, out SharePayload payload, out string? error) { if (!TryDecodePayload(shareString, out payload, out error)) { return false; } payload.Values = FilterAllowedValues(payload.Values); if (payload.Values.Count == 0) { error = ModL10n.Get("api.quick_share_empty"); return false; } return true; } internal static bool TryDecodePayload(string shareString, out SharePayload payload, out string? error) { payload = new SharePayload(); error = null; if (!TryDecodeRoot(shareString, out JObject root, out error)) { return false; } payload.Name = ((JToken)root).Value((object)"name") ?? ""; ref Dictionary> values = ref payload.Values; JToken obj = root["values"]; values = ParseValuesRaw((JObject?)(object)((obj is JObject) ? obj : null)); if (payload.Values.Count == 0) { error = ModL10n.Get("api.quick_share_empty"); return false; } return true; } internal static Dictionary> FilterAllowedValues(Dictionary> values) { Dictionary> dictionary = QuickSettingsValuesBuilder.CreateMap(); foreach (KeyValuePair> value in values) { foreach (KeyValuePair item in value.Value) { string normalized; string error; if (!ModConfigRegistry.IsSaveOverrideAllowed(value.Key, item.Key)) { ModLog.Debug("QuickSettingsShare", "Skipped unknown/disallowed key " + value.Key + "/" + item.Key + " in share import."); } else if (ModConfigRegistry.TryNormalizeRawValue(value.Key, item.Key, item.Value, out normalized, out error)) { QuickSettingsValuesBuilder.Set(dictionary, value.Key, item.Key, normalized); } else { ModLog.Warn("QuickSettingsShare", "Skipped invalid share value " + value.Key + "/" + item.Key + "."); } } } return dictionary; } private static JObject SerializeValues(Dictionary> values) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Expected O, but got Unknown JObject val = new JObject(); foreach (KeyValuePair> value in values) { JObject val2 = new JObject(); foreach (KeyValuePair item in value.Value) { val2[item.Key] = JToken.op_Implicit(item.Value); } val[value.Key] = (JToken)(object)val2; } return val; } private static bool TryDecodeRoot(string shareString, out JObject root, out string? error) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Expected O, but got Unknown root = new JObject(); error = null; if (string.IsNullOrWhiteSpace(shareString)) { error = ModL10n.Get("api.quick_share_invalid"); return false; } string text = shareString.Trim(); if (!text.StartsWith("MPE1:", StringComparison.OrdinalIgnoreCase)) { error = ModL10n.Get("api.quick_share_invalid"); return false; } string encoded = text.Substring("MPE1:".Length); byte[] input; try { input = Base64UrlDecode(encoded); } catch (Exception ex) { error = ModL10n.Get("api.quick_share_invalid") + " " + ex.Message; return false; } byte[] bytes; try { bytes = Decompress(input); } catch (Exception ex2) { error = ModL10n.Get("api.quick_share_invalid") + " " + ex2.Message; return false; } JObject val = ModJson.Deserialize(Encoding.UTF8.GetString(bytes)); if (val == null) { error = ModL10n.Get("api.quick_share_invalid"); return false; } if (((JToken)val).Value((object)"v") != 1) { error = ModL10n.Get("api.quick_share_unsupported_version"); return false; } root = val; return true; } private static Dictionary> ParseValuesRaw(JObject? valuesObject) { Dictionary> dictionary = QuickSettingsValuesBuilder.CreateMap(); if (valuesObject == null) { return dictionary; } foreach (KeyValuePair item in valuesObject) { JToken value = item.Value; JObject val = (JObject)(object)((value is JObject) ? value : null); if (val == null) { continue; } foreach (KeyValuePair item2 in val) { if (item2.Value != null) { QuickSettingsValuesBuilder.Set(dictionary, item.Key, item2.Key, ((object)item2.Value).ToString()); } } } return dictionary; } private static byte[] Compress(byte[] input) { using MemoryStream memoryStream = new MemoryStream(); using (GZipStream gZipStream = new GZipStream(memoryStream, CompressionLevel.Optimal, leaveOpen: true)) { gZipStream.Write(input, 0, input.Length); } return memoryStream.ToArray(); } private static byte[] Decompress(byte[] input) { using MemoryStream stream = new MemoryStream(input); using GZipStream gZipStream = new GZipStream(stream, CompressionMode.Decompress); using MemoryStream memoryStream = new MemoryStream(); gZipStream.CopyTo(memoryStream); return memoryStream.ToArray(); } private static string Base64UrlEncode(byte[] data) { return Convert.ToBase64String(data).TrimEnd('=').Replace('+', '-') .Replace('/', '_'); } private static byte[] Base64UrlDecode(string encoded) { string text = encoded.Replace('-', '+').Replace('_', '/'); switch (text.Length % 4) { case 2: text += "=="; break; case 3: text += "="; break; } return Convert.FromBase64String(text); } } internal static class UserQuickSettingsStore { private const string Feature = "UserQuickSettings"; private const string HeaderSectionId = "MimesisPlayerEnhancement_QuickPresets"; private const string SchemaVersionKey = "SchemaVersion"; private const int CurrentSchemaVersion = 1; private const string UserPrefix = "user:"; private static readonly Regex SlugPattern = new Regex("^[a-z0-9][a-z0-9_-]{0,63}$", RegexOptions.Compiled); private static readonly Dictionary Cache = new Dictionary(StringComparer.OrdinalIgnoreCase); private static bool _loaded; private static bool _dirty; internal static IReadOnlyCollection GetAll() { EnsureLoaded(); return Cache.Values; } internal static bool TryGet(string presetId, out QuickSettingPreset preset) { EnsureLoaded(); return Cache.TryGetValue(presetId, out preset); } internal static bool TryCreateOrUpdate(string presetId, string name, Dictionary> values, bool overwriteExisting, out QuickSettingPreset preset, out string? error) { preset = null; error = null; if (BuiltinQuickSettings.IsBuiltin(presetId)) { error = ModL10n.Get("api.quick_preset_builtin_readonly"); return false; } if (!TryNormalizeUserPresetId(presetId, out string normalizedId, out error)) { return false; } EnsureLoaded(); bool flag = Cache.ContainsKey(normalizedId); if (flag && !overwriteExisting) { error = ModL10n.Get("api.quick_preset_exists"); return false; } string text = DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture); QuickSettingPreset quickSettingPreset = (flag ? Cache[normalizedId] : new QuickSettingPreset { Id = normalizedId, IsBuiltin = false, CreatedUtc = text }); quickSettingPreset.Name = name.Trim(); quickSettingPreset.UpdatedUtc = text; quickSettingPreset.Values = QuickSettingsValuesBuilder.CloneValues(FilterAllowedValues(values)); Cache[normalizedId] = quickSettingPreset; preset = quickSettingPreset; _dirty = true; FlushToDisk(waitForCompletion: false); return true; } internal static bool TryDelete(string presetId, out string? error) { error = null; if (BuiltinQuickSettings.IsBuiltin(presetId)) { error = ModL10n.Get("api.quick_preset_builtin_readonly"); return false; } EnsureLoaded(); if (!Cache.Remove(presetId)) { error = ModL10n.Get("api.quick_preset_not_found"); return false; } _dirty = true; FlushToDisk(waitForCompletion: false); return true; } internal static string CreatePresetIdFromName(string name) { string text = Slugify(name); if (string.IsNullOrEmpty(text)) { text = "preset"; } string text2 = "user:" + text; EnsureLoaded(); if (!Cache.ContainsKey(text2)) { return text2; } for (int i = 2; i < 1000; i++) { text2 = "user:" + text + "_" + i; if (!Cache.ContainsKey(text2)) { return text2; } } return "user:" + text + "_" + Guid.NewGuid().ToString("N").Substring(0, 8); } internal static bool TryNormalizeUserPresetId(string presetId, out string normalizedId, out string? error) { normalizedId = ""; error = null; if (string.IsNullOrWhiteSpace(presetId)) { error = ModL10n.Get("api.quick_preset_invalid_id"); return false; } normalizedId = presetId.Trim(); if (!normalizedId.StartsWith("user:", StringComparison.OrdinalIgnoreCase)) { normalizedId = "user:" + normalizedId; } string text = normalizedId.Substring("user:".Length); if (!SlugPattern.IsMatch(text)) { error = ModL10n.Get("api.quick_preset_invalid_id"); return false; } normalizedId = "user:" + text.ToLowerInvariant(); return true; } internal static void EnsureLoaded() { if (!_loaded) { Cache.Clear(); string userQuickPresetsPath = SaveSidecarPaths.GetUserQuickPresetsPath(); if (string.IsNullOrEmpty(userQuickPresetsPath)) { _loaded = true; return; } ParseDocument(SparseTomlConfig.Load(AtomicFileIO.ReadText(userQuickPresetsPath, "UserQuickSettings"))); _loaded = true; _dirty = false; } } internal static void FlushToDisk(bool waitForCompletion) { if (!_dirty) { return; } string userQuickPresetsPath = SaveSidecarPaths.GetUserQuickPresetsPath(); if (!string.IsNullOrEmpty(userQuickPresetsPath)) { SparseTomlConfig.Document doc = SerializeDocument(); if (SparseTomlConfig.IsEmpty(doc)) { BackgroundFileWriteQueue.EnqueueDelete(userQuickPresetsPath, "UserQuickSettings", waitForCompletion); } else { BackgroundFileWriteQueue.EnqueueText(userQuickPresetsPath, SparseTomlConfig.Serialize(doc), "UserQuickSettings", waitForCompletion); } _dirty = false; } } private static void ParseDocument(SparseTomlConfig.Document doc) { Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (string item in doc.SectionOrder) { if (!string.Equals(item, "MimesisPlayerEnhancement_QuickPresets", StringComparison.OrdinalIgnoreCase) && doc.Sections.TryGetValue(item, out Dictionary value)) { if (TryParseMetaSection(item, value, out string presetId, out QuickSettingPreset preset)) { dictionary[presetId] = preset; } else { TryParseValuesSection(item, value, dictionary); } } } foreach (QuickSettingPreset value2 in dictionary.Values) { Cache[value2.Id] = value2; } } private static bool TryParseMetaSection(string sectionId, Dictionary keys, out string presetId, out QuickSettingPreset preset) { presetId = ""; preset = null; if (!sectionId.StartsWith("user:", StringComparison.OrdinalIgnoreCase) || sectionId.Contains(".values.", StringComparison.OrdinalIgnoreCase)) { return false; } presetId = sectionId; if (!TryNormalizeUserPresetId(presetId, out presetId, out string _)) { return false; } preset = new QuickSettingPreset { Id = presetId, IsBuiltin = false, Name = (keys.TryGetValue("Name", out string value) ? value : presetId), CreatedUtc = (keys.TryGetValue("CreatedUtc", out string value2) ? value2 : ""), UpdatedUtc = (keys.TryGetValue("UpdatedUtc", out string value3) ? value3 : ""), Values = QuickSettingsValuesBuilder.CreateMap() }; return true; } private static bool TryParseValuesSection(string sectionId, Dictionary keys, Dictionary building) { int num = sectionId.IndexOf(".values.", StringComparison.OrdinalIgnoreCase); if (num <= 0) { return false; } string normalizedId = sectionId.Substring(0, num); if (!TryNormalizeUserPresetId(normalizedId, out normalizedId, out string _)) { return false; } string sectionId2 = sectionId.Substring(num + ".values.".Length); if (!building.TryGetValue(normalizedId, out QuickSettingPreset value)) { QuickSettingPreset obj = new QuickSettingPreset { Id = normalizedId, IsBuiltin = false, Name = normalizedId, Values = QuickSettingsValuesBuilder.CreateMap() }; value = (building[normalizedId] = obj); } foreach (KeyValuePair key in keys) { if (ModConfigRegistry.IsSaveOverrideAllowed(sectionId2, key.Key)) { value.SetValue(sectionId2, key.Key, key.Value); } } return true; } private static SparseTomlConfig.Document SerializeDocument() { SparseTomlConfig.Document document = new SparseTomlConfig.Document(); if (Cache.Count == 0) { return document; } document.SectionOrder.Add("MimesisPlayerEnhancement_QuickPresets"); document.Sections["MimesisPlayerEnhancement_QuickPresets"] = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["SchemaVersion"] = 1.ToString(CultureInfo.InvariantCulture) }; foreach (QuickSettingPreset value in Cache.Values) { string id = value.Id; document.SectionOrder.Add(id); document.Sections[id] = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["Name"] = value.Name ?? value.Id, ["CreatedUtc"] = value.CreatedUtc ?? "", ["UpdatedUtc"] = value.UpdatedUtc ?? "" }; foreach (KeyValuePair> value2 in value.Values) { if (value2.Value.Count != 0) { string text = value.Id + ".values." + value2.Key; document.SectionOrder.Add(text); document.Sections[text] = new Dictionary(value2.Value, StringComparer.OrdinalIgnoreCase); } } } return document; } private static Dictionary> FilterAllowedValues(Dictionary> values) { Dictionary> dictionary = QuickSettingsValuesBuilder.CreateMap(); foreach (KeyValuePair> value in values) { foreach (KeyValuePair item in value.Value) { if (ModConfigRegistry.IsSaveOverrideAllowed(value.Key, item.Key) && ModConfigRegistry.TryNormalizeRawValue(value.Key, item.Key, item.Value, out string normalized, out string _)) { QuickSettingsValuesBuilder.Set(dictionary, value.Key, item.Key, normalized); } } } return dictionary; } private static string Slugify(string name) { if (string.IsNullOrWhiteSpace(name)) { return ""; } string text = name.Trim().ToLowerInvariant(); char[] array = new char[text.Length]; int num = 0; bool flag = false; string text2 = text; foreach (char c in text2) { if (char.IsLetterOrDigit(c)) { array[num++] = c; flag = false; } else if (!flag && num > 0) { array[num++] = '_'; flag = true; } } while (num > 0 && array[num - 1] == '_') { num--; } if (num != 0) { return new string(array, 0, num); } return ""; } } internal enum SidecarKind { SlotDocument, Speech, Statistics } internal static class SaveSidecarPaths { private const string SidecarExtension = ".sav"; private const string SidecarInfix = ".mpe-"; internal static string? GetSaveFolderPath() { PlatformMgr instance = MonoSingleton.Instance; if ((Object)(object)instance == (Object)null) { return null; } string saveFileFolderPath = instance.GetSaveFileFolderPath(); if (!string.IsNullOrEmpty(saveFileFolderPath)) { return saveFileFolderPath; } return null; } internal static string? GetSaveFileStem(int slotId) { if (slotId < 0) { return null; } string saveFileName = MMSaveGameData.GetSaveFileName(slotId); if (!string.IsNullOrEmpty(saveFileName)) { return Path.GetFileNameWithoutExtension(saveFileName); } return null; } internal static string? GetSidecarPath(int slotId, string kind) { string saveFolderPath = GetSaveFolderPath(); string saveFileStem = GetSaveFileStem(slotId); if (string.IsNullOrEmpty(saveFolderPath) || string.IsNullOrEmpty(saveFileStem) || string.IsNullOrEmpty(kind)) { return null; } return Path.Combine(saveFolderPath, saveFileStem + ".mpe-" + kind + ".sav"); } internal static string? GetSlotDocumentPath(int slotId) { return GetSidecarPath(slotId, "slot"); } internal static string? GetSpeechPath(int slotId) { return GetSidecarPath(slotId, "speech"); } internal static string? GetUserQuickPresetsPath() { string saveFolderPath = GetSaveFolderPath(); if (string.IsNullOrEmpty(saveFolderPath)) { return null; } return Path.Combine(saveFolderPath, "MMGameData.mpe-quick-presets.sav"); } internal static string? GetStatisticsPath(int slotId) { return GetSidecarPath(slotId, "stats"); } internal static IEnumerable EnumerateSidecarFiles(int slotId, SidecarKind? filter = null) { string saveFolderPath = GetSaveFolderPath(); string stem = GetSaveFileStem(slotId); if (string.IsNullOrEmpty(saveFolderPath) || string.IsNullOrEmpty(stem) || !Directory.Exists(saveFolderPath)) { yield break; } string searchPattern = stem + ".mpe-*"; string[] files = Directory.GetFiles(saveFolderPath, searchPattern); foreach (string text in files) { if (!filter.HasValue || MatchesFilter(text, stem, filter.Value)) { yield return text; } } } internal static void DeleteSidecarFile(string filePath, string logFeature = "Persistence") { AtomicFileIO.Delete(filePath, logFeature); } internal static void DeleteSidecars(int slotId, params SidecarKind[] kinds) { foreach (SidecarKind value in kinds) { foreach (string item in EnumerateSidecarFiles(slotId, value)) { DeleteSidecarFile(item); } } } internal static void DeleteAllFilesForSlot(int slotId, string logFeature = "SaveSlotSidecar") { string saveFolderPath = GetSaveFolderPath(); string saveFileStem = GetSaveFileStem(slotId); if (string.IsNullOrEmpty(saveFolderPath) || string.IsNullOrEmpty(saveFileStem) || !Directory.Exists(saveFolderPath)) { return; } string userQuickPresetsPath = GetUserQuickPresetsPath(); HashSet hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); string[] files = Directory.GetFiles(saveFolderPath, saveFileStem + "*"); for (int i = 0; i < files.Length; i++) { string text = files[i]; if (text.EndsWith(".bak", StringComparison.OrdinalIgnoreCase)) { string text2 = text; text = text2.Substring(0, text2.Length - 4); } else if (text.EndsWith(".tmp", StringComparison.OrdinalIgnoreCase)) { string text2 = text; text = text2.Substring(0, text2.Length - 4); } if (string.IsNullOrEmpty(userQuickPresetsPath) || !string.Equals(text, userQuickPresetsPath, StringComparison.OrdinalIgnoreCase)) { hashSet.Add(text); } } foreach (string item in hashSet) { AtomicFileIO.Delete(item, logFeature); } } private static bool MatchesFilter(string filePath, string stem, SidecarKind filter) { string fileName = Path.GetFileName(filePath); string text = stem + ".mpe-"; if (!fileName.StartsWith(text, StringComparison.OrdinalIgnoreCase)) { return false; } return filter switch { SidecarKind.Speech => fileName.Equals(text + "speech.sav", StringComparison.OrdinalIgnoreCase), SidecarKind.SlotDocument => fileName.Equals(text + "slot.sav", StringComparison.OrdinalIgnoreCase), SidecarKind.Statistics => fileName.Equals(text + "stats.sav", StringComparison.OrdinalIgnoreCase), _ => false, }; } } internal static class SaveSlotConfigProfile { internal const string SectionId = "MimesisPlayerEnhancement_Profile"; internal const string KeyMode = "Mode"; internal const string KeyPresetId = "PresetId"; internal const string KeyPresetRevision = "PresetRevision"; internal static bool IsProfileSection(string sectionId) { return string.Equals(sectionId, "MimesisPlayerEnhancement_Profile", StringComparison.OrdinalIgnoreCase); } internal static SaveConfigProfileState Parse(SparseTomlConfig.Document doc) { SaveConfigProfileState saveConfigProfileState = new SaveConfigProfileState(); if (!doc.Sections.TryGetValue("MimesisPlayerEnhancement_Profile", out Dictionary value)) { saveConfigProfileState.Mode = InferLegacyMode(doc); return saveConfigProfileState; } if (value.TryGetValue("Mode", out var value2)) { saveConfigProfileState.Mode = ParseMode(value2); } else { saveConfigProfileState.Mode = InferLegacyMode(doc); } if (value.TryGetValue("PresetId", out var value3)) { saveConfigProfileState.PresetId = value3?.Trim() ?? ""; } if (value.TryGetValue("PresetRevision", out var value4) && int.TryParse(value4, out var result)) { saveConfigProfileState.PresetRevision = result; } if (saveConfigProfileState.Mode == SaveConfigProfileMode.Quick && string.IsNullOrWhiteSpace(saveConfigProfileState.PresetId)) { saveConfigProfileState.Mode = SaveConfigProfileMode.Custom; } return saveConfigProfileState; } internal static SaveConfigProfileState TryReadProfileForSlot(int slotId) { if (SaveSlotDocumentStore.TryReadFromDisk(slotId, out SaveSlotDocument document) && document != null) { return document.SettingsProfile; } return new SaveConfigProfileState(); } internal static string GetDisplayLabel(SaveConfigProfileState profile) { return profile.Mode switch { SaveConfigProfileMode.Global => ModL10n.Get("quicksettings.profile.global"), SaveConfigProfileMode.Custom => ModL10n.Get("quicksettings.profile.custom"), SaveConfigProfileMode.Quick => QuickSettingsCatalog.GetDisplayName(profile.PresetId), _ => ModL10n.Get("quicksettings.profile.global"), }; } internal static void WriteProfileSection(SparseTomlConfig.Document doc, SaveConfigProfileState profile) { if (profile.Mode == SaveConfigProfileMode.Global) { RemoveProfileSection(doc); return; } if (!doc.Sections.TryGetValue("MimesisPlayerEnhancement_Profile", out Dictionary value)) { doc.SectionOrder.Add("MimesisPlayerEnhancement_Profile"); value = new Dictionary(StringComparer.OrdinalIgnoreCase); doc.Sections["MimesisPlayerEnhancement_Profile"] = value; } value["Mode"] = FormatMode(profile.Mode); if (profile.Mode == SaveConfigProfileMode.Quick) { value["PresetId"] = profile.PresetId; if (profile.PresetRevision > 0) { value["PresetRevision"] = profile.PresetRevision.ToString(); } else { value.Remove("PresetRevision"); } } else { value.Remove("PresetId"); value.Remove("PresetRevision"); } } internal static void RemoveProfileSection(SparseTomlConfig.Document doc) { if (doc.Sections.TryGetValue("MimesisPlayerEnhancement_Profile", out Dictionary _)) { doc.Sections.Remove("MimesisPlayerEnhancement_Profile"); doc.SectionOrder.Remove("MimesisPlayerEnhancement_Profile"); } } internal static bool HasGameplayOverrides(SparseTomlConfig.Document doc) { foreach (string item in doc.SectionOrder) { if (!IsProfileSection(item) && doc.Sections.TryGetValue(item, out Dictionary value) && value.Count > 0) { return true; } } return false; } private static SaveConfigProfileMode InferLegacyMode(SparseTomlConfig.Document doc) { if (!HasGameplayOverrides(doc)) { return SaveConfigProfileMode.Global; } return SaveConfigProfileMode.Custom; } private static SaveConfigProfileMode ParseMode(string? raw) { if (string.IsNullOrWhiteSpace(raw)) { return SaveConfigProfileMode.Global; } return raw.Trim().ToLowerInvariant() switch { "quick" => SaveConfigProfileMode.Quick, "custom" => SaveConfigProfileMode.Custom, "global" => SaveConfigProfileMode.Global, _ => SaveConfigProfileMode.Global, }; } private static string FormatMode(SaveConfigProfileMode mode) { return mode switch { SaveConfigProfileMode.Quick => "quick", SaveConfigProfileMode.Custom => "custom", _ => "global", }; } } internal static class SaveSlotConfigStore { private const string Feature = "SaveSlotConfig"; private static int _activeSlotId = -1; private static bool _isApplyingOverrides; private static bool _dirty; private static SparseTomlConfig.Document _runtimeDoc = new SparseTomlConfig.Document(); private static SaveConfigProfileState _runtimeProfile = new SaveConfigProfileState(); internal static int ActiveSlotId => _activeSlotId; internal static bool IsApplyingOverrides => _isApplyingOverrides; internal static SaveConfigProfileState ActiveProfile => _runtimeProfile; internal static void LoadForSlot(int slotId) { if (!ModConfig.IsInitialized || !MimesisSaveManager.IsValidSaveSlotId(slotId)) { return; } ModConfigChangeTracker.BeginBatch(); _isApplyingOverrides = true; try { ModConfig.ReloadGlobalFromFile(); _activeSlotId = slotId; _runtimeDoc = SaveSlotDocumentStore.BuildRuntimeConfigDoc(); _runtimeProfile = SaveSlotDocumentStore.GetSettingsProfile(); _dirty = false; ApplyActiveProfile(slotId); ModConfig.SanitizeFloatEntries(); } finally { _isApplyingOverrides = false; ModConfigChangeTracker.EndBatch(); } } internal static bool TrySetProfileMode(int slotId, SaveConfigProfileMode mode, string? presetId, out string? error, bool waitForCompletion = false) { error = null; if (!EnsureActiveSlot(slotId, out error)) { return false; } ModConfigChangeTracker.BeginBatch(); try { switch (mode) { case SaveConfigProfileMode.Quick: { if (string.IsNullOrWhiteSpace(presetId)) { error = ModL10n.Get("api.quick_preset_not_found"); return false; } if (!QuickSettingsCatalog.TryResolvePreset(presetId, out QuickSettingPreset preset)) { error = ModL10n.Get("api.quick_preset_not_found"); return false; } ClearGameplayOverrideSections(_runtimeDoc); _runtimeProfile = new SaveConfigProfileState { Mode = SaveConfigProfileMode.Quick, PresetId = preset.Id, PresetRevision = preset.Revision }; SaveSlotConfigProfile.WriteProfileSection(_runtimeDoc, _runtimeProfile); QuickSettingsResolver.ApplyPresetValues(preset.Values); break; } case SaveConfigProfileMode.Global: ClearGameplayOverrideSections(_runtimeDoc); _runtimeProfile = new SaveConfigProfileState { Mode = SaveConfigProfileMode.Global }; SaveSlotConfigProfile.RemoveProfileSection(_runtimeDoc); ModConfig.ReloadGlobalFromFile(); break; default: if (_runtimeProfile.Mode == SaveConfigProfileMode.Quick) { MaterializeEffectiveConfigToDoc(); } _runtimeProfile = new SaveConfigProfileState { Mode = SaveConfigProfileMode.Custom }; SaveSlotConfigProfile.WriteProfileSection(_runtimeDoc, _runtimeProfile); break; } _dirty = true; ModConfig.SanitizeFloatEntries(); } finally { ModConfigChangeTracker.EndBatch(); } if (waitForCompletion) { FlushToDisk(slotId, waitForCompletion: true); } return true; } internal static bool TryApplyQuickPreset(int slotId, string presetId, out string? error, bool waitForCompletion = false) { return TrySetProfileMode(slotId, SaveConfigProfileMode.Quick, presetId, out error, waitForCompletion); } internal static void MaterializeEffectiveConfigToDoc() { ClearGameplayOverrideSections(_runtimeDoc); Dictionary> values = QuickSettingsValuesBuilder.CollectValuesDifferingFromGlobal(); MergeValuesIntoDoc(_runtimeDoc, values); } internal static bool TrySetOverride(int slotId, string sectionId, string key, string rawValue, out string? error, bool waitForCompletion = false) { error = null; if (!ModConfig.IsInitialized) { error = ModL10n.Get("api.config_not_initialized"); return false; } if (!EnsureActiveSlot(slotId, out error)) { return false; } if (!ModConfigRegistry.IsSaveOverrideAllowed(sectionId, key)) { error = ModL10n.Get("api.save_override_not_allowed"); return false; } if (!ModConfigRegistry.TryNormalizeRawValue(sectionId, key, rawValue, out string normalized, out error)) { return false; } if (!ModConfigRegistry.TryGetGlobalRawValue(sectionId, key, out string globalRaw)) { error = ModL10n.Get("api.unknown_setting"); return false; } if (SaveSlotDocumentStore.GetDocumentPath(slotId) == null) { error = ModL10n.Get("api.save_slot_path_unavailable"); return false; } if (_runtimeProfile.Mode == SaveConfigProfileMode.Quick) { MaterializeEffectiveConfigToDoc(); _runtimeProfile = new SaveConfigProfileState { Mode = SaveConfigProfileMode.Custom }; SaveSlotConfigProfile.WriteProfileSection(_runtimeDoc, _runtimeProfile); } ModConfigChangeTracker.BeginBatch(); string effectiveValue; try { if (!ModConfigRegistry.TryApplyNormalizedEntry(sectionId, key, normalized, out effectiveValue, out error)) { return false; } } finally { ModConfigChangeTracker.EndBatch(); } if (ModConfigRegistry.RawValuesEqual(sectionId, key, effectiveValue, globalRaw)) { RemoveKey(_runtimeDoc, sectionId, key); } else { EnsureSection(_runtimeDoc, sectionId); _runtimeDoc.Sections[sectionId][key] = effectiveValue; } if (_runtimeProfile.Mode != SaveConfigProfileMode.Custom) { _runtimeProfile = new SaveConfigProfileState { Mode = SaveConfigProfileMode.Custom }; SaveSlotConfigProfile.WriteProfileSection(_runtimeDoc, _runtimeProfile); } _dirty = true; if (waitForCompletion) { FlushToDisk(slotId, waitForCompletion: true); } return true; } internal static void ClearOverrideKey(int slotId, string sectionId, string key) { if (slotId == _activeSlotId && _runtimeDoc.Sections.TryGetValue(sectionId, out Dictionary value) && value.ContainsKey(key)) { RemoveKey(_runtimeDoc, sectionId, key); _dirty = true; } } internal static void FlushToDisk(int slotId, bool waitForCompletion = false) { if (slotId == _activeSlotId && _dirty) { SaveSlotDocumentStore.WriteRuntimeConfigDoc(_runtimeDoc, _runtimeProfile); SaveSlotDocumentStore.FlushToDisk(slotId, waitForCompletion); _dirty = false; } } internal static void ForceFlushToDisk(int slotId, bool waitForCompletion = false) { if (slotId == _activeSlotId) { SaveSlotDocumentStore.WriteRuntimeConfigDoc(_runtimeDoc, _runtimeProfile); SaveSlotDocumentStore.ForceFlushToDisk(slotId, waitForCompletion); _dirty = false; } } internal static void ClearRuntimeToGlobal() { _activeSlotId = -1; _runtimeDoc = new SparseTomlConfig.Document(); _runtimeProfile = new SaveConfigProfileState(); _dirty = false; if (ModConfig.IsInitialized) { ModConfigChangeTracker.BeginBatch(); try { ModConfig.ReloadGlobalFromFile(); } finally { ModConfigChangeTracker.CancelBatch(); } ModConfigChangeTracker.NotifyFullReload(); } } internal static bool IsOverridden(int slotId, string sectionId, string key) { if (slotId != _activeSlotId) { return false; } if (_runtimeProfile.Mode == SaveConfigProfileMode.Quick) { return false; } if (_runtimeProfile.Mode == SaveConfigProfileMode.Global) { return false; } if (_runtimeDoc.Sections.TryGetValue(sectionId, out Dictionary value)) { return value.ContainsKey(key); } return false; } internal static bool TryGetOverrideRaw(int slotId, string sectionId, string key, out string rawValue) { rawValue = ""; if (slotId != _activeSlotId) { return false; } if (_runtimeProfile.Mode == SaveConfigProfileMode.Quick) { if (!QuickSettingsCatalog.TryResolvePreset(_runtimeProfile.PresetId, out QuickSettingPreset preset)) { return false; } return preset.TryGetValue(sectionId, key, out rawValue); } if (_runtimeDoc.Sections.TryGetValue(sectionId, out Dictionary value)) { return value.TryGetValue(key, out rawValue); } return false; } private static void ApplyActiveProfile(int slotId) { switch (_runtimeProfile.Mode) { case SaveConfigProfileMode.Quick: { if (!QuickSettingsCatalog.TryResolvePreset(_runtimeProfile.PresetId, out QuickSettingPreset preset)) { ModLog.Warn("SaveSlotConfig", $"Quick preset not found for slot {slotId} — id={_runtimeProfile.PresetId}; falling back to global."); _runtimeProfile = new SaveConfigProfileState { Mode = SaveConfigProfileMode.Global }; break; } if (_runtimeProfile.PresetRevision != preset.Revision) { ModLog.Info("SaveSlotConfig", $"Re-applying updated quick preset — slot={slotId}, id={preset.Id}, revision={preset.Revision}"); } QuickSettingsResolver.ApplyPresetValues(preset.Values); _runtimeProfile.PresetRevision = preset.Revision; break; } case SaveConfigProfileMode.Custom: ApplyGameplayDoc(_runtimeDoc, slotId); break; } } private static void ApplyGameplayDoc(SparseTomlConfig.Document doc, int slotId) { foreach (string item in doc.SectionOrder) { if (SaveSlotConfigProfile.IsProfileSection(item) || !doc.Sections.TryGetValue(item, out Dictionary value)) { continue; } foreach (KeyValuePair item2 in value) { if (ModConfigRegistry.IsSaveOverrideAllowed(item, item2.Key) && !ModConfigRegistry.TrySetEntryValue(item, item2.Key, item2.Value, out string _)) { ModLog.Warn("SaveSlotConfig", $"Skipped invalid override {item}/{item2.Key} for slot {slotId}."); } } } } private static bool EnsureActiveSlot(int slotId, out string? error) { error = null; if (slotId != _activeSlotId) { error = ModL10n.Get("api.save_slot_not_loaded"); return false; } return true; } private static void ClearGameplayOverrideSections(SparseTomlConfig.Document doc) { List list = new List(); foreach (string item in doc.SectionOrder) { if (!SaveSlotConfigProfile.IsProfileSection(item)) { list.Add(item); } } foreach (string item2 in list) { doc.Sections.Remove(item2); doc.SectionOrder.Remove(item2); } } private static void MergeValuesIntoDoc(SparseTomlConfig.Document doc, Dictionary> values) { foreach (KeyValuePair> value in values) { foreach (KeyValuePair item in value.Value) { EnsureSection(doc, value.Key); doc.Sections[value.Key][item.Key] = item.Value; } } } private static void EnsureSection(SparseTomlConfig.Document doc, string sectionId) { if (!doc.Sections.ContainsKey(sectionId)) { doc.SectionOrder.Add(sectionId); doc.Sections[sectionId] = new Dictionary(StringComparer.OrdinalIgnoreCase); } } private static void RemoveKey(SparseTomlConfig.Document doc, string sectionId, string key) { if (doc.Sections.TryGetValue(sectionId, out Dictionary value)) { value.Remove(key); if (value.Count == 0) { doc.Sections.Remove(sectionId); doc.SectionOrder.Remove(sectionId); } } } } internal static class SaveSlotDocumentStore { private const string Feature = "SaveSlotDocument"; private static readonly object Gate = new object(); private static SaveSlotDocument _document = new SaveSlotDocument(); private static int _loadedSlotId = -1; private static bool _dirty; internal static int LoadedSlotId => _loadedSlotId; internal static int LoadedPlayerCount { get { lock (Gate) { return _document.Players?.Count ?? 0; } } } internal static string? GetDocumentPath(int slotId) { return SaveSidecarPaths.GetSlotDocumentPath(slotId); } internal static void LoadForSlot(int slotId) { if (slotId < 0) { return; } lock (Gate) { _document = new SaveSlotDocument(); _loadedSlotId = slotId; _dirty = false; string documentPath = GetDocumentPath(slotId); if (string.IsNullOrEmpty(documentPath)) { return; } try { string text = AtomicFileIO.ReadText(documentPath, "SaveSlotDocument"); if (!string.IsNullOrEmpty(text)) { SaveSlotDocument saveSlotDocument = ModJson.Deserialize(text); if (saveSlotDocument != null) { _document = NormalizeLoaded(saveSlotDocument); } } } catch (Exception ex) { ModLog.Warn("SaveSlotDocument", "Failed to load slot document — " + ex.Message); } } } internal static void Clear() { lock (Gate) { _document = new SaveSlotDocument(); _loadedSlotId = -1; _dirty = false; } } internal static void FlushToDisk(int slotId, bool waitForCompletion = false) { if (slotId < 0 || slotId != _loadedSlotId) { return; } lock (Gate) { if (_dirty) { WriteSnapshotToDisk(slotId, waitForCompletion); } } } internal static void ForceFlushToDisk(int slotId, bool waitForCompletion = false) { if (slotId < 0 || slotId != _loadedSlotId) { return; } lock (Gate) { WriteSnapshotToDisk(slotId, waitForCompletion); } } private static void WriteSnapshotToDisk(int slotId, bool waitForCompletion) { string documentPath = GetDocumentPath(slotId); if (string.IsNullOrEmpty(documentPath)) { return; } try { SaveSlotDocument saveSlotDocument = CloneDocument(_document); saveSlotDocument.Version = 1; if (!HasPersistedContent(saveSlotDocument)) { BackgroundFileWriteQueue.EnqueueDelete(documentPath, "SaveSlotDocument", waitForCompletion); } else { BackgroundFileWriteQueue.EnqueueText(documentPath, ModJson.Serialize(saveSlotDocument), "SaveSlotDocument", waitForCompletion); } _dirty = false; } catch (Exception ex) { ModLog.Warn("SaveSlotDocument", "Failed to save slot document — " + ex.Message); } } internal static bool TryReadFromDisk(int slotId, out SaveSlotDocument? document) { document = null; if (slotId < 0) { return false; } lock (Gate) { if (slotId == _loadedSlotId) { document = CloneDocument(_document); return HasPersistedContent(document); } } string documentPath = GetDocumentPath(slotId); if (string.IsNullOrEmpty(documentPath)) { return false; } try { string text = AtomicFileIO.ReadText(documentPath, "SaveSlotDocument"); if (!string.IsNullOrEmpty(text)) { SaveSlotDocument saveSlotDocument = ModJson.Deserialize(text); if (saveSlotDocument != null) { document = NormalizeLoaded(saveSlotDocument); return HasPersistedContent(document); } } return false; } catch (Exception ex) { ModLog.Debug("SaveSlotDocument", $"Failed to read slot document for slot {slotId} — {ex.Message}"); return false; } } internal static SaveConfigProfileState GetSettingsProfile() { lock (Gate) { return CloneProfile(_document.SettingsProfile); } } internal static void ApplyPlayerEntries(Action visitor) { if (_loadedSlotId < 0) { return; } lock (Gate) { if (_document.Players == null) { return; } foreach (KeyValuePair player in _document.Players) { if (ulong.TryParse(player.Key, out var result) && result != 0L) { visitor(result, player.Value); } } } } internal static SparseTomlConfig.Document BuildRuntimeConfigDoc() { lock (Gate) { SparseTomlConfig.Document document = new SparseTomlConfig.Document(); SaveSlotConfigProfile.WriteProfileSection(document, _document.SettingsProfile); MergeOverridesIntoDoc(document, _document.ConfigOverrides); return document; } } internal static void WriteRuntimeConfigDoc(SparseTomlConfig.Document doc, SaveConfigProfileState profile) { if (_loadedSlotId < 0) { return; } lock (Gate) { _document.SettingsProfile = CloneProfile(profile); _document.ConfigOverrides = ExtractGameplayOverrides(doc); Dictionary> configOverrides = _document.ConfigOverrides; if (configOverrides != null && configOverrides.Count == 0) { _document.ConfigOverrides = null; } _dirty = true; } } internal static string? TryReadLobbyNameForSlot(int slotId) { if (!TryReadLobbySection(slotId, out SaveSlotLobbySection lobby) || string.IsNullOrWhiteSpace(lobby?.BaseLobbyName)) { return null; } return lobby.BaseLobbyName.Trim(); } internal static bool HasPersistedPublicPreference(int slotId) { if (TryReadLobbySection(slotId, out SaveSlotLobbySection lobby)) { return lobby?.IsPublicLobby.HasValue ?? false; } return false; } internal static void RememberLobbyRuntimeState(int slotId, string? baseLobbyName = null, bool? isPublicLobby = null) { if (slotId < 0) { return; } EnsureSlotBound(slotId); if (slotId != _loadedSlotId) { return; } lock (Gate) { SaveSlotDocument document = _document; if (document.Lobby == null) { document.Lobby = new SaveSlotLobbySection(); } bool flag = false; if (baseLobbyName != null) { string text = baseLobbyName.Trim(); if (string.IsNullOrWhiteSpace(text)) { return; } if (!string.Equals(_document.Lobby.BaseLobbyName, text, StringComparison.Ordinal)) { _document.Lobby.BaseLobbyName = text; flag = true; } } if (isPublicLobby.HasValue && _document.Lobby.IsPublicLobby != isPublicLobby) { _document.Lobby.IsPublicLobby = isPublicLobby; flag = true; } if (flag) { _dirty = true; } } } internal static void CaptureLobbyFromController(int slotId) { if (ModConfig.EnableJoinAnytime.Value && JoinAnytimeLobbyController.TryExportLobbyState(out string baseLobbyName, out bool isPublicLobby)) { RememberLobbyRuntimeState(slotId, baseLobbyName, isPublicLobby); } } internal static string ResolveDisplayName(int slotId, ulong steamId, string? fallback) { if (steamId == 0L) { return ""; } if (TryGetName(slotId, steamId, out string name) && IsUsableName(name, steamId)) { return name; } if (IsUsableName(fallback, steamId)) { return fallback; } return steamId.ToString(); } internal static bool TryGetName(int slotId, ulong steamId, out string? name) { name = null; if (slotId < 0 || steamId == 0L) { return false; } if (slotId == _loadedSlotId) { lock (Gate) { SaveSlotPlayerEntry entry; return TryGetPlayerEntryLocked(steamId, out entry) && IsUsableName(entry.DisplayName, steamId) && (name = entry.DisplayName) != null; } } if (!TryReadFromDisk(slotId, out SaveSlotDocument document) || document?.Players == null) { return false; } string key = steamId.ToString(); if (!document.Players.TryGetValue(key, out SaveSlotPlayerEntry value) || !IsUsableName(value.DisplayName, steamId)) { return false; } name = value.DisplayName; return true; } internal static bool TryGetVoiceId(int slotId, ulong steamId, out string? voiceId) { voiceId = null; if (slotId < 0 || steamId == 0L) { return false; } if (slotId == _loadedSlotId) { lock (Gate) { if (!TryGetPlayerEntryLocked(steamId, out SaveSlotPlayerEntry entry) || string.IsNullOrWhiteSpace(entry.VoiceId)) { return false; } voiceId = entry.VoiceId; return true; } } if (!TryReadFromDisk(slotId, out SaveSlotDocument document) || document?.Players == null) { return false; } if (!document.Players.TryGetValue(steamId.ToString(), out SaveSlotPlayerEntry value) || string.IsNullOrWhiteSpace(value.VoiceId)) { return false; } voiceId = value.VoiceId; return true; } internal static bool UpsertPlayer(ulong steamId, string displayName, string? voiceId = null) { if (_loadedSlotId < 0 || steamId == 0L) { return false; } bool flag = IsUsableName(displayName, steamId); bool flag2 = !string.IsNullOrWhiteSpace(voiceId); if (!flag && !flag2) { return false; } lock (Gate) { SaveSlotDocument document = _document; if (document.Players == null) { document.Players = new Dictionary(); } string key = steamId.ToString(); if (!_document.Players.TryGetValue(key, out SaveSlotPlayerEntry value)) { value = new SaveSlotPlayerEntry(); _document.Players[key] = value; } bool flag3 = false; if (flag && value.DisplayName != displayName) { value.DisplayName = displayName; flag3 = true; } if (flag2 && value.VoiceId != voiceId) { value.VoiceId = voiceId; flag3 = true; } if (flag3) { _dirty = true; } return flag3; } } internal static bool RemovePlayer(ulong steamId) { if (_loadedSlotId < 0 || steamId == 0L) { return false; } lock (Gate) { if (_document.Players == null || !_document.Players.Remove(steamId.ToString())) { return false; } if (_document.Players.Count == 0) { _document.Players = null; } _dirty = true; return true; } } internal static void ApplyVoiceMappingsToRuntime(Action applyMapping) { if (_loadedSlotId < 0) { return; } lock (Gate) { if (_document.Players == null) { return; } foreach (KeyValuePair player in _document.Players) { if (ulong.TryParse(player.Key, out var result) && result != 0L && !string.IsNullOrWhiteSpace(player.Value.VoiceId)) { applyMapping(result, player.Value.VoiceId); } } } } internal static bool IsUsableName(string? name, ulong steamId) { if (!string.IsNullOrWhiteSpace(name)) { return name != steamId.ToString(); } return false; } private static void EnsureSlotBound(int slotId) { if (slotId >= 0 && MimesisSaveManager.IsValidSaveSlotId(slotId) && slotId != _loadedSlotId) { LoadForSlot(slotId); } } private static bool TryReadLobbySection(int slotId, out SaveSlotLobbySection? lobby) { lobby = null; if (slotId < 0) { return false; } if (slotId == _loadedSlotId) { lock (Gate) { if (_document.Lobby == null) { return false; } lobby = CloneLobby(_document.Lobby); return true; } } if (!TryReadFromDisk(slotId, out SaveSlotDocument document) || document?.Lobby == null) { return false; } lobby = CloneLobby(document.Lobby); return true; } private static bool TryGetPlayerEntryLocked(ulong steamId, out SaveSlotPlayerEntry? entry) { entry = null; if (_document.Players == null) { return false; } return _document.Players.TryGetValue(steamId.ToString(), out entry); } private static Dictionary>? ExtractGameplayOverrides(SparseTomlConfig.Document doc) { Dictionary> dictionary = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (string item in doc.SectionOrder) { if (!SaveSlotConfigProfile.IsProfileSection(item) && doc.Sections.TryGetValue(item, out Dictionary value) && value.Count != 0) { dictionary[item] = new Dictionary(value, StringComparer.OrdinalIgnoreCase); } } if (dictionary.Count <= 0) { return null; } return dictionary; } private static void MergeOverridesIntoDoc(SparseTomlConfig.Document doc, Dictionary>? overrides) { if (overrides == null) { return; } foreach (KeyValuePair> @override in overrides) { if (!doc.Sections.TryGetValue(@override.Key, out Dictionary value)) { doc.SectionOrder.Add(@override.Key); value = new Dictionary(StringComparer.OrdinalIgnoreCase); doc.Sections[@override.Key] = value; } foreach (KeyValuePair item in @override.Value) { value[item.Key] = item.Value; } } } private static bool HasPersistedContent(SaveSlotDocument document) { if (document.Lobby != null && (!string.IsNullOrWhiteSpace(document.Lobby.BaseLobbyName) || document.Lobby.IsPublicLobby.HasValue)) { return true; } if (document.SettingsProfile.Mode != SaveConfigProfileMode.Global) { return true; } Dictionary> configOverrides = document.ConfigOverrides; if (configOverrides != null && configOverrides.Count > 0) { return true; } Dictionary players = document.Players; if (players != null && players.Count > 0) { return true; } return false; } private static SaveSlotDocument NormalizeLoaded(SaveSlotDocument loaded) { if (loaded.Version <= 0) { loaded.Version = 1; } if (loaded.Lobby != null && string.IsNullOrWhiteSpace(loaded.Lobby.BaseLobbyName)) { loaded.Lobby.BaseLobbyName = null; } else if (loaded.Lobby?.BaseLobbyName != null) { loaded.Lobby.BaseLobbyName = loaded.Lobby.BaseLobbyName.Trim(); } Dictionary players = loaded.Players; if (players != null && players.Count == 0) { loaded.Players = null; } Dictionary> configOverrides = loaded.ConfigOverrides; if (configOverrides != null && configOverrides.Count == 0) { loaded.ConfigOverrides = null; } if (loaded.SettingsProfile.Mode == SaveConfigProfileMode.Quick && string.IsNullOrWhiteSpace(loaded.SettingsProfile.PresetId)) { loaded.SettingsProfile.Mode = SaveConfigProfileMode.Custom; } return loaded; } private static SaveSlotDocument CloneDocument(SaveSlotDocument source) { return new SaveSlotDocument { Version = ((source.Version <= 0) ? 1 : source.Version), Lobby = ((source.Lobby == null) ? null : CloneLobby(source.Lobby)), SettingsProfile = CloneProfile(source.SettingsProfile), ConfigOverrides = CloneOverrides(source.ConfigOverrides), Players = ClonePlayers(source.Players) }; } private static SaveSlotLobbySection CloneLobby(SaveSlotLobbySection source) { return new SaveSlotLobbySection { BaseLobbyName = source.BaseLobbyName, IsPublicLobby = source.IsPublicLobby }; } private static SaveConfigProfileState CloneProfile(SaveConfigProfileState source) { return new SaveConfigProfileState { Mode = source.Mode, PresetId = (source.PresetId ?? ""), PresetRevision = source.PresetRevision }; } private static Dictionary> CloneOverrides(Dictionary>? source) { Dictionary> dictionary = new Dictionary>(StringComparer.OrdinalIgnoreCase); if (source == null) { return dictionary; } foreach (KeyValuePair> item in source) { dictionary[item.Key] = new Dictionary(item.Value, StringComparer.OrdinalIgnoreCase); } return dictionary; } private static Dictionary? ClonePlayers(Dictionary? source) { if (source == null) { return null; } Dictionary dictionary = new Dictionary(); foreach (KeyValuePair item in source) { dictionary[item.Key] = new SaveSlotPlayerEntry { DisplayName = item.Value.DisplayName, VoiceId = item.Value.VoiceId }; } if (dictionary.Count <= 0) { return null; } return dictionary; } private static bool OverridesEqual(Dictionary>? a, Dictionary>? b) { if (a == null || a.Count == 0) { if (b != null) { return b.Count == 0; } return true; } if (b == null || a.Count != b.Count) { return false; } foreach (KeyValuePair> item in a) { if (!b.TryGetValue(item.Key, out Dictionary value) || item.Value.Count != value.Count) { return false; } foreach (KeyValuePair item2 in item.Value) { if (!value.TryGetValue(item2.Key, out var value2) || value2 != item2.Value) { return false; } } } return true; } } internal static class SaveSlotSidecarPersistence { private const string Feature = "SaveSlotSidecar"; private static int _loadingSidecarSlot = -1; internal static void OnSaveSlotLoaded(int slotId) { if (!MimesisSaveManager.IsHost() || !MimesisSaveManager.IsValidSaveSlotId(slotId)) { return; } if (_loadingSidecarSlot == slotId) { ModLog.Debug("SaveSlotSidecar", $"Skipping reentrant sidecar load for save slot {slotId}."); return; } _loadingSidecarSlot = slotId; try { SaveSlotDocumentStore.LoadForSlot(slotId); PlayerRegistry.LoadForSlot(slotId, forceReload: true); SaveSlotConfigStore.LoadForSlot(slotId); SpeechEventArchivePatches.EnsurePoolLoaded(slotId); JoinAnytimeLobbyController.OnSaveSlotSidecarLoaded(slotId); int count = PlayerRegistry.GetAllGlobals().Count; int loadedPlayerCount = SaveSlotDocumentStore.LoadedPlayerCount; ModLog.Info("SaveSlotSidecar", $"Loaded slot sidecars for save slot {slotId} — stats={count}, roster={loadedPlayerCount}."); WebDashboardOfflinePlayerCache.RebuildSync(PlayerRegistry.Revision); WebDashboardSnapshotCache.MarkDirty(); WebDashboardSnapshotCache.RequestFullPublish(); } finally { if (_loadingSidecarSlot == slotId) { _loadingSidecarSlot = -1; } } } internal static void EnsureSaveSlotLoaded(int slotId) { if (!MimesisSaveManager.IsHost() || !MimesisSaveManager.IsValidSaveSlotId(slotId)) { return; } int activeSlotId = SaveSlotConfigStore.ActiveSlotId; if (activeSlotId >= 0) { if (activeSlotId != slotId) { ModLog.Debug("SaveSlotSidecar", $"Ignoring sidecar reload — active slot {activeSlotId}, requested {slotId}."); } } else { OnSaveSlotLoaded(slotId); } } internal static void OnGameSaved(int slotId, IReadOnlyList? playerNames, bool isAutoSave) { if (MimesisSaveManager.IsHost() && MimesisSaveManager.IsValidSaveSlotId(slotId)) { EnsureSlotBoundForSave(slotId); bool flag = !isAutoSave; Stopwatch stopwatch = ((flag && ModConfig.EnableDebugLogging.Value) ? Stopwatch.StartNew() : null); SpeechEventPoolManager.ProcessDeferredUpdates(); PlayerRegistry.MergeLiveSessionRoster(); SpeechEventPoolManager.SyncVoiceMappingsToDocument(); PlayerRegistry.SyncRosterToDocument(); MimesisSaveManager.SaveMimesisData(slotId); PlayerRegistry.PersistStatistics(flag); SaveSlotConfigStore.ForceFlushToDisk(slotId, flag); SaveSlotDocumentStore.CaptureLobbyFromController(slotId); SaveSlotDocumentStore.ForceFlushToDisk(slotId, flag); if (flag) { PersistenceWriteQueue.FlushAllSync(); BackgroundFileWriteQueue.FlushAllSync(); } if (stopwatch != null) { stopwatch.Stop(); ModLog.Debug("SaveSlotSidecar", $"Manual save sidecar flush completed in {stopwatch.ElapsedMilliseconds} ms — slot={slotId}."); } ModLog.Info("SaveSlotSidecar", $"Persisted slot sidecars for save slot {slotId} (auto={isAutoSave})."); } } private static void EnsureSlotBoundForSave(int slotId) { if (SaveSlotDocumentStore.LoadedSlotId != slotId || SaveSlotConfigStore.ActiveSlotId != slotId) { ModLog.Info("SaveSlotSidecar", $"Late-binding sidecar stores for save slot {slotId} at save time."); OnSaveSlotLoaded(slotId); } } internal static void OnSessionEnded() { _loadingSidecarSlot = -1; PlayerRegistry.ResetSessionRuntimeState(); SpeechEventArchivePatches.InvalidatePoolLoaded(); SaveSlotConfigStore.ClearRuntimeToGlobal(); SaveSlotDocumentStore.Clear(); } internal static void FlushAllSync() { int activeSlotId = SaveSlotConfigStore.ActiveSlotId; if (activeSlotId >= 0) { SpeechEventPoolManager.ProcessDeferredUpdates(); SpeechEventPoolManager.SyncVoiceMappingsToDocument(); PlayerRegistry.PersistStatistics(waitForCompletion: true); SaveSlotConfigStore.ForceFlushToDisk(activeSlotId, waitForCompletion: true); SaveSlotDocumentStore.CaptureLobbyFromController(activeSlotId); SaveSlotDocumentStore.ForceFlushToDisk(activeSlotId, waitForCompletion: true); } PersistenceWriteQueue.FlushAllSync(); StatisticsStore.FlushAllSync(); BackgroundFileWriteQueue.FlushAllSync(); } } internal static class SparseTomlConfig { internal sealed class Document { internal readonly List SectionOrder = new List(); internal readonly Dictionary> Sections = new Dictionary>(StringComparer.OrdinalIgnoreCase); } internal static Document Load(string? text) { Document document = new Document(); if (string.IsNullOrWhiteSpace(text)) { return document; } string text2 = null; string[] array = text.Split('\n'); for (int i = 0; i < array.Length; i++) { string text3 = array[i].Trim(); if (text3.Length == 0 || text3.StartsWith('#')) { continue; } if (text3.StartsWith('[') && text3.EndsWith(']') && text3.Length > 2) { string text4 = text3; text2 = text4.Substring(1, text4.Length - 1 - 1).Trim(); if (!document.Sections.ContainsKey(text2)) { document.SectionOrder.Add(text2); document.Sections[text2] = new Dictionary(StringComparer.OrdinalIgnoreCase); } } else { if (text2 == null) { continue; } int num = text3.IndexOf('='); if (num <= 0) { continue; } string text5 = text3.Substring(0, num).Trim(); string text6 = text3.Substring(num + 1).Trim(); if (text6.Length >= 2 && text6.StartsWith('"') && text6.EndsWith('"')) { string text4 = text6; text6 = text4.Substring(1, text4.Length - 1 - 1); } if (text5.Length != 0) { if (!document.Sections.TryGetValue(text2, out Dictionary value)) { value = new Dictionary(StringComparer.OrdinalIgnoreCase); document.Sections[text2] = value; document.SectionOrder.Add(text2); } value[text5] = text6; } } } return document; } internal static string Serialize(Document doc) { if (ModConfig.IsInitialized) { PurgeUnregisteredEntries(doc, allowProfileSection: true); } StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < doc.SectionOrder.Count; i++) { string text = doc.SectionOrder[i]; if (!doc.Sections.TryGetValue(text, out Dictionary value) || value.Count == 0) { continue; } if (stringBuilder.Length > 0) { stringBuilder.AppendLine(); } stringBuilder.Append('[').Append(text).AppendLine("]"); foreach (string item in ModConfigRegistry.GetEntryOrder(text)) { if (value.TryGetValue(item, out var value2)) { stringBuilder.Append(item).Append(" = ").AppendLine(FormatValue(value2)); } } if (SaveSlotConfigProfile.IsProfileSection(text)) { WriteProfileKeys(stringBuilder, value); } } return stringBuilder.ToString(); } private static void WriteProfileKeys(StringBuilder sb, Dictionary keys) { WriteProfileKeyIfPresent(sb, keys, "Mode"); WriteProfileKeyIfPresent(sb, keys, "PresetId"); WriteProfileKeyIfPresent(sb, keys, "PresetRevision"); } private static void WriteProfileKeyIfPresent(StringBuilder sb, Dictionary keys, string key) { if (keys.TryGetValue(key, out string value)) { sb.Append(key).Append(" = ").AppendLine(FormatValue(value)); } } internal static string SerializeRaw(Document doc) { StringBuilder stringBuilder = new StringBuilder(); foreach (string item in doc.SectionOrder) { if (!doc.Sections.TryGetValue(item, out Dictionary value) || value.Count == 0) { continue; } if (stringBuilder.Length > 0) { stringBuilder.AppendLine(); } stringBuilder.Append('[').Append(item).AppendLine("]"); foreach (KeyValuePair item2 in value.OrderBy, string>((KeyValuePair pair) => pair.Key, StringComparer.Ordinal)) { stringBuilder.Append(item2.Key).Append(" = ").AppendLine(FormatValue(item2.Value)); } } string text = stringBuilder.ToString(); if (text.Length != 0) { return text + Environment.NewLine; } return text; } internal static bool PurgeUnregisteredEntries(Document doc, bool allowProfileSection = false) { bool flag = false; HashSet hashSet = new HashSet(ModConfigRegistry.GetSectionOrder(), StringComparer.OrdinalIgnoreCase); foreach (string item in doc.SectionOrder.ToList()) { if (allowProfileSection && SaveSlotConfigProfile.IsProfileSection(item)) { if (doc.Sections.TryGetValue(item, out Dictionary value)) { flag |= PurgeProfileSectionKeys(value); if (value.Count == 0) { doc.Sections.Remove(item); doc.SectionOrder.Remove(item); flag = true; } } } else if (!hashSet.Contains(item)) { doc.Sections.Remove(item); doc.SectionOrder.Remove(item); flag = true; } else { if (!doc.Sections.TryGetValue(item, out Dictionary value2)) { continue; } foreach (string item2 in value2.Keys.ToList()) { if (!ModConfigRegistry.TryGetEntry(item, item2, out MelonPreferences_Entry _)) { value2.Remove(item2); flag = true; } } if (value2.Count == 0) { doc.Sections.Remove(item); doc.SectionOrder.Remove(item); flag = true; } } } return flag; } private static bool PurgeProfileSectionKeys(Dictionary keys) { bool result = false; foreach (string item in keys.Keys.ToList()) { if (!string.Equals(item, "Mode", StringComparison.OrdinalIgnoreCase) && !string.Equals(item, "PresetId", StringComparison.OrdinalIgnoreCase) && !string.Equals(item, "PresetRevision", StringComparison.OrdinalIgnoreCase)) { keys.Remove(item); result = true; } } return result; } internal static bool IsEmpty(Document doc) { foreach (KeyValuePair> section in doc.Sections) { if (SaveSlotConfigProfile.IsProfileSection(section.Key) && section.Value.Count > 0) { return false; } if (section.Value.Count > 0 && !SaveSlotConfigProfile.IsProfileSection(section.Key)) { return false; } } return true; } internal static void RepairTomletCompatibility(string? filePath) { if (string.IsNullOrEmpty(filePath) || !File.Exists(filePath)) { return; } string[] array = File.ReadAllLines(filePath); bool flag = false; for (int i = 0; i < array.Length; i++) { if (TryRepairAssignmentLine(array[i], out string repaired) && repaired != array[i]) { array[i] = repaired; flag = true; } } if (flag) { AtomicFileIO.WriteText(filePath, string.Join(Environment.NewLine, array) + Environment.NewLine, "SparseTomlConfig"); } } internal static bool TryRepairAssignmentLine(string line, out string repaired) { repaired = line; int num = line.IndexOf('='); if (num <= 0 || line.TrimStart().StartsWith('[')) { return false; } string text = line.Substring(0, num); string text2 = line.Substring(num + 1); string text3 = text2; string text4 = ""; int num2 = text2.IndexOf('#'); if (num2 >= 0) { text3 = text2.Substring(0, num2); text4 = text2.Substring(num2); } text3 = text3.Trim(); if (!NeedsQuotingForTomlet(text3)) { return false; } string value = UnquoteTomlString(text3); repaired = text + "= " + FormatValue(value) + text4; return true; } private static string FormatValue(string value) { if (NeedsQuotingForTomlet(value)) { return QuoteTomlString(value); } return value; } private static bool NeedsQuotingForTomlet(string value) { if (value.Length == 0) { return true; } if (value.Length >= 2 && value.StartsWith('"') && value.EndsWith('"')) { return false; } if (string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) || string.Equals(value, "false", StringComparison.OrdinalIgnoreCase)) { return false; } if (long.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var _) || float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var _)) { return false; } return true; } private static string UnquoteTomlString(string value) { if (value.Length >= 2 && value.StartsWith('"') && value.EndsWith('"')) { return value.Substring(1, value.Length - 1 - 1).Replace("\\\"", "\"").Replace("\\\\", "\\"); } return value; } private static string QuoteTomlString(string value) { return "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; } } public static class ModLog { internal static readonly ColorARGB SuccessGreen = ColorARGB.Green; internal static readonly ColorARGB FailureRed = ColorARGB.Red; internal static readonly ColorARGB PartialYellow = ColorARGB.Yellow; internal static readonly ColorARGB Neutral = MelonLogger.DefaultTextColor; private static readonly MethodInfo? PassLogMsgMethod = typeof(MelonLogger).GetMethod("PassLogMsg", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[5] { typeof(ColorARGB), typeof(string), typeof(ColorARGB), typeof(string), typeof(string) }, null); private static bool UseLegacyConsoleColors => MelonUtils.IsUnderWineOrSteamProton(); private static bool IsDebugEnabled { get { if (ModConfig.IsInitialized) { return ModConfig.EnableDebugLogging?.Value ?? false; } return false; } } internal static string FeatureSection(string feature, string title) { return feature + "][" + title; } internal static void PassLogSegmented(string section, string stripped, params (ColorARGB? color, string text)[] segments) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) if (PassLogMsgMethod == null) { MelonLogger.Msg(stripped); return; } string text = BuildMessageBody(segments, plain: true); ColorARGB val = PickMessageColor(segments); PassLogMsgMethod.Invoke(null, new object[5] { val, text, Neutral, section, stripped }); } internal static string BuildMessageBody((ColorARGB? color, string text)[] segments, bool plain) { //IL_0032: Unknown result type (might be due to invalid IL or missing references) StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < segments.Length; i++) { var (val, text) = segments[i]; stringBuilder.Append((plain || !val.HasValue) ? text : ConsoleExtensions.Pastel(text, val.Value)); } return stringBuilder.ToString(); } internal static ColorARGB PickMessageColor((ColorARGB? color, string text)[] segments) { //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Unknown result type (might be due to invalid IL or missing references) bool flag = segments.Any(delegate((ColorARGB? color, string text) s) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) if (s.color.HasValue) { ColorARGB value = s.color.Value; return ((ColorARGB)(ref value)).Equals(FailureRed); } return false; }); bool flag2 = segments.Any(delegate((ColorARGB? color, string text) s) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) if (s.color.HasValue) { ColorARGB value = s.color.Value; return ((ColorARGB)(ref value)).Equals(SuccessGreen); } return false; }); if (flag && flag2) { return PartialYellow; } if (flag) { return FailureRed; } if (flag2) { return SuccessGreen; } ColorARGB[] array = (from s in segments where s.color.HasValue select s.color.Value).Distinct().Take(2).ToArray(); if (array.Length != 1) { return Neutral; } return array[0]; } public static void Info(string feature, string message) { TryMelonLog(delegate { MelonLogger.Msg("[" + feature + "] " + message); }); } public static void Warn(string feature, string message) { TryMelonLog(delegate { MelonLogger.Warning("[" + feature + "] " + message); }); } public static void WarnRed(string feature, string message) { string body = "[" + feature + "] " + message; if (UseLegacyConsoleColors) { ConsoleColor foregroundColor = Console.ForegroundColor; try { Console.ForegroundColor = ConsoleColor.Red; TryMelonLog(delegate { MelonLogger.Warning(body); }); return; } finally { Console.ForegroundColor = foregroundColor; } } TryMelonLog(delegate { //IL_0006: Unknown result type (might be due to invalid IL or missing references) MelonLogger.Warning(ConsoleExtensions.Pastel(body, FailureRed)); }); } public static void Error(string feature, string message) { TryMelonLog(delegate { MelonLogger.Error("[" + feature + "] " + message); }); } public static void Debug(string feature, string message) { if (IsDebugEnabled) { TryMelonLog(delegate { MelonLogger.Msg("[" + feature + ":debug] " + message); }); } } public static void Debug(string feature, Func messageFactory) { if (IsDebugEnabled) { TryMelonLog(delegate { MelonLogger.Msg("[" + feature + ":debug] " + messageFactory()); }); } } private static void TryMelonLog(Action write) { try { write(); } catch (NullReferenceException) { } } } internal readonly struct PlayerLifecycleContribution { public string Feature { get; } public string Detail { get; } public PlayerLifecycleContribution(string feature, string detail) { Feature = feature; Detail = detail; } } internal enum PersistenceConnectPhase { Skipped, Connecting, Connected } internal readonly struct PersistenceConnectOutcome { internal PersistenceConnectPhase Phase { get; } internal string? Detail { get; } public PersistenceConnectOutcome(PersistenceConnectPhase phase, string? detail) { Phase = phase; Detail = detail; } } internal static class PlayerLifecycleCoordinator { private sealed class ConnectState { internal SpeechEventArchive Archive; internal ulong SteamId; internal bool ConnectingLogged; internal bool ConnectedLogged; internal PersistenceConnectPhase PersistencePhase; internal readonly Dictionary Contributions = new Dictionary(); } private sealed class DisconnectState { internal SpeechEventArchive? Archive; internal ulong SteamId; internal long PlayerUid; internal bool DisconnectedLogged; internal readonly Dictionary Contributions = new Dictionary(); } private const string LogFeature = "Session"; private static readonly string[] ContributionOrder = new string[2] { "Persistence", "Statistics" }; private static readonly Dictionary _connectByArchive = new Dictionary(); private static readonly Dictionary _connectBySteam = new Dictionary(); private static readonly Dictionary _disconnectBySteam = new Dictionary(); private static readonly Dictionary _pendingStatisticsConnect = new Dictionary(); private static readonly string[] ConnectingContributionOrder = new string[1] { "Persistence" }; internal static void FinishArchiveConnect(SpeechEventArchive archive) { if (!((Object)(object)archive == (Object)null)) { ConnectState orCreateConnectState = GetOrCreateConnectState(archive); ApplyPendingStatisticsConnect(orCreateConnectState); TryFlushConnect(orCreateConnectState); } } internal static void SetPersistenceOutcome(SpeechEventArchive archive, PersistenceConnectOutcome outcome, bool flush = false) { if (!((Object)(object)archive == (Object)null)) { ConnectState orCreateConnectState = GetOrCreateConnectState(archive); orCreateConnectState.PersistencePhase = outcome.Phase; if (string.IsNullOrEmpty(outcome.Detail)) { orCreateConnectState.Contributions.Remove("Persistence"); } else { orCreateConnectState.Contributions["Persistence"] = new PlayerLifecycleContribution("Persistence", outcome.Detail); } if (flush) { TryFlushConnect(orCreateConnectState); } } } internal static void TryFlushConnect(SpeechEventArchive archive) { if ((Object)(object)archive != (Object)null && _connectByArchive.TryGetValue(archive, out ConnectState value)) { TryFlushConnect(value); } } internal static void NotifyStatisticsConnect(ulong steamId, PlayerLifecycleContribution? contribution) { if (steamId == 0L || !contribution.HasValue) { return; } ConnectState connectState = FindConnectStateForSteam(steamId); if (connectState == null) { _pendingStatisticsConnect[steamId] = contribution.Value; return; } BindConnectSteam(connectState, steamId); connectState.Contributions[contribution.Value.Feature] = contribution.Value; if (!connectState.ConnectedLogged) { TryFlushConnect(connectState); } } internal static void OnArchiveDisconnecting(SpeechEventArchive archive, PlayerLifecycleContribution? contribution, ulong steamIdHint = 0uL, long playerUidHint = 0L) { if (!((Object)(object)archive == (Object)null)) { ulong steamId = steamIdHint; long playerUid = playerUidHint; if (steamId == 0L && playerUid == 0L) { VoiceEventStats.TryCaptureArchiveIdentity(archive, out playerUid, out var _, out steamId); } DisconnectState orCreateDisconnectState = GetOrCreateDisconnectState(steamId, archive, playerUid); if (contribution.HasValue) { orCreateDisconnectState.Contributions[contribution.Value.Feature] = contribution.Value; } TryFlushDisconnect(orCreateDisconnectState); } } internal static void NotifyStatisticsDisconnect(ulong steamId, PlayerLifecycleContribution? contribution) { if (steamId != 0L) { DisconnectState orCreateDisconnectState = GetOrCreateDisconnectState(steamId, null, 0L); orCreateDisconnectState.SteamId = steamId; if (contribution.HasValue) { orCreateDisconnectState.Contributions[contribution.Value.Feature] = contribution.Value; } if (!ShouldDeferDisconnectForPersistence(steamId)) { TryFlushDisconnect(orCreateDisconnectState); } } } internal static void ClearConnectState(SpeechEventArchive archive) { if (!((Object)(object)archive == (Object)null) && _connectByArchive.Remove(archive, out ConnectState value) && value.SteamId != 0L) { _connectBySteam.Remove(value.SteamId); } } internal static void ClearAll() { _connectByArchive.Clear(); _connectBySteam.Clear(); _disconnectBySteam.Clear(); _pendingStatisticsConnect.Clear(); } private static ConnectState GetOrCreateConnectState(SpeechEventArchive archive) { if (_connectByArchive.TryGetValue(archive, out ConnectState value)) { EnsureSteamId(value, archive); return value; } ConnectState connectState = new ConnectState { Archive = archive }; EnsureSteamId(connectState, archive); _connectByArchive[archive] = connectState; return connectState; } private static void EnsureSteamId(ConnectState state, SpeechEventArchive archive) { if (state.SteamId == 0L && VoiceEventStats.TryGetConnectionInfo(archive, out PlayerConnectionInfo info) && info.SteamId != 0L) { BindConnectSteam(state, info.SteamId); } } private static void BindConnectSteam(ConnectState state, ulong steamId) { if (steamId != 0L) { state.SteamId = steamId; _connectBySteam[steamId] = state; ApplyPendingStatisticsConnect(state); } } private static void ApplyPendingStatisticsConnect(ConnectState state) { if (state.SteamId != 0L && _pendingStatisticsConnect.Remove(state.SteamId, out var value)) { state.Contributions[value.Feature] = value; } } private static ConnectState? FindConnectStateForSteam(ulong steamId) { if (_connectBySteam.TryGetValue(steamId, out ConnectState value)) { return value; } ConnectState connectState = null; ConnectState connectState2 = null; int num = 0; foreach (ConnectState value2 in _connectByArchive.Values) { if (value2.SteamId == 0L) { num++; connectState2 = value2; if (TryArchiveMatchesSteam(value2.Archive, steamId)) { connectState = value2; break; } } } if (connectState != null) { BindConnectSteam(connectState, steamId); return connectState; } if (num == 1 && connectState2 != null) { BindConnectSteam(connectState2, steamId); return connectState2; } return null; } private static bool TryArchiveMatchesSteam(SpeechEventArchive archive, ulong steamId) { if ((Object)(object)archive == (Object)null || steamId == 0L) { return false; } if (VoiceEventStats.TryCaptureArchiveIdentity(archive, out var playerUid, out var isLocal, out var steamId2)) { if (steamId2 == steamId) { return true; } if (playerUid != 0L && GameSessionAccess.ResolveSteamId(playerUid, isLocal) == steamId) { return true; } } if (VoiceEventStats.TryGetConnectionInfo(archive, out PlayerConnectionInfo info)) { return info.SteamId == steamId; } return false; } private static bool ShouldDeferDisconnectForPersistence(ulong steamId) { if (MimesisSaveManager.IsHost()) { return steamId != GameSessionAccess.GetLocalSteamId(); } return false; } private static DisconnectState GetOrCreateDisconnectState(ulong steamId, SpeechEventArchive? archive, long playerUid) { if (steamId != 0L && _disconnectBySteam.TryGetValue(steamId, out DisconnectState value)) { if ((Object)(object)archive != (Object)null) { DisconnectState disconnectState = value; if (disconnectState.Archive == null) { disconnectState.Archive = archive; } } if (playerUid != 0L) { value.PlayerUid = playerUid; } value.SteamId = steamId; return value; } if (steamId == 0L && _disconnectBySteam.Count == 1) { KeyValuePair keyValuePair = _disconnectBySteam.First(); DisconnectState disconnectState = keyValuePair.Value; if (disconnectState.Archive == null) { disconnectState.Archive = archive; } if (playerUid != 0L) { keyValuePair.Value.PlayerUid = playerUid; } if (steamId != 0L) { keyValuePair.Value.SteamId = steamId; } return keyValuePair.Value; } DisconnectState disconnectState2 = new DisconnectState { Archive = archive, SteamId = steamId, PlayerUid = playerUid }; if (steamId != 0L) { _disconnectBySteam[steamId] = disconnectState2; } return disconnectState2; } private static void TryFlushDisconnect(DisconnectState state) { if (!state.DisconnectedLogged) { state.DisconnectedLogged = true; LogLifecycle("disconnected", state.Archive, OrderedContributions(state.Contributions), state.SteamId, state.PlayerUid); if (state.SteamId != 0L) { _disconnectBySteam.Remove(state.SteamId); } } } private static bool HasMinimumIdentity(ConnectState state) { if (state.SteamId != 0L) { return true; } if ((Object)(object)state.Archive != (Object)null && VoiceEventStats.TryGetConnectionInfo(state.Archive, out PlayerConnectionInfo info)) { if (info.SteamId == 0L) { return info.PlayerUid != 0; } return true; } return false; } private static void TryFlushConnect(ConnectState state) { if (state.PersistencePhase == PersistenceConnectPhase.Connecting) { if (!state.ConnectingLogged && HasMinimumIdentity(state)) { state.ConnectingLogged = true; LogLifecycle("connecting", state.Archive, OrderedContributions(state.Contributions, ConnectingContributionOrder), state.SteamId, 0L); } } else if (!state.ConnectedLogged && HasMinimumIdentity(state)) { state.ConnectedLogged = true; LogLifecycle("connected", state.Archive, OrderedContributions(state.Contributions), state.SteamId, 0L); } } internal static List OrderedContributions(Dictionary contributions, string[]? order = null) { if (order == null) { order = ContributionOrder; } List list = new List(); string[] array = order; foreach (string key in array) { if (contributions.TryGetValue(key, out var value)) { list.Add(value); } } return list; } private static void LogLifecycle(string verb, SpeechEventArchive? archive, IReadOnlyList contributions, ulong steamIdFallback = 0uL, long playerUidFallback = 0L) { string text = ResolveLifecycleIdentity(archive, steamIdFallback, playerUidFallback); string text2 = FormatContributions(contributions); ModLog.Info("Session", string.IsNullOrEmpty(text2) ? ("Player " + verb + " — " + text) : ("Player " + verb + " — " + text + " — " + text2)); } private static string ResolveLifecycleIdentity(SpeechEventArchive? archive, ulong steamIdFallback, long playerUidFallback = 0L) { if ((Object)(object)archive != (Object)null && VoiceEventStats.TryGetConnectionInfo(archive, out PlayerConnectionInfo info)) { if (info.SteamId == 0L && steamIdFallback != 0L) { info.SteamId = steamIdFallback; } if (info.PlayerUid == 0L && playerUidFallback != 0L) { info.PlayerUid = playerUidFallback; } return VoiceEventStats.FormatLifecycleIdentity(archive, info); } if (steamIdFallback != 0L) { return VoiceEventStats.DescribeSteamPlayer(steamIdFallback, playerUidFallback, archive); } if (!((Object)(object)archive != (Object)null)) { return VoiceEventStats.DescribeSteamPlayer(steamIdFallback, playerUidFallback, archive); } return VoiceEventStats.DescribePlayer(archive); } internal static string FormatContributions(IReadOnlyList contributions) { if (contributions.Count == 0) { return string.Empty; } StringBuilder stringBuilder = new StringBuilder(); foreach (PlayerLifecycleContribution contribution in contributions) { if (!string.IsNullOrEmpty(contribution.Detail)) { if (stringBuilder.Length > 0) { stringBuilder.Append("; "); } stringBuilder.Append(contribution.Feature).Append(": ").Append(contribution.Detail); } } return stringBuilder.ToString(); } } public sealed class Mod : MelonMod { private Harmony? _harmony; private float _nextEncounterSpawnProcessTime; private float _nextLocaleRefreshTime; private bool _isInitializing; private static bool _globalConfigFlushed; private readonly object _pendingSyncLock = new object(); private ModConfigChangeInfo? _pendingSyncFromConfig; public override void OnInitializeMelon() { //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Expected O, but got Unknown _isInitializing = true; try { GameLocaleAccess.CaptureMainThread(); ModL10n.Initialize(); ModConfig.Initialize(((MelonBase)this).LoggerInstance); SceneScopedConfigGate.Initialize(); SceneScopedConfigGate.SetDeferredModuleSyncAction(FeatureModules.SyncModuleByName); if (!GameVersionChecker.TryAllowLoad()) { throw new InvalidOperationException("Mimesis Player Enhancement refused to load — game version mismatch (expected 0.3.1)."); } _harmony = new Harmony("com.mimesis.playerenhancement"); foreach (FeatureModule item in FeatureModules.All) { item.ApplyPatches(_harmony); } SceneScopedConfigPatches.Apply(_harmony); SyncFromConfig(ModConfigChangeInfo.FullReload); LogStartupSummary(); ModConfig.Changed += SyncFromConfig; Application.quitting += OnApplicationQuitting; } finally { _isInitializing = false; } } private static void OnApplicationQuitting() { WebDashboardServer.PrepareApplicationQuit(); FlushGlobalConfigOnShutdown(); } public override void OnPreferencesSaved(string filepath) { if (IsOurConfigFile(filepath)) { ModConfig.NormalizeSavedFloats(); GlobalConfigStore.RestorePendingToDisk(); } } public override void OnPreferencesLoaded(string filepath) { if (IsOurConfigFile(filepath)) { ModConfig.SanitizeFloatEntries(); ModConfig.NormalizeSavedFloats(); if (!_isInitializing) { ModConfig.NotifyFileReloaded(); } } } public override void OnUpdate() { FlushDeferredSyncFromConfig(); foreach (FeatureModule item in FeatureModules.All) { if (!item.ThrottledUpdate && (item.SessionScope != SessionScope.HostOnly || FeatureModuleSessionHooks.IsModuleActive(item.Name))) { item.OnUpdate(); } } if ((ModConfig.EnableSpawnScaling.Value || ModConfig.EnableLootMultiplicator.Value || WeatherCycleScheduler.NeedsThrottledProcessing()) && Time.time >= _nextEncounterSpawnProcessTime) { _nextEncounterSpawnProcessTime = Time.time + 1f; foreach (FeatureModule item2 in FeatureModules.All) { if (item2.ThrottledUpdate && (item2.SessionScope != SessionScope.HostOnly || FeatureModuleSessionHooks.IsModuleActive(item2.Name))) { item2.OnUpdate(); } } } SessionLifecycle.Tick(); if (Time.time >= _nextLocaleRefreshTime) { _nextLocaleRefreshTime = Time.time + 2f; ModConfigLocalization.RefreshIfLanguageChanged(); } } public override void OnDeinitializeMelon() { Application.quitting -= OnApplicationQuitting; FlushGlobalConfigOnShutdown(); SessionLifecycle.NotifySessionEndedIfActive(); foreach (FeatureModule item in FeatureModules.All) { item.OnDeinitialize(); } HostStatusCache.Invalidate(); ModConfig.Changed -= SyncFromConfig; if (_harmony != null) { _harmony.UnpatchSelf(); ModLog.Debug("Startup", "Harmony patches removed."); } } private static bool IsOurConfigFile(string filepath) { return string.Equals(filepath, ModConfig.FilePath, StringComparison.OrdinalIgnoreCase); } private static void FlushGlobalConfigOnShutdown() { if (!_globalConfigFlushed) { _globalConfigFlushed = true; ModConfig.FlushGlobalToDisk(); } } private void SyncFromConfig(ModConfigChangeInfo change) { if (ModConfig.IsInitialized) { if (!GameLocaleAccess.IsMainThread) { QueueDeferredSyncFromConfig(change); } else { ApplySyncFromConfig(change); } } } private void QueueDeferredSyncFromConfig(ModConfigChangeInfo change) { lock (_pendingSyncLock) { _pendingSyncFromConfig = CoalescePendingSync(_pendingSyncFromConfig, change); } } private void FlushDeferredSyncFromConfig() { ModConfigChangeInfo pendingSyncFromConfig; lock (_pendingSyncLock) { pendingSyncFromConfig = _pendingSyncFromConfig; _pendingSyncFromConfig = null; } if (pendingSyncFromConfig != null) { ApplySyncFromConfig(pendingSyncFromConfig); } } private static ModConfigChangeInfo CoalescePendingSync(ModConfigChangeInfo? pending, ModConfigChangeInfo incoming) { if (pending == null) { return incoming; } if (pending.IsFullReload || incoming.IsFullReload) { return ModConfigChangeInfo.FullReload; } List merged = new List(); HashSet seen = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (ModConfigKeyChange changedKey in pending.ChangedKeys) { AddKey(changedKey); } foreach (ModConfigKeyChange changedKey2 in incoming.ChangedKeys) { AddKey(changedKey2); } return new ModConfigChangeInfo { ChangedKeys = merged }; void AddKey(ModConfigKeyChange keyChange) { string item = keyChange.SectionId + "\0" + keyChange.Key; if (seen.Add(item)) { merged.Add(keyChange); } } } private void ApplySyncFromConfig(ModConfigChangeInfo change) { SceneScopedConfigGate.OnConfigChangePrepared(change); HashSet hashSet = (change.IsFullReload ? null : ModConfigRegistry.GetAffectedModuleNames(change)); foreach (FeatureModule item in FeatureModules.All) { if ((hashSet == null || hashSet.Contains(item.Name)) && !SceneScopedConfigGate.ShouldDeferModuleSync(item.Name, change)) { item.SyncFromConfig(); } } ModLog.Debug("Config", "Synced — " + BuildConfigSummary()); } private void LogStartupSummary() { ModLog.Info("Startup", "v26.8.2 loaded — " + BuildConfigSummary()); } private static string BuildConfigSummary() { int num = (ModConfig.EnableMorePlayers.Value ? ModConfig.MaxPlayers.Value : 4); return $"MorePlayers={ModConfig.EnableMorePlayers.Value} (session cap {num}), " + $"MoreVoices={ModConfig.EnableMoreVoices.Value}" + (ModConfig.EnableMoreVoices.Value ? ($" (indoor {ModConfig.MaxIndoorVoiceEvents.Value}, deathmatch {ModConfig.MaxDeathMatchVoiceEvents.Value}, outdoor {ModConfig.MaxOutdoorVoiceEvents.Value}; " + $"unifiedIndoorOutdoor={ModConfig.UnifyIndoorOutdoorVoices.Value}; " + $"maintenance={ModConfig.RecordVoiceInMaintenance.Value}, tram={ModConfig.RecordVoiceInTram.Value}, possession={ModConfig.RecordVoiceDuringMimicPossession.Value})") : "") + $", Persistence={ModConfig.EnablePersistence.Value}, " + $"Statistics={ModConfig.EnableStatistics.Value}, " + $"JoinAnytime={ModConfig.EnableJoinAnytime.Value}, " + $"SpawnScaling={ModConfig.EnableSpawnScaling.Value}, " + $"LootMultiplicator={ModConfig.EnableLootMultiplicator.Value}, " + $"Economy={ModConfig.EnableEconomy.Value}, " + $"DungeonTime={ModConfig.EnableDungeonTime.Value}, " + $"MimicTuning={ModConfig.EnableMimicTuning.Value}" + (ModConfig.EnableMimicTuning.Value ? (" (voice=" + ModConfig.MimicVoiceTuningMode.Value + ", inventory=" + ModConfig.MimicInventoryCopyMode.Value + (ModConfig.EnableMimicPossessionTuning.Value ? (", possession" + (ModConfig.RandomizeMimicPossessionDuration.Value ? $", duration={ModConfig.MimicPossessionMinTimeSeconds.Value}-{ModConfig.MimicPossessionMaxTimeSeconds.Value}s" : "") + $", cooltime×{ModConfig.MimicPossessionCooltimeMultiplier.Value}") : "") + ")") : "") + $", PlayerTuning={ModConfig.EnablePlayerTuning.Value}, " + $"DungeonRandomizer={ModConfig.EnableDungeonRandomizer.Value}, " + $"Ui=(saveSlots={ModConfig.EnableExtendedSaveSlots.Value}, spectatorList={ModConfig.EnableExtendedSpectatorPlayerList.Value}, inGameMenuPlayerList={ModConfig.EnableExtendedInGameMenuPlayerList.Value})" + $", WebDashboard=({ModConfig.WebDashboardListenAddress.Value}:{ModConfig.WebDashboardListenPort.Value})" + $", DebugLogging={ModConfig.EnableDebugLogging.Value}"; } } internal static class GameVersionChecker { private const string Feature = "Startup"; internal static bool TryAllowLoad() { string text = Normalize(UnityInformationHandler.GameVersion); string text2 = Normalize("0.3.1"); if (VersionsMatch(text, text2)) { ModLog.Debug("Startup", "Game version matches — " + text); return true; } ModLog.Error("Startup", "Plugin refused to load — game version mismatch — detected=" + text + ", expected=" + text2 + " — plugin v26.8.2 built for MIMESIS " + text2 + " — install a matching release: https://github.com/Kandru/mimesis-player-enhancements/releases"); return false; } private static string Normalize(string? value) { if (!string.IsNullOrWhiteSpace(value)) { return value.Trim(); } return string.Empty; } private static bool VersionsMatch(string detected, string expected) { if (string.IsNullOrEmpty(detected) || string.IsNullOrEmpty(expected)) { return false; } return string.Equals(detected, expected, StringComparison.OrdinalIgnoreCase); } } public static class VersionInfo { public const string ModuleVersion = "26.8.2"; public const string GameVersion = "0.3.1"; internal const string ReleasesUrl = "https://github.com/Kandru/mimesis-player-enhancements/releases"; } } namespace MimesisPlayerEnhancement.Util { internal static class AtomicFileIO { internal const string BackupSuffix = ".bak"; internal const string TempSuffix = ".tmp"; internal static void WriteBytes(string filePath, byte[] data, string logFeature = "Persistence") { string text = filePath + ".tmp"; string destFileName = filePath + ".bak"; File.WriteAllBytes(text, data); if (File.Exists(filePath)) { try { File.Copy(filePath, destFileName, overwrite: true); } catch (Exception ex) { ModLog.Warn(logFeature, "Backup failed for " + Path.GetFileName(filePath) + ": " + ex.Message); } } if (File.Exists(filePath)) { File.Delete(filePath); } File.Move(text, filePath); } internal static void WriteText(string filePath, string text, string logFeature = "Persistence") { WriteBytes(filePath, Encoding.UTF8.GetBytes(text), logFeature); } internal static byte[]? ReadBytes(string filePath, string logFeature = "Persistence") { if (File.Exists(filePath)) { try { byte[] array = File.ReadAllBytes(filePath); if (array.Length != 0) { return array; } } catch (Exception ex) { ModLog.Warn(logFeature, "Main file read failed (" + Path.GetFileName(filePath) + "): " + ex.Message); } } string path = filePath + ".bak"; if (File.Exists(path)) { try { byte[] array2 = File.ReadAllBytes(path); if (array2.Length != 0) { ModLog.Warn(logFeature, "Recovered from backup: " + Path.GetFileName(path)); return array2; } } catch (Exception ex2) { ModLog.Error(logFeature, "Backup also failed (" + Path.GetFileName(path) + "): " + ex2.Message); } } return null; } internal static string? ReadText(string filePath, string logFeature = "Persistence") { byte[] array = ReadBytes(filePath, logFeature); if (array != null) { return Encoding.UTF8.GetString(array); } return null; } internal static void DeleteVolatileSiblings(string filePath, string logFeature = "Persistence") { string[] array = new string[2] { filePath + ".bak", filePath + ".tmp" }; foreach (string path in array) { if (File.Exists(path)) { try { File.Delete(path); ModLog.Debug(logFeature, "Deleted stale file: " + Path.GetFileName(path)); } catch (Exception ex) { ModLog.Warn(logFeature, "Failed to delete " + Path.GetFileName(path) + ": " + ex.Message); } } } } internal static void Delete(string filePath, string logFeature = "Persistence") { if (File.Exists(filePath)) { try { File.Delete(filePath); ModLog.Debug(logFeature, "Deleted stale file: " + Path.GetFileName(filePath)); } catch (Exception ex) { ModLog.Warn(logFeature, "Failed to delete " + Path.GetFileName(filePath) + ": " + ex.Message); } } DeleteVolatileSiblings(filePath, logFeature); } } internal enum FileWriteKind { Text, Bytes, Delete } internal sealed class FileWritePayload { internal FileWriteKind Kind; internal string? Text; internal byte[]? Bytes; } internal static class BackgroundFileWriteQueue { private sealed class PendingWrite { internal FileWritePayload? Latest; internal string LogFeature = "FileIO"; internal Task? WriteTask; } private static readonly ConcurrentDictionary InFlight = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); internal static void EnqueueText(string path, string text, string logFeature = "FileIO", bool waitForCompletion = false) { Enqueue(path, new FileWritePayload { Kind = FileWriteKind.Text, Text = text }, logFeature, waitForCompletion); } internal static void EnqueueBytes(string path, byte[] bytes, string logFeature = "FileIO", bool waitForCompletion = false) { Enqueue(path, new FileWritePayload { Kind = FileWriteKind.Bytes, Bytes = bytes }, logFeature, waitForCompletion); } internal static void EnqueueDelete(string path, string logFeature = "FileIO", bool waitForCompletion = false) { Enqueue(path, new FileWritePayload { Kind = FileWriteKind.Delete }, logFeature, waitForCompletion); } internal static void FlushAllSync() { WaitForInFlightWrites(); } private static void Enqueue(string path, FileWritePayload payload, string logFeature, bool waitForCompletion) { PendingWrite orAdd = InFlight.GetOrAdd(path, (string _) => new PendingWrite()); lock (orAdd) { orAdd.Latest = payload; orAdd.LogFeature = logFeature; Task writeTask = orAdd.WriteTask; if (writeTask != null && !writeTask.IsCompleted) { if (!waitForCompletion) { return; } } else { StartWriteTask(orAdd, path, payload, logFeature); if (!waitForCompletion) { return; } } } if (waitForCompletion) { WaitForPendingWrites(path, orAdd); } } private static void StartWriteTask(PendingWrite pending, string path, FileWritePayload payload, string logFeature) { pending.WriteTask = Task.Run(delegate { WriteToDisk(path, payload, logFeature); }); } private static void WriteToDisk(string path, FileWritePayload payload, string logFeature) { try { switch (payload.Kind) { case FileWriteKind.Text: EnsureDirectory(path); AtomicFileIO.WriteText(path, payload.Text ?? string.Empty, logFeature); break; case FileWriteKind.Bytes: EnsureDirectory(path); AtomicFileIO.WriteBytes(path, payload.Bytes ?? Array.Empty(), logFeature); break; case FileWriteKind.Delete: AtomicFileIO.Delete(path, logFeature); break; } } catch (Exception ex) { ModLog.Warn(logFeature, "Background file write failed (" + Path.GetFileName(path) + "): " + ex.Message); } CompleteWrite(path, payload); } private static void EnsureDirectory(string filePath) { string directoryName = Path.GetDirectoryName(filePath); if (!string.IsNullOrEmpty(directoryName)) { Directory.CreateDirectory(directoryName); } } private static void CompleteWrite(string path, FileWritePayload written) { if (!InFlight.TryGetValue(path, out PendingWrite value)) { return; } lock (value) { if (value.Latest != null && !PayloadsEqual(value.Latest, written)) { FileWritePayload newer = value.Latest; string feature = value.LogFeature; value.Latest = null; value.WriteTask = Task.Run(delegate { WriteToDisk(path, newer, feature); }); } else { value.Latest = null; InFlight.TryRemove(path, out PendingWrite _); } } } private static bool PayloadsEqual(FileWritePayload a, FileWritePayload b) { if (a.Kind != b.Kind) { return false; } return a.Kind switch { FileWriteKind.Text => a.Text == b.Text, FileWriteKind.Bytes => a.Bytes == b.Bytes || (a.Bytes != null && b.Bytes != null && a.Bytes.AsSpan().SequenceEqual(b.Bytes)), FileWriteKind.Delete => true, _ => false, }; } private static void WaitForInFlightWrites() { KeyValuePair[] array = InFlight.ToArray(); for (int i = 0; i < array.Length; i++) { KeyValuePair keyValuePair = array[i]; WaitForPendingWrites(keyValuePair.Key, keyValuePair.Value); } } private static void WaitForPendingWrites(string path, PendingWrite pending) { while (true) { Task task; lock (pending) { Task writeTask = pending.WriteTask; task = ((writeTask != null && !writeTask.IsCompleted) ? pending.WriteTask : null); if (task == null && pending.Latest != null) { FileWritePayload latest = pending.Latest; string logFeature = pending.LogFeature; StartWriteTask(pending, path, latest, logFeature); writeTask = pending.WriteTask; task = ((writeTask != null && !writeTask.IsCompleted) ? pending.WriteTask : null); } } if (task == null) { break; } TaskWaitHelper.WaitSync(task, "FileIO", "Background file write", "Background file write timed out after 30 seconds."); } } } internal static class CsvIdSetParser { internal static HashSet Parse(string? csv, string logFeature, string tokenLabel) { HashSet hashSet = new HashSet(); if (string.IsNullOrWhiteSpace(csv)) { return hashSet; } string[] array = csv.Split(','); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length != 0) { if (int.TryParse(text, out var result) && result > 0) { hashSet.Add(result); continue; } ModLog.Debug(logFeature, "Ignoring invalid " + tokenLabel + " token: '" + text + "'"); } } return hashSet; } } [Flags] internal enum DungeonRoomApplyKind { SpawnScaling = 1, MapPlacedEncounters = 2, LootScaling = 4, LootSpawnFilter = 0x40, FixedLootCoordination = 8, DungeonTime = 0x10, Weather = 0x20, SpawnScalingSkippedOnce = 0x100, LootScalingSkippedOnce = 0x200 } internal static class DungeonRoomAppliedSet { private sealed class RoomApplyState { internal DungeonRoomApplyKind Flags; } private static readonly ConditionalWeakTable States = new ConditionalWeakTable(); internal static bool IsApplied(DungeonRoom room, DungeonRoomApplyKind kind) { if (States.TryGetValue(room, out RoomApplyState value)) { return (value.Flags & kind) != 0; } return false; } internal static void MarkApplied(DungeonRoom room, DungeonRoomApplyKind kind) { GetOrCreateState(room).Flags |= kind; } internal static void ClearApplied(DungeonRoom room, DungeonRoomApplyKind kinds) { if (States.TryGetValue(room, out RoomApplyState value)) { value.Flags &= ~kinds; } } internal static bool MarkSkippedOnce(DungeonRoom room, DungeonRoomApplyKind skipKind) { RoomApplyState orCreateState = GetOrCreateState(room); if ((orCreateState.Flags & skipKind) != 0) { return false; } orCreateState.Flags |= skipKind; return true; } private static RoomApplyState GetOrCreateState(DungeonRoom room) { if (!States.TryGetValue(room, out RoomApplyState value)) { value = new RoomApplyState(); States.Add(room, value); } return value; } } internal sealed class DungeonRoomStateRegistry where T : class { private readonly ConditionalWeakTable _states = new ConditionalWeakTable(); private readonly List> _tracked = new List>(); internal T GetOrCreate(DungeonRoom room, Func factory) { if (!_states.TryGetValue(room, out var value)) { value = factory(); _states.Add(room, value); Track(room); } return value; } internal bool TryGet(DungeonRoom room, out T state) { if (_states.TryGetValue(room, out var value) && value != null) { state = value; return true; } state = null; return false; } internal IEnumerable> EnumerateAll() { PruneDead(); List> list = _tracked.ToList(); foreach (WeakReference item in list) { if (item.TryGetTarget(out var target) && _states.TryGetValue(target, out var value) && value != null) { yield return new KeyValuePair(target, value); } } } private void Track(DungeonRoom room) { PruneDead(); foreach (WeakReference item in _tracked) { if (item.TryGetTarget(out var target) && target == room) { return; } } _tracked.Add(new WeakReference(room)); } private void PruneDead() { for (int num = _tracked.Count - 1; num >= 0; num--) { if (!_tracked[num].TryGetTarget(out DungeonRoom _)) { _tracked.RemoveAt(num); } } } } internal static class DungeonRoomSessionTime { private static readonly FieldInfo SessionEndTimeField = AccessTools.Field(typeof(DungeonRoom), "_sessionEndTime") ?? throw new InvalidOperationException("DungeonRoom._sessionEndTime not found"); private static readonly FieldInfo CurrentTimeField = AccessTools.Field(typeof(DungeonRoom), "_currentTime") ?? throw new InvalidOperationException("DungeonRoom._currentTime not found"); private static readonly FieldInfo ElapsedTimeField = AccessTools.Field(typeof(DungeonRoom), "_elapsedTime") ?? throw new InvalidOperationException("DungeonRoom._elapsedTime not found"); internal static bool TryGetRemainingMilliseconds(DungeonRoom room, out long remainingMs) { remainingMs = 0L; if (room == null) { return false; } long num = (long)SessionEndTimeField.GetValue(room); long num2 = (long)CurrentTimeField.GetValue(room); remainingMs = num - num2; return remainingMs > 0; } internal static bool TryAdjustEndTime(DungeonRoom room, long deltaMs, out long newEndTime) { newEndTime = 0L; if (room == null || deltaMs == 0L) { return false; } long num = (long)SessionEndTimeField.GetValue(room); long num2 = (long)CurrentTimeField.GetValue(room); newEndTime = num + deltaMs; if (newEndTime <= num2) { return false; } SessionEndTimeField.SetValue(room, newEndTime); return true; } internal static long GetElapsedMilliseconds(DungeonRoom room) { return (long)ElapsedTimeField.GetValue(room); } internal static void SetElapsedMilliseconds(DungeonRoom room, long elapsedMs) { ElapsedTimeField.SetValue(room, elapsedMs); } internal static long GetCurrentMilliseconds(DungeonRoom room) { return (long)CurrentTimeField.GetValue(room); } internal static void SetCurrentMilliseconds(DungeonRoom room, long currentMs) { CurrentTimeField.SetValue(room, currentMs); } } internal static class EmbeddedAssets { private const string RootPrefix = "Assets."; private static readonly Assembly Assembly = typeof(EmbeddedAssets).Assembly; internal static bool TryReadFeature(string featureFolder, string relativePath, out byte[] bytes, out string extension) { bytes = Array.Empty(); extension = ""; string text = ToResourceName(featureFolder, relativePath); if (text == null) { return false; } try { using Stream stream = Assembly.GetManifestResourceStream(text); if (stream == null) { return false; } using MemoryStream memoryStream = new MemoryStream(); stream.CopyTo(memoryStream); bytes = memoryStream.ToArray(); extension = Path.GetExtension(text); return true; } catch { return false; } } internal static IReadOnlyList ListFeatureFiles(string featureFolder) { if (string.IsNullOrWhiteSpace(featureFolder)) { return Array.Empty(); } string text = featureFolder.Trim('/').Replace('/', '.').Replace('\\', '.'); string text2 = "Assets." + text + "."; List list = new List(); string[] manifestResourceNames = Assembly.GetManifestResourceNames(); foreach (string text3 in manifestResourceNames) { if (text3.StartsWith(text2, StringComparison.Ordinal)) { string text4 = text3.Substring(text2.Length); if (!string.IsNullOrEmpty(text4)) { list.Add(text4); } } } return list; } private static string? ToResourceName(string featureFolder, string relativePath) { if (string.IsNullOrWhiteSpace(featureFolder)) { return null; } string text = relativePath.TrimStart('/').Replace('\\', '/'); if (string.IsNullOrEmpty(text) || text.Contains("..", StringComparison.Ordinal)) { return null; } string text2 = featureFolder.Trim('/').Replace('/', '.').Replace('\\', '.'); string text3 = text.Replace('/', '.'); return "Assets." + text2 + "." + text3; } } internal sealed class EmbeddedAudioClipCache { private readonly string _assetFolder; private readonly string _featureTag; private readonly string _tempSubfolder; private readonly Dictionary _clipsByFileName = new Dictionary(StringComparer.OrdinalIgnoreCase); internal bool HasCachedClips => _clipsByFileName.Count > 0; internal EmbeddedAudioClipCache(string assetFolder, string featureTag, string tempSubfolder) { _assetFolder = assetFolder; _featureTag = featureTag; _tempSubfolder = tempSubfolder; } internal AudioClip? TryGetCachedClip(string fileName) { if (string.IsNullOrWhiteSpace(fileName)) { return null; } if (!_clipsByFileName.TryGetValue(fileName, out AudioClip value) || !((Object)(object)value != (Object)null)) { return null; } return value; } internal bool TryPreloadClip(string fileName) { return (Object)(object)TryGetClip(fileName) != (Object)null; } internal AudioClip? TryGetClip(string fileName) { if (string.IsNullOrWhiteSpace(fileName)) { return null; } if (_clipsByFileName.TryGetValue(fileName, out AudioClip value) && (Object)(object)value != (Object)null) { return value; } if (!EmbeddedAssets.TryReadFeature(_assetFolder, fileName, out byte[] bytes, out string extension)) { return null; } AudioClip val = DecodeClip(bytes, extension, fileName); if ((Object)(object)val != (Object)null) { _clipsByFileName[fileName] = val; } return val; } internal void Clear() { foreach (AudioClip value in _clipsByFileName.Values) { if ((Object)(object)value != (Object)null) { Object.Destroy((Object)(object)value); } } _clipsByFileName.Clear(); } private AudioClip? DecodeClip(byte[] bytes, string extension, string fileName) { string text = (string.IsNullOrWhiteSpace(extension) ? ".mp3" : extension); if (!text.StartsWith(".", StringComparison.Ordinal)) { text = "." + text; } string text2 = Path.Combine(MelonEnvironment.UserDataDirectory, _tempSubfolder, $"decode-{Guid.NewGuid():N}{text}"); try { string directoryName = Path.GetDirectoryName(text2); if (!string.IsNullOrEmpty(directoryName)) { Directory.CreateDirectory(directoryName); } File.WriteAllBytes(text2, bytes); return LoadClipFromFile(text2, text, fileName); } catch (Exception ex) { ModLog.Warn(_featureTag, "Embedded audio clip decode failed — " + fileName + ", " + ex.Message); return null; } finally { TryDelete(text2); } } private AudioClip? LoadClipFromFile(string filePath, string extension, string clipName) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Invalid comparison between Unknown and I4 AudioType val = ResolveAudioType(extension); UnityWebRequest audioClip = UnityWebRequestMultimedia.GetAudioClip("file://" + filePath, val); try { UnityWebRequestAsyncOperation val2 = audioClip.SendWebRequest(); while (!((AsyncOperation)val2).isDone) { } if ((int)audioClip.result != 1) { ModLog.Warn(_featureTag, "Embedded audio clip request failed — " + clipName + ", " + audioClip.error); return null; } AudioClip content = DownloadHandlerAudioClip.GetContent(audioClip); if ((Object)(object)content != (Object)null) { ((Object)content).name = Path.GetFileNameWithoutExtension(clipName); } return content; } finally { ((IDisposable)audioClip)?.Dispose(); } } private static AudioType ResolveAudioType(string extension) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) string text = extension.ToLowerInvariant(); if (!(text == ".wav")) { if (text == ".ogg") { return (AudioType)14; } return (AudioType)13; } return (AudioType)20; } private static void TryDelete(string path) { try { if (File.Exists(path)) { File.Delete(path); } } catch { } } } internal sealed class EmbeddedAudioVariantCatalog { private static readonly string[] AudioExtensions = new string[2] { ".wav", ".ogg" }; private static readonly Random RandomSource = new Random(); private readonly string _assetFolder; private readonly string _featureTag; private readonly string _variantNoun; internal EmbeddedAudioVariantCatalog(string assetFolder, string featureTag, string variantNoun) { _assetFolder = assetFolder; _featureTag = featureTag; _variantNoun = variantNoun; } internal IReadOnlyList ListVariantFileNames() { List list = new List(); foreach (string item in EmbeddedAssets.ListFeatureFiles(_assetFolder)) { if (IsAudioFile(item)) { list.Add(item); } } list.Sort(StringComparer.OrdinalIgnoreCase); return list; } internal IReadOnlyList ListVariantOptionValues() { List list = new List(); foreach (string item in ListVariantFileNames()) { list.Add(Path.GetFileNameWithoutExtension(item)); } return list; } internal string GetDefaultVariantOptionValue() { IReadOnlyList readOnlyList = ListVariantOptionValues(); if (readOnlyList.Count <= 0) { return ""; } return readOnlyList[0]; } internal string NormalizeVariantOptionValue(string? value) { IReadOnlyList readOnlyList = ListVariantOptionValues(); if (readOnlyList.Count == 0) { return value?.Trim() ?? ""; } string text = value?.Trim() ?? ""; if (string.IsNullOrEmpty(text)) { return readOnlyList[0]; } for (int i = 0; i < readOnlyList.Count; i++) { if (string.Equals(readOnlyList[i], text, StringComparison.OrdinalIgnoreCase)) { return readOnlyList[i]; } } return readOnlyList[0]; } internal string NormalizeRandomPoolValue(string? value) { return VariantIdListParser.NormalizeCsv(value, ListVariantOptionValues(), _featureTag, _variantNoun); } internal string ResolveRandomVariant(string? poolCsv) { IReadOnlyList readOnlyList = ListVariantFileNames(); if (readOnlyList.Count == 0) { return ""; } List pool = VariantIdListParser.ParseOrdered(poolCsv, ListVariantOptionValues(), _featureTag, _variantNoun); IReadOnlyList readOnlyList2 = FilterVariants(readOnlyList, pool); return readOnlyList2[RandomSource.Next(readOnlyList2.Count)]; } internal string? ResolveSpecificVariant(string configuredValue) { IReadOnlyList readOnlyList = ListVariantFileNames(); if (readOnlyList.Count == 0) { return null; } string text = configuredValue?.Trim() ?? ""; if (string.IsNullOrEmpty(text)) { return readOnlyList[0]; } for (int i = 0; i < readOnlyList.Count; i++) { string text2 = readOnlyList[i]; if (string.Equals(text2, text, StringComparison.OrdinalIgnoreCase) || string.Equals(Path.GetFileNameWithoutExtension(text2), text, StringComparison.OrdinalIgnoreCase)) { return text2; } } return readOnlyList[0]; } internal static string FormatVariantDisplayName(string optionValue) { if (string.IsNullOrWhiteSpace(optionValue)) { return optionValue; } string[] array = optionValue.Replace('_', ' ').Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries); if (array.Length == 0) { return optionValue; } for (int i = 0; i < array.Length; i++) { array[i] = ToTitleCaseWord(array[i]); } return string.Join(' ', array); } private static string ToTitleCaseWord(string word) { if (string.IsNullOrEmpty(word)) { return word; } if (word.Length == 1) { return word.ToUpperInvariant(); } return char.ToUpperInvariant(word[0]) + word.Substring(1).ToLowerInvariant(); } private static IReadOnlyList FilterVariants(IReadOnlyList variants, List pool) { if (pool.Count == 0) { return variants; } List list = new List(); HashSet hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < pool.Count; i++) { hashSet.Add(pool[i]); } for (int j = 0; j < variants.Count; j++) { string text = variants[j]; string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(text); if (hashSet.Contains(text) || hashSet.Contains(fileNameWithoutExtension)) { list.Add(text); } } if (list.Count <= 0) { return variants; } return list; } private static bool IsAudioFile(string fileName) { string extension = Path.GetExtension(fileName); for (int i = 0; i < AudioExtensions.Length; i++) { if (string.Equals(extension, AudioExtensions[i], StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } } internal static class EncounterSpawnTiming { internal const float RetryIntervalSeconds = 1f; } internal static class EntityDisplayNameFormatter { private static readonly Regex PascalCaseSplitPattern = new Regex("(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", RegexOptions.Compiled); internal static string Humanize(string? name) { if (string.IsNullOrWhiteSpace(name)) { return string.Empty; } return PascalCaseSplitPattern.Replace(name.Trim(), " "); } internal static string GetArticle(string humanizedName) { if (string.IsNullOrWhiteSpace(humanizedName)) { return "a"; } bool flag; switch (char.ToLowerInvariant(humanizedName[0])) { case 'a': case 'e': case 'i': case 'o': case 'u': flag = true; break; default: flag = false; break; } if (!flag) { return "a"; } return "an"; } internal static string FormatWithArticle(string humanizedName, bool capitalizeArticle) { if (string.IsNullOrWhiteSpace(humanizedName)) { return string.Empty; } if (GameLocaleAccess.GetCurrentLanguage() == "de") { return (capitalizeArticle ? "Ein" : "ein") + " " + humanizedName; } string text = GetArticle(humanizedName); if (capitalizeArticle) { text = char.ToUpperInvariant(text[0]) + text.Substring(1); } return text + " " + humanizedName; } internal static string Pluralize(string humanizedName) { if (string.IsNullOrWhiteSpace(humanizedName)) { return string.Empty; } if (humanizedName.EndsWith("s", StringComparison.OrdinalIgnoreCase)) { return humanizedName; } char c = humanizedName[humanizedName.Length - 1]; if ((c == 'O' || c == 'o') ? true : false) { return humanizedName; } return humanizedName + "s"; } } internal sealed class FeatureModule { private readonly Action _applyPatches; private readonly Action? _syncFromConfig; private readonly Action? _onUpdate; private readonly Action? _onDeinitialize; private readonly Action? _onSessionStarted; private readonly Action? _onSessionEnded; internal string Name { get; } internal SessionScope SessionScope { get; } internal bool ThrottledUpdate { get; } internal FeatureModule(string name, Action applyPatches, Action? syncFromConfig = null, Action? onUpdate = null, bool throttledUpdate = false, Action? onDeinitialize = null, SessionScope sessionScope = SessionScope.HostOnly, Action? onSessionStarted = null, Action? onSessionEnded = null) { Name = name; SessionScope = sessionScope; _applyPatches = applyPatches; _syncFromConfig = syncFromConfig; _onUpdate = onUpdate; _onDeinitialize = onDeinitialize; _onSessionStarted = onSessionStarted; _onSessionEnded = onSessionEnded; if (throttledUpdate) { ThrottledUpdate = true; } } internal void ApplyPatches(Harmony harmony) { _applyPatches(harmony); } internal void SyncFromConfig() { _syncFromConfig?.Invoke(); } internal void OnUpdate() { _onUpdate?.Invoke(); } internal void OnDeinitialize() { _onDeinitialize?.Invoke(); } internal void InvokeSessionStarted(SessionRole role, int slotId) { _onSessionStarted?.Invoke(role, slotId); } internal void InvokeSessionEnded() { _onSessionEnded?.Invoke(); } } internal static class FeatureModules { internal static IReadOnlyList All { get; } = new <>z__ReadOnlyArray(new FeatureModule[20] { new FeatureModule("Persistence", PersistencePatches.Apply, PersistenceRuntime.RefreshFromConfig, SpeechEventPoolManager.ProcessDeferredUpdates, throttledUpdate: false, PersistenceWriteQueue.FlushAllSync, SessionScope.HostOnly, null, delegate { SpeechEventPoolManager.OnSessionEnded(clearArchiveRegistry: true); }), new FeatureModule("Statistics", StatisticsPatches.Apply, StatisticsTracker.RefreshFromConfig, delegate { if (ModConfig.EnableStatistics.Value) { StatisticsTracker.OnUpdate(); } }, throttledUpdate: false, SaveSlotSidecarPersistence.FlushAllSync, SessionScope.HostOnly, null, StatisticsTracker.OnSessionEnded), new FeatureModule("SaveSlotSidecar", SaveSlotSidecarPersistencePatches.Apply, null, null, throttledUpdate: false, null, SessionScope.HostOnly, null, SaveSlotSidecarPersistence.OnSessionEnded), new FeatureModule("HostConfigSync", HostConfigSyncPatches.Apply, null, HostConfigSyncRuntime.OnUpdate, throttledUpdate: false, null, SessionScope.ClientAllowed, HostConfigSyncRuntime.OnSessionStarted, HostConfigSyncRuntime.OnSessionEnded), new FeatureModule("MoreVoices", MoreVoicesPatches.Apply, MoreVoicesPatches.RefreshFromConfig, null, throttledUpdate: false, VoicePerformanceRuntime.ClearAll, SessionScope.HostOnly, null, VoicePerformanceRuntime.ClearAll), new FeatureModule("PlayerAnnouncements", PlayerAnnouncementsPatches.Apply, null, null, throttledUpdate: false, null, SessionScope.HostOnly, null, delegate { PlayerAnnouncements.ResetForSessionEnd(); BossSpawnAnnouncer.ResetForSessionEnd(); MapRunStatsTracker.ResetForSessionEnd(); }), new FeatureModule("MorePlayers", MorePlayersPatches.Apply, MorePlayersPatches.RefreshFromConfig, null, throttledUpdate: false, null, SessionScope.HostOnly, null, MorePlayersPatches.OnSessionEnded), new FeatureModule("JoinAnytime", JoinAnytimePatches.Apply, JoinAnytimeRuntime.RefreshFromConfig, JoinAnytimeRuntime.OnUpdate, throttledUpdate: false, null, SessionScope.HostOnly, null, JoinAnytimeRuntime.ResetSessionState), new FeatureModule("SpawnScaling", SpawnScalingPatches.Apply, SpawnScalingPatches.RefreshFromConfig, delegate { if (SceneScopedConfigGate.Spawn.EnableSpawnScaling) { MapPlacedEncounterScheduler.ProcessPendingEncounters(); } }, throttledUpdate: true, null, SessionScope.HostOnly, null, SpawnScalingPatches.OnSessionEnded), new FeatureModule("LootMultiplicator", LootMultiplicatorPatches.Apply, LootMultiplicatorPatches.RefreshFromConfig, delegate { if (SceneScopedConfigGate.Loot.EnableLootMultiplicator) { FixedLootSpawnCoordinator.ProcessPendingRespawns(); } }, throttledUpdate: true, null, SessionScope.HostOnly, null, FixedLootSpawnCoordinator.ClearPendingRespawns), new FeatureModule("SavegamePreparation", SavegamePreparationPatches.Apply, null, null, throttledUpdate: false, null, SessionScope.HostOnly, null, SavegamePreparationPatches.OnSessionEnded), new FeatureModule("Economy", EconomyPatches.Apply, EconomyPatches.RefreshFromConfig, null, throttledUpdate: false, null, SessionScope.HostOnly, null, EconomyPatches.OnSessionEnded), new FeatureModule("DungeonTime", DungeonTimePatches.Apply, DungeonTimePatches.RefreshFromConfig, null, throttledUpdate: false, null, SessionScope.ClientAllowed, null, DungeonTimePatches.OnSessionEnded), new FeatureModule("MimicTuning", MimicTuningPatches.Apply, MimicTuningRuntime.RefreshFromConfig, null, throttledUpdate: false, null, SessionScope.HostOnly, null, MimicTuningRuntime.OnSessionEnded), new FeatureModule("Ui", UiPatches.Apply, UiRuntime.RefreshFromConfig, UiRuntime.OnUpdate, throttledUpdate: false, null, SessionScope.ClientAllowed, null, UiRuntime.OnSessionEnded), new FeatureModule("Privacy", PrivacyPatches.Apply, PrivacyRuntime.RefreshFromConfig, null, throttledUpdate: false, PrivacyRuntime.RestoreOnShutdown, SessionScope.Global), new FeatureModule("PlayerTuning", PlayerTuningPatches.Apply, PlayerTuningApplier.RefreshFromConfig, null, throttledUpdate: false, PlayerTuningApplier.RestoreOnShutdown, SessionScope.HostOnly, null, PlayerTuningApplier.OnSessionEnded), new FeatureModule("DungeonRandomizer", DungeonRandomizerPatches.Apply, null, null, throttledUpdate: false, null, SessionScope.HostOnly, null, DungeonRandomizerRuntime.OnSessionEnded), new FeatureModule("Weather", WeatherPatches.Apply, WeatherPatches.RefreshFromConfig, WeatherCycleScheduler.ProcessPendingTransitions, throttledUpdate: true), new FeatureModule("WebDashboard", WebDashboardServer.Apply, WebDashboardServer.SyncFromConfig, WebDashboardServer.OnUpdate, throttledUpdate: false, WebDashboardServer.StopOnDeinit, SessionScope.Global, null, delegate { WebDashboardSnapshotCache.OnSessionEnded(); WebDashboardUiDebugController.OnSessionEnded(); WebDashboardPendingKickScheduler.ClearOnSessionEnded(); }) }); internal static void ResetSharedSessionState() { PlayerLifecycleCoordinator.ClearAll(); } internal static void SyncModuleByName(string moduleName) { foreach (FeatureModule item in All) { if (string.Equals(item.Name, moduleName, StringComparison.Ordinal)) { item.SyncFromConfig(); break; } } } } internal enum SessionScope { Global, HostOnly, ClientAllowed } internal static class FeatureModuleSessionHooks { private static readonly HashSet _activeModules = new HashSet(); internal static bool IsModuleActive(string moduleName) { return _activeModules.Contains(moduleName); } internal static void InvokeSessionStarted(SessionRole role, int slotId) { _activeModules.Clear(); foreach (FeatureModule module in FeatureModules.All) { if (ShouldActivate(module.SessionScope, role)) { _activeModules.Add(module.Name); InvokeSafely(module.Name, delegate { module.InvokeSessionStarted(role, slotId); }); } } } internal static void InvokeSessionEnded() { foreach (FeatureModule item in FeatureModules.All) { if (_activeModules.Contains(item.Name)) { InvokeSafely(item.Name, item.InvokeSessionEnded); } } FeatureModules.ResetSharedSessionState(); _activeModules.Clear(); } private static bool ShouldActivate(SessionScope scope, SessionRole role) { return scope switch { SessionScope.Global => true, SessionScope.HostOnly => role == SessionRole.Host, SessionScope.ClientAllowed => role == SessionRole.Host || role == SessionRole.Client, _ => false, }; } private static void InvokeSafely(string moduleName, Action action) { try { action(); } catch (Exception ex) { ModLog.Warn("Session", "Module " + moduleName + " session hook failed — " + ex.Message); } } } internal static class FeatureToggleGate { internal const float NeutralMultiplier = 1f; } internal static class GameLocaleAccess { private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly MethodInfo? GetL10NTextMethod = AccessTools.Method(typeof(Hub), "GetL10NText", new Type[2] { typeof(string), typeof(object[]) }, (Type[])null); private static readonly PropertyInfo? L10NManagerLanguageProperty = typeof(L10NManager).GetProperty("language", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static volatile string _cachedLanguage = "en"; private static int _mainThreadId; internal static bool IsMainThread { get { int mainThreadId = _mainThreadId; if (mainThreadId != 0) { return Environment.CurrentManagedThreadId == mainThreadId; } return true; } } internal static void CaptureMainThread() { _mainThreadId = Environment.CurrentManagedThreadId; _cachedLanguage = ResolveLanguageFromUnity(); } internal static string GetCurrentLanguage() { int mainThreadId = _mainThreadId; if (mainThreadId != 0 && Environment.CurrentManagedThreadId != mainThreadId) { return _cachedLanguage; } if (mainThreadId == 0) { _mainThreadId = Environment.CurrentManagedThreadId; } _cachedLanguage = ResolveLanguageFromUnity(); return _cachedLanguage; } private static string ResolveLanguageFromUnity() { try { L10NManager val = Object.FindAnyObjectByType(); if ((Object)(object)val != (Object)null && L10NManagerLanguageProperty?.GetValue(val) is string language && TryResolveSupportedLocale(language, out string locale)) { return locale; } } catch { } return "en"; } internal static string NormalizeLanguageCode(string? language) { if (!TryResolveSupportedLocale(language, out string locale)) { return "en"; } return locale; } internal static bool TryResolveSupportedLocale(string? language, out string locale) { locale = "en"; string text = NormalizeLanguageTag(language); if (string.IsNullOrEmpty(text)) { return false; } foreach (string availableLocale in ModL10n.GetAvailableLocales()) { if (string.Equals(availableLocale, text, StringComparison.OrdinalIgnoreCase)) { locale = availableLocale; return true; } } return false; } internal static string GetL10NText(string key, params object[] formattingArgs) { if (!IsMainThread) { return key; } if (GetL10NTextMethod != null) { return (GetL10NTextMethod.Invoke(null, new object[2] { key, formattingArgs }) as string) ?? key; } return key; } private static string NormalizeLanguageTag(string? language) { if (string.IsNullOrWhiteSpace(language)) { return string.Empty; } string text = language.Trim().Replace('_', '-').ToLowerInvariant(); int num = text.IndexOf('-', StringComparison.Ordinal); if (num > 0) { text = text.Substring(0, num); } return text; } } internal static class GameNetworkApi { private const BindingFlags StaticFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; public static Assembly? GetGameAssembly() { try { Assembly assembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault((Assembly a) => a.GetName().Name == "Assembly-CSharp"); if (assembly != null) { return assembly; } return typeof(IVroom).Assembly; } catch { return null; } } public static object? GetServerSocket() { object vWorld = GetVWorld(); object obj; if (vWorld != null) { obj = ReflectionHelper.GetFieldValue(vWorld, "_sdrServer"); if (obj == null) { return ReflectionHelper.GetFieldValue(vWorld, "_rudpServer"); } } else { obj = null; } return obj; } public static void SetMaximumClients(object serverSocket, int value) { ReflectionHelper.SetFieldValue(serverSocket, "_maximumClients", value); } private static object? GetVWorld() { object hub = GetHub(); if (hub != null) { return ReflectionHelper.GetFieldValue(hub, "k__BackingField"); } return null; } public static object? GetHub() { return (GetGameAssembly()?.GetType("Hub"))?.GetProperty("s", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null); } } internal static class ReflectionHelper { private const BindingFlags DefaultFlags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; public static object? GetFieldValue(object target, string fieldName) { return target?.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(target); } public static T GetFieldValue(object target, string fieldName) { object fieldValue = GetFieldValue(target, fieldName); if (fieldValue != null) { return (T)fieldValue; } return default(T); } public static void SetFieldValue(object target, string fieldName, object value) { target?.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.SetValue(target, value); } } internal static class GameSessionAccess { private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo? HubVworldField = typeof(Hub).GetField("vworld", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly PropertyInfo? HubVworldProperty = typeof(Hub).GetProperty("vworld", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? HubPdataField = typeof(Hub).GetField("pdata", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? HubTimeutilField = typeof(Hub).GetField("timeutil", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly PropertyInfo? HubTimeutilProperty = typeof(Hub).GetProperty("timeutil", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static VWorld? TryGetVWorld() { if ((Object)(object)Hub.s == (Object)null) { return null; } object? obj = HubVworldField?.GetValue(Hub.s); return (VWorld?)(((obj is VWorld) ? obj : null) ?? ((object)/*isinst with value type is only supported in some contexts*/)); } internal static PersistentData? TryGetPdata() { if ((Object)(object)Hub.s == (Object)null || HubPdataField == null) { return null; } object? value = HubPdataField.GetValue(Hub.s); return (PersistentData?)((value is PersistentData) ? value : null); } internal static TimeUtil? TryGetTimeUtil() { if ((Object)(object)Hub.s == (Object)null) { return null; } object? obj = HubTimeutilField?.GetValue(Hub.s); return (TimeUtil?)(((obj is TimeUtil) ? obj : null) ?? ((object)/*isinst with value type is only supported in some contexts*/)); } internal static int GetSaveSlotId() { VWorld? obj = TryGetVWorld(); if (obj == null) { return -1; } return obj.SaveSlotID; } internal static bool IsValidSaveSlotId(int slotId) { if (slotId == -1) { return false; } if (ModConfig.IsInitialized && ModConfig.EnableExtendedSaveSlots.Value) { if (slotId == 0) { return true; } int maxManualSlots = SaveSlotDiscovery.GetMaxManualSlots(); if (slotId >= 1) { return slotId <= maxManualSlots; } return false; } return MMSaveGameData.CheckSaveSlotID(slotId, true); } internal static bool TryGetActiveSaveSlotId(out int slotId) { slotId = GetSaveSlotId(); if (HostStatusCache.IsHostFast()) { return IsValidSaveSlotId(slotId); } return false; } internal static float GetCurrentTickSec() { try { TimeUtil? obj = TryGetTimeUtil(); return ((float?)((obj != null) ? new long?(obj.GetCurrentTickSec()) : ((long?)null))) ?? 0f; } catch { return 0f; } } internal static ulong ResolveSteamId(long playerUid, bool isLocal) { if (playerUid != 0L) { try { PersistentData val = TryGetPdata(); if (val?.actorUIDToSteamID != null && val.actorUIDToSteamID.TryGetValue(playerUid, out var value)) { return value; } } catch { } } if (isLocal) { return GetLocalSteamId(); } return 0uL; } internal static ulong GetLocalSteamId() { ulong num = TryGetPdataLocalSteamId(); if (num == 0L) { return LocalPlayerHelper.TryGetPlatformSteamId(); } return num; } private static ulong TryGetPdataLocalSteamId() { try { PersistentData? obj = TryGetPdata(); return (obj != null) ? obj.GetUserSteamIDUInt64() : 0; } catch { return 0uL; } } internal static GameSessionInfo? TryGetGameSessionInfo() { VWorld? obj = TryGetVWorld(); if (obj == null) { return null; } VRoomManager vRoomManager = obj.VRoomManager; if (vRoomManager == null) { return null; } return vRoomManager.GetGameSessionInfo(); } internal static PlayReportManager? TryGetPlayReportManager() { GameSessionInfo? obj = TryGetGameSessionInfo(); if (obj == null) { return null; } return obj.PlayReportManager; } internal static bool IsSteamIdRegisteredInSession(ulong steamId) { if (steamId == 0L) { return false; } GameSessionInfo? obj = TryGetGameSessionInfo(); if (obj == null) { return false; } return obj.TotalPlayerSteamIDs?.ContainsKey(steamId) == true; } } internal static class HarmonyPatchHelper { private sealed class CategoryAudit { private readonly HashSet _okLabels = new HashSet(StringComparer.Ordinal); private readonly List _missLabels = new List(); internal void AddOk(string label) { _okLabels.Add(label); } internal void AddMiss(string label) { _missLabels.Add(label); } internal List<(string text, bool ok)> ToEntries() { List<(string, bool)> list = new List<(string, bool)>(); foreach (string item in _okLabels.OrderBy((string s) => s, StringComparer.Ordinal)) { list.Add((item + " (ok)", true)); } foreach (string missLabel in _missLabels) { list.Add((missLabel + " (miss)", false)); } return list; } } internal static IEnumerable GetNestedPatchTypes(Type containerType) { return from t in containerType.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic) where t.GetCustomAttributes(typeof(HarmonyPatch), inherit: false).Length != 0 select t; } internal static IEnumerable GetNamespacePatchTypes(Type anchorType, string suffix = ".Patches") { return from t in anchorType.Assembly.GetTypes() where t.Namespace == anchorType.Namespace + suffix && t.GetCustomAttributes(typeof(HarmonyPatch), inherit: false).Length != 0 select t; } internal static void ApplyPatchTypes(Harmony harmony, string feature, IEnumerable patchTypes) { int num = 0; int num2 = 0; Dictionary audits = new Dictionary(StringComparer.Ordinal); foreach (Type patchType in patchTypes) { string category = ResolvePatchCategory(patchType, feature); CategoryAudit orCreateCategory = GetOrCreateCategory(audits, category); try { List list = harmony.CreateClassProcessor(patchType).Patch(); if (list != null && list.Count > 0) { num += list.Count; foreach (MethodInfo item in list) { orCreateCategory.AddOk(FormatMethodLabel(item)); } } else { num2++; orCreateCategory.AddMiss(patchType.Name); if (patchType.GetMethod("TargetMethod", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) != null) { ModLog.Warn(feature, "Patch " + patchType.Name + " — TargetMethod returned no patches"); } else { ModLog.Warn(feature, "Patch class " + patchType.Name + " did not apply to any methods."); } } } catch (Exception ex) { num2++; orCreateCategory.AddMiss(patchType.Name); ModLog.Warn(feature, "Patch " + patchType.Name + " failed — " + ex.Message); } } LogPatchAudits(feature, audits); LogPatchSummary(feature, num, num2); } private static void LogPatchSummary(string feature, int applied, int failed) { string text = $"{applied} patch(es), {failed} failure(s)."; ColorARGB? item = PickOutcomeLineColor(applied, failed); ModLog.PassLogSegmented(ModLog.FeatureSection(feature, "Patches Applied"), text, (item, text)); } internal static MethodInfo? GetEnumeratorMoveNext(Type declaringType, string methodName, Type[]? parameters = null) { if (((parameters == null) ? AccessTools.Method(declaringType, methodName, (Type[])null, (Type[])null) : AccessTools.Method(declaringType, methodName, parameters, (Type[])null)) == null) { return null; } string value = "<" + methodName + ">"; Type[] nestedTypes = declaringType.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic); foreach (Type type in nestedTypes) { if (type.Name.StartsWith(value, StringComparison.Ordinal)) { MethodInfo methodInfo = AccessTools.Method(type, "MoveNext", (Type[])null, (Type[])null); if (methodInfo != null) { return methodInfo; } } } return null; } private static void LogPatchAudits(string feature, Dictionary audits) { if (!ModConfig.EnableDebugLogging.Value || audits.Count == 0) { return; } foreach (KeyValuePair item2 in audits.OrderBy, string>((KeyValuePair e) => e.Key, StringComparer.Ordinal)) { List<(string, bool)> list = item2.Value.ToEntries(); if (list.Count != 0) { int num = list.Count<(string, bool)>(((string text, bool ok) e) => !e.ok); string text = string.Join(", ", list.Select<(string, bool), string>(((string text, bool ok) e) => e.text)); ColorARGB? item = PickOutcomeLineColor(list.Count - num, num); string title = (string.Equals(item2.Key, feature, StringComparison.Ordinal) ? "Patch Audit" : ("Patch Audit/" + item2.Key)); ModLog.PassLogSegmented(ModLog.FeatureSection(feature, title), text, (item, text)); } } } private static CategoryAudit GetOrCreateCategory(Dictionary audits, string category) { if (!audits.TryGetValue(category, out CategoryAudit value)) { value = (audits[category] = new CategoryAudit()); } return value; } private static string ResolvePatchCategory(Type patchType, string feature) { Type declaringType = patchType.DeclaringType; if ((object)declaringType != null) { string text = TrimPatchesSuffix(declaringType.Name); if (text.Length > 0) { return text; } } string text2 = patchType.Namespace; if (text2 != null && text2.EndsWith(".Patches", StringComparison.Ordinal) && text2.Length > ".Patches".Length) { string text3 = text2; int length = ".Patches".Length; string text4 = text3.Substring(0, text3.Length - length); int num = text4.LastIndexOf('.'); string text5 = ((num >= 0) ? text4.Substring(num + 1) : text4); if (text5.Length > 0 && !string.Equals(text5, "Config", StringComparison.Ordinal) && !string.Equals(text5, "Util", StringComparison.Ordinal)) { return text5; } } return feature; } private static string TrimPatchesSuffix(string name) { if (name.EndsWith("Patches", StringComparison.Ordinal)) { string text = name; int length = "Patches".Length; return text.Substring(0, text.Length - length); } if (name.EndsWith("Patch", StringComparison.Ordinal)) { string text = name; int length = "Patch".Length; return text.Substring(0, text.Length - length); } return name; } private static string FormatMethodLabel(MethodBase method) { if (TryParseHarmonyDmdName(method.Name, out string typeName, out string methodName)) { return methodName + "/" + typeName; } string text = method.DeclaringType?.Name ?? "?"; return (method.IsConstructor ? ".ctor" : method.Name) + "/" + text; } private static bool TryParseHarmonyDmdName(string name, out string typeName, out string methodName) { typeName = "?"; methodName = name; if (!name.StartsWith("DMD<", StringComparison.Ordinal) || !name.EndsWith(">", StringComparison.Ordinal) || name.Length < 6) { return false; } string text = name.Substring(4, name.Length - 1 - 4); int num = text.LastIndexOf("::", StringComparison.Ordinal); if (num <= 0 || num + 2 >= text.Length) { return false; } string text2 = text.Substring(0, num); methodName = text.Substring(num + 2); if (methodName.Length == 0) { return false; } int num2 = text2.LastIndexOf('.'); typeName = ((num2 >= 0) ? text2.Substring(num2 + 1) : text2); int num3 = typeName.LastIndexOf('+'); if (num3 >= 0) { typeName = typeName.Substring(num3 + 1); } return typeName.Length > 0; } private static ColorARGB? PickOutcomeLineColor(int succeeded, int failed) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) if (failed == 0) { if (succeeded <= 0) { return null; } return ModLog.SuccessGreen; } return (succeeded > 0) ? ModLog.PartialYellow : ModLog.FailureRed; } } internal static class HostApplyGate { internal static bool IsParticipantClient() { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Invalid comparison between Unknown and I4 PersistentData? obj = GameSessionAccess.TryGetPdata(); if (obj == null) { return false; } return (int)obj.ClientMode == 1; } internal static bool ShouldApplyHostOnlyFeature(Func? isFeatureEnabled = null) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Invalid comparison between Unknown and I4 //IL_0025: Unknown result type (might be due to invalid IL or missing references) if (isFeatureEnabled != null && !isFeatureEnabled()) { return false; } PersistentData val = GameSessionAccess.TryGetPdata(); if (val != null && (int)val.ClientMode == 1) { return false; } if (val != null && (int)val.ClientMode != 0) { return MimesisSaveManager.IsHost(); } return true; } } internal static class HostStatusCache { private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private const BindingFlags StaticFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; private const int RecheckIntervalFrames = 60; private static readonly Type? NetworkManagerType = Type.GetType("FishNet.Managing.NetworkManager, FishNet.Runtime"); private static readonly PropertyInfo? NetworkManagerInstanceProp = NetworkManagerType?.GetProperty("Instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); private static readonly PropertyInfo? NetworkManagerIsServerProp = NetworkManagerType?.GetProperty("IsServer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static FieldInfo? _sessionManagerField; private static FieldInfo? _hostSessionContextField; private static FieldInfo? _vPlayerField; private static PropertyInfo? _vPlayerIsHostProp; private static bool? _cachedIsHost; private static int _lastCheckFrame = -60; internal static void Invalidate() { _cachedIsHost = null; _lastCheckFrame = -60; } internal static bool IsHostFast() { int frameCount = Time.frameCount; if (_cachedIsHost.HasValue && frameCount - _lastCheckFrame < 60) { return _cachedIsHost.Value; } _cachedIsHost = ComputeIsHost(); _lastCheckFrame = frameCount; return _cachedIsHost.Value; } private static bool ComputeIsHost() { if (!IsHostViaNetwork()) { return IsHostViaVWorld(); } return true; } private static bool IsHostViaNetwork() { if (NetworkManagerType == null || NetworkManagerInstanceProp == null || NetworkManagerIsServerProp == null) { return false; } try { object value = NetworkManagerInstanceProp.GetValue(null); return value != null && (bool)NetworkManagerIsServerProp.GetValue(value); } catch { return false; } } private static bool IsHostViaVWorld() { try { VWorld val = GameSessionAccess.TryGetVWorld(); if (val == null) { return false; } EnsureVWorldHostChainResolved(((object)val).GetType()); if (_sessionManagerField == null || _hostSessionContextField == null || _vPlayerField == null || _vPlayerIsHostProp == null) { return false; } object value = _sessionManagerField.GetValue(val); if (value == null) { return false; } object value2 = _hostSessionContextField.GetValue(value); if (value2 == null) { return false; } object value3 = _vPlayerField.GetValue(value2); return value3 != null && (bool)_vPlayerIsHostProp.GetValue(value3); } catch { return false; } } private static void EnsureVWorldHostChainResolved(Type vworldType) { if (_sessionManagerField != null) { return; } _sessionManagerField = vworldType.GetField("_sessionManager", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (_sessionManagerField == null) { return; } _hostSessionContextField = _sessionManagerField.FieldType.GetField("_hostSessionContext", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (!(_hostSessionContextField == null)) { _vPlayerField = _hostSessionContextField.FieldType.GetField("_vPlayer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (!(_vPlayerField == null)) { _vPlayerIsHostProp = _vPlayerField.FieldType.GetProperty("IsHost", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } } } } internal static class HubGameDataAccess { private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly PropertyInfo? HubDatamanProperty = typeof(Hub).GetProperty("dataman", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly PropertyInfo? HubDynamicDataManProperty = typeof(Hub).GetProperty("dynamicDataMan", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static ExcelDataManager? Excel { get { if (!((Object)(object)Hub.s == (Object)null)) { object? obj = HubDatamanProperty?.GetValue(Hub.s); DataManager val = (DataManager)((obj is DataManager) ? obj : null); if (val != null) { return val.ExcelDataManager; } } return null; } } internal static DynamicDataManager? DynamicData { get { if (!((Object)(object)Hub.s == (Object)null)) { object? obj = HubDynamicDataManProperty?.GetValue(Hub.s); DynamicDataManager val = (DynamicDataManager)((obj is DynamicDataManager) ? obj : null); if (val != null) { return val; } } return null; } } } internal static class InGameMessageHelper { private const string Feature = "InGameMessage"; internal const string MessagePrefix = "[PlayerEnhancements]"; private const string LocalOnlyRichTextColor = "#88CCFF"; private const BindingFlags InstanceMemberFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo EnterColor1Field = typeof(UIPrefab_PlayerEnterInfo).GetField("EnterColor1", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo EnterColor2Field = typeof(UIPrefab_PlayerEnterInfo).GetField("EnterColor2", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo ExitColor1Field = typeof(UIPrefab_PlayerEnterInfo).GetField("ExitColor1", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo ExitColor2Field = typeof(UIPrefab_PlayerEnterInfo).GetField("ExitColor2", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static PropertyInfo? _tmpTextProperty; private static PropertyInfo? _tmpColorGradientProperty; private static ConstructorInfo? _vertexGradientConstructor; private static PropertyInfo? _componentGameObjectProperty; private static readonly Dictionary ListFieldCache = new Dictionary(); private static MethodInfo? _getTickMethod; private static bool _loggedTickFallback; internal static void ShowModMessage(string message, bool isEntering = true, bool localOnly = false, bool ignoreFeatureToggles = false) { if ((!ignoreFeatureToggles && !ModConfig.ShowStatisticsToasts.Value && !ModConfig.ShowPlayerAnnouncements.Value) || string.IsNullOrWhiteSpace(message)) { return; } string message2 = FormatModMessage(message, localOnly); try { if (!TryEnqueueRawPlayerInfo(message2, isEntering)) { ModLog.Debug("InGameMessage", "Player enter info UI unavailable for mod toast."); } } catch (Exception ex) { ModLog.Debug("InGameMessage", "Mod toast failed: " + ex.Message); } } internal static string FormatModMessage(string message, bool localOnly) { string text = "[PlayerEnhancements] " + message.Trim(); if (localOnly) { text = "" + text + ""; } return text; } internal static IEnumerator? TryCreateExtendedPlayerEnterInfoUpdates(UIPrefab_PlayerEnterInfo ui) { IList listField = GetListField(ui, "currentDisplayed"); IList listField2 = GetListField(ui, "displayStartTimeMilliSec"); IList listField3 = GetListField(ui, "isEnteringFlags"); IList listField4 = GetListField(ui, "PlayerInfos"); if (listField == null || listField2 == null || listField3 == null || listField4 == null) { ModLog.Warn("InGameMessage", "Extended toast coroutine unavailable — UIPrefab_PlayerEnterInfo fields not found"); return null; } return RunExtendedPlayerEnterInfoUpdates(ui, listField, listField2, listField3, listField4); } private static IEnumerator RunExtendedPlayerEnterInfoUpdates(UIPrefab_PlayerEnterInfo ui, IList currentDisplayed, IList displayStartTimeMilliSec, IList isEnteringFlags, IList playerInfos) { long fadeOutMs = ui.fadeOutDisplayTimeMilliSec; Color enterColor1 = (Color)EnterColor1Field.GetValue(ui); Color enterColor2 = (Color)EnterColor2Field.GetValue(ui); Color exitColor1 = (Color)ExitColor1Field.GetValue(ui); Color exitColor2 = (Color)ExitColor2Field.GetValue(ui); while (true) { yield return (object)new WaitUntil((Func)(() => currentDisplayed.Count > 0)); long currentTickMilliSec = GetCurrentTickMilliSec(); while (displayStartTimeMilliSec.Count > 0) { long displayDurationMilliSec = GetDisplayDurationMilliSec(ui, 0); long num = Convert.ToInt64(displayStartTimeMilliSec[0]); if (currentTickMilliSec - num <= displayDurationMilliSec + fadeOutMs) { break; } currentDisplayed.RemoveAt(0); displayStartTimeMilliSec.RemoveAt(0); isEnteringFlags.RemoveAt(0); } for (int num2 = 0; num2 < playerInfos.Count; num2++) { object label = playerInfos[num2]; if (num2 < currentDisplayed.Count) { long displayDurationMilliSec2 = GetDisplayDurationMilliSec(ui, num2); long num3 = Convert.ToInt64(displayStartTimeMilliSec[num2]); float num4 = Mathf.Clamp01((float)(currentTickMilliSec - num3 - displayDurationMilliSec2) / (float)fadeOutMs); bool num5 = (bool)isEnteringFlags[num2]; Color c = (num5 ? enterColor1 : exitColor1); c.a = 1f - num4; Color c2 = (num5 ? enterColor2 : exitColor2); c2.a = 1f - num4; SetLabelText(label, (string)currentDisplayed[num2]); SetLabelGradient(label, c, c2); SetLabelActive(label, active: true); } else { SetLabelActive(label, active: false); SetLabelText(label, string.Empty); } } yield return null; } } internal static long GetDisplayDurationMilliSec(UIPrefab_PlayerEnterInfo ui, int index) { IList listField = GetListField(ui, "currentDisplayed"); if (listField == null || index >= listField.Count || !(listField[index] is string text) || !text.Contains("[PlayerEnhancements]")) { return ui.displayTimeSecMilliSec; } return (long)(Mathf.Max(1f, ModConfig.ModToastDurationSeconds.Value) * 1000f); } private static void EnsureTmpReflection(object label) { if (!(_tmpTextProperty != null)) { Type type = label.GetType(); _tmpTextProperty = type.GetProperty("text", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); _tmpColorGradientProperty = type.GetProperty("colorGradient", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); _componentGameObjectProperty = typeof(Component).GetProperty("gameObject", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); _vertexGradientConstructor = type.Assembly.GetType("TMPro.VertexGradient")?.GetConstructor(new Type[4] { typeof(Color), typeof(Color), typeof(Color), typeof(Color) }); } } private static void SetLabelText(object label, string text) { EnsureTmpReflection(label); _tmpTextProperty?.SetValue(label, text); } private static void SetLabelGradient(object label, Color c1, Color c2) { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) EnsureTmpReflection(label); if (!(_vertexGradientConstructor == null) && !(_tmpColorGradientProperty == null)) { object value = _vertexGradientConstructor.Invoke(new object[4] { c1, c1, c2, c2 }); _tmpColorGradientProperty.SetValue(label, value); } } private static void SetLabelActive(object label, bool active) { EnsureTmpReflection(label); object? obj = _componentGameObjectProperty?.GetValue(label); GameObject val = (GameObject)((obj is GameObject) ? obj : null); if (val != null) { val.SetActive(active); } } private static bool TryEnqueueRawPlayerInfo(string message, bool isEntering) { UIPrefab_PlayerEnterInfo playerEnterInfoUi = GetPlayerEnterInfoUi(); if (playerEnterInfoUi == null) { return false; } IList listField = GetListField(playerEnterInfoUi, "currentDisplayed"); IList listField2 = GetListField(playerEnterInfoUi, "displayStartTimeMilliSec"); IList listField3 = GetListField(playerEnterInfoUi, "isEnteringFlags"); IList listField4 = GetListField(playerEnterInfoUi, "PlayerInfos"); if (listField == null || listField2 == null || listField3 == null || listField4 == null) { return false; } if (listField4.Count <= 0) { return false; } TrimOldestIfFull(listField, listField2, listField3, listField4.Count); listField.Add(message); listField2.Add(GetCurrentTickMilliSec()); listField3.Add(isEntering); return true; } private static void TrimOldestIfFull(IList currentDisplayed, IList displayStartTime, IList isEnteringFlags, int maxVisible) { while (currentDisplayed.Count >= maxVisible && currentDisplayed.Count > 0) { currentDisplayed.RemoveAt(0); displayStartTime.RemoveAt(0); isEnteringFlags.RemoveAt(0); } } private static IList? GetListField(object target, string fieldName) { if (!ListFieldCache.TryGetValue(fieldName, out FieldInfo value)) { value = target.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); ListFieldCache[fieldName] = value; } return value?.GetValue(target) as IList; } private static long GetCurrentTickMilliSec() { try { TimeUtil val = GameSessionAccess.TryGetTimeUtil(); if ((Object)(object)val == (Object)null) { return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); } if ((object)_getTickMethod == null) { _getTickMethod = ((object)val).GetType().GetMethod("GetCurrentTickMilliSec", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null); } return (_getTickMethod == null) ? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() : Convert.ToInt64(_getTickMethod.Invoke(val, null)); } catch (Exception ex) { if (!_loggedTickFallback) { _loggedTickFallback = true; ModLog.Warn("InGameMessage", "Game tick unavailable — falling back to UTC clock for toast timing — " + ex.Message); } return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); } } private static UIPrefab_PlayerEnterInfo? GetPlayerEnterInfoUi() { PersistentData val = GameSessionAccess.TryGetPdata(); if (val == null) { return null; } object obj = ((object)val).GetType().GetField("main", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(val); if (obj != null) { object? obj2 = obj.GetType().GetField("playerEnterInfoUI", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(obj); return (UIPrefab_PlayerEnterInfo?)((obj2 is UIPrefab_PlayerEnterInfo) ? obj2 : null); } return null; } } internal static class LocalPlayerHelper { private const BindingFlags InstanceMemberFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo? UniqueUserPathField = typeof(PlatformMgr).GetField("_uniqueUserPath", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static ulong TryGetLocalSteamId() { return GameSessionAccess.GetLocalSteamId(); } internal static ulong TryGetPlatformSteamId() { try { return TryReadPlatformSteamId(MonoSingleton.Instance); } catch { return 0uL; } } internal static bool IsLocalSteamId(ulong steamId) { if (steamId == 0L) { return false; } ulong localSteamId = GameSessionAccess.GetLocalSteamId(); if (localSteamId != 0L) { return localSteamId == steamId; } return false; } private static ulong TryReadPlatformSteamId(PlatformMgr platformMgr) { string text = UniqueUserPathField?.GetValue(platformMgr) as string; if (string.IsNullOrEmpty(text) || !ulong.TryParse(text, out var result)) { return 0uL; } return result; } } internal static class ModJson { private sealed class FieldCamelCaseContractResolver : DefaultContractResolver { public FieldCamelCaseContractResolver() { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Expected O, but got Unknown ((DefaultContractResolver)this).NamingStrategy = (NamingStrategy)new CamelCaseNamingStrategy(); } protected override List GetSerializableMembers(Type objectType) { return ((IEnumerable)(from field in objectType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) where !field.IsDefined(typeof(NonSerializedAttribute)) select field)).ToList(); } protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) { return ((DefaultContractResolver)this).CreateProperties(type, (MemberSerialization)2); } } private static readonly JsonSerializerSettings Settings = CreateSettings(); internal static string Serialize(T value) { return JsonConvert.SerializeObject((object)value, Settings); } internal static T? Deserialize(string json) where T : class { if (string.IsNullOrWhiteSpace(json)) { return null; } try { return JsonConvert.DeserializeObject(json, Settings); } catch { return null; } } private static JsonSerializerSettings CreateSettings() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: 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_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected O, but got Unknown return new JsonSerializerSettings { ContractResolver = (IContractResolver)(object)new FieldCamelCaseContractResolver(), Formatting = (Formatting)0, DateFormatHandling = (DateFormatHandling)0, DateTimeZoneHandling = (DateTimeZoneHandling)1 }; } } internal static class ModL10n { private const string DefaultLocale = "en"; private const string LocaleFolder = "Locale"; private static readonly Regex NamedPlaceholderPattern = new Regex("\\{(\\w+)\\}", RegexOptions.Compiled); private static readonly Dictionary LocaleRoots = new Dictionary(StringComparer.OrdinalIgnoreCase); private static readonly List AvailableLocales = new List(); private static readonly object InitLock = new object(); private static bool _initialized; internal static IReadOnlyList GetAvailableLocales() { EnsureInitialized(); return AvailableLocales; } internal static void Initialize() { EnsureInitialized(); } internal static string Get(string key, params object[] args) { EnsureInitialized(); return Resolve(key, GameLocaleAccess.GetCurrentLanguage(), args); } internal static string GetForLocale(string locale, string key, params object[] args) { EnsureInitialized(); return Resolve(key, GameLocaleAccess.NormalizeLanguageCode(locale), args); } internal static string? GetConfigSectionTitle(string sectionId) { return GetConfigSectionTitle(sectionId, null); } internal static string? GetConfigSectionTitle(string sectionId, string? locale) { return GetOptional("config." + sectionId + "._section", locale); } internal static string? GetConfigSectionDescription(string sectionId) { return GetConfigSectionDescription(sectionId, null); } internal static string? GetConfigSectionDescription(string sectionId, string? locale) { return GetOptional("config." + sectionId + "._description", locale); } internal static string? GetConfigEntryTitle(string sectionId, string key) { return GetConfigEntryTitle(sectionId, key, null); } internal static string? GetConfigEntryTitle(string sectionId, string key, string? locale) { return GetOptional("config." + sectionId + "." + key + ".title", locale); } internal static string? GetConfigEntryDescription(string sectionId, string key) { return GetConfigEntryDescription(sectionId, key, null); } internal static string? GetConfigEntryDescription(string sectionId, string key, string? locale) { return GetOptional("config." + sectionId + "." + key + ".description", locale); } internal static string? GetConfigSelectOptionLabel(string sectionId, string key, string value) { return GetConfigSelectOptionLabel(sectionId, key, value, null); } internal static string? GetConfigSelectOptionLabel(string sectionId, string key, string value, string? locale) { return GetOptional("config." + sectionId + "." + key + ".options." + value, locale); } private static void EnsureInitialized() { if (_initialized) { return; } lock (InitLock) { if (_initialized) { return; } foreach (string item in EmbeddedAssets.ListFeatureFiles("Locale")) { if (item.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) { string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(item); if (!string.IsNullOrWhiteSpace(fileNameWithoutExtension)) { LoadLocale(fileNameWithoutExtension); } } } if (!LocaleRoots.ContainsKey("en")) { LoadLocale("en"); } _initialized = true; } } private static void LoadLocale(string locale) { if (!EmbeddedAssets.TryReadFeature("Locale", locale + ".json", out byte[] bytes, out string _)) { return; } JObject val = ModJson.Deserialize(Encoding.UTF8.GetString(bytes)); if (val == null) { return; } LocaleRoots[locale] = val; bool flag = false; foreach (string availableLocale in AvailableLocales) { if (string.Equals(availableLocale, locale, StringComparison.OrdinalIgnoreCase)) { flag = true; break; } } if (!flag) { AvailableLocales.Add(locale); } } private static string Resolve(string key, string locale, object[] args) { string text = Lookup(locale, key) ?? Lookup("en", key); if (string.IsNullOrEmpty(text)) { return key; } return Format(text, args); } private static string? GetOptional(string key) { return GetOptional(key, null); } private static string? GetOptional(string key, string? locale) { EnsureInitialized(); return Lookup(string.IsNullOrWhiteSpace(locale) ? GameLocaleAccess.GetCurrentLanguage() : GameLocaleAccess.NormalizeLanguageCode(locale), key) ?? Lookup("en", key); } private static string? Lookup(string locale, string key) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Invalid comparison between Unknown and I4 if (!LocaleRoots.TryGetValue(locale, out JObject value) || value == null) { return null; } JToken val = SelectToken(value, key); if (val == null || (int)val.Type != 8) { return null; } return Extensions.Value((IEnumerable)val); } private static JToken? SelectToken(JObject root, string key) { string[] array = key.Split('.'); JToken val = (JToken)(object)root; string[] array2 = array; foreach (string text in array2) { JObject val2 = (JObject)(object)((val is JObject) ? val : null); if (val2 == null) { return null; } val = val2[text]; if (val == null) { return null; } } return val; } private static string Format(string template, object[] args) { if (args.Length == 0) { return template; } if (args.Length == 1 && args[0] is object[] array) { args = array; } if (args.Length == 1) { object obj = args[0]; IReadOnlyDictionary namedArgs = obj as IReadOnlyDictionary; object value; if (namedArgs != null) { return NamedPlaceholderPattern.Replace(template, (Match match) => (!namedArgs.TryGetValue(match.Groups[1].Value, out value)) ? match.Value : (Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty)); } } if (args.Length == 1) { object obj = args[0]; Dictionary mutableNamedArgs = obj as Dictionary; object value; if (mutableNamedArgs != null) { return NamedPlaceholderPattern.Replace(template, (Match match) => (!mutableNamedArgs.TryGetValue(match.Groups[1].Value, out value)) ? match.Value : (Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty)); } } return string.Format(CultureInfo.InvariantCulture, template, args); } } internal static class ModLocaleAssets { private const string LocaleFolder = "Locale"; internal static bool TryReadLocaleJson(string locale, out byte[] bytes) { bytes = Array.Empty(); locale = GameLocaleAccess.NormalizeLanguageCode(locale); if (EmbeddedAssets.TryReadFeature("Locale", locale + ".json", out bytes, out string extension)) { return true; } if (locale != "en") { return EmbeddedAssets.TryReadFeature("Locale", "en.json", out bytes, out extension); } return false; } } internal static class ReflectionFieldCache { private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly ConcurrentDictionary<(Type Type, string Name), FieldInfo?> Fields = new ConcurrentDictionary<(Type, string), FieldInfo>(); internal static FieldInfo? GetField(Type type, string name) { return Fields.GetOrAdd((type, name), ((Type Type, string Name) key) => key.Type.GetField(key.Name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)); } internal static FieldInfo? GetField(object target, string name) { return GetField(target.GetType(), name); } } internal static class ScalingMath { internal const int VanillaPlayerBaseline = 4; internal const float DefaultPerPlayerMultiplier = 0.1f; internal static float GetAdditiveMultiplier(float generalMultiplier, float perPlayerMultiplier, int playerCount, int baselinePlayerCount) { int num = Math.Max(1, baselinePlayerCount); int num2 = playerCount - num; if (num2 <= 0 || perPlayerMultiplier <= 0f) { return Math.Max(0f, generalMultiplier); } return Math.Max(0f, generalMultiplier + (float)num2 * perPlayerMultiplier); } internal static int ScaleCount(int vanilla, float multiplier) { if (vanilla != 0) { if (!(multiplier <= 0f)) { return Math.Max(1, (int)Math.Round((float)vanilla * multiplier)); } return 0; } return 0; } internal static int ScaleCountWithImplicitBase(int vanilla, float multiplier, int implicitWhenZero) { return ScaleCount((vanilla > 0) ? vanilla : implicitWhenZero, multiplier); } } internal static class SceneScopedConfigDeferralLogic { private sealed class LiveApplyKeyComparer : IEqualityComparer<(string Module, string Key)> { internal static readonly LiveApplyKeyComparer Instance = new LiveApplyKeyComparer(); public bool Equals((string Module, string Key) x, (string Module, string Key) y) { if (string.Equals(x.Module, y.Module, StringComparison.OrdinalIgnoreCase)) { return string.Equals(x.Key, y.Key, StringComparison.OrdinalIgnoreCase); } return false; } public int GetHashCode((string Module, string Key) obj) { return HashCode.Combine(StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Module), StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Key)); } } internal static readonly HashSet GatedModules = new HashSet { "LootMultiplicator", "SpawnScaling", "Economy", "DungeonTime", "DungeonRandomizer" }; private static readonly HashSet<(string Module, string Key)> LiveApplyKeys = new HashSet<(string, string)>(LiveApplyKeyComparer.Instance) { ("DungeonTime", "EnableRealtimeTramClock"), ("DungeonTime", "TimeMultiplier") }; internal static bool ShouldDefer(string moduleName, ModConfigChangeInfo change, bool isGameplaySceneActive, bool isMasterEnabled, string? masterToggleKey) { if (!GatedModules.Contains(moduleName)) { return false; } if (IsMasterToggleDisabledChange(moduleName, change, isMasterEnabled, masterToggleKey)) { return false; } if (HasLiveApplyKeyChange(moduleName, change)) { return false; } if (!isGameplaySceneActive) { return false; } return IsModuleAffected(moduleName, change); } internal static bool IsModuleAffected(string moduleName, ModConfigChangeInfo change) { if (change.IsFullReload) { return true; } string sectionId = "MimesisPlayerEnhancement_" + moduleName; return change.ChangedKeys.Any((ModConfigKeyChange keyChange) => string.Equals(keyChange.SectionId, sectionId, StringComparison.OrdinalIgnoreCase) && !IsLiveApplyKey(moduleName, keyChange.Key)); } internal static bool HasLiveApplyKeyChange(string moduleName, ModConfigChangeInfo change) { if (change.IsFullReload || !GatedModules.Contains(moduleName)) { return false; } string sectionId = "MimesisPlayerEnhancement_" + moduleName; return change.ChangedKeys.Any((ModConfigKeyChange keyChange) => string.Equals(keyChange.SectionId, sectionId, StringComparison.OrdinalIgnoreCase) && IsLiveApplyKey(moduleName, keyChange.Key)); } internal static bool IsLiveApplyKey(string moduleName, string key) { return LiveApplyKeys.Contains((moduleName, key)); } internal static bool IsMasterToggleDisabledChange(string moduleName, ModConfigChangeInfo change, bool isMasterEnabled, string? masterToggleKey) { if (!GatedModules.Contains(moduleName) || isMasterEnabled) { return false; } if (change.IsFullReload) { return true; } if (string.IsNullOrWhiteSpace(masterToggleKey)) { return false; } string sectionId = "MimesisPlayerEnhancement_" + moduleName; return change.ChangedKeys.Any((ModConfigKeyChange keyChange) => string.Equals(keyChange.SectionId, sectionId, StringComparison.OrdinalIgnoreCase) && string.Equals(keyChange.Key, masterToggleKey, StringComparison.OrdinalIgnoreCase)); } internal static string[] TakeModulesForDeferredSync(HashSet deferredModules) { if (deferredModules.Count == 0) { return Array.Empty(); } string[] result = deferredModules.ToArray(); deferredModules.Clear(); return result; } } internal enum SceneScopeKind { None, Maintenance, Tram, Dungeon, Deathmatch } internal static class SceneScopedConfigGate { private const string Feature = "Config"; private static readonly Dictionary> EnableReaders = new Dictionary>(StringComparer.Ordinal) { ["LootMultiplicator"] = () => ModConfig.EnableLootMultiplicator.Value, ["SpawnScaling"] = () => ModConfig.EnableSpawnScaling.Value, ["Economy"] = () => ModConfig.EnableEconomy.Value, ["DungeonTime"] = () => ModConfig.EnableDungeonTime.Value, ["DungeonRandomizer"] = () => ModConfig.EnableDungeonRandomizer.Value }; private static SceneScopeKind _activeKind = SceneScopeKind.None; private static LootMultiplicatorSceneConfig _activeLoot; private static SpawnScalingSceneConfig _activeSpawn; private static EconomySceneConfig _activeEconomy; private static DungeonTimeSceneConfig _activeDungeonTime; private static DungeonRandomizerSceneConfig _activeDungeonRandomizer; private static LootMultiplicatorSceneConfig _pendingLoot; private static SpawnScalingSceneConfig _pendingSpawn; private static EconomySceneConfig _pendingEconomy; private static DungeonTimeSceneConfig _pendingDungeonTime; private static DungeonRandomizerSceneConfig _pendingDungeonRandomizer; private static readonly HashSet DeferredModules = new HashSet(StringComparer.Ordinal); private static bool _deferInfoLogged; private static Action? _deferredModuleSyncAction; private static readonly List DungeonRunEndCleanups = new List(); internal static SceneScopeKind ActiveKind => _activeKind; internal static LootMultiplicatorSceneConfig Loot => _activeLoot; internal static SpawnScalingSceneConfig Spawn => _activeSpawn; internal static EconomySceneConfig Economy => _activeEconomy; internal static DungeonTimeSceneConfig DungeonTime => _activeDungeonTime; internal static DungeonRandomizerSceneConfig DungeonRandomizer => _activeDungeonRandomizer; internal static void SetDeferredModuleSyncAction(Action syncAction) { _deferredModuleSyncAction = syncAction; } internal static void RegisterDungeonRunEndCleanup(Action cleanup) { if (cleanup != null && !DungeonRunEndCleanups.Contains(cleanup)) { DungeonRunEndCleanups.Add(cleanup); } } internal static void InvokeDungeonRunEndCleanup() { for (int i = 0; i < DungeonRunEndCleanups.Count; i++) { try { DungeonRunEndCleanups[i](); } catch (Exception ex) { ModLog.Warn("Config", "Dungeon run end cleanup failed — " + ex.Message); } } } internal static void Initialize() { CaptureAllActiveFromModConfig(); CaptureAllPendingFromModConfig(); _activeKind = SceneScopeKind.None; DeferredModules.Clear(); _deferInfoLogged = false; } internal static void SeedActiveForTests(SceneScopeKind kind, LootMultiplicatorSceneConfig loot) { _activeKind = kind; _activeLoot = loot; _pendingLoot = loot; DeferredModules.Clear(); _deferInfoLogged = false; } internal static void TransitionToScene(SceneScopeKind kind, LootMultiplicatorSceneConfig loot) { if (_activeKind != kind) { if (_activeKind != SceneScopeKind.None) { EndScene(); } _activeKind = kind; _activeLoot = loot; _pendingLoot = loot; DeferredModules.Clear(); _deferInfoLogged = false; } } internal static void OnConfigChangePrepared(ModConfigChangeInfo change) { CaptureAllPendingFromModConfig(); if (!IsGameplaySceneActive()) { ApplyPendingToActive(); DeferredModules.Clear(); _deferInfoLogged = false; return; } foreach (string gatedModule in SceneScopedConfigDeferralLogic.GatedModules) { if (SceneScopedConfigDeferralLogic.IsModuleAffected(gatedModule, change)) { if (IsMasterToggleDisabledChange(gatedModule, change)) { ApplyActiveSnapshotForModule(gatedModule); } else { DeferredModules.Add(gatedModule); } } } if (DeferredModules.Count > 0 && !_deferInfoLogged) { _deferInfoLogged = true; ModLog.Info("Config", string.Format("Config change deferred until scene end — scene={0}, modules={1}", _activeKind, string.Join(", ", DeferredModules))); } } internal static bool ShouldDeferModuleSync(string moduleName, ModConfigChangeInfo change) { return SceneScopedConfigDeferralLogic.ShouldDefer(moduleName, change, IsGameplaySceneActive(), IsMasterEnabled(moduleName), TryGetMasterToggleKey(moduleName)); } internal static void TransitionToScene(SceneScopeKind kind) { if (_activeKind != kind) { if (_activeKind != SceneScopeKind.None) { EndScene(); } _activeKind = kind; CaptureAllActiveFromModConfig(); CaptureAllPendingFromModConfig(); DeferredModules.Clear(); _deferInfoLogged = false; } } internal static void EndScene() { if (_activeKind != SceneScopeKind.None) { CommitPendingOnSceneEnd(); FlushDeferredModuleSyncIfConfigured(); _activeKind = SceneScopeKind.None; } } internal static void CommitPendingOnSceneEnd() { ApplyPendingToActive(); } internal static void FlushDeferredModuleSync(Action syncAction) { string[] array = SceneScopedConfigDeferralLogic.TakeModulesForDeferredSync(DeferredModules); _deferInfoLogged = false; string[] array2 = array; foreach (string obj in array2) { syncAction(obj); } } private static void FlushDeferredModuleSyncIfConfigured() { if (_deferredModuleSyncAction == null) { DeferredModules.Clear(); _deferInfoLogged = false; } else { FlushDeferredModuleSync(_deferredModuleSyncAction); } } internal static bool IsModuleSyncDeferred(string moduleName) { return DeferredModules.Contains(moduleName); } internal static bool IsGameplaySceneActive() { return _activeKind != SceneScopeKind.None; } private static void ApplyPendingToActive() { _activeLoot = _pendingLoot; _activeSpawn = _pendingSpawn; _activeEconomy = _pendingEconomy; _activeDungeonTime = _pendingDungeonTime; _activeDungeonRandomizer = _pendingDungeonRandomizer; LootItemFilter.ReloadFromSceneSnapshot(); } private static void CaptureAllActiveFromModConfig() { _activeLoot = LootMultiplicatorSceneConfig.CaptureFromModConfig(); _activeSpawn = SpawnScalingSceneConfig.CaptureFromModConfig(); _activeEconomy = EconomySceneConfig.CaptureFromModConfig(); _activeDungeonTime = DungeonTimeSceneConfig.CaptureFromModConfig(); _activeDungeonRandomizer = DungeonRandomizerSceneConfig.CaptureFromModConfig(); LootItemFilter.ReloadFromSceneSnapshot(); } private static void CaptureAllPendingFromModConfig() { _pendingLoot = LootMultiplicatorSceneConfig.CaptureFromModConfig(); _pendingSpawn = SpawnScalingSceneConfig.CaptureFromModConfig(); _pendingEconomy = EconomySceneConfig.CaptureFromModConfig(); _pendingDungeonTime = DungeonTimeSceneConfig.CaptureFromModConfig(); _pendingDungeonRandomizer = DungeonRandomizerSceneConfig.CaptureFromModConfig(); } private static void ApplyActiveSnapshotForModule(string moduleName) { switch (moduleName) { case "LootMultiplicator": _activeLoot = _pendingLoot; LootItemFilter.ReloadFromSceneSnapshot(); break; case "SpawnScaling": _activeSpawn = _pendingSpawn; break; case "Economy": _activeEconomy = _pendingEconomy; break; case "DungeonTime": _activeDungeonTime = _pendingDungeonTime; break; case "DungeonRandomizer": _activeDungeonRandomizer = _pendingDungeonRandomizer; break; } DeferredModules.Remove(moduleName); } private static bool IsMasterToggleDisabledChange(string moduleName, ModConfigChangeInfo change) { return SceneScopedConfigDeferralLogic.IsMasterToggleDisabledChange(moduleName, change, IsMasterEnabled(moduleName), TryGetMasterToggleKey(moduleName)); } private static bool IsMasterEnabled(string moduleName) { if (EnableReaders.TryGetValue(moduleName, out Func value)) { return value(); } return false; } private static string? TryGetMasterToggleKey(string moduleName) { if (!ModConfigRegistry.TryGetFeatureToggleKey("MimesisPlayerEnhancement_" + moduleName, out string key)) { return null; } return key; } } internal readonly struct LootMultiplicatorSceneConfig { internal bool EnableLootMultiplicator { get; } internal int LootMultiplicatorBaselinePlayerCount { get; } internal float MapLootMultiplier { get; } internal float MapLootPerPlayerMultiplier { get; } internal float DropLootMultiplier { get; } internal float DropLootPerPlayerMultiplier { get; } internal string LootItemFilterMode { get; } internal string LootAllowlist { get; } internal string LootBlocklist { get; } internal bool AutoScaleMapLootBudgetForFilter { get; } internal int ConvertFakeActorDyingDropChancePercent { get; } internal LootMultiplicatorSceneConfig(bool enableLootMultiplicator, int lootMultiplicatorBaselinePlayerCount, float mapLootMultiplier, float mapLootPerPlayerMultiplier, float dropLootMultiplier, float dropLootPerPlayerMultiplier, string lootItemFilterMode, string lootAllowlist, string lootBlocklist, bool autoScaleMapLootBudgetForFilter, int convertFakeActorDyingDropChancePercent) { EnableLootMultiplicator = enableLootMultiplicator; LootMultiplicatorBaselinePlayerCount = lootMultiplicatorBaselinePlayerCount; MapLootMultiplier = mapLootMultiplier; MapLootPerPlayerMultiplier = mapLootPerPlayerMultiplier; DropLootMultiplier = dropLootMultiplier; DropLootPerPlayerMultiplier = dropLootPerPlayerMultiplier; LootItemFilterMode = lootItemFilterMode; LootAllowlist = lootAllowlist; LootBlocklist = lootBlocklist; AutoScaleMapLootBudgetForFilter = autoScaleMapLootBudgetForFilter; ConvertFakeActorDyingDropChancePercent = convertFakeActorDyingDropChancePercent; } internal static LootMultiplicatorSceneConfig CaptureFromModConfig() { return new LootMultiplicatorSceneConfig(ModConfig.EnableLootMultiplicator.Value, ModConfig.LootMultiplicatorBaselinePlayerCount.Value, ModConfig.MapLootMultiplier.Value, ModConfig.MapLootPerPlayerMultiplier.Value, ModConfig.DropLootMultiplier.Value, ModConfig.DropLootPerPlayerMultiplier.Value, ModConfig.LootItemFilterMode.Value ?? "", ModConfig.LootAllowlist.Value ?? "", ModConfig.LootBlocklist.Value ?? "", ModConfig.AutoScaleMapLootBudgetForFilter.Value, ModConfig.ConvertFakeActorDyingDropChancePercent.Value); } } internal readonly struct SpawnScalingSceneConfig { internal bool EnableSpawnScaling { get; } internal int SpawnScalingBaselinePlayerCount { get; } internal float MimicSpawnMultiplier { get; } internal float MimicSpawnPerPlayerMultiplier { get; } internal string MimicWaveMode { get; } internal float MimicWaveInitialDelaySeconds { get; } internal float MimicWaveInitialDelayMinSeconds { get; } internal float MimicWaveInitialDelayMaxSeconds { get; } internal float MimicWaveIntervalSeconds { get; } internal float MimicWaveIntervalMinSeconds { get; } internal float MimicWaveIntervalMaxSeconds { get; } internal float BossSpawnMultiplier { get; } internal float BossSpawnPerPlayerMultiplier { get; } internal float GruntSpawnMultiplier { get; } internal float GruntSpawnPerPlayerMultiplier { get; } internal string GruntWaveMode { get; } internal float GruntWaveInitialDelaySeconds { get; } internal float GruntWaveInitialDelayMinSeconds { get; } internal float GruntWaveInitialDelayMaxSeconds { get; } internal float GruntWaveIntervalSeconds { get; } internal float GruntWaveIntervalMinSeconds { get; } internal float GruntWaveIntervalMaxSeconds { get; } internal float SpecialSpawnMultiplier { get; } internal float SpecialSpawnPerPlayerMultiplier { get; } internal float TrapSpawnMultiplier { get; } internal float TrapSpawnPerPlayerMultiplier { get; } internal string TrapRespawnMode { get; } internal float TrapRespawnDelaySeconds { get; } internal float TrapRespawnDelayMinSeconds { get; } internal float TrapRespawnDelayMaxSeconds { get; } internal float TrapRespawnMinPlayerDistanceMeters { get; } internal float OtherSpawnMultiplier { get; } internal float OtherSpawnPerPlayerMultiplier { get; } internal float BonusEncounterDelayMinSeconds { get; } internal float BonusEncounterDelayMaxSeconds { get; } internal float BonusEncounterMinPlayerDistanceMeters { get; } internal SpawnScalingSceneConfig(bool enableSpawnScaling, int spawnScalingBaselinePlayerCount, float mimicSpawnMultiplier, float mimicSpawnPerPlayerMultiplier, string mimicWaveMode, float mimicWaveInitialDelaySeconds, float mimicWaveInitialDelayMinSeconds, float mimicWaveInitialDelayMaxSeconds, float mimicWaveIntervalSeconds, float mimicWaveIntervalMinSeconds, float mimicWaveIntervalMaxSeconds, float bossSpawnMultiplier, float bossSpawnPerPlayerMultiplier, float gruntSpawnMultiplier, float gruntSpawnPerPlayerMultiplier, string gruntWaveMode, float gruntWaveInitialDelaySeconds, float gruntWaveInitialDelayMinSeconds, float gruntWaveInitialDelayMaxSeconds, float gruntWaveIntervalSeconds, float gruntWaveIntervalMinSeconds, float gruntWaveIntervalMaxSeconds, float specialSpawnMultiplier, float specialSpawnPerPlayerMultiplier, float trapSpawnMultiplier, float trapSpawnPerPlayerMultiplier, string trapRespawnMode, float trapRespawnDelaySeconds, float trapRespawnDelayMinSeconds, float trapRespawnDelayMaxSeconds, float trapRespawnMinPlayerDistanceMeters, float otherSpawnMultiplier, float otherSpawnPerPlayerMultiplier, float bonusEncounterDelayMinSeconds, float bonusEncounterDelayMaxSeconds, float bonusEncounterMinPlayerDistanceMeters) { EnableSpawnScaling = enableSpawnScaling; SpawnScalingBaselinePlayerCount = spawnScalingBaselinePlayerCount; MimicSpawnMultiplier = mimicSpawnMultiplier; MimicSpawnPerPlayerMultiplier = mimicSpawnPerPlayerMultiplier; MimicWaveMode = mimicWaveMode; MimicWaveInitialDelaySeconds = mimicWaveInitialDelaySeconds; MimicWaveInitialDelayMinSeconds = mimicWaveInitialDelayMinSeconds; MimicWaveInitialDelayMaxSeconds = mimicWaveInitialDelayMaxSeconds; MimicWaveIntervalSeconds = mimicWaveIntervalSeconds; MimicWaveIntervalMinSeconds = mimicWaveIntervalMinSeconds; MimicWaveIntervalMaxSeconds = mimicWaveIntervalMaxSeconds; BossSpawnMultiplier = bossSpawnMultiplier; BossSpawnPerPlayerMultiplier = bossSpawnPerPlayerMultiplier; GruntSpawnMultiplier = gruntSpawnMultiplier; GruntSpawnPerPlayerMultiplier = gruntSpawnPerPlayerMultiplier; GruntWaveMode = gruntWaveMode; GruntWaveInitialDelaySeconds = gruntWaveInitialDelaySeconds; GruntWaveInitialDelayMinSeconds = gruntWaveInitialDelayMinSeconds; GruntWaveInitialDelayMaxSeconds = gruntWaveInitialDelayMaxSeconds; GruntWaveIntervalSeconds = gruntWaveIntervalSeconds; GruntWaveIntervalMinSeconds = gruntWaveIntervalMinSeconds; GruntWaveIntervalMaxSeconds = gruntWaveIntervalMaxSeconds; SpecialSpawnMultiplier = specialSpawnMultiplier; SpecialSpawnPerPlayerMultiplier = specialSpawnPerPlayerMultiplier; TrapSpawnMultiplier = trapSpawnMultiplier; TrapSpawnPerPlayerMultiplier = trapSpawnPerPlayerMultiplier; TrapRespawnMode = trapRespawnMode; TrapRespawnDelaySeconds = trapRespawnDelaySeconds; TrapRespawnDelayMinSeconds = trapRespawnDelayMinSeconds; TrapRespawnDelayMaxSeconds = trapRespawnDelayMaxSeconds; TrapRespawnMinPlayerDistanceMeters = trapRespawnMinPlayerDistanceMeters; OtherSpawnMultiplier = otherSpawnMultiplier; OtherSpawnPerPlayerMultiplier = otherSpawnPerPlayerMultiplier; BonusEncounterDelayMinSeconds = bonusEncounterDelayMinSeconds; BonusEncounterDelayMaxSeconds = bonusEncounterDelayMaxSeconds; BonusEncounterMinPlayerDistanceMeters = bonusEncounterMinPlayerDistanceMeters; } internal static SpawnScalingSceneConfig CaptureFromModConfig() { return new SpawnScalingSceneConfig(ModConfig.EnableSpawnScaling.Value, ModConfig.SpawnScalingBaselinePlayerCount.Value, ModConfig.MimicSpawnMultiplier.Value, ModConfig.MimicSpawnPerPlayerMultiplier.Value, ModConfig.MimicWaveMode.Value ?? "", ModConfig.MimicWaveInitialDelaySeconds.Value, ModConfig.MimicWaveInitialDelayMinSeconds.Value, ModConfig.MimicWaveInitialDelayMaxSeconds.Value, ModConfig.MimicWaveIntervalSeconds.Value, ModConfig.MimicWaveIntervalMinSeconds.Value, ModConfig.MimicWaveIntervalMaxSeconds.Value, ModConfig.BossSpawnMultiplier.Value, ModConfig.BossSpawnPerPlayerMultiplier.Value, ModConfig.GruntSpawnMultiplier.Value, ModConfig.GruntSpawnPerPlayerMultiplier.Value, ModConfig.GruntWaveMode.Value ?? "", ModConfig.GruntWaveInitialDelaySeconds.Value, ModConfig.GruntWaveInitialDelayMinSeconds.Value, ModConfig.GruntWaveInitialDelayMaxSeconds.Value, ModConfig.GruntWaveIntervalSeconds.Value, ModConfig.GruntWaveIntervalMinSeconds.Value, ModConfig.GruntWaveIntervalMaxSeconds.Value, ModConfig.SpecialSpawnMultiplier.Value, ModConfig.SpecialSpawnPerPlayerMultiplier.Value, ModConfig.TrapSpawnMultiplier.Value, ModConfig.TrapSpawnPerPlayerMultiplier.Value, ModConfig.TrapRespawnMode.Value ?? "", ModConfig.TrapRespawnDelaySeconds.Value, ModConfig.TrapRespawnDelayMinSeconds.Value, ModConfig.TrapRespawnDelayMaxSeconds.Value, ModConfig.TrapRespawnMinPlayerDistanceMeters.Value, ModConfig.OtherSpawnMultiplier.Value, ModConfig.OtherSpawnPerPlayerMultiplier.Value, ModConfig.BonusEncounterDelayMinSeconds.Value, ModConfig.BonusEncounterDelayMaxSeconds.Value, ModConfig.BonusEncounterMinPlayerDistanceMeters.Value); } } internal readonly struct EconomySceneConfig { internal bool EnableEconomy { get; } internal int EconomyBaselinePlayerCount { get; } internal float ScrapSellValueMultiplier { get; } internal float ScrapSellValuePerPlayerMultiplier { get; } internal float ShopBuyPriceMultiplier { get; } internal float ShopBuyPricePerPlayerMultiplier { get; } internal int ShopDiscountMinPercent { get; } internal int ShopDiscountMaxPercent { get; } internal int ShopDiscountChancePercent { get; } internal float ReinforcePriceMultiplier { get; } internal float ReinforcePricePerPlayerMultiplier { get; } internal bool RetainUnspentCurrencyBetweenCycles { get; } internal EconomySceneConfig(bool enableEconomy, int economyBaselinePlayerCount, float scrapSellValueMultiplier, float scrapSellValuePerPlayerMultiplier, float shopBuyPriceMultiplier, float shopBuyPricePerPlayerMultiplier, int shopDiscountMinPercent, int shopDiscountMaxPercent, int shopDiscountChancePercent, float reinforcePriceMultiplier, float reinforcePricePerPlayerMultiplier, bool retainUnspentCurrencyBetweenCycles) { EnableEconomy = enableEconomy; EconomyBaselinePlayerCount = economyBaselinePlayerCount; ScrapSellValueMultiplier = scrapSellValueMultiplier; ScrapSellValuePerPlayerMultiplier = scrapSellValuePerPlayerMultiplier; ShopBuyPriceMultiplier = shopBuyPriceMultiplier; ShopBuyPricePerPlayerMultiplier = shopBuyPricePerPlayerMultiplier; ShopDiscountMinPercent = shopDiscountMinPercent; ShopDiscountMaxPercent = shopDiscountMaxPercent; ShopDiscountChancePercent = shopDiscountChancePercent; ReinforcePriceMultiplier = reinforcePriceMultiplier; ReinforcePricePerPlayerMultiplier = reinforcePricePerPlayerMultiplier; RetainUnspentCurrencyBetweenCycles = retainUnspentCurrencyBetweenCycles; } internal static EconomySceneConfig CaptureFromModConfig() { return new EconomySceneConfig(ModConfig.EnableEconomy.Value, ModConfig.EconomyBaselinePlayerCount.Value, ModConfig.ScrapSellValueMultiplier.Value, ModConfig.ScrapSellValuePerPlayerMultiplier.Value, ModConfig.ShopBuyPriceMultiplier.Value, ModConfig.ShopBuyPricePerPlayerMultiplier.Value, ModConfig.ShopDiscountMinPercent.Value, ModConfig.ShopDiscountMaxPercent.Value, ModConfig.ShopDiscountChancePercent.Value, ModConfig.ReinforcePriceMultiplier.Value, ModConfig.ReinforcePricePerPlayerMultiplier.Value, ModConfig.RetainUnspentCurrencyBetweenCycles.Value); } } internal readonly struct DungeonTimeSceneConfig { internal bool EnableDungeonTime { get; } internal int DungeonTimeBaselinePlayerCount { get; } internal float ExtraShiftSecondsPerPlayerAboveBaseline { get; } internal StartTimePreset StartTimePreset { get; } internal DungeonTimeSceneConfig(bool enableDungeonTime, int dungeonTimeBaselinePlayerCount, float extraShiftSecondsPerPlayerAboveBaseline, StartTimePreset startTimePreset) { EnableDungeonTime = enableDungeonTime; DungeonTimeBaselinePlayerCount = dungeonTimeBaselinePlayerCount; ExtraShiftSecondsPerPlayerAboveBaseline = extraShiftSecondsPerPlayerAboveBaseline; StartTimePreset = startTimePreset; } internal static DungeonTimeSceneConfig CaptureFromModConfig() { return new DungeonTimeSceneConfig(ModConfig.EnableDungeonTime.Value, ModConfig.DungeonTimeBaselinePlayerCount.Value, ModConfig.ExtraShiftSecondsPerPlayerAboveBaseline.Value, DungeonTimeClockResolver.ParseStartTimePreset(ModConfig.StartTimePreset.Value)); } } internal readonly struct DungeonRandomizerSceneConfig { internal bool EnableDungeonRandomizer { get; } internal bool RandomizeDungeonPick { get; } internal string DungeonPickPoolMode { get; } internal string DungeonAllowlist { get; } internal string DungeonBlocklist { get; } internal bool IgnoreDungeonExcludeList { get; } internal bool RandomizeMapVariant { get; } internal DungeonSeedFlavor SeedFlavor { get; } internal DungeonRandomizerSceneConfig(bool enableDungeonRandomizer, bool randomizeDungeonPick, string dungeonPickPoolMode, string dungeonAllowlist, string dungeonBlocklist, bool ignoreDungeonExcludeList, bool randomizeMapVariant, DungeonSeedFlavor seedFlavor) { EnableDungeonRandomizer = enableDungeonRandomizer; RandomizeDungeonPick = randomizeDungeonPick; DungeonPickPoolMode = dungeonPickPoolMode; DungeonAllowlist = dungeonAllowlist; DungeonBlocklist = dungeonBlocklist; IgnoreDungeonExcludeList = ignoreDungeonExcludeList; RandomizeMapVariant = randomizeMapVariant; SeedFlavor = seedFlavor; } internal static DungeonRandomizerSceneConfig CaptureFromModConfig() { if (!DungeonSeedFlavorUtil.TryParse(ModConfig.DungeonSeedFlavor.Value ?? "Vanilla", out var flavor)) { flavor = DungeonSeedFlavor.Vanilla; } return new DungeonRandomizerSceneConfig(ModConfig.EnableDungeonRandomizer.Value, ModConfig.RandomizeDungeonPick.Value, ModConfig.DungeonPickPoolMode.Value ?? "", ModConfig.DungeonAllowlist.Value ?? "", ModConfig.DungeonBlocklist.Value ?? "", ModConfig.IgnoreDungeonExcludeList.Value, ModConfig.RandomizeMapVariant.Value, flavor); } } internal static class SessionContextAccess { private const BindingFlags InstanceMemberFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo? HostSessionContextField = typeof(SessionManager).GetField("_hostSessionContext", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? ContextsField = typeof(SessionManager).GetField("m_Contexts", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? SessionVPlayerField = typeof(SessionContext).GetField("_vPlayer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? SessionManagerField = typeof(VWorld).GetField("_sessionManager", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static SessionManager? GetSessionManager() { try { VWorld val = GameSessionAccess.TryGetVWorld(); if (val == null) { return null; } object? obj = SessionManagerField?.GetValue(val); return (SessionManager?)((obj is SessionManager) ? obj : null); } catch { return null; } } internal static IEnumerable EnumerateSessionContexts(SessionManager sessionManager) { HashSet seen = new HashSet(); object? obj = HostSessionContextField?.GetValue(sessionManager); SessionContext val = (SessionContext)((obj is SessionContext) ? obj : null); if (val != null && val != null && seen.Add(val)) { yield return val; } if (!(ContextsField?.GetValue(sessionManager) is Dictionary dictionary)) { yield break; } List list = dictionary.Values.ToList(); foreach (SessionContext item in list) { if (item != null && seen.Add(item)) { yield return item; } } } internal static VPlayer? GetVPlayer(SessionContext context) { object? obj = SessionVPlayerField?.GetValue(context); return (VPlayer?)((obj is VPlayer) ? obj : null); } internal static bool TryGetPlayerByUid(long uid, out VPlayer? player) { player = null; if (uid == 0L) { return false; } SessionManager sessionManager = GetSessionManager(); if (sessionManager == null) { return false; } foreach (SessionContext item in EnumerateSessionContexts(sessionManager)) { if (item.GetPlayerUID() == uid) { VPlayer vPlayer = GetVPlayer(item); if (vPlayer != null) { player = vPlayer; return true; } } } return false; } internal static bool TryGetSessionContextByUid(long uid, out SessionContext? context) { context = null; if (uid == 0L) { return false; } SessionManager sessionManager = GetSessionManager(); if (sessionManager == null) { return false; } foreach (SessionContext item in EnumerateSessionContexts(sessionManager)) { if (item.GetPlayerUID() == uid) { context = item; return true; } } return false; } internal static bool TryGetSessionContextBySessionId(SessionManager sessionManager, long sessionId, out SessionContext? context) { context = null; if (sessionId == 0L || sessionManager == null) { return false; } object? obj = HostSessionContextField?.GetValue(sessionManager); SessionContext val = (SessionContext)((obj is SessionContext) ? obj : null); if (val != null && val != null && val.GetSessionID() == sessionId) { context = val; return true; } if (ContextsField?.GetValue(sessionManager) is Dictionary dictionary && dictionary.TryGetValue(sessionId, out var value)) { context = value; return true; } return false; } internal static bool TryGetHostPlayerUid(out long hostUid) { hostUid = 0L; SessionManager sessionManager = GetSessionManager(); if (sessionManager == null) { return false; } foreach (SessionContext item in EnumerateSessionContexts(sessionManager)) { VPlayer vPlayer = GetVPlayer(item); if (vPlayer != null && vPlayer.IsHost) { hostUid = ((VActor)vPlayer).UID; return true; } } return false; } internal static SessionContext? FindHostSessionContext(SessionManager sessionManager) { foreach (SessionContext item in EnumerateSessionContexts(sessionManager)) { VPlayer vPlayer = GetVPlayer(item); if (vPlayer != null && vPlayer.IsHost) { return item; } } object? obj = HostSessionContextField?.GetValue(sessionManager); return (SessionContext?)((obj is SessionContext) ? obj : null); } internal static bool TryGetSessionId(SessionContext context, out long sessionId) { sessionId = 0L; if (context == null) { return false; } try { sessionId = context.GetSessionID(); return sessionId != 0; } catch { return false; } } internal static void DisconnectSession(SessionManager sessionManager, long sessionId, DisconnectReason reason) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) sessionManager.Remove(sessionId, reason); } } internal enum SessionRole { None, Host, Client } internal static class SessionLifecycle { private const int EnsureSlotRetryIntervalFrames = 60; private static bool _wasSessionJoined; private static bool _sessionStartedRaised; private static int _ensureSlotRetryFrame = -60; private static SessionRole _role = SessionRole.None; private static int _activeSlotId = -1; internal static void Tick() { bool flag = IsSessionJoined(); if (_wasSessionJoined && !flag) { EndSession(); } else if (flag) { TryRaiseSessionStarted(); TryEnsureActiveSlotLoaded(); } _wasSessionJoined = flag; } internal static void NotifySessionEndedIfActive() { if (_wasSessionJoined) { EndSession(); _wasSessionJoined = false; } } private static void EndSession() { HostStatusCache.Invalidate(); FeatureModuleSessionHooks.InvokeSessionEnded(); _sessionStartedRaised = false; _role = SessionRole.None; _activeSlotId = -1; } private static void TryRaiseSessionStarted() { if (!_sessionStartedRaised) { SessionRole sessionRole = ResolveRole(); int num; switch (sessionRole) { case SessionRole.None: return; default: num = -1; break; case SessionRole.Host: num = ResolveHostSlotId(); break; } int num2 = num; if (sessionRole != SessionRole.Host || num2 >= 0) { _role = sessionRole; _activeSlotId = num2; _sessionStartedRaised = true; HostStatusCache.Invalidate(); FeatureModuleSessionHooks.InvokeSessionStarted(sessionRole, num2); } } } private static SessionRole ResolveRole() { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Invalid comparison between Unknown and I4 //IL_0020: Unknown result type (might be due to invalid IL or missing references) PersistentData val = GameSessionAccess.TryGetPdata(); if (val == null) { if (!MimesisSaveManager.IsHost()) { return SessionRole.None; } return SessionRole.Host; } if ((int)val.ClientMode == 1) { return SessionRole.Client; } if ((int)val.ClientMode == 0 || MimesisSaveManager.IsHost()) { return SessionRole.Host; } return SessionRole.None; } private static int ResolveHostSlotId() { int activeSlotId = SaveSlotConfigStore.ActiveSlotId; if (activeSlotId >= 0) { return activeSlotId; } int saveSlotId = GameSessionAccess.GetSaveSlotId(); if (!MimesisSaveManager.IsValidSaveSlotId(saveSlotId)) { return -1; } return saveSlotId; } private static void TryEnsureActiveSlotLoaded() { if (_role != SessionRole.Host && !MimesisSaveManager.IsHost()) { return; } int frameCount = Time.frameCount; if (frameCount - _ensureSlotRetryFrame < 60) { return; } _ensureSlotRetryFrame = frameCount; if (SaveSlotConfigStore.ActiveSlotId >= 0) { return; } int saveSlotId = GameSessionAccess.GetSaveSlotId(); if (MimesisSaveManager.IsValidSaveSlotId(saveSlotId)) { SaveSlotSidecarPersistence.EnsureSaveSlotLoaded(saveSlotId); if (_sessionStartedRaised && _activeSlotId < 0) { _activeSlotId = saveSlotId; } } } private static bool IsSessionJoined() { return GameSessionAccess.TryGetPdata()?.SessionJoined ?? false; } } internal static class SessionPlayerCountHelper { internal const int VanillaPlayerBaseline = 4; private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly PropertyInfo? VWorldRoomManagerProperty = typeof(VWorld).GetProperty("VRoomManager", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static int ResolveFromRoom(IVroom? room) { if (room != null) { try { return room.GetMemberCount(); } catch { } } return ResolveFromSession(); } internal static int ResolveFromSession() { if (!TryResolveExactFromSession(out var playerCount)) { return 4; } return playerCount; } internal static bool TryResolveExactFromSession(out int playerCount) { playerCount = 0; VWorld val = GameSessionAccess.TryGetVWorld(); if (val == null) { return false; } object? obj = VWorldRoomManagerProperty?.GetValue(val); VRoomManager val2 = (VRoomManager)((obj is VRoomManager) ? obj : null); if (val2 == null) { return false; } try { int playerCountInSession = val2.GetPlayerCountInSession(); if (playerCountInSession <= 0) { return false; } playerCount = playerCountInSession; return true; } catch { return false; } } internal static bool TryResolveLobbyPlayerCount(out int playerCount) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Invalid comparison between Unknown and I4 if (TryResolveExactFromSession(out playerCount)) { return true; } PersistentData val = GameSessionAccess.TryGetPdata(); if (val != null && val.SessionJoined && (int)val.ClientMode == 1) { int num = ResolveVoicePlayerCount(); playerCount = ((num > 1) ? num : 2); return true; } int num2 = ResolveVoicePlayerCount(); if (num2 > 0) { playerCount = num2; return true; } playerCount = 0; return false; } internal static bool IsMultiplayerLobby() { if (TryResolveLobbyPlayerCount(out var playerCount)) { return playerCount > 1; } return false; } private static int ResolveVoicePlayerCount() { try { VoiceManager? obj = MoreVoicesVoiceAccess.TryGetVoiceManager(); return ((obj == null) ? ((int?)null) : obj.Players?.Count).GetValueOrDefault(); } catch { return 0; } } internal static int ResolveFromSession(GameSessionInfo? info) { if (((info != null) ? info.TotalPlayerSteamIDs : null) == null || info.TotalPlayerSteamIDs.Count <= 0) { return ResolveFromSession(); } return info.TotalPlayerSteamIDs.Count; } } internal static class SpawnDataFieldScaler { internal static bool TryScaleStackCount(object spawnData, float multiplier, out int before, out int after) { before = 0; after = 0; FieldInfo field = ReflectionFieldCache.GetField(spawnData, "StackCount"); if (field == null) { return false; } before = (int)(field.GetValue(spawnData) ?? ((object)0)); after = ScalingMath.ScaleCountWithImplicitBase(before, multiplier, 1); field.SetValue(spawnData, after); return true; } internal static bool TryScaleMaxRespawnCount(object spawnData, float multiplier, out int before, out int after) { before = 0; after = 0; FieldInfo field = ReflectionFieldCache.GetField(spawnData, "MaxRespawnCount"); if (field == null) { return false; } before = (int)(field.GetValue(spawnData) ?? ((object)0)); if (before <= 0) { return false; } after = ScalingMath.ScaleCount(before, multiplier); field.SetValue(spawnData, after); return true; } } internal interface ISpawnDataRoomIndex { bool TryGetRoomForSpawnData(SpawnedActorData data, out DungeonRoom room); } internal static class SpawnDataRoomLookup { internal static bool TryFindRoomState(IEnumerable> entries, SpawnedActorData spawnData, out TState state, out DungeonRoom room) where TState : class, ISpawnDataRoomIndex { foreach (KeyValuePair entry in entries) { if (entry.Value.TryGetRoomForSpawnData(spawnData, out room)) { state = entry.Value; return true; } } state = null; room = null; return false; } } internal static class TaskWaitHelper { private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30.0); internal static void WaitSync(Task? task, string logFeature, string waitLabel, string? timeoutMessage = null) { if (task == null || task.IsCompleted) { return; } try { if (!task.Wait(Timeout) && timeoutMessage != null) { ModLog.Warn(logFeature, timeoutMessage); } } catch (Exception ex) { ModLog.Warn(logFeature, waitLabel + " wait failed: " + ex.Message); } } } internal static class VariantIdListParser { internal static List ParseOrdered(string? csv, IReadOnlyList allowedValues, string logFeature, string tokenLabel) { List list = new List(); if (string.IsNullOrWhiteSpace(csv)) { return list; } HashSet hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); string[] array = csv.Split(','); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (!string.IsNullOrEmpty(text) && !hashSet.Contains(text)) { string text2 = FindCanonicalValue(text, allowedValues); if (text2 == null) { ModLog.Warn(logFeature, "Unknown " + tokenLabel + " in list — " + text); continue; } hashSet.Add(text2); list.Add(text2); } } return list; } internal static string NormalizeCsv(string? csv, IReadOnlyList allowedValues, string logFeature, string tokenLabel) { List values = ParseOrdered(csv, allowedValues, logFeature, tokenLabel); return string.Join(",", values); } private static string? FindCanonicalValue(string token, IReadOnlyList allowedValues) { for (int i = 0; i < allowedValues.Count; i++) { if (string.Equals(allowedValues[i], token, StringComparison.OrdinalIgnoreCase)) { return allowedValues[i]; } } return null; } } } namespace MimesisPlayerEnhancement.Util.Players { internal sealed class PlayerConnectionInfo { internal long PlayerUid; internal string DisplayName = ""; internal string ConnectionRole = ""; internal ulong SteamId; internal string ConnectionAddress = ""; internal int VoiceLineCount; } internal static class PlayerPresenceEvents { internal static void OnPlayerRegistered(ulong steamId, int slotId) { if (steamId != 0L) { if (ShouldLoadRegistryOnRegister(steamId, MimesisSaveManager.IsHost(), MimesisSaveManager.IsValidSaveSlotId(slotId))) { PlayerRegistry.LoadForSlot(slotId); } StatisticsTracker.OnPlayerRegistered(steamId, slotId); } } internal static void OnPlayerUnregistered(ulong steamId) { if (ShouldHandleUnregister(steamId, ModConfig.EnableStatistics.Value)) { StatisticsTracker.OnPlayerUnregistered(steamId); } } internal static void OnArchiveStarted(SpeechEventArchive? archive, int slotId) { if (!CanStartArchivePresence((Object)(object)archive != (Object)null, ModConfig.EnableStatistics.Value, MimesisSaveManager.IsValidSaveSlotId(slotId), slotId, PlayerRegistry.LoadedSlotId)) { return; } if (!StatisticsArchiveIdentity.IsArchiveIdentityReady(archive)) { return; } ulong steamId = StatisticsArchiveIdentity.ResolveSteamIdFromArchive(archive); if (ShouldApplyArchivePresence(steamId)) { if (!string.IsNullOrEmpty(archive.PlayerId)) { PlayerRegistry.UpdateVoiceId(steamId, archive.PlayerId); } StatisticsTracker.HandleArchiveStarted(archive, slotId); } } internal static bool ShouldLoadRegistryOnRegister(ulong steamId, bool isHost, bool validSlot) { return steamId != 0 && isHost && validSlot; } internal static bool ShouldHandleUnregister(ulong steamId, bool statisticsEnabled) { return steamId != 0 && statisticsEnabled; } internal static bool CanStartArchivePresence(bool archivePresent, bool statisticsEnabled, bool validSlot, int slotId, int loadedSlotId) { if (archivePresent && statisticsEnabled && validSlot) { return slotId == loadedSlotId; } return false; } internal static bool ShouldApplyArchivePresence(ulong steamId) { return steamId != 0; } } internal sealed class PlayerRecord { internal ulong SteamId; internal string DisplayName = ""; internal string VoiceId = ""; internal bool IsOnline; internal DateTime? ConnectedSinceUtc; } internal static class PlayerRegistry { private const string Feature = "Players"; private static readonly Dictionary Records = new Dictionary(); private static int _loadedSlotId = -999; private static int _revision; internal static int LoadedSlotId => _loadedSlotId; internal static int Revision => Volatile.Read(in _revision); internal static void BumpRevision() { Interlocked.Increment(ref _revision); } internal static void LoadForSlot(int slotId, bool forceReload = false) { if (!MimesisSaveManager.IsHost() || !MimesisSaveManager.IsValidSaveSlotId(slotId)) { return; } if (!forceReload && slotId == _loadedSlotId) { StatisticsWriteQueue.Configure(slotId); return; } try { _loadedSlotId = slotId; Records.Clear(); StatisticsStore.LoadSlot(slotId); StatisticsRunTracker.SyncZoneFromGameSession(); foreach (KeyValuePair global in StatisticsHistory.Document.Globals) { if (global.Key != 0L) { PlayerRecord playerRecord = CreateRecord(global.Key); playerRecord.DisplayName = global.Value.DisplayName ?? ""; Records[global.Key] = playerRecord; } } MergeRosterFromDocument(); StatisticsWriteQueue.Configure(slotId); BumpRevision(); ModLog.Info("Players", $"Loaded player registry for save slot {slotId} ({Records.Count} players)."); } catch (Exception ex) { _loadedSlotId = -999; Records.Clear(); ModLog.Warn("Players", $"LoadForSlot({slotId}) failed — {ex.Message}"); } } internal static void ResetSessionRuntimeState() { foreach (PlayerRecord value in Records.Values) { value.VoiceId = ""; value.IsOnline = false; value.ConnectedSinceUtc = null; } _loadedSlotId = -999; BumpRevision(); } internal static PlayerRecord GetOrCreate(ulong steamId) { if (Records.TryGetValue(steamId, out PlayerRecord value)) { return value; } PlayerRecord playerRecord = CreateRecord(steamId); Records[steamId] = playerRecord; StatisticsHistory.EnsureGlobal(steamId, playerRecord.DisplayName); return playerRecord; } internal static bool TryGetRecord(ulong steamId, out PlayerRecord record) { return Records.TryGetValue(steamId, out record); } internal static bool TryGetGlobal(ulong steamId, out PlayerGlobalStats global) { if (StatisticsHistory.Document.Globals.TryGetValue(steamId, out global)) { return true; } global = null; return false; } internal static IReadOnlyCollection GetConnectedSteamIds() { List list = new List(); foreach (PlayerRecord value in Records.Values) { if (value.IsOnline) { list.Add(value.SteamId); } } return list; } internal static bool HasAnyConnected() { foreach (PlayerRecord value in Records.Values) { if (value.IsOnline) { return true; } } return false; } internal static void ForEachConnected(Action action) { foreach (PlayerRecord value in Records.Values) { if (value.IsOnline) { action(value.SteamId); } } } internal static IReadOnlyList GetAllGlobals() { return new <>z__ReadOnlyArray(StatisticsHistory.Document.Globals.Values.ToArray()); } internal static bool UpdateDisplayName(ulong steamId, string displayName) { if (steamId == 0L || !SaveSlotDocumentStore.IsUsableName(displayName, steamId)) { return false; } PlayerRecord orCreate = GetOrCreate(steamId); bool flag = orCreate.DisplayName != displayName; orCreate.DisplayName = displayName; StatisticsHistory.EnsureGlobal(steamId, displayName).DisplayName = displayName; if (flag) { BumpRevision(); } return flag; } internal static string ApplyResolvedDisplayName(ulong steamId, string fallback) { if (steamId == 0L) { if (!string.IsNullOrWhiteSpace(fallback)) { return fallback; } return steamId.ToString(); } string text = StatisticsDisplayNameResolver.Resolve(steamId, fallback); SyncDisplayNameFields(steamId, text); return text; } internal static bool UpdateVoiceId(ulong steamId, string voiceId) { if (steamId == 0L || string.IsNullOrWhiteSpace(voiceId)) { return false; } PlayerRecord orCreate = GetOrCreate(steamId); if (orCreate.VoiceId == voiceId) { return false; } orCreate.VoiceId = voiceId; BumpRevision(); return true; } internal static bool TryGetVoiceId(ulong steamId, out string voiceId) { if (Records.TryGetValue(steamId, out PlayerRecord value) && !string.IsNullOrEmpty(value.VoiceId)) { voiceId = value.VoiceId; return true; } voiceId = ""; return false; } internal static IReadOnlyDictionary GetVoiceMappings() { Dictionary dictionary = new Dictionary(); foreach (KeyValuePair record in Records) { if (!string.IsNullOrEmpty(record.Value.VoiceId)) { dictionary[record.Key] = record.Value.VoiceId; } } return dictionary; } internal static void MarkDisconnected(ulong steamId) { if (steamId != 0L && Records.TryGetValue(steamId, out PlayerRecord value)) { bool num = value.IsOnline || value.ConnectedSinceUtc.HasValue; value.IsOnline = false; value.ConnectedSinceUtc = null; if (num) { BumpRevision(); } } } internal static bool IsConnected(ulong steamId) { if (Records.TryGetValue(steamId, out PlayerRecord value)) { return value.IsOnline; } return false; } internal static bool TryGetConnectedSince(ulong steamId, out DateTime since) { if (Records.TryGetValue(steamId, out PlayerRecord value) && value.IsOnline && value.ConnectedSinceUtc.HasValue) { since = value.ConnectedSinceUtc.Value; return true; } since = default(DateTime); return false; } internal static void SetConnectedSince(ulong steamId, DateTime since) { if (steamId != 0L) { PlayerRecord orCreate = GetOrCreate(steamId); bool num = !orCreate.IsOnline || orCreate.ConnectedSinceUtc != since; orCreate.ConnectedSinceUtc = since; orCreate.IsOnline = true; if (num) { BumpRevision(); } } } internal static void MergeLiveSessionRoster() { SessionManager sessionManager = WebDashboardSessionAccess.GetSessionManager(); if (sessionManager != null) { foreach (SessionContext item in WebDashboardSessionAccess.EnumerateSessionContexts(sessionManager)) { VPlayer vPlayer = WebDashboardSessionAccess.GetVPlayer(item); if (vPlayer == null) { continue; } ulong num = ((vPlayer.SteamID != 0L) ? vPlayer.SteamID : GameSessionAccess.ResolveSteamId(((VActor)vPlayer).UID, vPlayer.IsHost)); if (num != 0L) { string displayName = StatisticsDisplayNameResolver.Resolve(num, ((VActor)vPlayer).ActorName); UpdateDisplayName(num, displayName); if (SpeechEventPoolManager.TryResolveVoiceIdForSteam(num, out string voiceId) && !string.IsNullOrWhiteSpace(voiceId)) { UpdateVoiceId(num, voiceId); } } } } GameSessionInfo val = GameSessionAccess.TryGetGameSessionInfo(); if (((val != null) ? val.TotalPlayerSteamIDs : null) == null) { return; } foreach (ulong key in val.TotalPlayerSteamIDs.Keys) { if (key != 0L) { string displayName2 = StatisticsDisplayNameResolver.Resolve(key, string.Empty); UpdateDisplayName(key, displayName2); } } } internal static void SyncRosterToDocument() { if (_loadedSlotId < 0) { return; } foreach (PlayerRecord value in Records.Values) { bool num = SaveSlotDocumentStore.IsUsableName(value.DisplayName, value.SteamId); bool flag = !string.IsNullOrWhiteSpace(value.VoiceId); if (num || flag) { SaveSlotDocumentStore.UpsertPlayer(value.SteamId, value.DisplayName, value.VoiceId); } } } internal static void PersistStatistics(bool waitForCompletion) { if (_loadedSlotId >= 0) { StatisticsWriteQueue.SaveLoadedSlot(waitForCompletion); } } internal static bool RemovePlayer(ulong steamId, bool waitForCompletion = true) { if (steamId == 0L || !Records.Remove(steamId)) { return false; } StatisticsHistory.Document.Globals.Remove(steamId); BumpRevision(); StatisticsStore.SaveSlot(_loadedSlotId, waitForCompletion); SaveSlotDocumentStore.RemovePlayer(steamId); SyncRosterToDocument(); return true; } internal static bool RemoveIfNeverConnected(ulong steamId) { if (steamId == 0L || IsConnected(steamId) || !Records.Remove(steamId)) { return false; } StatisticsHistory.Document.Globals.Remove(steamId); BumpRevision(); return true; } internal static bool TryGetLoadedSlotId(out int slotId) { slotId = _loadedSlotId; return _loadedSlotId >= 0; } private static PlayerRecord CreateRecord(ulong steamId) { return new PlayerRecord { SteamId = steamId }; } private static void MergeRosterFromDocument() { SaveSlotDocumentStore.ApplyPlayerEntries(delegate(ulong steamId, SaveSlotPlayerEntry entry) { if (SaveSlotDocumentStore.IsUsableName(entry.DisplayName, steamId)) { UpdateDisplayName(steamId, entry.DisplayName); } if (!string.IsNullOrWhiteSpace(entry.VoiceId)) { UpdateVoiceId(steamId, entry.VoiceId); } }); } private static void SyncDisplayNameFields(ulong steamId, string displayName) { PlayerRecord orCreate = GetOrCreate(steamId); PlayerGlobalStats playerGlobalStats = StatisticsHistory.EnsureGlobal(steamId, displayName); bool flag = playerGlobalStats.DisplayName != displayName; playerGlobalStats.DisplayName = displayName; if (SaveSlotDocumentStore.IsUsableName(displayName, steamId)) { flag |= orCreate.DisplayName != displayName; orCreate.DisplayName = displayName; } if (flag) { BumpRevision(); } } } internal static class VoiceEventStats { internal static int GetVoiceLineCount(SpeechEventArchive? archive) { if ((Object)(object)archive == (Object)null) { return 0; } try { return archive.events?.Count ?? 0; } catch { return 0; } } internal static string ResolveDisplayName(SpeechEventArchive? archive, long playerUid, bool isLocal) { ulong num = GameSessionAccess.ResolveSteamId(playerUid, isLocal); if (num != 0L) { if (PlayerRegistry.TryGetRecord(num, out PlayerRecord record) && SaveSlotDocumentStore.IsUsableName(record.DisplayName, num)) { return record.DisplayName; } string text = StatisticsDisplayNameResolver.Resolve(num, string.Empty); if (SaveSlotDocumentStore.IsUsableName(text, num)) { return text; } } if ((Object)(object)archive != (Object)null) { try { FishNetDissonancePlayer player = archive.Player; ProtoActor val = ((player != null) ? player.ProtoActorCache : null); if ((Object)(object)val != (Object)null && !string.IsNullOrWhiteSpace(val.nickName)) { return val.nickName; } } catch { } } if (playerUid != 0L) { string text2 = ResolveNickNameFromActorMap(playerUid); if (!string.IsNullOrWhiteSpace(text2)) { return text2; } } if (isLocal) { ulong localSteamId = GameSessionAccess.GetLocalSteamId(); if (localSteamId != 0L) { string text3 = StatisticsDisplayNameResolver.Resolve(localSteamId, string.Empty); if (SaveSlotDocumentStore.IsUsableName(text3, localSteamId)) { return text3; } } } return "(pending)"; } internal static string GetVoiceId(SpeechEventArchive? archive) { if ((Object)(object)archive == (Object)null) { return "?"; } try { string playerId = archive.PlayerId; return string.IsNullOrEmpty(playerId) ? "(pending)" : playerId; } catch { return "(unavailable)"; } } internal static string DescribePlayer(SpeechEventArchive? archive) { if ((Object)(object)archive == (Object)null) { return "archive=null"; } if (!TryGetConnectionInfo(archive, out PlayerConnectionInfo info)) { return "archive=unavailable"; } return FormatLifecycleIdentity(archive, info); } internal static string FormatLifecycleIdentity(SpeechEventArchive? archive, PlayerConnectionInfo info) { return FormatFull(info, ResolveDisplayVoiceId(archive, info.SteamId)); } internal static string ResolveDisplayVoiceId(SpeechEventArchive? archive, ulong steamId = 0uL) { string voiceId = GetVoiceId(archive); switch (voiceId) { default: return voiceId; case "(pending)": case "(unavailable)": case "?": { if (steamId != 0L && PlayerRegistry.TryGetVoiceId(steamId, out string voiceId2) && !string.IsNullOrEmpty(voiceId2)) { return voiceId2; } if (archive?.events != null) { string text = ResolveDominantEventPlayerName(archive); if (!string.IsNullOrEmpty(text)) { return text; } } return voiceId; } } } private static string? ResolveDominantEventPlayerName(SpeechEventArchive archive) { try { SyncList events = archive.events; if (events == null || events.Count == 0) { return null; } Dictionary dictionary = new Dictionary(); string result = null; int num = 0; for (int i = 0; i < events.Count; i++) { SpeechEvent val = events[i]; if (val != null && !string.IsNullOrEmpty(val.PlayerName)) { dictionary.TryGetValue(val.PlayerName, out var value); value++; dictionary[val.PlayerName] = value; if (value > num) { num = value; result = val.PlayerName; } } } return result; } catch { return null; } } internal static string DescribePlayerBrief(SpeechEventArchive? archive) { if ((Object)(object)archive == (Object)null) { return "player=unknown"; } if (!TryGetConnectionInfo(archive, out PlayerConnectionInfo info)) { return "player=unavailable"; } return FormatBrief(info); } internal static bool TryCaptureArchiveIdentity(SpeechEventArchive? archive, out long playerUid, out bool isLocal, out ulong steamId) { return StatisticsArchiveIdentity.TryCaptureArchiveIdentity(archive, out playerUid, out isLocal, out steamId); } internal static string DescribeSteamPlayer(ulong steamId, long playerUid = 0L, SpeechEventArchive? archive = null) { SessionContext val = FindSessionContext(playerUid, steamId); if (playerUid == 0L && val != null) { try { playerUid = val.GetPlayerUID(); } catch { } } if (playerUid == 0L && steamId != 0L) { playerUid = ResolvePlayerUidFromSteamId(steamId); } bool isLocal = steamId != 0L && steamId == GameSessionAccess.GetLocalSteamId(); if (TryGetConnectionInfo(val, playerUid, steamId, isLocal, out PlayerConnectionInfo info)) { if ((Object)(object)archive != (Object)null) { info.VoiceLineCount = GetVoiceLineCount(archive); } return FormatFull(info, ResolveDisplayVoiceId(archive, steamId)); } return string.Format("steamId={0} uid={1}", steamId, (playerUid == 0L) ? "(pending)" : playerUid.ToString()); } private static string FormatBrief(PlayerConnectionInfo info) { string text = ((info.SteamId == 0L) ? "(pending)" : info.SteamId.ToString()); return "name=" + info.DisplayName + " steamId=" + text; } private static string FormatFull(PlayerConnectionInfo info, string voiceId) { string text = ((info.PlayerUid == 0L) ? "(pending)" : info.PlayerUid.ToString()); string text2 = ((info.SteamId == 0L) ? "(pending)" : info.SteamId.ToString()); return $"uid={text} name={info.DisplayName} role={info.ConnectionRole} steamId={text2} ip={info.ConnectionAddress} voiceId={voiceId} voiceLines={info.VoiceLineCount}"; } internal static bool TryGetConnectionInfo(SpeechEventArchive? archive, out PlayerConnectionInfo info) { return TryGetConnectionInfo(archive, null, 0uL, out info); } internal static bool TryGetConnectionInfo(SpeechEventArchive? archive, SessionContext? knownContext, ulong steamIdHint, out PlayerConnectionInfo info) { info = new PlayerConnectionInfo(); if ((Object)(object)archive == (Object)null) { return false; } long num = 0L; bool isLocal = false; try { num = archive.PlayerUID; isLocal = archive.IsLocal; } catch { } SessionContext val = knownContext; ulong num2 = steamIdHint; if (val != null) { try { if (num2 == 0L) { num2 = val.SteamID; } if (num == 0L) { num = val.GetPlayerUID(); } } catch { } } if (num2 == 0L) { num2 = ResolveSteamId(num, isLocal, val); } if (val == null) { val = FindSessionContext(num, num2); } if (val != null && num2 == 0L) { num2 = ResolveSteamId(num, isLocal, val); } if (val != null && num == 0L) { try { long playerUID = val.GetPlayerUID(); if (playerUID != 0L) { num = playerUID; } } catch { } } if (num == 0L && num2 != 0L) { num = ResolvePlayerUidFromSteamId(num2); } return TryBuildConnectionInfo(archive, num, isLocal, num2, val, out info); } internal static bool TryGetConnectionInfo(SessionContext? session, long playerUid, ulong steamId, bool isLocal, out PlayerConnectionInfo info) { return TryBuildConnectionInfo(null, playerUid, isLocal, steamId, session, out info); } private static bool TryBuildConnectionInfo(SpeechEventArchive? archive, long playerUid, bool isLocal, ulong steamIdValue, SessionContext? session, out PlayerConnectionInfo info) { info = new PlayerConnectionInfo { PlayerUid = playerUid, DisplayName = ResolveDisplayName(archive, playerUid, isLocal), ConnectionRole = (isLocal ? "host" : "client"), SteamId = steamIdValue, ConnectionAddress = ResolveConnectionAddress(isLocal, session), VoiceLineCount = GetVoiceLineCount(archive) }; return true; } private static long ResolvePlayerUidFromSteamId(ulong steamId) { if (steamId == 0L) { return 0L; } try { PersistentData val = GameSessionAccess.TryGetPdata(); if (val?.actorUIDToSteamID != null) { foreach (KeyValuePair item in val.actorUIDToSteamID) { if (item.Value == steamId) { return item.Key; } } } } catch { } SessionManager sessionManager = SessionContextAccess.GetSessionManager(); if (sessionManager == null) { return 0L; } foreach (SessionContext item2 in SessionContextAccess.EnumerateSessionContexts(sessionManager)) { try { if (item2.SteamID == steamId) { return item2.GetPlayerUID(); } } catch { } } return 0L; } private static ulong ResolveSteamId(long playerUid, bool isLocal, SessionContext? session) { if (session != null) { try { ulong steamID = session.SteamID; if (steamID != 0L) { return steamID; } } catch { } } return GameSessionAccess.ResolveSteamId(playerUid, isLocal); } private static string ResolveConnectionAddress(bool isLocal, SessionContext? session) { if (isLocal) { return "local"; } if (session == null) { return "(unavailable)"; } try { ISession session2 = session.Session; IPEndPoint iPEndPoint = ((session2 != null) ? session2.GetRemoteEndPoint() : null); if (iPEndPoint != null) { string text = iPEndPoint.Address.ToString(); return (iPEndPoint.Port > 0) ? $"{text}:{iPEndPoint.Port}" : text; } } catch { } try { if (session.IsSDRLink) { return "steam-sdr"; } } catch { } return "(unavailable)"; } private static SessionContext? FindSessionContext(long playerUid, ulong steamId) { SessionManager sessionManager = SessionContextAccess.GetSessionManager(); if (sessionManager == null) { return null; } foreach (SessionContext item in SessionContextAccess.EnumerateSessionContexts(sessionManager)) { if (MatchesSessionContext(item, playerUid, steamId)) { return item; } } return null; } private static bool MatchesSessionContext(SessionContext context, long playerUid, ulong steamId) { if (context == null) { return false; } try { if (playerUid != 0L && context.GetPlayerUID() == playerUid) { return true; } if (steamId != 0L && context.SteamID == steamId) { return true; } } catch { } return false; } private static string? ResolveNickNameFromActorMap(long playerUid) { try { PersistentData? obj = GameSessionAccess.TryGetPdata(); object obj2; if (obj == null) { obj2 = null; } else { GameMainBase main = obj.main; obj2 = ((main != null) ? main.GetProtoActorMap() : null); } Dictionary dictionary = (Dictionary)obj2; if (dictionary == null) { return null; } foreach (ProtoActor value in dictionary.Values) { if (!((Object)(object)value == (Object)null) && value.UID == playerUid) { return string.IsNullOrWhiteSpace(value.nickName) ? null : value.nickName; } } } catch { } return null; } } } namespace MimesisPlayerEnhancement.Util.Patches { internal static class SceneScopedConfigPatches { [HarmonyPatch(typeof(VRoomManager), "EnterWaitingRoom")] internal static class VRoomManagerEnterWaitingRoomScenePatch { [HarmonyPostfix] private static void Postfix() { try { SceneScopedConfigGate.TransitionToScene(SceneScopeKind.Tram); } catch (Exception ex) { ModLog.Warn("Config", "EnterWaitingRoom scene transition failed — " + ex.Message); } } } [HarmonyPatch(typeof(VRoomManager), "EnterMaintenenceRoom")] internal static class VRoomManagerEnterMaintenenceRoomScenePatch { [HarmonyPostfix] private static void Postfix() { try { SceneScopedConfigGate.TransitionToScene(SceneScopeKind.Maintenance); } catch (Exception ex) { ModLog.Warn("Config", "EnterMaintenenceRoom scene transition failed — " + ex.Message); } } } [HarmonyPatch(typeof(VRoomManager), "CreateGameRoom")] internal static class VRoomManagerCreateGameRoomScenePatch { [HarmonyPrefix] private static void Prefix() { try { SceneScopedConfigGate.TransitionToScene(SceneScopeKind.Dungeon); } catch (Exception ex) { ModLog.Warn("Config", "CreateGameRoom scene transition failed — " + ex.Message); } } } [HarmonyPatch(typeof(VRoomManager), "EnterDungeon")] internal static class VRoomManagerEnterDungeonScenePatch { [HarmonyPostfix] private static void Postfix() { try { SceneScopedConfigGate.TransitionToScene(SceneScopeKind.Dungeon); } catch (Exception ex) { ModLog.Warn("Config", "EnterDungeon scene transition failed — " + ex.Message); } } } [HarmonyPatch(typeof(VRoomManager), "EnterDeathMatchRoom")] internal static class VRoomManagerEnterDeathMatchRoomScenePatch { [HarmonyPostfix] private static void Postfix() { try { SceneScopedConfigGate.TransitionToScene(SceneScopeKind.Deathmatch); } catch (Exception ex) { ModLog.Warn("Config", "EnterDeathMatchRoom scene transition failed — " + ex.Message); } } } [HarmonyPatch(typeof(DungeonRoom), "SetDungeonState")] internal static class DungeonRoomSetDungeonStateScenePatch { [HarmonyPostfix] private static void Postfix(DungeonState state) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Invalid comparison between Unknown and I4 //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Invalid comparison between Unknown and I4 try { if ((int)state == 2 || (int)state == 3) { SceneScopedConfigGate.CommitPendingOnSceneEnd(); SceneScopedConfigGate.FlushDeferredModuleSync(FeatureModules.SyncModuleByName); SceneScopedConfigGate.InvokeDungeonRunEndCleanup(); } } catch (Exception ex) { ModLog.Warn("Config", "SetDungeonState scene commit failed — " + ex.Message); } } } [HarmonyPatch(typeof(VRoomManager), "OnDungeonFinished")] internal static class VRoomManagerOnDungeonFinishedScenePatch { [HarmonyPostfix] private static void Postfix() { try { SceneScopedConfigGate.EndScene(); } catch (Exception ex) { ModLog.Warn("Config", "OnDungeonFinished scene end failed — " + ex.Message); } } } private const string Feature = "Config"; public static void Apply(Harmony harmony) { HarmonyPatchHelper.ApplyPatchTypes(harmony, "Config", HarmonyPatchHelper.GetNestedPatchTypes(typeof(SceneScopedConfigPatches))); } } } namespace MimesisPlayerEnhancement.Ui { internal static class ModButton { private static readonly Color ButtonFallbackColor = new Color(0.22f, 0.18f, 0.1f, 1f); internal static Button Create(Transform parent, ModUiAssets assets, string label, bool expandWidth, UnityAction? onClick) { //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_00d8: Unknown result type (might be due to invalid IL or missing references) GameObject val = ModUiLayout.CreateChild("FooterButton", parent); ModUiLayout.PrepareLayoutGroupChild(val.GetComponent()); LayoutElement val2 = val.AddComponent(); val2.minHeight = assets.ButtonHeight; val2.preferredHeight = assets.ButtonHeight; val2.flexibleWidth = (expandWidth ? 1f : 0f); if (!expandWidth) { val2.preferredWidth = Mathf.Min(assets.ButtonWidth * 1.4f, 320f); } Image val3 = val.AddComponent(); ModUiFactory.ApplySprite(val3, assets.ButtonSprite, assets.ButtonImageType, ButtonFallbackColor); Button val4 = val.AddComponent