Decompiled source of MimesisPlayerEnhancement v26.6.3
MimesisPlayerEnhancement.dll
Decompiled 11 hours ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.Collections.Specialized; using System.Diagnostics; using System.Globalization; using System.IO; 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.Threading; using System.Threading.Tasks; using Bifrost.ConstEnum; using Bifrost.Cooked; using DunGen; using FishNet.Object.Synchronizing; using HarmonyLib; using MelonLoader; using MelonLoader.Logging; using MelonLoader.Pastel; using MelonLoader.Preferences; using MelonLoader.Utils; using Microsoft.CodeAnalysis; using MimesisPlayerEnhancement; using MimesisPlayerEnhancement.Features.DungeonRandomizer; using MimesisPlayerEnhancement.Features.DungeonTime; using MimesisPlayerEnhancement.Features.JoinAnytime; using MimesisPlayerEnhancement.Features.LootMultiplicator; using MimesisPlayerEnhancement.Features.MoneyMultiplier; using MimesisPlayerEnhancement.Features.MorePlayers; using MimesisPlayerEnhancement.Features.MoreVoices; using MimesisPlayerEnhancement.Features.Persistence; using MimesisPlayerEnhancement.Features.PlayerAnnouncements; using MimesisPlayerEnhancement.Features.SpawnScaling; using MimesisPlayerEnhancement.Features.SpectatorTransition; using MimesisPlayerEnhancement.Features.Statistics; using MimesisPlayerEnhancement.Features.Statistics.Models; using MimesisPlayerEnhancement.Features.WebDashboard; using MimesisPlayerEnhancement.Features.WebDashboard.Models; using MimesisPlayerEnhancement.Util; using Mimic.Actors; using Mimic.Voice; using Mimic.Voice.SpeechSystem; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using ReluProtocol; using ReluProtocol.C2S; using ReluProtocol.Enum; using ReluReplay.Data; using ReluReplay.Serializer; using Steamworks; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: MelonInfo(typeof(Mod), "MimesisPlayerEnhancement", "26.6.3", "kalle", null)] [assembly: MelonGame("ReLUGames", "MIMESIS")] [assembly: HarmonyDontPatchAll] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyVersion("0.0.0.0")] [module: RefSafetyRules(11)] 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 { public static class ModConfig { private const string MainCategoryId = "MimesisPlayerEnhancement"; private static MelonPreferences_Category _mainCategory = null; private static MelonPreferences_Category _morePlayersCategory = null; private static MelonPreferences_Category _moreVoicesCategory = null; private static MelonPreferences_Category _persistenceCategory = null; private static MelonPreferences_Category _statisticsCategory = null; private static MelonPreferences_Category _playerAnnouncementsCategory = null; private static MelonPreferences_Category _joinAnytimeCategory = null; private static MelonPreferences_Category _spawnScalingCategory = null; private static MelonPreferences_Category _lootMultiplicatorCategory = null; private static MelonPreferences_Category _moneyMultiplierCategory = null; private static MelonPreferences_Category _dungeonTimeCategory = null; private static MelonPreferences_Category _spectatorTransitionCategory = null; private static MelonPreferences_Category _dungeonRandomizerCategory = null; private static MelonPreferences_Category _webDashboardCategory = null; private static readonly List<MelonPreferences_Entry<float>> FloatEntries = new List<MelonPreferences_Entry<float>>(); 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<bool> EnableMorePlayers { get; private set; } = null; public static MelonPreferences_Entry<int> MaxPlayers { get; private set; } = null; public static MelonPreferences_Entry<bool> EnableMoreVoices { get; private set; } = null; public static MelonPreferences_Entry<int> MaxIndoorVoiceEvents { get; private set; } = null; public static MelonPreferences_Entry<int> MaxDeathMatchVoiceEvents { get; private set; } = null; public static MelonPreferences_Entry<int> MaxOutdoorVoiceEvents { get; private set; } = null; public static MelonPreferences_Entry<bool> EnablePersistence { get; private set; } = null; public static MelonPreferences_Entry<bool> EnableStatistics { get; private set; } = null; public static MelonPreferences_Entry<int> SessionReconnectGraceMinutes { get; private set; } = null; public static MelonPreferences_Entry<bool> ShowStatisticsToasts { get; private set; } = null; public static MelonPreferences_Entry<bool> ShowPlayerAnnouncements { get; private set; } = null; public static MelonPreferences_Entry<float> ModToastDurationSeconds { get; private set; } = null; public static MelonPreferences_Entry<bool> EnableJoinAnytime { get; private set; } = null; public static MelonPreferences_Entry<bool> EnableSpawnScaling { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoScaleMimicSpawnsByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry<float> MimicSpawnMultiplier { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoScaleBossSpawnsByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry<float> BossSpawnMultiplier { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoScaleJakoSpawnsByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry<float> JakoSpawnMultiplier { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoScaleSpecialSpawnsByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry<float> SpecialSpawnMultiplier { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoScaleTrapSpawnsByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry<float> TrapSpawnMultiplier { get; private set; } = null; public static MelonPreferences_Entry<float> FixedSpawnRespawnDelayMinSeconds { get; private set; } = null; public static MelonPreferences_Entry<float> FixedSpawnRespawnDelayMaxSeconds { get; private set; } = null; public static MelonPreferences_Entry<float> FixedSpawnRespawnMinPlayerDistanceMeters { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoScaleOtherSpawnsByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry<float> OtherSpawnMultiplier { get; private set; } = null; public static MelonPreferences_Entry<bool> EnableLootMultiplicator { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoScaleMapConsumableLootByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry<float> MapConsumableLootMultiplier { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoScaleMapEquipmentLootByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry<float> MapEquipmentLootMultiplier { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoScaleMapMiscellanyLootByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry<float> MapMiscellanyLootMultiplier { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoScaleDropConsumableLootByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry<float> DropConsumableLootMultiplier { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoScaleDropEquipmentLootByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry<float> DropEquipmentLootMultiplier { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoScaleDropMiscellanyLootByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry<float> DropMiscellanyLootMultiplier { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoScaleTriggerConsumableLootByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry<float> TriggerConsumableLootMultiplier { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoScaleTriggerEquipmentLootByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry<float> TriggerEquipmentLootMultiplier { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoScaleTriggerMiscellanyLootByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry<float> TriggerMiscellanyLootMultiplier { get; private set; } = null; public static MelonPreferences_Entry<bool> EnableMoneyMultiplier { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoScaleStartupMoneyByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry<float> StartupMoneyMultiplier { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoScaleRoundGoalMoneyByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry<float> RoundGoalMoneyMultiplier { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoScaleScrapSellValueByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry<float> ScrapSellValueMultiplier { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoScaleShopBuyPriceByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry<float> ShopBuyPriceMultiplier { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoScaleShopItemsByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry<float> ShopItemsMultiplier { get; private set; } = null; public static MelonPreferences_Entry<int> ShopDiscountMinPercent { get; private set; } = null; public static MelonPreferences_Entry<int> ShopDiscountMaxPercent { get; private set; } = null; public static MelonPreferences_Entry<int> ShopDiscountChancePercent { get; private set; } = null; public static MelonPreferences_Entry<bool> AutoScaleReinforcePriceByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry<float> ReinforcePriceMultiplier { get; private set; } = null; public static MelonPreferences_Entry<bool> EnableDungeonTime { get; private set; } = null; public static MelonPreferences_Entry<int> DungeonTimeBaselinePlayerCount { get; private set; } = null; public static MelonPreferences_Entry<float> ExtraShiftSecondsPerPlayerAboveBaseline { get; private set; } = null; public static MelonPreferences_Entry<bool> EnableSpectatorTransition { get; private set; } = null; public static MelonPreferences_Entry<float> DyingWaitTimeMultiplier { get; private set; } = null; public static MelonPreferences_Entry<float> DeadCameraDurationMultiplier { get; private set; } = null; public static MelonPreferences_Entry<bool> EnableDungeonRandomizer { get; private set; } = null; public static MelonPreferences_Entry<bool> RandomizeDungeonPick { get; private set; } = null; public static MelonPreferences_Entry<string> DungeonPickPoolMode { get; private set; } = null; public static MelonPreferences_Entry<string> DungeonAllowlist { get; private set; } = null; public static MelonPreferences_Entry<string> DungeonBlocklist { get; private set; } = null; public static MelonPreferences_Entry<bool> IgnoreDungeonExcludeList { get; private set; } = null; public static MelonPreferences_Entry<bool> RandomizeLayoutFlow { get; private set; } = null; public static MelonPreferences_Entry<bool> RandomizeMapVariant { get; private set; } = null; public static MelonPreferences_Entry<bool> RandomizeDungeonSeed { get; private set; } = null; public static MelonPreferences_Entry<bool> EnableWebDashboard { get; private set; } = null; public static MelonPreferences_Entry<string> WebDashboardListenAddress { get; private set; } = null; public static MelonPreferences_Entry<int> WebDashboardListenPort { get; private set; } = null; public static MelonPreferences_Entry<bool> EnableDebugLogging { get; private set; } = null; public static event Action? Changed; public static void Initialize(Instance logger) { FilePath = Path.Combine(MelonEnvironment.UserDataDirectory, "MimesisPlayerEnhancement.cfg"); _mainCategory = CreateCategory("MimesisPlayerEnhancement", "Mimesis Player Enhancement"); MainCategory = _mainCategory; _morePlayersCategory = CreateCategory("MimesisPlayerEnhancement_MorePlayers", "More Players"); _moreVoicesCategory = CreateCategory("MimesisPlayerEnhancement_MoreVoices", "More Voices"); _persistenceCategory = CreateCategory("MimesisPlayerEnhancement_Persistence", "Persistence"); _statisticsCategory = CreateCategory("MimesisPlayerEnhancement_Statistics", "Statistics"); _playerAnnouncementsCategory = CreateCategory("MimesisPlayerEnhancement_PlayerAnnouncements", "Player Announcements"); _joinAnytimeCategory = CreateCategory("MimesisPlayerEnhancement_JoinAnytime", "Join Anytime"); _spawnScalingCategory = CreateCategory("MimesisPlayerEnhancement_SpawnScaling", "Spawn Scaling"); _lootMultiplicatorCategory = CreateCategory("MimesisPlayerEnhancement_LootMultiplicator", "Loot Multiplicator"); _moneyMultiplierCategory = CreateCategory("MimesisPlayerEnhancement_MoneyMultiplier", "Money Multiplier"); _dungeonTimeCategory = CreateCategory("MimesisPlayerEnhancement_DungeonTime", "Dungeon Time"); _spectatorTransitionCategory = CreateCategory("MimesisPlayerEnhancement_SpectatorTransition", "Spectator Transition"); _dungeonRandomizerCategory = CreateCategory("MimesisPlayerEnhancement_DungeonRandomizer", "Dungeon Randomizer"); _webDashboardCategory = CreateCategory("MimesisPlayerEnhancement_WebDashboard", "Web Dashboard"); ModToastDurationSeconds = _mainCategory.CreateEntry<float>("ModToastDurationSeconds", 5f, "Mod Toast Duration (seconds)", "How long [PlayerEnhancements] toasts stay visible before fading. Vanilla join/leave toasts are unchanged (~2 seconds). Each player controls this locally.", false, false, (ValueValidator)null, (string)null); EnableDebugLogging = _mainCategory.CreateEntry<bool>("EnableDebugLogging", false, "Enable Debug Logging", "Emit verbose diagnostic lines to the MelonLoader console.", false, false, (ValueValidator)null, (string)null); EnableMorePlayers = _morePlayersCategory.CreateEntry<bool>("EnableMorePlayers", true, "Enable More Players", "Raise the multiplayer player cap above 4.", false, false, (ValueValidator)null, (string)null); MaxPlayers = _morePlayersCategory.CreateEntry<int>("MaxPlayers", 32, "Max Players", "Maximum players in a session including the host (1 = solo, 2 = host + 1 client, etc.).", false, false, (ValueValidator)null, (string)null); EnableMoreVoices = _moreVoicesCategory.CreateEntry<bool>("EnableMoreVoices", true, "Enable More Voices", "Raise per-player voice recording limits.", false, false, (ValueValidator)null, (string)null); MaxIndoorVoiceEvents = _moreVoicesCategory.CreateEntry<int>("MaxIndoorVoiceEvents", 3000, "Max Indoor Voice Events", "Maximum stored voice events per player in indoor dungeon runs (default game limit is much lower).", false, false, (ValueValidator)null, (string)null); MaxDeathMatchVoiceEvents = _moreVoicesCategory.CreateEntry<int>("MaxDeathMatchVoiceEvents", 3000, "Max Deathmatch Voice Events", "Maximum stored voice events per player in deathmatch (default game limit is much lower).", false, false, (ValueValidator)null, (string)null); MaxOutdoorVoiceEvents = _moreVoicesCategory.CreateEntry<int>("MaxOutdoorVoiceEvents", 3000, "Max Outdoor Voice Events", "Maximum stored voice events per player outdoors (default game limit is much lower).", false, false, (ValueValidator)null, (string)null); EnablePersistence = _persistenceCategory.CreateEntry<bool>("EnablePersistence", true, "Enable Voice Persistence", "Save and restore mimic voice recordings across save/load.", false, false, (ValueValidator)null, (string)null); EnableStatistics = _statisticsCategory.CreateEntry<bool>("EnableStatistics", true, "Enable Player Statistics", "Track per-session and global player statistics per save slot.", false, false, (ValueValidator)null, (string)null); SessionReconnectGraceMinutes = _statisticsCategory.CreateEntry<int>("SessionReconnectGraceMinutes", 5, "Session Reconnect Grace (minutes)", "Reuse the previous session when a player reconnects within this many minutes.", false, false, (ValueValidator)null, (string)null); ShowStatisticsToasts = _statisticsCategory.CreateEntry<bool>("ShowStatisticsToasts", true, "Show Statistics Toasts", "Show mod stats toasts in plain English (session intro for you, global stats on join/leave). Does not replace the game's own connect messages.", false, false, (ValueValidator)null, (string)null); ShowPlayerAnnouncements = _playerAnnouncementsCategory.CreateEntry<bool>("ShowPlayerAnnouncements", true, "Show Player Announcements", "Show in-game toasts for dungeon run settings, boss spawns, and your per-map stats when you die. Does not replace the game's own messages.", false, false, (ValueValidator)null, (string)null); EnableJoinAnytime = _joinAnytimeCategory.CreateEntry<bool>("EnableJoinAnytime", true, "Enable Join Anytime", "Allow players to join a session after it has already started.", false, false, (ValueValidator)null, (string)null); EnableSpawnScaling = _spawnScalingCategory.CreateEntry<bool>("EnableSpawnScaling", true, "Enable Spawn Scaling", "Scale dungeon monster spawn budgets by type. Host only.", false, false, (ValueValidator)null, (string)null); AutoScaleMimicSpawnsByPlayerCount = _spawnScalingCategory.CreateEntry<bool>("AutoScaleMimicSpawnsByPlayerCount", true, "Auto Scale Mimic Spawns By Player Count", "When enabled, multiply mimic spawn budgets by player count / 4 for sessions with more than 4 players (stacks with MimicSpawnMultiplier).", false, false, (ValueValidator)null, (string)null); MimicSpawnMultiplier = _spawnScalingCategory.CreateEntry<float>("MimicSpawnMultiplier", 1f, "Mimic Spawn Multiplier", "Mimic spawn budget multiplier (1 = vanilla, 2 = double).", false, false, (ValueValidator)null, (string)null); AutoScaleBossSpawnsByPlayerCount = _spawnScalingCategory.CreateEntry<bool>("AutoScaleBossSpawnsByPlayerCount", true, "Auto Scale Boss Spawns By Player Count", "When enabled, multiply boss spawn budgets by player count / 4 for sessions with more than 4 players (stacks with BossSpawnMultiplier).", false, false, (ValueValidator)null, (string)null); BossSpawnMultiplier = _spawnScalingCategory.CreateEntry<float>("BossSpawnMultiplier", 1f, "Boss Spawn Multiplier", "Boss spawn budget multiplier (1 = vanilla, 2 = double).", false, false, (ValueValidator)null, (string)null); AutoScaleJakoSpawnsByPlayerCount = _spawnScalingCategory.CreateEntry<bool>("AutoScaleJakoSpawnsByPlayerCount", true, "Auto Scale Jako Spawns By Player Count", "When enabled, multiply jako spawn budgets by player count / 4 for sessions with more than 4 players (stacks with JakoSpawnMultiplier).", false, false, (ValueValidator)null, (string)null); JakoSpawnMultiplier = _spawnScalingCategory.CreateEntry<float>("JakoSpawnMultiplier", 1f, "Jako Spawn Multiplier", "Jako (normal monster) spawn budget multiplier (1 = vanilla, 2 = double).", false, false, (ValueValidator)null, (string)null); AutoScaleSpecialSpawnsByPlayerCount = _spawnScalingCategory.CreateEntry<bool>("AutoScaleSpecialSpawnsByPlayerCount", true, "Auto Scale Special Spawns By Player Count", "When enabled, multiply special spawn budgets by player count / 4 for sessions with more than 4 players (stacks with SpecialSpawnMultiplier).", false, false, (ValueValidator)null, (string)null); SpecialSpawnMultiplier = _spawnScalingCategory.CreateEntry<float>("SpecialSpawnMultiplier", 1f, "Special Spawn Multiplier", "Special monster spawn budget multiplier (1 = vanilla, 2 = double).", false, false, (ValueValidator)null, (string)null); AutoScaleTrapSpawnsByPlayerCount = _spawnScalingCategory.CreateEntry<bool>("AutoScaleTrapSpawnsByPlayerCount", true, "Auto Scale Trap Spawns By Player Count", "When enabled, multiply trap spawn counts by player count / 4 for sessions with more than 4 players (stacks with TrapSpawnMultiplier).", false, false, (ValueValidator)null, (string)null); TrapSpawnMultiplier = _spawnScalingCategory.CreateEntry<float>("TrapSpawnMultiplier", 1f, "Trap Spawn Multiplier", "Trap spawn multiplier (1 = vanilla, 2 = double). Map-placed traps use unused markers first, then respawn at the same marker when gone.", false, false, (ValueValidator)null, (string)null); FixedSpawnRespawnDelayMinSeconds = _spawnScalingCategory.CreateEntry<float>("FixedSpawnRespawnDelayMinSeconds", 5f, "Fixed Spawn Respawn Delay Min Seconds", "Minimum random delay before a map-placed monster or trap respawns at the same marker when no unused markers remain.", false, false, (ValueValidator)null, (string)null); FixedSpawnRespawnDelayMaxSeconds = _spawnScalingCategory.CreateEntry<float>("FixedSpawnRespawnDelayMaxSeconds", 30f, "Fixed Spawn Respawn Delay Max Seconds", "Maximum random delay before a map-placed monster or trap respawns at the same marker when no unused markers remain.", false, false, (ValueValidator)null, (string)null); FixedSpawnRespawnMinPlayerDistanceMeters = _spawnScalingCategory.CreateEntry<float>("FixedSpawnRespawnMinPlayerDistanceMeters", 10f, "Fixed Spawn Respawn Min Player Distance Meters", "After the respawn delay, wait until no players are within this distance (meters) before spawning at the marker. Set to 0 to respawn immediately.", false, false, (ValueValidator)null, (string)null); AutoScaleOtherSpawnsByPlayerCount = _spawnScalingCategory.CreateEntry<bool>("AutoScaleOtherSpawnsByPlayerCount", true, "Auto Scale Other Spawns By Player Count", "When enabled, multiply other spawn counts by player count / 4 for sessions with more than 4 players (stacks with OtherSpawnMultiplier).", false, false, (ValueValidator)null, (string)null); OtherSpawnMultiplier = _spawnScalingCategory.CreateEntry<float>("OtherSpawnMultiplier", 1f, "Other Spawn Multiplier", "Spawn multiplier for entities that are not mimics, bosses, jakos, specials, or traps.", false, false, (ValueValidator)null, (string)null); EnableLootMultiplicator = _lootMultiplicatorCategory.CreateEntry<bool>("EnableLootMultiplicator", true, "Enable Loot Multiplicator", "Scale how much loot appears in a run. Host only. See each Map/Drop/Trigger entry below for what it affects.", false, false, (ValueValidator)null, (string)null); AutoScaleMapConsumableLootByPlayerCount = _lootMultiplicatorCategory.CreateEntry<bool>("AutoScaleMapConsumableLootByPlayerCount", true, "Auto Scale Map Consumable Loot By Player Count", "Map loot = items placed on the dungeon map (spawn markers, shelves, floors). Consumables = ammo, healing, and other used-up items. When enabled, multiply by player count / 4 above 4 players (stacks with MapConsumableLootMultiplier).", false, false, (ValueValidator)null, (string)null); MapConsumableLootMultiplier = _lootMultiplicatorCategory.CreateEntry<float>("MapConsumableLootMultiplier", 1f, "Map Consumable Loot Multiplier", "Multiplier for consumables on map spawn points: stack size and respawn count at room load. 1 = vanilla, 2 = double. Fixed map loot (specific item at a marker) may also use unused loot markers and respawn at the same spot. Random loot pools only get stack/respawn scaling.", false, false, (ValueValidator)null, (string)null); AutoScaleMapEquipmentLootByPlayerCount = _lootMultiplicatorCategory.CreateEntry<bool>("AutoScaleMapEquipmentLootByPlayerCount", true, "Auto Scale Map Equipment Loot By Player Count", "Map loot = items placed on the dungeon map. Equipment = tools, weapons, and gear you equip. When enabled, multiply by player count / 4 above 4 players (stacks with MapEquipmentLootMultiplier).", false, false, (ValueValidator)null, (string)null); MapEquipmentLootMultiplier = _lootMultiplicatorCategory.CreateEntry<float>("MapEquipmentLootMultiplier", 1f, "Map Equipment Loot Multiplier", "Multiplier for equipment on map spawn points: stack size and respawn count at room load. 1 = vanilla, 2 = double. Fixed map loot (specific item at a marker) may also use unused loot markers and respawn at the same spot. Random loot pools only get stack/respawn scaling.", false, false, (ValueValidator)null, (string)null); AutoScaleMapMiscellanyLootByPlayerCount = _lootMultiplicatorCategory.CreateEntry<bool>("AutoScaleMapMiscellanyLootByPlayerCount", true, "Auto Scale Map Miscellany Loot By Player Count", "Map loot = items placed on the dungeon map. Miscellany = other pickup items (keys, misc objects). When enabled, multiply by player count / 4 above 4 players (stacks with MapMiscellanyLootMultiplier).", false, false, (ValueValidator)null, (string)null); MapMiscellanyLootMultiplier = _lootMultiplicatorCategory.CreateEntry<float>("MapMiscellanyLootMultiplier", 1f, "Map Miscellany Loot Multiplier", "Multiplier for miscellany on map spawn points: stack size and respawn count at room load. 1 = vanilla, 2 = double. Fixed map loot (specific item at a marker) may also use unused loot markers and respawn at the same spot. Random loot pools only get stack/respawn scaling. Random pools use the dominant item type in the pool to pick a multiplier.", false, false, (ValueValidator)null, (string)null); AutoScaleDropConsumableLootByPlayerCount = _lootMultiplicatorCategory.CreateEntry<bool>("AutoScaleDropConsumableLootByPlayerCount", true, "Auto Scale Drop Consumable Loot By Player Count", "Drop loot = items from enemy death tables when killed. Consumables = ammo, healing, and other used-up items. When enabled, multiply by player count / 4 above 4 players (stacks with DropConsumableLootMultiplier).", false, false, (ValueValidator)null, (string)null); DropConsumableLootMultiplier = _lootMultiplicatorCategory.CreateEntry<float>("DropConsumableLootMultiplier", 1f, "Drop Consumable Loot Multiplier", "Multiplier for consumables in enemy death drops: duplicates extra item IDs in the drop list and scales consumable stack count on spawn. 1 = vanilla, 2 = double.", false, false, (ValueValidator)null, (string)null); AutoScaleDropEquipmentLootByPlayerCount = _lootMultiplicatorCategory.CreateEntry<bool>("AutoScaleDropEquipmentLootByPlayerCount", true, "Auto Scale Drop Equipment Loot By Player Count", "Drop loot = items from enemy death tables when killed. Equipment = tools, weapons, and gear you equip. When enabled, multiply by player count / 4 above 4 players (stacks with DropEquipmentLootMultiplier).", false, false, (ValueValidator)null, (string)null); DropEquipmentLootMultiplier = _lootMultiplicatorCategory.CreateEntry<float>("DropEquipmentLootMultiplier", 1f, "Drop Equipment Loot Multiplier", "Multiplier for equipment in enemy death drops: duplicates extra item IDs in the drop list. Stack scaling on spawn is best-effort for non-consumables. 1 = vanilla, 2 = double.", false, false, (ValueValidator)null, (string)null); AutoScaleDropMiscellanyLootByPlayerCount = _lootMultiplicatorCategory.CreateEntry<bool>("AutoScaleDropMiscellanyLootByPlayerCount", true, "Auto Scale Drop Miscellany Loot By Player Count", "Drop loot = items from enemy death tables when killed. Miscellany = other pickup items. When enabled, multiply by player count / 4 above 4 players (stacks with DropMiscellanyLootMultiplier).", false, false, (ValueValidator)null, (string)null); DropMiscellanyLootMultiplier = _lootMultiplicatorCategory.CreateEntry<float>("DropMiscellanyLootMultiplier", 1f, "Drop Miscellany Loot Multiplier", "Multiplier for miscellany in enemy death drops: duplicates extra item IDs in the drop list. Stack scaling on spawn is best-effort for non-consumables. 1 = vanilla, 2 = double.", false, false, (ValueValidator)null, (string)null); AutoScaleTriggerConsumableLootByPlayerCount = _lootMultiplicatorCategory.CreateEntry<bool>("AutoScaleTriggerConsumableLootByPlayerCount", true, "Auto Scale Trigger Consumable Loot By Player Count", "Trigger loot = items spawned by map events/trigger volumes (EventAction spawns only). Consumables = ammo, healing, and other used-up items. When enabled, multiply by player count / 4 above 4 players (stacks with TriggerConsumableLootMultiplier).", false, false, (ValueValidator)null, (string)null); TriggerConsumableLootMultiplier = _lootMultiplicatorCategory.CreateEntry<float>("TriggerConsumableLootMultiplier", 1f, "Trigger Consumable Loot Multiplier", "Multiplier for consumables from map events/triggers: scales consumable stack count when the item spawns. 1 = vanilla, 2 = double.", false, false, (ValueValidator)null, (string)null); AutoScaleTriggerEquipmentLootByPlayerCount = _lootMultiplicatorCategory.CreateEntry<bool>("AutoScaleTriggerEquipmentLootByPlayerCount", true, "Auto Scale Trigger Equipment Loot By Player Count", "Trigger loot = items spawned by map events/trigger volumes (EventAction spawns only). Equipment = tools, weapons, and gear you equip. When enabled, multiply by player count / 4 above 4 players (stacks with TriggerEquipmentLootMultiplier).", false, false, (ValueValidator)null, (string)null); TriggerEquipmentLootMultiplier = _lootMultiplicatorCategory.CreateEntry<float>("TriggerEquipmentLootMultiplier", 1f, "Trigger Equipment Loot Multiplier", "Multiplier for equipment from map events/triggers: stack scaling on spawn is best-effort for non-consumables. 1 = vanilla, 2 = double.", false, false, (ValueValidator)null, (string)null); AutoScaleTriggerMiscellanyLootByPlayerCount = _lootMultiplicatorCategory.CreateEntry<bool>("AutoScaleTriggerMiscellanyLootByPlayerCount", true, "Auto Scale Trigger Miscellany Loot By Player Count", "Trigger loot = items spawned by map events/trigger volumes (EventAction spawns only). Miscellany = other pickup items. When enabled, multiply by player count / 4 above 4 players (stacks with TriggerMiscellanyLootMultiplier).", false, false, (ValueValidator)null, (string)null); TriggerMiscellanyLootMultiplier = _lootMultiplicatorCategory.CreateEntry<float>("TriggerMiscellanyLootMultiplier", 1f, "Trigger Miscellany Loot Multiplier", "Multiplier for miscellany from map events/triggers: stack scaling on spawn is best-effort for non-consumables. 1 = vanilla, 2 = double.", false, false, (ValueValidator)null, (string)null); EnableMoneyMultiplier = _moneyMultiplierCategory.CreateEntry<bool>("EnableMoneyMultiplier", true, "Enable Money Multiplier", "Scale startup money, round goal quota, scrap/sell values, shop buy prices, shop item count, and reinforce costs. Host only.", false, false, (ValueValidator)null, (string)null); AutoScaleStartupMoneyByPlayerCount = _moneyMultiplierCategory.CreateEntry<bool>("AutoScaleStartupMoneyByPlayerCount", true, "Auto Scale Startup Money By Player Count", "When enabled, multiply startup money by player count / 4 for sessions with more than 4 players (stacks with StartupMoneyMultiplier).", false, false, (ValueValidator)null, (string)null); StartupMoneyMultiplier = _moneyMultiplierCategory.CreateEntry<float>("StartupMoneyMultiplier", 1f, "Startup Money Multiplier", "Starting maintenance-room currency on a new game or session reset (1 = vanilla, 2 = double).", false, false, (ValueValidator)null, (string)null); AutoScaleRoundGoalMoneyByPlayerCount = _moneyMultiplierCategory.CreateEntry<bool>("AutoScaleRoundGoalMoneyByPlayerCount", true, "Auto Scale Round Goal Money By Player Count", "When enabled, multiply the stage target currency (quota) by player count / 4 for sessions with more than 4 players (stacks with RoundGoalMoneyMultiplier).", false, false, (ValueValidator)null, (string)null); RoundGoalMoneyMultiplier = _moneyMultiplierCategory.CreateEntry<float>("RoundGoalMoneyMultiplier", 1f, "Round Goal Money Multiplier", "Target currency required to finish a stage (1 = vanilla, 2 = double).", false, false, (ValueValidator)null, (string)null); AutoScaleScrapSellValueByPlayerCount = _moneyMultiplierCategory.CreateEntry<bool>("AutoScaleScrapSellValueByPlayerCount", true, "Auto Scale Scrap Sell Value By Player Count", "When enabled, multiply item scrap/sell values by player count / 4 for sessions with more than 4 players (stacks with ScrapSellValueMultiplier).", false, false, (ValueValidator)null, (string)null); ScrapSellValueMultiplier = _moneyMultiplierCategory.CreateEntry<float>("ScrapSellValueMultiplier", 1f, "Scrap Sell Value Multiplier", "Currency earned when scrapping items and item value counted toward the tram quota (1 = vanilla, 2 = double).", false, false, (ValueValidator)null, (string)null); AutoScaleShopBuyPriceByPlayerCount = _moneyMultiplierCategory.CreateEntry<bool>("AutoScaleShopBuyPriceByPlayerCount", true, "Auto Scale Shop Buy Price By Player Count", "When enabled, multiply maintenance shop buy prices by player count / 4 for sessions with more than 4 players (stacks with ShopBuyPriceMultiplier).", false, false, (ValueValidator)null, (string)null); ShopBuyPriceMultiplier = _moneyMultiplierCategory.CreateEntry<float>("ShopBuyPriceMultiplier", 1f, "Shop Buy Price Multiplier", "Maintenance shop and vending-machine kiosk purchase cost multiplier (1 = vanilla, 2 = double). Applied when shop items are initialized each maintenance round.", false, false, (ValueValidator)null, (string)null); AutoScaleShopItemsByPlayerCount = _moneyMultiplierCategory.CreateEntry<bool>("AutoScaleShopItemsByPlayerCount", true, "Auto Scale Shop Items By Player Count", "When enabled, multiply maintenance shop item count by player count / 4 for sessions with more than 4 players (stacks with ShopItemsMultiplier).", false, false, (ValueValidator)null, (string)null); ShopItemsMultiplier = _moneyMultiplierCategory.CreateEntry<float>("ShopItemsMultiplier", 1f, "Shop Items Multiplier", "Number of unique items offered in the maintenance shop (1 = vanilla, 2 = double). Extra items are rolled from vending-machine shop groups on the map.", false, false, (ValueValidator)null, (string)null); ShopDiscountMinPercent = _moneyMultiplierCategory.CreateEntry<int>("ShopDiscountMinPercent", 0, "Shop Discount Min Percent", "Minimum shop discount percentage when a discount is rolled (0-100). Only used when ShopDiscountChancePercent is above 0.", false, false, (ValueValidator)null, (string)null); ShopDiscountMaxPercent = _moneyMultiplierCategory.CreateEntry<int>("ShopDiscountMaxPercent", 100, "Shop Discount Max Percent", "Maximum shop discount percentage when a discount is rolled (0-100). Must be >= ShopDiscountMinPercent.", false, false, (ValueValidator)null, (string)null); ShopDiscountChancePercent = _moneyMultiplierCategory.CreateEntry<int>("ShopDiscountChancePercent", 0, "Shop Discount Chance Percent", "Chance per shop item to receive a discount between min and max percent (0 = vanilla shop discounts, 100 = every item discounted).", false, false, (ValueValidator)null, (string)null); AutoScaleReinforcePriceByPlayerCount = _moneyMultiplierCategory.CreateEntry<bool>("AutoScaleReinforcePriceByPlayerCount", true, "Auto Scale Reinforce Price By Player Count", "When enabled, multiply item reinforcement costs by player count / 4 for sessions with more than 4 players (stacks with ReinforcePriceMultiplier).", false, false, (ValueValidator)null, (string)null); ReinforcePriceMultiplier = _moneyMultiplierCategory.CreateEntry<float>("ReinforcePriceMultiplier", 1f, "Reinforce Price Multiplier", "Maintenance item reinforcement cost multiplier (1 = vanilla, 2 = double).", false, false, (ValueValidator)null, (string)null); EnableDungeonTime = _dungeonTimeCategory.CreateEntry<bool>("EnableDungeonTime", true, "Enable Dungeon Time", "Extend dungeon shift length on the host when player count exceeds the baseline.", false, false, (ValueValidator)null, (string)null); DungeonTimeBaselinePlayerCount = _dungeonTimeCategory.CreateEntry<int>("DungeonTimeBaselinePlayerCount", 4, "Dungeon Time Baseline Player Count", "No extra shift time at or below this player count (vanilla is 4). Minimum is 1.", false, false, (ValueValidator)null, (string)null); ExtraShiftSecondsPerPlayerAboveBaseline = _dungeonTimeCategory.CreateEntry<float>("ExtraShiftSecondsPerPlayerAboveBaseline", 10f, "Extra Shift Seconds Per Player Above Baseline", "Real seconds added to the shift deadline for each player above the baseline. Minimum is 0.", false, false, (ValueValidator)null, (string)null); EnableSpectatorTransition = _spectatorTransitionCategory.CreateEntry<bool>("EnableSpectatorTransition", true, "Enable Spectator Transition", "Shorten downed time and dead-camera duration before entering spectator mode.", false, false, (ValueValidator)null, (string)null); DyingWaitTimeMultiplier = _spectatorTransitionCategory.CreateEntry<float>("DyingWaitTimeMultiplier", 1f, "Dying Wait Time Multiplier", "Scales server down/dying time before spectator (1 = vanilla, 0 = instant). Also shortens the teammate revive window. Host only.", false, false, (ValueValidator)null, (string)null); DeadCameraDurationMultiplier = _spectatorTransitionCategory.CreateEntry<float>("DeadCameraDurationMultiplier", 1f, "Dead Camera Duration Multiplier", "Scales local dead-camera transition time before spectator (1 = vanilla, 0 = instant). Applies on each machine with the mod loaded.", false, false, (ValueValidator)null, (string)null); EnableDungeonRandomizer = _dungeonRandomizerCategory.CreateEntry<bool>("EnableDungeonRandomizer", false, "Enable Dungeon Randomizer", "Randomize dungeon selection on the host: tram dungeon pick, layout flow, map variant, and procedural seed. Host only.", false, false, (ValueValidator)null, (string)null); RandomizeDungeonPick = _dungeonRandomizerCategory.CreateEntry<bool>("RandomizeDungeonPick", true, "Randomize Dungeon Pick", "Override which dungeon master ID is rolled on the tram.", false, false, (ValueValidator)null, (string)null); DungeonPickPoolMode = _dungeonRandomizerCategory.CreateEntry<string>("DungeonPickPoolMode", "WidenVanilla", "Dungeon Pick Pool Mode", "WidenVanilla = keep cycle weights but allow repeats sooner; AllActiveUniform = pick uniformly from all active dungeons (ignores cycle table).", false, false, (ValueValidator)null, (string)null); DungeonAllowlist = _dungeonRandomizerCategory.CreateEntry<string>("DungeonAllowlist", "", "Dungeon Allowlist", "Comma-separated dungeon master IDs. When non-empty, only these IDs are eligible.", false, false, (ValueValidator)null, (string)null); DungeonBlocklist = _dungeonRandomizerCategory.CreateEntry<string>("DungeonBlocklist", "", "Dungeon Blocklist", "Comma-separated dungeon master IDs to exclude from the pool.", false, false, (ValueValidator)null, (string)null); IgnoreDungeonExcludeList = _dungeonRandomizerCategory.CreateEntry<bool>("IgnoreDungeonExcludeList", true, "Ignore Dungeon Exclude List", "When using WidenVanilla, do not exclude recently played dungeons from the tram roll.", false, false, (ValueValidator)null, (string)null); RandomizeLayoutFlow = _dungeonRandomizerCategory.CreateEntry<bool>("RandomizeLayoutFlow", true, "Randomize Layout Flow", "Pick DunGen layout flows uniformly from each dungeon's candidates instead of using weighted vanilla rolls.", false, false, (ValueValidator)null, (string)null); RandomizeMapVariant = _dungeonRandomizerCategory.CreateEntry<bool>("RandomizeMapVariant", true, "Randomize Map Variant", "Pick map variants uniformly from each dungeon's MapIDs instead of vanilla selection.", false, false, (ValueValidator)null, (string)null); RandomizeDungeonSeed = _dungeonRandomizerCategory.CreateEntry<bool>("RandomizeDungeonSeed", true, "Randomize Dungeon Seed", "Replace the procedural dungeon seed with a new random value when a dungeon is chosen.", false, false, (ValueValidator)null, (string)null); EnableWebDashboard = _webDashboardCategory.CreateEntry<bool>("EnableWebDashboard", false, "Enable Web Dashboard", "Serve a local web UI for connected players and host moderation. Default bind is loopback only.", false, false, (ValueValidator)null, (string)null); WebDashboardListenAddress = _webDashboardCategory.CreateEntry<string>("WebDashboardListenAddress", "127.0.0.1", "Listen Address", "HTTP bind address. Use 127.0.0.1 for local-only access.", false, false, (ValueValidator)null, (string)null); WebDashboardListenPort = _webDashboardCategory.CreateEntry<int>("WebDashboardListenPort", 8001, "Listen Port", "TCP port for the local web dashboard.", false, false, (ValueValidator)null, (string)null); if (MaxPlayers.Value < 1) { logger.Warning("MaxPlayers must be at least 1; resetting to 1."); MaxPlayers.Value = 1; } SanitizeShopDiscountPercents(logger); OnDungeonPickPoolModeChanged(logger, DungeonPickPoolMode.Value); ((MelonEventBase<LemonAction<int, int>>)(object)MaxPlayers.OnEntryValueChanged).Subscribe((LemonAction<int, int>)delegate(int _, int value) { if (value < 1) { logger.Warning("MaxPlayers must be at least 1; resetting to 1."); MaxPlayers.Value = 1; } else { NotifyChanged(); } }, 0, false); ((MelonEventBase<LemonAction<int, int>>)(object)MaxIndoorVoiceEvents.OnEntryValueChanged).Subscribe((LemonAction<int, int>)delegate(int _, int value) { if (value < 1) { logger.Warning("MaxIndoorVoiceEvents must be at least 1; resetting to 1."); MaxIndoorVoiceEvents.Value = 1; } else { NotifyChanged(); } }, 0, false); ((MelonEventBase<LemonAction<int, int>>)(object)MaxDeathMatchVoiceEvents.OnEntryValueChanged).Subscribe((LemonAction<int, int>)delegate(int _, int value) { if (value < 1) { logger.Warning("MaxDeathMatchVoiceEvents must be at least 1; resetting to 1."); MaxDeathMatchVoiceEvents.Value = 1; } else { NotifyChanged(); } }, 0, false); ((MelonEventBase<LemonAction<int, int>>)(object)MaxOutdoorVoiceEvents.OnEntryValueChanged).Subscribe((LemonAction<int, int>)delegate(int _, int value) { if (value < 1) { logger.Warning("MaxOutdoorVoiceEvents must be at least 1; resetting to 1."); MaxOutdoorVoiceEvents.Value = 1; } else { NotifyChanged(); } }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)EnableMorePlayers.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)EnableMoreVoices.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)EnablePersistence.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<int, int>>)(object)SessionReconnectGraceMinutes.OnEntryValueChanged).Subscribe((LemonAction<int, int>)delegate(int _, int value) { if (value < 1) { logger.Warning("SessionReconnectGraceMinutes must be at least 1; resetting to 1."); SessionReconnectGraceMinutes.Value = 1; } else { NotifyChanged(); } }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)EnableStatistics.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)ShowStatisticsToasts.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)ShowPlayerAnnouncements.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)ModToastDurationSeconds.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { if (value < 1f) { logger.Warning("ModToastDurationSeconds must be at least 1; resetting to 1."); ModToastDurationSeconds.Value = 1f; } else { NotifyChanged(); } }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)EnableJoinAnytime.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)EnableSpawnScaling.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)AutoScaleMimicSpawnsByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)AutoScaleBossSpawnsByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)AutoScaleJakoSpawnsByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)AutoScaleSpecialSpawnsByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)AutoScaleTrapSpawnsByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)FixedSpawnRespawnDelayMinSeconds.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnFixedSpawnRespawnDelayChanged(logger, value, FixedSpawnRespawnDelayMinSeconds); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)FixedSpawnRespawnDelayMaxSeconds.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnFixedSpawnRespawnDelayChanged(logger, value, FixedSpawnRespawnDelayMaxSeconds); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)FixedSpawnRespawnMinPlayerDistanceMeters.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnFixedSpawnRespawnMinPlayerDistanceChanged(logger, value); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)AutoScaleOtherSpawnsByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)EnableLootMultiplicator.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)AutoScaleMapConsumableLootByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)AutoScaleMapEquipmentLootByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)AutoScaleMapMiscellanyLootByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)AutoScaleDropConsumableLootByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)AutoScaleDropEquipmentLootByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)AutoScaleDropMiscellanyLootByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)AutoScaleTriggerConsumableLootByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)AutoScaleTriggerEquipmentLootByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)AutoScaleTriggerMiscellanyLootByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)MimicSpawnMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, MimicSpawnMultiplier); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)BossSpawnMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, BossSpawnMultiplier); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)JakoSpawnMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, JakoSpawnMultiplier); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)SpecialSpawnMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, SpecialSpawnMultiplier); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)TrapSpawnMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, TrapSpawnMultiplier); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)OtherSpawnMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, OtherSpawnMultiplier); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)MapConsumableLootMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, MapConsumableLootMultiplier); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)MapEquipmentLootMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, MapEquipmentLootMultiplier); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)MapMiscellanyLootMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, MapMiscellanyLootMultiplier); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)DropConsumableLootMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, DropConsumableLootMultiplier); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)DropEquipmentLootMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, DropEquipmentLootMultiplier); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)DropMiscellanyLootMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, DropMiscellanyLootMultiplier); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)TriggerConsumableLootMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, TriggerConsumableLootMultiplier); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)TriggerEquipmentLootMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, TriggerEquipmentLootMultiplier); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)TriggerMiscellanyLootMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, TriggerMiscellanyLootMultiplier); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)EnableMoneyMultiplier.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)AutoScaleStartupMoneyByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)AutoScaleRoundGoalMoneyByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)AutoScaleScrapSellValueByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)AutoScaleShopBuyPriceByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)AutoScaleShopItemsByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)AutoScaleReinforcePriceByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)StartupMoneyMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, StartupMoneyMultiplier); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)RoundGoalMoneyMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, RoundGoalMoneyMultiplier); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)ScrapSellValueMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, ScrapSellValueMultiplier); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)ShopBuyPriceMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, ShopBuyPriceMultiplier); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)ShopItemsMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, ShopItemsMultiplier); }, 0, false); ((MelonEventBase<LemonAction<int, int>>)(object)ShopDiscountMinPercent.OnEntryValueChanged).Subscribe((LemonAction<int, int>)delegate(int _, int value) { OnShopDiscountPercentChanged(logger, value, ShopDiscountMinPercent); }, 0, false); ((MelonEventBase<LemonAction<int, int>>)(object)ShopDiscountMaxPercent.OnEntryValueChanged).Subscribe((LemonAction<int, int>)delegate(int _, int value) { OnShopDiscountPercentChanged(logger, value, ShopDiscountMaxPercent); }, 0, false); ((MelonEventBase<LemonAction<int, int>>)(object)ShopDiscountChancePercent.OnEntryValueChanged).Subscribe((LemonAction<int, int>)delegate(int _, int value) { OnShopDiscountPercentChanged(logger, value, ShopDiscountChancePercent); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)ReinforcePriceMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, ReinforcePriceMultiplier); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)EnableDungeonTime.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<int, int>>)(object)DungeonTimeBaselinePlayerCount.OnEntryValueChanged).Subscribe((LemonAction<int, int>)delegate(int _, int value) { if (value < 1) { logger.Warning("DungeonTimeBaselinePlayerCount must be at least 1; resetting to 1."); DungeonTimeBaselinePlayerCount.Value = 1; } else { NotifyChanged(); } }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)ExtraShiftSecondsPerPlayerAboveBaseline.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnExtraShiftSecondsPerPlayerChanged(logger, value); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)EnableSpectatorTransition.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)DyingWaitTimeMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, DyingWaitTimeMultiplier); }, 0, false); ((MelonEventBase<LemonAction<float, float>>)(object)DeadCameraDurationMultiplier.OnEntryValueChanged).Subscribe((LemonAction<float, float>)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, DeadCameraDurationMultiplier); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)EnableDungeonRandomizer.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)RandomizeDungeonPick.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<string, string>>)(object)DungeonPickPoolMode.OnEntryValueChanged).Subscribe((LemonAction<string, string>)delegate(string _, string value) { OnDungeonPickPoolModeChanged(logger, value); }, 0, false); ((MelonEventBase<LemonAction<string, string>>)(object)DungeonAllowlist.OnEntryValueChanged).Subscribe((LemonAction<string, string>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<string, string>>)(object)DungeonBlocklist.OnEntryValueChanged).Subscribe((LemonAction<string, string>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)IgnoreDungeonExcludeList.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)RandomizeLayoutFlow.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)RandomizeMapVariant.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)RandomizeDungeonSeed.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)EnableWebDashboard.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<string, string>>)(object)WebDashboardListenAddress.OnEntryValueChanged).Subscribe((LemonAction<string, string>)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase<LemonAction<int, int>>)(object)WebDashboardListenPort.OnEntryValueChanged).Subscribe((LemonAction<int, int>)delegate(int _, int value) { if ((value < 1 || value > 65535) ? true : false) { logger.Warning("WebDashboardListenPort must be between 1 and 65535; resetting to 8001."); WebDashboardListenPort.Value = 8001; } else { NotifyChanged(); } }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)EnableDebugLogging.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate { NotifyChanged(); }, 0, false); RegisterFloatEntries(); ModConfigFloatHelper.SanitizeAll(FloatEntries); NormalizeSavedFloats(); ModConfigRegistry.Rebuild(); IsInitialized = true; } public static void SaveToFile() { ModConfigRegistry.SaveToFile(); } 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() { NotifyChanged(); } public static void NormalizeSavedFloats() { ModConfigFloatHelper.NormalizeSavedFloats(FilePath, FloatEntries); } internal static void SanitizeFloatEntries() { ModConfigFloatHelper.SanitizeAll(FloatEntries); } private static void RegisterFloatEntries() { FloatEntries.AddRange(new <>z__ReadOnlyArray<MelonPreferences_Entry<float>>(new MelonPreferences_Entry<float>[27] { MimicSpawnMultiplier, BossSpawnMultiplier, JakoSpawnMultiplier, SpecialSpawnMultiplier, TrapSpawnMultiplier, FixedSpawnRespawnDelayMinSeconds, FixedSpawnRespawnDelayMaxSeconds, FixedSpawnRespawnMinPlayerDistanceMeters, OtherSpawnMultiplier, MapConsumableLootMultiplier, MapEquipmentLootMultiplier, MapMiscellanyLootMultiplier, DropConsumableLootMultiplier, DropEquipmentLootMultiplier, DropMiscellanyLootMultiplier, TriggerConsumableLootMultiplier, TriggerEquipmentLootMultiplier, TriggerMiscellanyLootMultiplier, StartupMoneyMultiplier, RoundGoalMoneyMultiplier, ScrapSellValueMultiplier, ShopBuyPriceMultiplier, ShopItemsMultiplier, ReinforcePriceMultiplier, ExtraShiftSecondsPerPlayerAboveBaseline, DyingWaitTimeMultiplier, DeadCameraDurationMultiplier })); } private static void OnExtraShiftSecondsPerPlayerChanged(Instance logger, float value) { if (value < 0f) { logger.Warning("ExtraShiftSecondsPerPlayerAboveBaseline must be >= 0; resetting to 0."); ExtraShiftSecondsPerPlayerAboveBaseline.Value = 0f; } else { ModConfigFloatHelper.SanitizeEntry(ExtraShiftSecondsPerPlayerAboveBaseline); NotifyChanged(); } } private static void OnFixedSpawnRespawnDelayChanged(Instance logger, float value, MelonPreferences_Entry<float> entry) { if (value < 0f) { logger.Warning(((MelonPreferences_Entry)entry).Identifier + " must be >= 0; resetting to 0."); entry.Value = 0f; return; } float value2 = FixedSpawnRespawnDelayMinSeconds.Value; if (FixedSpawnRespawnDelayMaxSeconds.Value < value2) { logger.Warning("FixedSpawnRespawnDelayMaxSeconds must be >= FixedSpawnRespawnDelayMinSeconds; syncing max to min."); FixedSpawnRespawnDelayMaxSeconds.Value = value2; } ModConfigFloatHelper.SanitizeEntry(entry); NotifyChanged(); } private static void OnFixedSpawnRespawnMinPlayerDistanceChanged(Instance logger, float value) { if (value < 0f) { logger.Warning("FixedSpawnRespawnMinPlayerDistanceMeters must be >= 0; resetting to 0."); FixedSpawnRespawnMinPlayerDistanceMeters.Value = 0f; } else { ModConfigFloatHelper.SanitizeEntry(FixedSpawnRespawnMinPlayerDistanceMeters); NotifyChanged(); } } private static void OnSpawnMultiplierChanged(Instance logger, float value, MelonPreferences_Entry<float> entry) { if (value < 0f) { logger.Warning(((MelonPreferences_Entry)entry).Identifier + " must be >= 0; resetting to 0."); entry.Value = 0f; } else { ModConfigFloatHelper.SanitizeEntry(entry); NotifyChanged(); } } private static void SanitizeShopDiscountPercents(Instance logger) { OnShopDiscountPercentChanged(logger, ShopDiscountMinPercent.Value, ShopDiscountMinPercent); OnShopDiscountPercentChanged(logger, ShopDiscountMaxPercent.Value, ShopDiscountMaxPercent); OnShopDiscountPercentChanged(logger, ShopDiscountChancePercent.Value, ShopDiscountChancePercent); } private static void OnShopDiscountPercentChanged(Instance logger, int value, MelonPreferences_Entry<int> entry) { if (value < 0) { logger.Warning(((MelonPreferences_Entry)entry).Identifier + " must be >= 0; resetting to 0."); entry.Value = 0; return; } if (value > 100) { logger.Warning(((MelonPreferences_Entry)entry).Identifier + " must be <= 100; resetting to 100."); entry.Value = 100; return; } if (ShopDiscountMaxPercent.Value < ShopDiscountMinPercent.Value) { logger.Warning("ShopDiscountMaxPercent must be >= ShopDiscountMinPercent; syncing max to min."); ShopDiscountMaxPercent.Value = ShopDiscountMinPercent.Value; } NotifyChanged(); } private static void OnDungeonPickPoolModeChanged(Instance logger, string value) { if (!string.Equals(value, "WidenVanilla", StringComparison.OrdinalIgnoreCase) && !string.Equals(value, "AllActiveUniform", StringComparison.OrdinalIgnoreCase)) { logger.Warning("DungeonPickPoolMode must be WidenVanilla or AllActiveUniform; resetting to WidenVanilla."); DungeonPickPoolMode.Value = "WidenVanilla"; } else { NotifyChanged(); } } private static MelonPreferences_Category CreateCategory(string id, string displayName) { MelonPreferences_Category obj = MelonPreferences.CreateCategory(id, displayName); obj.SetFilePath(FilePath); return obj; } private static void NotifyChanged() { ModConfigRegistry.NotifyRuntimeChange(); ModConfig.Changed?.Invoke(); } } internal static class ModConfigFloatHelper { internal const int DecimalPlaces = 2; private static bool _normalizingFile; 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<float> entry) { float num = Round(entry.Value); if (!entry.Value.Equals(num)) { entry.Value = num; } } internal static void SanitizeAll(IReadOnlyList<MelonPreferences_Entry<float>> entries) { for (int i = 0; i < entries.Count; i++) { SanitizeEntry(entries[i]); } } internal static void NormalizeSavedFloats(string filePath, IReadOnlyList<MelonPreferences_Entry<float>> entries) { if (_normalizingFile || !File.Exists(filePath)) { return; } HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); for (int i = 0; i < entries.Count; i++) { hashSet.Add(((MelonPreferences_Entry)entries[i]).Identifier); } string[] array = File.ReadAllLines(filePath); bool flag = false; for (int j = 0; j < array.Length; j++) { if (TryNormalizeLine(array[j], hashSet, out string normalized) && !(normalized == array[j])) { array[j] = normalized; flag = true; } } if (!flag) { return; } _normalizingFile = true; try { File.WriteAllLines(filePath, array); } finally { _normalizingFile = false; } } private static bool TryNormalizeLine(string line, HashSet<string> floatKeys, out string normalized) { normalized = line; int num = line.IndexOf('='); if (num < 0) { return false; } string text = line.Substring(0, num).Trim(); if (!floatKeys.Contains(text)) { return false; } string text2 = line; int num2 = num + 1; string text3 = text2.Substring(num2, text2.Length - num2); string text4 = text3; string text5 = ""; int num3 = text3.IndexOf('#'); if (num3 >= 0) { text4 = text3.Substring(0, num3); text2 = text3; num2 = num3; text5 = text2.Substring(num2, text2.Length - num2); } text4 = text4.Trim(); if (!float.TryParse(text4, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { return false; } string text6 = Format(result); if (text4 == text6) { return false; } normalized = text + " = " + text6 + text5; return true; } } internal static class ModConfigRegistry { private static readonly Dictionary<string, Dictionary<string, MelonPreferences_Entry>> EntriesBySection = new Dictionary<string, Dictionary<string, MelonPreferences_Entry>>(StringComparer.OrdinalIgnoreCase); internal static int Version { get; private set; } 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 bool TryGetEntry(string sectionId, string key, out MelonPreferences_Entry? entry) { entry = null; if (!string.IsNullOrWhiteSpace(sectionId) && !string.IsNullOrWhiteSpace(key) && EntriesBySection.TryGetValue(sectionId, out Dictionary<string, MelonPreferences_Entry> 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; } 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 void SaveToFile() { if (ModConfig.IsInitialized) { ModConfig.MainCategory.SaveToFile(false); } } internal static void NotifyRuntimeChange() { Version++; } 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<string, MelonPreferences_Entry> value)) { value = new Dictionary<string, MelonPreferences_Entry>(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; } } public static class ModLog { internal static readonly ColorARGB SuccessGreen = ColorARGB.Green; internal static readonly ColorARGB FailureRed = ColorARGB.Red; 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(); 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_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) if (PassLogMsgMethod == null) { MelonLogger.Msg(stripped); return; } bool useLegacyConsoleColors = UseLegacyConsoleColors; string text = BuildMessageBody(segments, useLegacyConsoleColors); ColorARGB val = (useLegacyConsoleColors ? PickMessageColor(segments) : Neutral); PassLogMsgMethod.Invoke(null, new object[5] { val, text, Neutral, section, stripped }); } private 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(); } private static ColorARGB PickMessageColor((ColorARGB? color, string text)[] segments) { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) if (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; })) { return FailureRed; } if (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; })) { 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) { MelonLogger.Msg("[" + feature + "] " + message); } public static void Warn(string feature, string message) { MelonLogger.Warning("[" + feature + "] " + message); } public static void Error(string feature, string message) { MelonLogger.Error("[" + feature + "] " + message); } public static void Debug(string feature, string message) { if (ModConfig.EnableDebugLogging.Value) { MelonLogger.Msg("[" + feature + ":debug] " + message); } } } public sealed class PlayerConnectionInfo { public long PlayerUid; public string DisplayName = ""; public string ConnectionRole = ""; public ulong SteamId; public string ConnectionAddress = ""; public int VoiceEventCount; } public static class VoiceEventStats { private const BindingFlags InstanceMemberFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; public static int GetEventCount(SpeechEventArchive? archive) { if ((Object)(object)archive == (Object)null) { return 0; } try { return archive.events?.Count ?? 0; } catch { return 0; } } public static string ResolveDisplayName(SpeechEventArchive? archive, long playerUid, bool isLocal) { 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 text = ResolveNickNameFromActorMap(playerUid); if (!string.IsNullOrWhiteSpace(text)) { return text; } } if (isLocal) { string hostNickName = GetHostNickName(); if (!string.IsNullOrWhiteSpace(hostNickName)) { return hostNickName; } } return "(pending)"; } public 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)"; } } public static string DescribePlayer(SpeechEventArchive? archive) { if ((Object)(object)archive == (Object)null) { return "archive=null"; } if (!TryGetConnectionInfo(archive, out PlayerConnectionInfo info)) { return "archive=unavailable"; } 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} voiceEvents={info.VoiceEventCount}"; } public static bool TryGetConnectionInfo(SpeechEventArchive? archive, out PlayerConnectionInfo info) { return TryGetConnectionInfo(archive, null, 0uL, out info); } public 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); } return TryBuildConnectionInfo(archive, num, isLocal, num2, val, out info); } public 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), VoiceEventCount = GetEventCount(archive) }; return true; } public static string DescribePlayerVerbose(SpeechEventArchive? archive) { string text = DescribePlayer(archive); if ((Object)(object)archive == (Object)null) { return text; } long playerUid = 0L; try { playerUid = archive.PlayerUID; } catch { } SessionContext val = FindSessionContext(playerUid, 0uL); string text2 = "(unknown)"; try { if (val != null) { text2 = val.ServerID.ToString(); } } catch { } return text + " voiceId=" + GetVoiceId(archive) + " serverId=" + text2; } private static ulong ResolveSteamId(long playerUid, bool isLocal, SessionContext? session) { if (session != null) { try { ulong steamID = session.SteamID; if (steamID != 0L) { return steamID; } } catch { } } if (playerUid != 0L) { try { object hubMember = GetHubMember("pdata"); if ((hubMember?.GetType().GetField("actorUIDToSteamID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))?.GetValue(hubMember) is Dictionary<long, ulong> dictionary && dictionary.TryGetValue(playerUid, out var value)) { return value; } } catch { } } if (!isLocal) { return 0uL; } return GetLocalSteamId(); } private static ulong GetLocalSteamId() { try { PlatformMgr instance = MonoSingleton<PlatformMgr>.Instance; if ((Object)(object)instance == (Object)null) { return 0uL; } string text = typeof(PlatformMgr).GetField("_uniqueUserPath", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(instance) as string; if (!string.IsNullOrEmpty(text) && ulong.TryParse(text, out var result)) { return result; } } catch { } return 0uL; } 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 = GetSessionManager(); if (sessionManager == null) { return null; } try { object? obj = typeof(SessionManager).GetField("_hostSessionContext", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(sessionManager); SessionContext val = (SessionContext)((obj is SessionContext) ? obj : null); if (val != null && MatchesSessionContext(val, playerUid, steamId)) { return val; } if (typeof(SessionManager).GetField("m_Contexts", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(sessionManager) is Dictionary<long, SessionContext> dictionary) { foreach (SessionContext value in dictionary.Values) { if (MatchesSessionContext(value, playerUid, steamId)) { return value; } } } } catch { } 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 SessionManager? GetSessionManager() { try { object hubMember = GetHubMember("vworld"); if (hubMember == null) { return null; } object? obj = hubMember.GetType().GetField("_sessionManager", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(hubMember); return (SessionManager?)((obj is SessionManager) ? obj : null); } catch { return null; } } private static string? ResolveNickNameFromActorMap(long playerUid) { try { object gameMain = GetGameMain(); if (gameMain == null) { return null; } if (!(gameMain.GetType().GetMethod("GetProtoActorMap", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.Invoke(gameMain, null) is Dictionary<int, ProtoActor> dictionary)) { 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; } private static string? GetHostNickName() { try { object gameMain = GetGameMain(); if (gameMain != null && gameMain.GetType().GetMethod("GetHostActorNickName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.Invoke(gameMain, null) is string text && !string.IsNullOrWhiteSpace(text)) { return text; } object hubMember = GetHubMember("pdata"); if (hubMember == null) { return null; } if (hubMember.GetType().GetField("MyNickName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(hubMember) is string text2 && !string.IsNullOrWhiteSpace(text2)) { return text2; } } catch { } return null; } private static object? GetGameMain() { object hubMember = GetHubMember("pdata"); return hubMember?.GetType().GetField("main", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(hubMember); } private static object? GetHubMember(string name) { if ((Object)(object)Hub.s == (Object)null) { return null; } Type typeFromHandle = typeof(Hub); FieldInfo field = typeFromHandle.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { return field.GetValue(Hub.s); } PropertyInfo property = typeFromHandle.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (!(property != null) || !property.CanRead) { return null; } return property.GetValue(Hub.s); } } public sealed class Mod : MelonMod { private Harmony? _harmony; private bool _statisticsWasEnabled; private float _nextFixedRespawnProcessTime; public override void OnInitializeMelon() { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Expected O, but got Unknown ModConfig.Initialize(((MelonBase)this).LoggerInstance); ModConfig.Changed += SyncFromConfig; _harmony = new Harmony("com.mimesis.playerenhancement"); foreach (IFeatureModule item in FeatureModules.All) { item.ApplyPatches(_harmony); } _statisticsWasEnabled = ModConfig.EnableStatistics.Value; SyncFromConfig(); LogStartupSummary(); } public override void OnPreferencesSaved(string filepath) { if (IsOurConfigFile(filepath)) { ModConfig.NormalizeSavedFloats(); } } public override void OnPreferencesLoaded(string filepath) { if (IsOurConfigFile(filepath)) { ModConfig.SanitizeFloatEntries(); ModConfig.NormalizeSavedFloats(); ModConfig.NotifyFileReloaded(); } } public override void OnUpdate() { foreach (IFeatureModule item in FeatureModules.All) { if (!(item is FeatureModule { ThrottledUpdate: not false })) { item.OnUpdate(); } } if ((!ModConfig.EnableSpawnScaling.Value && !ModConfig.EnableLootMultiplicator.Value) || !(Time.time >= _nextFixedRespawnProcessTime)) { return; } _nextFixedRespawnProcessTime = Time.time + 1f; foreach (IFeatureModule item2 in FeatureModules.All) { if (item2 is FeatureModule { ThrottledUpdate: not false }) { item2.OnUpdate(); } } } public override void OnDeinitializeMelon() { StatisticsWriteQueue.FlushAllSync(); WebDashboardServer.StopOnDeinit(); 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 void SyncFromConfig() { if (!ModConfig.IsInitialized) { return; } foreach (IFeatureModule item in FeatureModules.All) { item.SyncFromConfig(); } int num = (ModConfig.EnableMorePlayers.Value ? ModConfig.MaxPlayers.Value : 4); if (_statisticsWasEnabled && !ModConfig.EnableStatistics.Value) { StatisticsTracker.ClearRuntimeState(); } _statisticsWasEnabled = ModConfig.EnableStatistics.Value; ModLog.Debug("Config", $"Synced — 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})" : "") + $", Persistence={ModConfig.EnablePersistence.Value}, " + $"Statistics={ModConfig.EnableStatistics.Value}, " + $"JoinAnytime={ModConfig.EnableJoinAnytime.Value}, " + $"SpawnScaling={ModConfig.EnableSpawnScaling.Value}, " + $"LootMultiplicator={ModConfig.EnableLootMultiplicator.Value}, " + $"MoneyMultiplier={ModConfig.EnableMoneyMultiplier.Value}, " + $"DungeonTime={ModConfig.EnableDungeonTime.Value}, " + $"SpectatorTransition={ModConfig.EnableSpectatorTransition.Value}, " + $"DungeonRandomizer={ModConfig.EnableDungeonRandomizer.Value}, " + $"WebDashboard={ModConfig.EnableWebDashboard.Value}" + (ModConfig.EnableWebDashboard.Value ? $" ({ModConfig.WebDashboardListenAddress.Value}:{ModConfig.WebDashboardListenPort.Value})" : "") + $", DebugLogging={ModConfig.EnableDebugLogging.Value}"); } private void LogStartupSummary() { ModLog.Info("Startup", "v26.6.3 loaded — " + $"MorePlayers={ModConfig.EnableMorePlayers.Value}" + (ModConfig.EnableMorePlayers.Value ? $" (session cap {ModConfig.MaxPlayers.Value})" : "") + $", MoreVoices={ModConfig.EnableMoreVoices.Value}" + (ModConfig.EnableMoreVoices.Value ? $" (indoor {ModConfig.MaxIndoorVoiceEvents.Value}, deathmatch {ModConfig.MaxDeathMatchVoiceEvents.Value}, outdoor {ModConfig.MaxOutdoorVoiceEvents.Value})" : "") + $", Persistence={ModConfig.EnablePersistence.Value}" + $", Statistics={ModConfig.EnableStatistics.Value}, " + $"JoinAnytime={ModConfig.EnableJoinAnytime.Value}, " + $"SpawnScaling={ModConfig.EnableSpawnScaling.Value}, " + $"LootMultiplicator={ModConfig.EnableLootMultiplicator.Value}, " + $"MoneyMultiplier={ModConfig.EnableMoneyMultiplier.Value}, " + $"DungeonTime={ModConfig.EnableDungeonTime.Value}, " + $"SpectatorTransition={ModConfig.EnableSpectatorTransition.Value}, " + $"DungeonRandomizer={ModConfig.EnableDungeonRandomizer.Value}, " + $"WebDashboard={ModConfig.EnableWebDashboard.Value}" + (ModConfig.EnableWebDashboard.Value ? $" ({ModConfig.WebDashboardListenAddress.Value}:{ModConfig.WebDashboardListenPort.Value})" : "") + $", DebugLogging={ModConfig.EnableDebugLogging.Value}"); } } public static class VersionInfo { public const string ModuleVersion = "26.6.3"; } } namespace MimesisPlayerEnhancement.Util { internal interface IFeatureModule { string Name { get; } void ApplyPatches(Harmony harmony); void SyncFromConfig() { } void OnUpdate() { } } internal sealed class FeatureModule : IFeatureModule { private readonly Action<Harmony> _applyPatches; private readonly Action? _syncFromConfig; private readonly Action? _onUpdate; public string Name { get; } internal bool ThrottledUpdate { get; } public FeatureModule(string name, Action<Harmony> applyPatches, Action? syncFromConfig = null, Action? onUpdate = null, bool throttledUpdate = false) { Name = name; _applyPatches = applyPatches; _syncFromConfig = syncFromConfig; _onUpdate = onUpdate; if (throttledUpdate) { ThrottledUpdate = true; } } public void ApplyPatches(Harmony harmony) { _applyPatches(harmony); } public void SyncFromConfig() { _syncFromConfig?.Invoke(); } public void OnUpdate() { _onUpdate?.Invoke(); } } internal static class FeatureModules { internal static IReadOnlyList<IFeatureModule> All { get; } = new <>z__ReadOnlyArray<IFeatureModule>(new IFeatureModule[13] { new FeatureModule("MoreVoices", MoreVoicesPatches.Apply, MoreVoicesPatches.RefreshFromConfig), new FeatureModule("Persistence", PersistencePatches.Apply, null, delegate { if (ModConfig.EnablePersistence.Value) { SpeechEventPoolManager.ProcessDeferredUpdates(); } }), new FeatureModule("Statistics", StatisticsPatches.Apply, null, delegate { if (ModConfig.EnableStatistics.Value) { StatisticsTracker.OnUpdate(); } }), new FeatureModule("PlayerAnnouncements", PlayerAnnouncementPatches.Apply), new FeatureModule("MorePlayers", MorePlayersPatches.Apply, MorePlayersPatches.RefreshFromConfig), new FeatureModule("JoinAnytime", JoinAnytimePatches.Apply), new FeatureModule("SpawnScaling", SpawnScalingPatches.Apply, null, delegate { if (ModConfig.EnableSpawnScaling.Value) { FixedSpawnCoordinator.ProcessPendingRespawns(); } }, throttledUpdate: true), new FeatureModule("LootMultiplicator", LootMultiplicatorPatches.Apply, null, delegate { if (ModConfig.EnableLootMultiplicator.Value) { FixedLootSpawnCoordinator.ProcessPendingRespawns(); } }, throttledUpdate: true), new FeatureModule("MoneyMultiplier", MoneyMultiplierPatches.Apply), new FeatureModule("DungeonTime", DungeonTimePatches.Apply), new FeatureModule("SpectatorTransition", SpectatorTransitionPatches.Apply), new FeatureModule("DungeonRandomizer", DungeonRandomizerPatches.Apply), new FeatureModule("WebDashboard", WebDashboardServer.Apply, WebDashboardServer.SyncFromConfig, WebDashboardServer.OnUpdate) }); } internal static class FixedRespawnTiming { internal const float RetryIntervalSeconds = 1f; } 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 Type? GetIVroomType() { return GetGameAssembly()?.GetType("IVroom"); } public static Type? GetGameSessionInfoType() { return GetGameAssembly()?.GetType("GameSessionInfo"); } 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); } public static int GetRoomPlayerCount(object? room) { if (room == null) { return 0; } return (ReflectionHelper.GetFieldValue(room, "_vPlayerDict") as IDictionary)?.Count ?? 0; } private static object? GetVWorld() { object hub = GetHub(); if (hub != null) { return ReflectionHelper.GetFieldValue(hub, "<VWorld>k__BackingField"); } return null; } public static object? GetHub() { return (GetGameAssembly()?.GetType("Hub"))?.GetProperty("s", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(null); } public static object? GetVRoomManager() { Type type = GetGameAssembly()?.GetType("Hub"); object hub = GetHub(); if (!(type == null) && hub != null) { return type.GetProperty("VRoomManager", BindingFlags.Instance | BindingFlags.Public)?.GetValue(hub); } return 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<T>(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); } } public static class HarmonyPatchHelper { public struct PatchApplyResult { public int Applied; public int Failed; } public static IEnumerable<Type> GetNestedPatchTypes(Type containerType) { return from t in containerType.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic) where t.GetCustomAttributes(typeof(HarmonyPatch), inherit: false).Length != 0 select t; } public static IEnumerable<Type> 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; } public static PatchApplyResult ApplyPatchTypes(Harmony harmony, string feature, IEnumerable<Type> patchTypes) { int num = 0; int num2 = 0; foreach (Type patchType in patchTypes) { try { List<MethodInfo> list = harmony.CreateClassProcessor(patchType).Patch(); if (list != null && list.Count > 0) { num += list.Count; continue; } num2++; if (patchType.GetMethod("TargetMethod", BindingFlags.Static | BindingFlags.Public | Bindin