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> FloatEntries = new List>(); public static int Version => ModConfigRegistry.Version; public static bool IsInitialized { get; private set; } public static string FilePath { get; private set; } = ""; public static MelonPreferences_Category MainCategory { get; private set; } = null; public static MelonPreferences_Entry EnableMorePlayers { get; private set; } = null; public static MelonPreferences_Entry MaxPlayers { get; private set; } = null; public static MelonPreferences_Entry EnableMoreVoices { get; private set; } = null; public static MelonPreferences_Entry MaxIndoorVoiceEvents { get; private set; } = null; public static MelonPreferences_Entry MaxDeathMatchVoiceEvents { get; private set; } = null; public static MelonPreferences_Entry MaxOutdoorVoiceEvents { get; private set; } = null; public static MelonPreferences_Entry EnablePersistence { get; private set; } = null; public static MelonPreferences_Entry EnableStatistics { get; private set; } = null; public static MelonPreferences_Entry SessionReconnectGraceMinutes { get; private set; } = null; public static MelonPreferences_Entry ShowStatisticsToasts { get; private set; } = null; public static MelonPreferences_Entry ShowPlayerAnnouncements { get; private set; } = null; public static MelonPreferences_Entry ModToastDurationSeconds { get; private set; } = null; public static MelonPreferences_Entry EnableJoinAnytime { get; private set; } = null; public static MelonPreferences_Entry EnableSpawnScaling { get; private set; } = null; public static MelonPreferences_Entry AutoScaleMimicSpawnsByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry MimicSpawnMultiplier { get; private set; } = null; public static MelonPreferences_Entry AutoScaleBossSpawnsByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry BossSpawnMultiplier { get; private set; } = null; public static MelonPreferences_Entry AutoScaleJakoSpawnsByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry JakoSpawnMultiplier { get; private set; } = null; public static MelonPreferences_Entry AutoScaleSpecialSpawnsByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry SpecialSpawnMultiplier { get; private set; } = null; public static MelonPreferences_Entry AutoScaleTrapSpawnsByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry TrapSpawnMultiplier { get; private set; } = null; public static MelonPreferences_Entry FixedSpawnRespawnDelayMinSeconds { get; private set; } = null; public static MelonPreferences_Entry FixedSpawnRespawnDelayMaxSeconds { get; private set; } = null; public static MelonPreferences_Entry FixedSpawnRespawnMinPlayerDistanceMeters { get; private set; } = null; public static MelonPreferences_Entry AutoScaleOtherSpawnsByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry OtherSpawnMultiplier { get; private set; } = null; public static MelonPreferences_Entry EnableLootMultiplicator { get; private set; } = null; public static MelonPreferences_Entry AutoScaleMapConsumableLootByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry MapConsumableLootMultiplier { get; private set; } = null; public static MelonPreferences_Entry AutoScaleMapEquipmentLootByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry MapEquipmentLootMultiplier { get; private set; } = null; public static MelonPreferences_Entry AutoScaleMapMiscellanyLootByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry MapMiscellanyLootMultiplier { get; private set; } = null; public static MelonPreferences_Entry AutoScaleDropConsumableLootByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry DropConsumableLootMultiplier { get; private set; } = null; public static MelonPreferences_Entry AutoScaleDropEquipmentLootByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry DropEquipmentLootMultiplier { get; private set; } = null; public static MelonPreferences_Entry AutoScaleDropMiscellanyLootByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry DropMiscellanyLootMultiplier { get; private set; } = null; public static MelonPreferences_Entry AutoScaleTriggerConsumableLootByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry TriggerConsumableLootMultiplier { get; private set; } = null; public static MelonPreferences_Entry AutoScaleTriggerEquipmentLootByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry TriggerEquipmentLootMultiplier { get; private set; } = null; public static MelonPreferences_Entry AutoScaleTriggerMiscellanyLootByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry TriggerMiscellanyLootMultiplier { get; private set; } = null; public static MelonPreferences_Entry EnableMoneyMultiplier { get; private set; } = null; public static MelonPreferences_Entry AutoScaleStartupMoneyByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry StartupMoneyMultiplier { get; private set; } = null; public static MelonPreferences_Entry AutoScaleRoundGoalMoneyByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry RoundGoalMoneyMultiplier { get; private set; } = null; public static MelonPreferences_Entry AutoScaleScrapSellValueByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry ScrapSellValueMultiplier { get; private set; } = null; public static MelonPreferences_Entry AutoScaleShopBuyPriceByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry ShopBuyPriceMultiplier { get; private set; } = null; public static MelonPreferences_Entry AutoScaleShopItemsByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry ShopItemsMultiplier { get; private set; } = null; public static MelonPreferences_Entry ShopDiscountMinPercent { get; private set; } = null; public static MelonPreferences_Entry ShopDiscountMaxPercent { get; private set; } = null; public static MelonPreferences_Entry ShopDiscountChancePercent { get; private set; } = null; public static MelonPreferences_Entry AutoScaleReinforcePriceByPlayerCount { get; private set; } = null; public static MelonPreferences_Entry ReinforcePriceMultiplier { get; private set; } = null; public static MelonPreferences_Entry EnableDungeonTime { get; private set; } = null; public static MelonPreferences_Entry DungeonTimeBaselinePlayerCount { get; private set; } = null; public static MelonPreferences_Entry ExtraShiftSecondsPerPlayerAboveBaseline { get; private set; } = null; public static MelonPreferences_Entry EnableSpectatorTransition { get; private set; } = null; public static MelonPreferences_Entry DyingWaitTimeMultiplier { get; private set; } = null; public static MelonPreferences_Entry DeadCameraDurationMultiplier { get; private set; } = null; public static MelonPreferences_Entry EnableDungeonRandomizer { get; private set; } = null; public static MelonPreferences_Entry RandomizeDungeonPick { get; private set; } = null; public static MelonPreferences_Entry DungeonPickPoolMode { get; private set; } = null; public static MelonPreferences_Entry DungeonAllowlist { get; private set; } = null; public static MelonPreferences_Entry DungeonBlocklist { get; private set; } = null; public static MelonPreferences_Entry IgnoreDungeonExcludeList { get; private set; } = null; public static MelonPreferences_Entry RandomizeLayoutFlow { get; private set; } = null; public static MelonPreferences_Entry RandomizeMapVariant { get; private set; } = null; public static MelonPreferences_Entry RandomizeDungeonSeed { get; private set; } = null; public static MelonPreferences_Entry EnableWebDashboard { get; private set; } = null; public static MelonPreferences_Entry WebDashboardListenAddress { get; private set; } = null; public static MelonPreferences_Entry WebDashboardListenPort { get; private set; } = null; public static MelonPreferences_Entry 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("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("EnableDebugLogging", false, "Enable Debug Logging", "Emit verbose diagnostic lines to the MelonLoader console.", false, false, (ValueValidator)null, (string)null); EnableMorePlayers = _morePlayersCategory.CreateEntry("EnableMorePlayers", true, "Enable More Players", "Raise the multiplayer player cap above 4.", false, false, (ValueValidator)null, (string)null); MaxPlayers = _morePlayersCategory.CreateEntry("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("EnableMoreVoices", true, "Enable More Voices", "Raise per-player voice recording limits.", false, false, (ValueValidator)null, (string)null); MaxIndoorVoiceEvents = _moreVoicesCategory.CreateEntry("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("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("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("EnablePersistence", true, "Enable Voice Persistence", "Save and restore mimic voice recordings across save/load.", false, false, (ValueValidator)null, (string)null); EnableStatistics = _statisticsCategory.CreateEntry("EnableStatistics", true, "Enable Player Statistics", "Track per-session and global player statistics per save slot.", false, false, (ValueValidator)null, (string)null); SessionReconnectGraceMinutes = _statisticsCategory.CreateEntry("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("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("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("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("EnableSpawnScaling", true, "Enable Spawn Scaling", "Scale dungeon monster spawn budgets by type. Host only.", false, false, (ValueValidator)null, (string)null); AutoScaleMimicSpawnsByPlayerCount = _spawnScalingCategory.CreateEntry("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("MimicSpawnMultiplier", 1f, "Mimic Spawn Multiplier", "Mimic spawn budget multiplier (1 = vanilla, 2 = double).", false, false, (ValueValidator)null, (string)null); AutoScaleBossSpawnsByPlayerCount = _spawnScalingCategory.CreateEntry("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("BossSpawnMultiplier", 1f, "Boss Spawn Multiplier", "Boss spawn budget multiplier (1 = vanilla, 2 = double).", false, false, (ValueValidator)null, (string)null); AutoScaleJakoSpawnsByPlayerCount = _spawnScalingCategory.CreateEntry("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("JakoSpawnMultiplier", 1f, "Jako Spawn Multiplier", "Jako (normal monster) spawn budget multiplier (1 = vanilla, 2 = double).", false, false, (ValueValidator)null, (string)null); AutoScaleSpecialSpawnsByPlayerCount = _spawnScalingCategory.CreateEntry("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("SpecialSpawnMultiplier", 1f, "Special Spawn Multiplier", "Special monster spawn budget multiplier (1 = vanilla, 2 = double).", false, false, (ValueValidator)null, (string)null); AutoScaleTrapSpawnsByPlayerCount = _spawnScalingCategory.CreateEntry("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("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("ReinforcePriceMultiplier", 1f, "Reinforce Price Multiplier", "Maintenance item reinforcement cost multiplier (1 = vanilla, 2 = double).", false, false, (ValueValidator)null, (string)null); EnableDungeonTime = _dungeonTimeCategory.CreateEntry("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("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("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("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("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("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("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("RandomizeDungeonPick", true, "Randomize Dungeon Pick", "Override which dungeon master ID is rolled on the tram.", false, false, (ValueValidator)null, (string)null); DungeonPickPoolMode = _dungeonRandomizerCategory.CreateEntry("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("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("DungeonBlocklist", "", "Dungeon Blocklist", "Comma-separated dungeon master IDs to exclude from the pool.", false, false, (ValueValidator)null, (string)null); IgnoreDungeonExcludeList = _dungeonRandomizerCategory.CreateEntry("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("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("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("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("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("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("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>)(object)MaxPlayers.OnEntryValueChanged).Subscribe((LemonAction)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>)(object)MaxIndoorVoiceEvents.OnEntryValueChanged).Subscribe((LemonAction)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>)(object)MaxDeathMatchVoiceEvents.OnEntryValueChanged).Subscribe((LemonAction)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>)(object)MaxOutdoorVoiceEvents.OnEntryValueChanged).Subscribe((LemonAction)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>)(object)EnableMorePlayers.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)EnableMoreVoices.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)EnablePersistence.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)SessionReconnectGraceMinutes.OnEntryValueChanged).Subscribe((LemonAction)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>)(object)EnableStatistics.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)ShowStatisticsToasts.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)ShowPlayerAnnouncements.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)ModToastDurationSeconds.OnEntryValueChanged).Subscribe((LemonAction)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>)(object)EnableJoinAnytime.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)EnableSpawnScaling.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)AutoScaleMimicSpawnsByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)AutoScaleBossSpawnsByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)AutoScaleJakoSpawnsByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)AutoScaleSpecialSpawnsByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)AutoScaleTrapSpawnsByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)FixedSpawnRespawnDelayMinSeconds.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnFixedSpawnRespawnDelayChanged(logger, value, FixedSpawnRespawnDelayMinSeconds); }, 0, false); ((MelonEventBase>)(object)FixedSpawnRespawnDelayMaxSeconds.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnFixedSpawnRespawnDelayChanged(logger, value, FixedSpawnRespawnDelayMaxSeconds); }, 0, false); ((MelonEventBase>)(object)FixedSpawnRespawnMinPlayerDistanceMeters.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnFixedSpawnRespawnMinPlayerDistanceChanged(logger, value); }, 0, false); ((MelonEventBase>)(object)AutoScaleOtherSpawnsByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)EnableLootMultiplicator.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)AutoScaleMapConsumableLootByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)AutoScaleMapEquipmentLootByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)AutoScaleMapMiscellanyLootByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)AutoScaleDropConsumableLootByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)AutoScaleDropEquipmentLootByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)AutoScaleDropMiscellanyLootByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)AutoScaleTriggerConsumableLootByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)AutoScaleTriggerEquipmentLootByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)AutoScaleTriggerMiscellanyLootByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)MimicSpawnMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, MimicSpawnMultiplier); }, 0, false); ((MelonEventBase>)(object)BossSpawnMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, BossSpawnMultiplier); }, 0, false); ((MelonEventBase>)(object)JakoSpawnMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, JakoSpawnMultiplier); }, 0, false); ((MelonEventBase>)(object)SpecialSpawnMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, SpecialSpawnMultiplier); }, 0, false); ((MelonEventBase>)(object)TrapSpawnMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, TrapSpawnMultiplier); }, 0, false); ((MelonEventBase>)(object)OtherSpawnMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, OtherSpawnMultiplier); }, 0, false); ((MelonEventBase>)(object)MapConsumableLootMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, MapConsumableLootMultiplier); }, 0, false); ((MelonEventBase>)(object)MapEquipmentLootMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, MapEquipmentLootMultiplier); }, 0, false); ((MelonEventBase>)(object)MapMiscellanyLootMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, MapMiscellanyLootMultiplier); }, 0, false); ((MelonEventBase>)(object)DropConsumableLootMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, DropConsumableLootMultiplier); }, 0, false); ((MelonEventBase>)(object)DropEquipmentLootMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, DropEquipmentLootMultiplier); }, 0, false); ((MelonEventBase>)(object)DropMiscellanyLootMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, DropMiscellanyLootMultiplier); }, 0, false); ((MelonEventBase>)(object)TriggerConsumableLootMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, TriggerConsumableLootMultiplier); }, 0, false); ((MelonEventBase>)(object)TriggerEquipmentLootMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, TriggerEquipmentLootMultiplier); }, 0, false); ((MelonEventBase>)(object)TriggerMiscellanyLootMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, TriggerMiscellanyLootMultiplier); }, 0, false); ((MelonEventBase>)(object)EnableMoneyMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)AutoScaleStartupMoneyByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)AutoScaleRoundGoalMoneyByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)AutoScaleScrapSellValueByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)AutoScaleShopBuyPriceByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)AutoScaleShopItemsByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)AutoScaleReinforcePriceByPlayerCount.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)StartupMoneyMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, StartupMoneyMultiplier); }, 0, false); ((MelonEventBase>)(object)RoundGoalMoneyMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, RoundGoalMoneyMultiplier); }, 0, false); ((MelonEventBase>)(object)ScrapSellValueMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, ScrapSellValueMultiplier); }, 0, false); ((MelonEventBase>)(object)ShopBuyPriceMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, ShopBuyPriceMultiplier); }, 0, false); ((MelonEventBase>)(object)ShopItemsMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, ShopItemsMultiplier); }, 0, false); ((MelonEventBase>)(object)ShopDiscountMinPercent.OnEntryValueChanged).Subscribe((LemonAction)delegate(int _, int value) { OnShopDiscountPercentChanged(logger, value, ShopDiscountMinPercent); }, 0, false); ((MelonEventBase>)(object)ShopDiscountMaxPercent.OnEntryValueChanged).Subscribe((LemonAction)delegate(int _, int value) { OnShopDiscountPercentChanged(logger, value, ShopDiscountMaxPercent); }, 0, false); ((MelonEventBase>)(object)ShopDiscountChancePercent.OnEntryValueChanged).Subscribe((LemonAction)delegate(int _, int value) { OnShopDiscountPercentChanged(logger, value, ShopDiscountChancePercent); }, 0, false); ((MelonEventBase>)(object)ReinforcePriceMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, ReinforcePriceMultiplier); }, 0, false); ((MelonEventBase>)(object)EnableDungeonTime.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)DungeonTimeBaselinePlayerCount.OnEntryValueChanged).Subscribe((LemonAction)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>)(object)ExtraShiftSecondsPerPlayerAboveBaseline.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnExtraShiftSecondsPerPlayerChanged(logger, value); }, 0, false); ((MelonEventBase>)(object)EnableSpectatorTransition.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)DyingWaitTimeMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, DyingWaitTimeMultiplier); }, 0, false); ((MelonEventBase>)(object)DeadCameraDurationMultiplier.OnEntryValueChanged).Subscribe((LemonAction)delegate(float _, float value) { OnSpawnMultiplierChanged(logger, value, DeadCameraDurationMultiplier); }, 0, false); ((MelonEventBase>)(object)EnableDungeonRandomizer.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)RandomizeDungeonPick.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)DungeonPickPoolMode.OnEntryValueChanged).Subscribe((LemonAction)delegate(string _, string value) { OnDungeonPickPoolModeChanged(logger, value); }, 0, false); ((MelonEventBase>)(object)DungeonAllowlist.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)DungeonBlocklist.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)IgnoreDungeonExcludeList.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)RandomizeLayoutFlow.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)RandomizeMapVariant.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)RandomizeDungeonSeed.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)EnableWebDashboard.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)WebDashboardListenAddress.OnEntryValueChanged).Subscribe((LemonAction)delegate { NotifyChanged(); }, 0, false); ((MelonEventBase>)(object)WebDashboardListenPort.OnEntryValueChanged).Subscribe((LemonAction)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>)(object)EnableDebugLogging.OnEntryValueChanged).Subscribe((LemonAction)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>(new MelonPreferences_Entry[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 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 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 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 entry) { float num = Round(entry.Value); if (!entry.Value.Equals(num)) { entry.Value = num; } } internal static void SanitizeAll(IReadOnlyList> entries) { for (int i = 0; i < entries.Count; i++) { SanitizeEntry(entries[i]); } } internal static void NormalizeSavedFloats(string filePath, IReadOnlyList> entries) { if (_normalizingFile || !File.Exists(filePath)) { return; } HashSet hashSet = new HashSet(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 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> EntriesBySection = new Dictionary>(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 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 value)) { value = new Dictionary(StringComparer.OrdinalIgnoreCase); EntriesBySection[identifier] = value; } value[entry.Identifier] = entry; } private static bool TryParseValue(Type type, string rawValue, out object? value, out string? error) { value = null; error = null; rawValue = rawValue?.Trim() ?? ""; if (type == typeof(bool)) { if (bool.TryParse(rawValue, out var result)) { value = result; return true; } if (string.Equals(rawValue, "1", StringComparison.Ordinal) || string.Equals(rawValue, "on", StringComparison.OrdinalIgnoreCase) || string.Equals(rawValue, "yes", StringComparison.OrdinalIgnoreCase)) { value = true; return true; } if (string.Equals(rawValue, "0", StringComparison.Ordinal) || string.Equals(rawValue, "off", StringComparison.OrdinalIgnoreCase) || string.Equals(rawValue, "no", StringComparison.OrdinalIgnoreCase)) { value = false; return true; } error = "Invalid boolean value."; return false; } if (type == typeof(int)) { if (int.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result2)) { value = result2; return true; } error = "Invalid integer value."; return false; } if (type == typeof(float)) { if (float.TryParse(rawValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var result3)) { value = result3; return true; } error = "Invalid number value."; return false; } if (type == typeof(string)) { value = rawValue; return true; } error = "Unsupported setting type: " + type.Name + "."; return false; } } 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 dictionary && dictionary.TryGetValue(playerUid, out var value)) { return value; } } catch { } } if (!isLocal) { return 0uL; } return GetLocalSteamId(); } private static ulong GetLocalSteamId() { try { PlatformMgr instance = MonoSingleton.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 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 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 _applyPatches; private readonly Action? _syncFromConfig; private readonly Action? _onUpdate; public string Name { get; } internal bool ThrottledUpdate { get; } public FeatureModule(string name, Action 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 All { get; } = new <>z__ReadOnlyArray(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, "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(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 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 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 patchTypes) { int num = 0; int num2 = 0; foreach (Type patchType in patchTypes) { try { List list = harmony.CreateClassProcessor(patchType).Patch(); if (list != null && list.Count > 0) { num += list.Count; continue; } num2++; if (patchType.GetMethod("TargetMethod", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) != null) { ModLog.Warn(feature, "Patch " + patchType.Name + " — TargetMethod returned no patches"); } else { ModLog.Warn(feature, "Patch class " + patchType.Name + " did not apply to any methods."); } } catch (Exception ex) { num2++; ModLog.Warn(feature, "Patch " + patchType.Name + " failed — " + ex.Message); } } return new PatchApplyResult { Applied = num, Failed = num2 }; } public static void LogPatchSummary(string feature, PatchApplyResult result) { //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) string text = $"{result.Applied} patch(es)"; string text2 = $"{result.Failed} failure(s)."; string stripped = text + ", " + text2; ModLog.PassLogSegmented(ModLog.FeatureSection(feature, "Patches Applied"), stripped, ((result.Applied > 0) ? new ColorARGB?(ModLog.SuccessGreen) : ((ColorARGB?)null), text), (null, ", "), ((result.Failed > 0) ? new ColorARGB?(ModLog.FailureRed) : ((ColorARGB?)null), text2)); } public static void LogPatchAudit(string feature, Harmony harmony, IEnumerable<(string label, MethodBase? method)> checks) { //IL_0132: Unknown result type (might be due to invalid IL or missing references) //IL_012b: Unknown result type (might be due to invalid IL or missing references) if (!ModConfig.EnableDebugLogging.Value) { return; } List patched = harmony.GetPatchedMethods().ToList(); List<(string, bool)> list = new List<(string, bool)>(); foreach (var check in checks) { string item = check.label; MethodBase item2 = check.method; string item3 = ((item2 == null) ? (item + " (type/method not found)") : item); bool item4 = item2 != null && IsPatched(patched, item2); list.Add((item3, item4)); } if (list.Count == 0) { return; } string stripped = string.Join(", ", list.Select<(string, bool), string>(((string text, bool ok) e) => e.text)); (ColorARGB?, string)[] array = new(ColorARGB?, string)[list.Count * 2 - 1]; for (int num = 0; num < list.Count; num++) { if (num > 0) { array[num * 2 - 1] = (null, ", "); } var (item5, flag) = list[num]; array[num * 2] = (flag ? ModLog.SuccessGreen : ModLog.FailureRed, item5); } ModLog.PassLogSegmented(ModLog.FeatureSection(feature, "Patch Audit"), stripped, array); } public static bool IsPatched(IReadOnlyCollection patched, MethodBase? expected) { if (expected == null) { return false; } foreach (MethodBase item in patched) { if (item.MetadataToken == expected.MetadataToken && (object)item.Module == expected.Module) { return true; } if (item.Name != expected.Name || item.DeclaringType != expected.DeclaringType) { continue; } ParameterInfo[] parameters = item.GetParameters(); ParameterInfo[] parameters2 = expected.GetParameters(); if (parameters.Length != parameters2.Length) { continue; } bool flag = true; for (int i = 0; i < parameters.Length; i++) { if (parameters[i].ParameterType != parameters2[i].ParameterType) { flag = false; break; } } if (flag) { return true; } } return false; } } internal static class HostApplyGate { internal static bool IsParticipantClient() { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Invalid comparison between Unknown and I4 PersistentData? pdata = JoinAnytimeHub.GetPdata(); if (pdata == null) { return false; } return (int)pdata.ClientMode == 1; } internal static bool ShouldApplyHostOnlyFeature(Func? isFeatureEnabled = null) { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Invalid comparison between Unknown and I4 if (isFeatureEnabled != null && !isFeatureEnabled()) { return false; } if (IsParticipantClient()) { return false; } if (JoinAnytimeHub.GetPdata() != null) { PersistentData? pdata = JoinAnytimeHub.GetPdata(); if (pdata == null || (int)pdata.ClientMode != 0) { return MimesisSaveManager.IsHost(); } } return true; } } internal static class HostStatusCache { private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private const BindingFlags StaticFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; private const int RecheckIntervalFrames = 60; private static readonly Type? NetworkManagerType = Type.GetType("FishNet.Managing.NetworkManager, FishNet.Runtime"); private static readonly PropertyInfo? NetworkManagerInstanceProp = NetworkManagerType?.GetProperty("Instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); private static readonly PropertyInfo? NetworkManagerIsServerProp = NetworkManagerType?.GetProperty("IsServer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? HubVworldField = typeof(Hub).GetField("vworld", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly PropertyInfo? HubVworldProperty = typeof(Hub).GetProperty("vworld", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static FieldInfo? _sessionManagerField; private static FieldInfo? _hostSessionContextField; private static FieldInfo? _vPlayerField; private static PropertyInfo? _vPlayerIsHostProp; private static bool? _cachedIsHost; private static int _lastCheckFrame = -60; internal static void Invalidate() { _cachedIsHost = null; _lastCheckFrame = -60; } internal static bool IsHostFast() { int frameCount = Time.frameCount; if (_cachedIsHost.HasValue && frameCount - _lastCheckFrame < 60) { return _cachedIsHost.Value; } _cachedIsHost = ComputeIsHost(); _lastCheckFrame = frameCount; return _cachedIsHost.Value; } private static bool ComputeIsHost() { if (!IsHostViaNetwork()) { return IsHostViaVWorld(); } return true; } private static bool IsHostViaNetwork() { if (NetworkManagerType == null || NetworkManagerInstanceProp == null || NetworkManagerIsServerProp == null) { return false; } try { object value = NetworkManagerInstanceProp.GetValue(null); return value != null && (bool)NetworkManagerIsServerProp.GetValue(value); } catch { return false; } } private static bool IsHostViaVWorld() { try { object hubVworld = GetHubVworld(); if (hubVworld == null) { return false; } EnsureVWorldHostChainResolved(hubVworld.GetType()); if (_sessionManagerField == null || _hostSessionContextField == null || _vPlayerField == null || _vPlayerIsHostProp == null) { return false; } object value = _sessionManagerField.GetValue(hubVworld); if (value == null) { return false; } object value2 = _hostSessionContextField.GetValue(value); if (value2 == null) { return false; } object value3 = _vPlayerField.GetValue(value2); return value3 != null && (bool)_vPlayerIsHostProp.GetValue(value3); } catch { return false; } } private static object? GetHubVworld() { if (!((Object)(object)Hub.s == (Object)null)) { if (!(HubVworldField != null)) { return HubVworldProperty?.GetValue(Hub.s); } return HubVworldField.GetValue(Hub.s); } return null; } private static void EnsureVWorldHostChainResolved(Type vworldType) { if (_sessionManagerField != null) { return; } _sessionManagerField = vworldType.GetField("_sessionManager", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (_sessionManagerField == null) { return; } _hostSessionContextField = _sessionManagerField.FieldType.GetField("_hostSessionContext", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (!(_hostSessionContextField == null)) { _vPlayerField = _hostSessionContextField.FieldType.GetField("_vPlayer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (!(_vPlayerField == null)) { _vPlayerIsHostProp = _vPlayerField.FieldType.GetProperty("IsHost", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } } } } internal static class LocalPlayerHelper { private const BindingFlags InstanceMemberFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; internal static ulong TryGetLocalSteamId() { try { PlatformMgr instance = MonoSingleton.Instance; string text = typeof(PlatformMgr).GetField("_uniqueUserPath", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(instance) as string; ulong result; return (!string.IsNullOrEmpty(text) && ulong.TryParse(text, out result)) ? result : 0; } catch { return 0uL; } } internal static bool IsLocalSteamId(ulong steamId) { if (steamId == 0L) { return false; } try { PlatformMgr instance = MonoSingleton.Instance; string text = typeof(PlatformMgr).GetField("_uniqueUserPath", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(instance) as string; ulong result; return !string.IsNullOrEmpty(text) && ulong.TryParse(text, out result) && result == steamId; } catch { return false; } } } internal static class ModJson { private sealed class FieldCamelCaseContractResolver : DefaultContractResolver { public FieldCamelCaseContractResolver() { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Expected O, but got Unknown ((DefaultContractResolver)this).NamingStrategy = (NamingStrategy)new CamelCaseNamingStrategy(); } protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) { return ((DefaultContractResolver)this).CreateProperties(type, (MemberSerialization)2); } } private static readonly JsonSerializerSettings Settings = CreateSettings(); internal static string Serialize(T value) { return JsonConvert.SerializeObject((object)value, Settings); } internal static T? Deserialize(string json) where T : class { if (string.IsNullOrWhiteSpace(json)) { return null; } try { return JsonConvert.DeserializeObject(json, Settings); } catch { return null; } } private static JsonSerializerSettings CreateSettings() { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected O, but got Unknown return new JsonSerializerSettings { ContractResolver = (IContractResolver)(object)new FieldCamelCaseContractResolver(), Formatting = (Formatting)0, DateFormatHandling = (DateFormatHandling)0, DateTimeZoneHandling = (DateTimeZoneHandling)1 }; } } internal static class ReflectionFieldCache { private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly ConcurrentDictionary<(Type Type, string Name), FieldInfo?> Fields = new ConcurrentDictionary<(Type, string), FieldInfo>(); internal static FieldInfo? GetField(Type type, string name) { return Fields.GetOrAdd((type, name), ((Type Type, string Name) key) => key.Type.GetField(key.Name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)); } internal static FieldInfo? GetField(object target, string name) { return GetField(target.GetType(), name); } } internal static class ScalingMath { internal const int VanillaPlayerBaseline = 4; internal static float GetPlayerScale(int playerCount, bool autoScaleEnabled) { if (autoScaleEnabled && playerCount > 4) { return (float)playerCount / 4f; } return 1f; } internal static int ScaleCount(int vanilla, float multiplier) { if (vanilla != 0) { if (!(multiplier <= 0f)) { return Math.Max(1, (int)Math.Round((float)vanilla * multiplier)); } return 0; } return 0; } internal static int ScaleCountWithImplicitBase(int vanilla, float multiplier, int implicitWhenZero) { return ScaleCount((vanilla > 0) ? vanilla : implicitWhenZero, multiplier); } } internal static class SessionPlayerCountHelper { internal const int VanillaPlayerBaseline = 4; private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly PropertyInfo? HubVWorldProperty = typeof(Hub).GetProperty("vworld", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly PropertyInfo? VWorldRoomManagerProperty = typeof(VWorld).GetProperty("VRoomManager", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static int ResolveFromRoom(IVroom? room) { if (room != null) { try { return room.GetMemberCount(); } catch { } } return ResolveFromSession(); } internal static int ResolveFromSession() { if ((Object)(object)Hub.s == (Object)null) { return 4; } object? obj = HubVWorldProperty?.GetValue(Hub.s); VWorld val = (VWorld)((obj is VWorld) ? obj : null); if (val == null) { return 4; } object? obj2 = VWorldRoomManagerProperty?.GetValue(val); VRoomManager val2 = (VRoomManager)((obj2 is VRoomManager) ? obj2 : null); if (val2 == null) { return 4; } try { int playerCountInSession = val2.GetPlayerCountInSession(); return (playerCountInSession > 0) ? playerCountInSession : 4; } catch { return 4; } } internal static int ResolveFromSession(GameSessionInfo? info) { if (((info != null) ? info.TotalPlayerSteamIDs : null) == null || info.TotalPlayerSteamIDs.Count <= 0) { return ResolveFromSession(); } return info.TotalPlayerSteamIDs.Count; } } } namespace MimesisPlayerEnhancement.Features.WebDashboard { internal static class WebDashboardActionQueue { private static readonly ConcurrentQueue Pending = new ConcurrentQueue(); private static WebDashboardActionResult? _lastResult; internal static void Enqueue(WebDashboardPendingAction action) { Pending.Enqueue(action); } internal static void Process() { WebDashboardPendingAction result; while (Pending.TryDequeue(out result)) { _lastResult = WebDashboardModerationService.Execute(result); } } internal static WebDashboardActionResult? ConsumeLastResult() { WebDashboardActionResult? lastResult = _lastResult; _lastResult = null; return lastResult; } } internal static class WebDashboardAvatarService { private sealed class CachedAvatar { internal readonly byte[] Bytes; internal readonly string ContentType; internal CachedAvatar(byte[] bytes, string contentType) { Bytes = bytes; ContentType = contentType; } } private static readonly Dictionary ServeCache = new Dictionary(); private static string _assetsRoot = ""; private static byte[]? _defaultAvatarBytes; private static readonly byte[] DefaultAvatarSvgBytes = Encoding.UTF8.GetBytes(""); internal static void SetAssetsRoot(string path) { _assetsRoot = path; _defaultAvatarBytes = null; } internal static void StorePng(ulong steamId, byte[] png) { if (steamId != 0L && png.Length != 0) { StoreServeCache(steamId, new CachedAvatar(png, "image/png")); } } internal static void PrewarmForPlayers(IReadOnlyList steamIds) { if (WebDashboardGameAvatarSource.SyncFromInGameMenu()) { WebDashboardSnapshotCache.MarkDirty(); } foreach (ulong steamId in steamIds) { if (steamId != 0L && !TryGetServeCache(steamId, out CachedAvatar _) && WebDashboardGameAvatarSource.TryGetPng(steamId, out byte[] png)) { StoreServeCache(steamId, new CachedAvatar(png, "image/png")); } } } internal static void Invalidate(ulong steamId) { if (steamId == 0L) { return; } lock (ServeCache) { ServeCache.Remove(steamId); } } internal static void Clear() { lock (ServeCache) { ServeCache.Clear(); } WebDashboardGameAvatarSource.Clear(); } internal static bool TryServe(HttpListenerContext context, ulong steamId) { if (TryGetServeCache(steamId, out CachedAvatar cached)) { WriteImage(context, cached.Bytes, cached.ContentType, cacheable: true); return true; } WriteImage(context, GetDefaultAvatar(), "image/svg+xml", cacheable: false); return true; } private static bool TryGetServeCache(ulong steamId, out CachedAvatar cached) { lock (ServeCache) { if (ServeCache.TryGetValue(steamId, out cached)) { return cached.Bytes.Length != 0; } } cached = null; return false; } private static void StoreServeCache(ulong steamId, CachedAvatar entry) { lock (ServeCache) { if (ServeCache.Count >= 64) { ServeCache.Clear(); } ServeCache[steamId] = entry; } } private static byte[] GetDefaultAvatar() { byte[] array = Volatile.Read(in _defaultAvatarBytes); if (array != null) { return array; } byte[] array2 = LoadDefaultAvatarFromDisk(); Interlocked.Exchange(ref _defaultAvatarBytes, array2); return array2; } private static byte[] LoadDefaultAvatarFromDisk() { try { if (!string.IsNullOrEmpty(_assetsRoot)) { string path = Path.Combine(_assetsRoot, "img", "default-avatar.svg"); if (File.Exists(path)) { return File.ReadAllBytes(path); } } } catch { } return DefaultAvatarSvgBytes; } private static void WriteImage(HttpListenerContext context, byte[] bytes, string contentType, bool cacheable) { context.Response.StatusCode = 200; context.Response.ContentType = contentType; context.Response.ContentLength64 = bytes.Length; context.Response.Headers["Cache-Control"] = (cacheable ? "public, max-age=3600" : "no-cache"); context.Response.OutputStream.Write(bytes, 0, bytes.Length); context.Response.OutputStream.Close(); } } internal static class WebDashboardConfigBridge { private static readonly string[] CategoryOrder = new string[14] { "MimesisPlayerEnhancement", "MimesisPlayerEnhancement_MorePlayers", "MimesisPlayerEnhancement_MoreVoices", "MimesisPlayerEnhancement_Persistence", "MimesisPlayerEnhancement_Statistics", "MimesisPlayerEnhancement_PlayerAnnouncements", "MimesisPlayerEnhancement_JoinAnytime", "MimesisPlayerEnhancement_SpawnScaling", "MimesisPlayerEnhancement_LootMultiplicator", "MimesisPlayerEnhancement_MoneyMultiplier", "MimesisPlayerEnhancement_DungeonTime", "MimesisPlayerEnhancement_SpectatorTransition", "MimesisPlayerEnhancement_DungeonRandomizer", "MimesisPlayerEnhancement_WebDashboard" }; internal static WebDashboardSettingsDto BuildSettings() { if (!ModConfig.IsInitialized) { return new WebDashboardSettingsDto { ConfigPath = ModConfig.FilePath, ConfigVersion = ModConfig.Version }; } Dictionary dictionary = new Dictionary(); List list = new List(); 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<>)) { continue; } object? value = propertyInfo.GetValue(null); MelonPreferences_Entry val = (MelonPreferences_Entry)((value is MelonPreferences_Entry) ? value : null); if (val != null) { MelonPreferences_Category category = val.Category; string identifier = category.Identifier; if (!dictionary.TryGetValue(identifier, out var value2)) { value2 = (dictionary[identifier] = new WebDashboardConfigSectionDto { Id = identifier, Title = (category.DisplayName ?? identifier) }); list.Add(identifier); } string type = val.GetReflectedType()?.Name ?? "Unknown"; string value3 = SafeGetString((Func)val.GetValueAsString); string defaultValue = SafeGetString((Func)val.GetDefaultValueAsString); value2.Entries.Add(new WebDashboardConfigEntryDto { Key = val.Identifier, Title = (val.DisplayName ?? val.Identifier), Description = (val.Description ?? ""), Type = type, Value = value3, DefaultValue = defaultValue, IsHidden = val.IsHidden }); } } List list2 = new List(); HashSet hashSet = new HashSet(); string[] categoryOrder = CategoryOrder; foreach (string text in categoryOrder) { if (dictionary.TryGetValue(text, out var value4)) { SortEntries(value4); list2.Add(value4); hashSet.Add(text); } } foreach (string item in list) { if (!hashSet.Contains(item)) { WebDashboardConfigSectionDto webDashboardConfigSectionDto2 = dictionary[item]; SortEntries(webDashboardConfigSectionDto2); list2.Add(webDashboardConfigSectionDto2); } } return new WebDashboardSettingsDto { ConfigPath = ModConfig.FilePath, ConfigVersion = ModConfig.Version, Sections = list2 }; } internal static WebDashboardConfigUpdateResult ApplyUpdate(string sectionId, string key, string value) { if (!ModConfig.IsInitialized) { return new WebDashboardConfigUpdateResult { Success = false, Message = "Configuration is not initialized." }; } if (!ModConfig.TrySetEntryValue(sectionId, key, value, out string error)) { return new WebDashboardConfigUpdateResult { Success = false, Message = (error ?? "Invalid value.") }; } ModConfig.SaveToFile(); if (ModConfigRegistry.TryGetEntry(sectionId, key, out MelonPreferences_Entry entry) && entry != null) { return new WebDashboardConfigUpdateResult { Success = true, Message = "Saved.", SectionId = sectionId, Key = key, Value = SafeGetString((Func)entry.GetValueAsString), Type = (entry.GetReflectedType()?.Name ?? "Unknown") }; } return new WebDashboardConfigUpdateResult { Success = true, Message = "Saved." }; } private static void SortEntries(WebDashboardConfigSectionDto section) { section.Entries.Sort((WebDashboardConfigEntryDto a, WebDashboardConfigEntryDto b) => string.Compare(a.Key, b.Key, StringComparison.Ordinal)); } private static string SafeGetString(Func getter) { try { return getter() ?? ""; } catch { return ""; } } } internal static class WebDashboardConfigUpdateQueue { private sealed class PendingUpdate { internal string SectionId = ""; internal string Key = ""; internal string Value = ""; internal ManualResetEventSlim Done; internal WebDashboardConfigUpdateResult? Result; } private const int WaitTimeoutMs = 5000; private static readonly ConcurrentQueue Pending = new ConcurrentQueue(); internal static WebDashboardConfigUpdateResult EnqueueAndWait(string sectionId, string key, string value) { PendingUpdate pendingUpdate = new PendingUpdate { SectionId = sectionId, Key = key, Value = value, Done = new ManualResetEventSlim(initialState: false) }; Pending.Enqueue(pendingUpdate); object obj; if (pendingUpdate.Done.Wait(5000)) { obj = pendingUpdate.Result; if (obj == null) { return new WebDashboardConfigUpdateResult { Success = false, Message = "Setting update did not complete." }; } } else { obj = new WebDashboardConfigUpdateResult { Success = false, Message = "Timed out waiting for the game thread." }; } return (WebDashboardConfigUpdateResult)obj; } internal static void Process() { PendingUpdate result; while (Pending.TryDequeue(out result)) { try { result.Result = WebDashboardConfigBridge.ApplyUpdate(result.SectionId, result.Key, result.Value); } catch (Exception ex) { result.Result = new WebDashboardConfigUpdateResult { Success = false, Message = ex.Message }; } finally { result.Done.Set(); } } } } internal static class WebDashboardGameAvatarSource { private const string Feature = "WebDashboard"; private const BindingFlags InstanceMemberFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly Dictionary PngCache = new Dictionary(); private static FieldInfo? _uimanField; private static PropertyInfo? _uimanProperty; private static FieldInfo? _avatarCacheField; private static FieldInfo? _playerUiElementsField; private static FieldInfo? _playerInfosField; internal static bool TryGetPng(ulong steamId, out byte[] png) { png = Array.Empty(); if (steamId == 0L) { return false; } lock (PngCache) { if (PngCache.TryGetValue(steamId, out byte[] value)) { png = value; return value.Length != 0; } } SyncFromInGameMenu(); lock (PngCache) { if (PngCache.TryGetValue(steamId, out byte[] value2)) { png = value2; return value2.Length != 0; } } Texture2D val = TryGetTexture(steamId); if ((Object)(object)val != (Object)null) { return TryStoreTexture(steamId, val, out png); } return false; } internal static bool OnAvatarLoaded(ulong steamId, Texture2D texture) { lock (PngCache) { if (PngCache.TryGetValue(steamId, out byte[] value) && value.Length != 0) { return false; } } if (!TryStoreTexture(steamId, texture, out byte[] png)) { return false; } WebDashboardAvatarService.StorePng(steamId, png); return true; } internal static bool SyncFromInGameMenu(UIPrefab_InGameMenu? menu = null) { //IL_0044: Unknown result type (might be due to invalid IL or missing references) if (menu == null) { menu = ResolveInGameMenu(createIfMissing: false); } if ((Object)(object)menu == (Object)null) { return false; } bool result = false; Dictionary avatarCache = GetAvatarCache(menu); if (avatarCache != null) { foreach (KeyValuePair item in avatarCache) { if ((Object)(object)item.Value != (Object)null && OnAvatarLoaded(item.Key.m_SteamID, item.Value)) { result = true; } } } if (TrySyncFromPlayerUi(menu)) { result = true; } return result; } internal static void Clear() { lock (PngCache) { PngCache.Clear(); } } private static Texture2D? TryGetTexture(ulong steamId) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) CSteamID val = default(CSteamID); ((CSteamID)(ref val))..ctor(steamId); PumpSteamCallbacks(); SteamFriends.RequestUserInformation(val, false); Texture2D val2 = TryGetTextureFromInGameMenu(val, createIfMissing: false); if ((Object)(object)val2 != (Object)null) { return val2; } return LoadTextureFromSteamworks(val) ?? TryGetTextureFromInGameMenu(val, createIfMissing: true); } private static Texture2D? TryGetTextureFromInGameMenu(CSteamID steamId, bool createIfMissing) { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) UIPrefab_InGameMenu val = ResolveInGameMenu(createIfMissing); if ((Object)(object)val == (Object)null) { return null; } try { return TryReadAvatarCache(val, steamId) ?? val.GetSteamAvatar(steamId); } catch (Exception ex) { ModLog.Warn("WebDashboard", $"In-game menu avatar lookup failed for {steamId.m_SteamID}: {ex.Message}"); return null; } } private static UIPrefab_InGameMenu? ResolveInGameMenu(bool createIfMissing) { try { UIManager val = ResolveUiManager(); if ((Object)(object)val == (Object)null) { return null; } if (createIfMissing && (Object)(object)val.inGameMenu == (Object)null) { val.OpenInGameMenu(); val.HideIngameMenu(); } return val.inGameMenu; } catch { return null; } } private static UIManager? ResolveUiManager() { Hub s = Hub.s; if ((Object)(object)s == (Object)null) { return null; } if ((object)_uimanProperty == null) { _uimanProperty = typeof(Hub).GetProperty("uiman", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } object? obj = _uimanProperty?.GetValue(s); UIManager val = (UIManager)((obj is UIManager) ? obj : null); if (val != null) { return val; } if ((object)_uimanField == null) { _uimanField = typeof(Hub).GetField("uiman", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? typeof(Hub).GetField("k__BackingField", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } object? obj2 = _uimanField?.GetValue(s); return (UIManager?)((obj2 is UIManager) ? obj2 : null); } private static Dictionary? GetAvatarCache(UIPrefab_InGameMenu menu) { if ((object)_avatarCacheField == null) { _avatarCacheField = typeof(UIPrefab_InGameMenu).GetField("avatarCache", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } return _avatarCacheField?.GetValue(menu) as Dictionary; } private static Texture2D? TryReadAvatarCache(UIPrefab_InGameMenu menu, CSteamID steamId) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) Dictionary avatarCache = GetAvatarCache(menu); if (avatarCache == null || !avatarCache.TryGetValue(steamId, out var value)) { return null; } return value; } private static bool TrySyncFromPlayerUi(UIPrefab_InGameMenu menu) { if ((object)_playerUiElementsField == null) { _playerUiElementsField = typeof(UIPrefab_InGameMenu).GetField("playerUIElements", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } if ((object)_playerInfosField == null) { _playerInfosField = typeof(UIPrefab_InGameMenu).GetField("playerInfos", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } if (!(_playerUiElementsField?.GetValue(menu) is IList list) || !(_playerInfosField?.GetValue(menu) is IList list2)) { return false; } int num = Math.Min(list.Count, list2.Count); bool result = false; for (int i = 0; i < num; i++) { object obj = list2[i]; object obj2 = list[i]; if (obj == null || obj2 == null) { continue; } string text = obj.GetType().GetField("steamID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(obj) as string; if (string.IsNullOrEmpty(text) || !ulong.TryParse(text, out var result2) || result2 == 0L) { continue; } object obj3 = obj2.GetType().GetField("avatarButton", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(obj2); if (obj3 != null) { object obj4 = obj3.GetType().GetProperty("image", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(obj3); object obj5 = (obj4?.GetType().GetProperty("sprite", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))?.GetValue(obj4); object? obj6 = (obj5?.GetType().GetProperty("texture", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))?.GetValue(obj5); Texture2D val = (Texture2D)((obj6 is Texture2D) ? obj6 : null); if (!((Object)(object)val == (Object)null) && OnAvatarLoaded(result2, val)) { result = true; } } } return result; } private static bool TryStoreTexture(ulong steamId, Texture2D texture, out byte[] png) { png = Array.Empty(); if (!TryEncodeToPng(texture, out png)) { return false; } lock (PngCache) { if (PngCache.Count >= 64) { PngCache.Clear(); } PngCache[steamId] = png; } return true; } private static bool TryEncodeToPng(Texture2D source, out byte[] png) { //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Expected O, but got Unknown png = Array.Empty(); try { png = ImageConversion.EncodeToPNG(source); if (png.Length != 0) { return true; } } catch (Exception ex) { ModLog.Warn("WebDashboard", "Avatar PNG encode failed: " + ex.Message); } try { Texture2D val = new Texture2D(((Texture)source).width, ((Texture)source).height, (TextureFormat)4, false); val.SetPixels(source.GetPixels()); val.Apply(); png = ImageConversion.EncodeToPNG(val); Object.Destroy((Object)(object)val); if (png.Length != 0) { return true; } } catch (Exception ex2) { ModLog.Warn("WebDashboard", "Avatar PNG readable-copy encode failed: " + ex2.Message); } return false; } private static Texture2D? LoadTextureFromSteamworks(CSteamID steamId) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) int num = SteamFriends.GetMediumFriendAvatar(steamId); if (num <= 0) { num = SteamFriends.GetLargeFriendAvatar(steamId); } if (num <= 0) { num = SteamFriends.GetSmallFriendAvatar(steamId); } if (num <= 0) { return null; } uint num2 = default(uint); uint num3 = default(uint); if (!SteamUtils.GetImageSize(num, ref num2, ref num3) || num2 == 0 || num3 == 0) { return null; } byte[] array = new byte[num2 * num3 * 4]; if (SteamUtils.GetImageRGBA(num, array, array.Length)) { return CreateFlippedTexture(array, (int)num2, (int)num3); } return null; } private static Texture2D CreateFlippedTexture(byte[] rgba, int width, int height) { //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Expected O, but got Unknown //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Expected O, but got Unknown //IL_0033: Unknown result type (might be due to invalid IL or missing references) Texture2D val = new Texture2D(width, height, (TextureFormat)4, false); val.LoadRawTextureData(rgba); val.Apply(); Texture2D val2 = new Texture2D(width, height, (TextureFormat)4, false); for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { val2.SetPixel(i, j, val.GetPixel(i, height - 1 - j)); } } val2.Apply(); return val2; } private static void PumpSteamCallbacks() { try { SteamAPI.RunCallbacks(); } catch { } } } internal static class WebDashboardGameState { private static bool _sessionSticky; private static int _cachedSaveSlotId = -1; internal static bool IsInSession() { PersistentData pdata = JoinAnytimeHub.GetPdata(); if (pdata == null) { return _sessionSticky = false; } if (!pdata.SessionJoined) { _cachedSaveSlotId = -1; return _sessionSticky = false; } GameMainBase main = pdata.main; if ((main is InTramWaitingScene || main is GamePlayScene || main is MaintenanceScene || main is DeathMatchScene) ? true : false) { return _sessionSticky = true; } return _sessionSticky; } internal static bool IsHost() { return MimesisSaveManager.IsHost(); } internal static int GetSaveSlotId() { if (MimesisSaveManager.TryGetActiveSaveSlotId(out var slotId)) { _cachedSaveSlotId = slotId; return slotId; } if (!_sessionSticky || _cachedSaveSlotId < 0) { return -1; } return _cachedSaveSlotId; } } internal static class WebDashboardJson { private sealed class SnapshotEventDto { public WebDashboardStatusDto Status = new WebDashboardStatusDto(); public List Players = new List(); public LeaderboardApiResponse? Leaderboard; public MinimapApiResponse? Minimap; } private sealed class PlayersApiResponse { public List Players = new List(); } private sealed class PlayerApiDto { public string SteamId = ""; public long PlayerUid; public string DisplayName = ""; public bool IsHost; public bool IsLocal; public bool IsBanned; public int NetworkGrade = -1; public string ConnectionRole = ""; public string ConnectionAddress = ""; public int VoiceEventCount; public SessionStatsApiDto? CurrentSession; } private sealed class SessionStatsApiDto { public long CurrencyEarned; public long Kills; public long Deaths; public long Revives; public long MimicEncounterCount; public long ItemCarryCount; public long VoiceEvents; public long DamageToAlly; public long TotalConnectedSeconds; } private sealed class LeaderboardApiResponse { public int SaveSlotId; public string UpdatedAtUtc = ""; public List ConnectedSteamIds = new List(); public List Entries = new List(); } private sealed class LeaderboardEntryApiDto { public string SteamId = ""; public string DisplayName = ""; public long ItemCarryCount; public long DamageToAlly; public long MimicEncounterCount; public long TimeInStartingVolumeMs; public long CurrencyEarned; public long VoiceEvents; public long Deaths; public long Revives; public long Kills; public long TotalConnectedSeconds; public int SessionsCompleted; } private sealed class PlayerStatsApiDto { public int Version; public string SteamId = ""; public string DisplayName = ""; public GlobalStats Global = new GlobalStats(); public SessionStats? CurrentSession; public List RecentSessions = new List(); } private sealed class ErrorApiResponse { public int Error; public string Message = ""; } private sealed class MinimapApiResponse { public int LayoutVersion; public string LayoutKind = ""; public string DisplayMode = "hidden"; public string SceneLabel = ""; public string DefaultAreaId = ""; public WebDashboardMinimapBoundsDto Bounds = new WebDashboardMinimapBoundsDto(); public List Areas = new List(); public List Tiles = new List(); public List Connections = new List(); public WebDashboardMinimapTrainDto? Train; public List Markers = new List(); } private sealed class MinimapMarkerApiDto { public string SteamId = ""; public string DisplayName = ""; public float X; public float Z; public float Yaw; public string RoomName = ""; public string AreaId = ""; public string TileId = ""; public bool IsAlive = true; public bool IsHost; public bool IsLocal; } public static string SerializeStatus(WebDashboardStatusDto status) { return ModJson.Serialize(status); } public static string SerializePlayers(IReadOnlyList players) { List list = new List(); foreach (WebDashboardPlayerDto player in players) { list.Add(MapPlayer(player)); } return ModJson.Serialize(new PlayersApiResponse { Players = list }); } public static string SerializeLeaderboardResponse(LeaderboardDocument doc, IReadOnlyCollection connectedSteamIds) { List list = new List(); foreach (LeaderboardEntry entry in doc.Entries) { list.Add(MapLeaderboardEntry(entry)); } List list2 = new List(); foreach (ulong connectedSteamId in connectedSteamIds) { list2.Add(connectedSteamId.ToString()); } return ModJson.Serialize(new LeaderboardApiResponse { SaveSlotId = doc.SaveSlotId, UpdatedAtUtc = doc.UpdatedAtUtc.ToString("O", CultureInfo.InvariantCulture), ConnectedSteamIds = list2, Entries = list }); } public static string SerializePlayerStats(PlayerStatisticsDocument doc) { return ModJson.Serialize(MapPlayerStats(doc)); } public static string SerializeActionResult(WebDashboardActionResult result) { return ModJson.Serialize(result); } public static string SerializeError(int statusCode, string message) { return ModJson.Serialize(new ErrorApiResponse { Error = statusCode, Message = message }); } public static string SerializeSettings(WebDashboardSettingsDto settings) { return ModJson.Serialize(settings); } public static string SerializeConfigUpdateResult(WebDashboardConfigUpdateResult result) { return ModJson.Serialize(result); } public static string SerializeSnapshotEvent(WebDashboardSnapshot snapshot) { List list = new List(); foreach (WebDashboardPlayerDto player in snapshot.Players) { list.Add(MapPlayer(player)); } SnapshotEventDto snapshotEventDto = new SnapshotEventDto { Status = snapshot.Status, Players = list }; if (snapshot.Status.IsHost && !string.IsNullOrEmpty(snapshot.LeaderboardJson)) { snapshotEventDto.Leaderboard = ModJson.Deserialize(snapshot.LeaderboardJson); } if (snapshot.Status.InSession) { snapshotEventDto.Minimap = BuildMinimapResponse(snapshot.MinimapLayout, snapshot.MinimapMarkers, snapshot.MinimapTrain); } return ModJson.Serialize(snapshotEventDto); } public static string SerializeMinimap(WebDashboardMinimapLayoutDto layout, IReadOnlyList markers, WebDashboardMinimapTrainDto? train) { return ModJson.Serialize(BuildMinimapResponse(layout, markers, train)); } private static MinimapApiResponse BuildMinimapResponse(WebDashboardMinimapLayoutDto layout, IReadOnlyList markers, WebDashboardMinimapTrainDto? train) { List list = new List(); foreach (WebDashboardMinimapMarkerDto marker in markers) { list.Add(new MinimapMarkerApiDto { SteamId = marker.SteamId.ToString(), DisplayName = marker.DisplayName, X = marker.X, Z = marker.Z, Yaw = marker.Yaw, RoomName = marker.RoomName, AreaId = marker.AreaId, TileId = marker.TileId, IsAlive = marker.IsAlive, IsHost = marker.IsHost, IsLocal = marker.IsLocal }); } return new MinimapApiResponse { LayoutVersion = layout.LayoutVersion, LayoutKind = layout.LayoutKind, DisplayMode = layout.DisplayMode, SceneLabel = layout.SceneLabel, DefaultAreaId = layout.DefaultAreaId, Bounds = layout.Bounds, Areas = layout.Areas, Tiles = layout.Tiles, Connections = layout.Connections, Train = train, Markers = list }; } private static PlayerApiDto MapPlayer(WebDashboardPlayerDto player) { return new PlayerApiDto { SteamId = player.SteamId.ToString(), PlayerUid = player.PlayerUid, DisplayName = player.DisplayName, IsHost = player.IsHost, IsLocal = player.IsLocal, IsBanned = player.IsBanned, NetworkGrade = player.NetworkGrade, ConnectionRole = player.ConnectionRole, ConnectionAddress = player.ConnectionAddress, VoiceEventCount = player.VoiceEventCount, CurrentSession = ((player.CurrentSession == null) ? null : MapSessionStats(player.CurrentSession)) }; } private static SessionStatsApiDto MapSessionStats(WebDashboardSessionStatsDto stats) { return new SessionStatsApiDto { CurrencyEarned = stats.CurrencyEarned, Kills = stats.Kills, Deaths = stats.Deaths, Revives = stats.Revives, MimicEncounterCount = stats.MimicEncounterCount, ItemCarryCount = stats.ItemCarryCount, VoiceEvents = stats.VoiceEvents, DamageToAlly = stats.DamageToAlly, TotalConnectedSeconds = stats.TotalConnectedSeconds }; } private static LeaderboardEntryApiDto MapLeaderboardEntry(LeaderboardEntry entry) { return new LeaderboardEntryApiDto { SteamId = entry.SteamId.ToString(), DisplayName = entry.DisplayName, ItemCarryCount = entry.ItemCarryCount, DamageToAlly = entry.DamageToAlly, MimicEncounterCount = entry.MimicEncounterCount, TimeInStartingVolumeMs = entry.TimeInStartingVolumeMs, CurrencyEarned = entry.CurrencyEarned, VoiceEvents = entry.VoiceEvents, Deaths = entry.Deaths, Revives = entry.Revives, Kills = entry.Kills, TotalConnectedSeconds = entry.TotalConnectedSeconds, SessionsCompleted = entry.SessionsCompleted }; } private static PlayerStatsApiDto MapPlayerStats(PlayerStatisticsDocument doc) { return new PlayerStatsApiDto { Version = doc.Version, SteamId = doc.SteamId.ToString(), DisplayName = doc.DisplayName, Global = doc.Global, CurrentSession = doc.CurrentSession, RecentSessions = doc.RecentSessions }; } } internal static class WebDashboardMinimapAreaResolver { internal const string OutdoorAreaId = "outdoor"; internal const string IndoorAreaId = "indoor"; internal const string HubAreaId = "hub"; private static readonly FieldInfo? DungeonSpaceGroupField = AccessTools.Field(typeof(DungeonRoom), "_dungeonSpaceGroup"); private static readonly FieldInfo? SpaceGroupField = AccessTools.Field(typeof(DungeonRoom), "_spaceGroup"); internal static string? ResolvePlayerAreaId(GamePlayScene gps, DungeonRoom room, Vector3 position) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) if (WebDashboardMinimapOutdoorSource.IsPositionOutdoor(gps, position)) { return "outdoor"; } return ResolveIndoorAreaId(room, position); } internal static string? ResolveIndoorAreaId(DungeonRoom room, Vector3 position) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) if (TryResolveAreaFromSpaceGroup(position, TryGetIndoorTileGroup(room), out string areaId)) { return areaId; } if (!TryResolveInSpaceGroup(TryGetIndoorTileGroup(room), position)) { return null; } return "indoor"; } internal static bool IsIndoorAreaId(string areaId) { if (!(areaId == "indoor")) { return areaId.StartsWith("indoor-", StringComparison.Ordinal); } return true; } internal static string? ResolvePlayerArea(DungeonRoom? room, Vector3 position) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) if (room == null) { return null; } return ResolveIndoorAreaId(room, position); } private static bool TryResolveAreaFromSpaceGroup(Vector3 position, object? spaceGroupObj, out string? areaId) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) areaId = null; ISpaceGroup val = (ISpaceGroup)((spaceGroupObj is ISpaceGroup) ? spaceGroupObj : null); if (val == null) { return false; } try { IVSpace space = val.GetSpace(position); if (((space != null) ? space.Coordinate : null) is TileCoordinate val2) { areaId = WebDashboardMinimapTileRegistry.TryGetAreaId(val2.TileID); return !string.IsNullOrWhiteSpace(areaId); } if (((space != null) ? space.Coordinate : null) is GridCoordinate val3) { areaId = WebDashboardMinimapTileRegistry.TryGetGridAreaId(val3.X, val3.Y); return !string.IsNullOrWhiteSpace(areaId); } return false; } catch { return false; } } internal static bool ShouldHideMap(GameMainBase? main) { if (main is InTramWaitingScene || main is MaintenanceScene || main is DeathMatchScene) { return true; } return false; } internal static ISpaceGroup? TryGetOutdoorSpaceGroup(DungeonRoom room) { object? obj = SpaceGroupField?.GetValue(room); return (ISpaceGroup?)((obj is ISpaceGroup) ? obj : null); } internal static VSpaceTileGroup? TryGetOutdoorTileGroup(DungeonRoom room) { object? obj = SpaceGroupField?.GetValue(room); return (VSpaceTileGroup?)((obj is VSpaceTileGroup) ? obj : null); } internal static VSpaceGridGroup? TryGetOutdoorGridGroup(DungeonRoom room) { object? obj = SpaceGroupField?.GetValue(room); return (VSpaceGridGroup?)((obj is VSpaceGridGroup) ? obj : null); } internal static VSpaceTileGroup? TryGetIndoorTileGroup(DungeonRoom room) { object? obj = DungeonSpaceGroupField?.GetValue(room); return (VSpaceTileGroup?)((obj is VSpaceTileGroup) ? obj : null); } private static bool TryResolveInSpaceGroup(object? spaceGroupObj, Vector3 position) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) ISpaceGroup val = (ISpaceGroup)((spaceGroupObj is ISpaceGroup) ? spaceGroupObj : null); if (val == null) { return false; } try { return val.GetSpace(position) != null; } catch { return false; } } } internal sealed class WebDashboardMinimapDungeonGraph { internal List Tiles = new List(); internal Dictionary TileIds = new Dictionary(); internal List<(int From, int To)> Connections = new List<(int, int)>(); internal HashSet MainPath = new HashSet(); } internal static class WebDashboardMinimapDungeonSource { private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo? RuntimeDungeonField = typeof(GamePlayScene).GetField("runtimeDungeon", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? DungeonGeneratorField = typeof(GamePlayScene).GetField("dungeonGenerator", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? DungeonSpaceGroupField = AccessTools.Field(typeof(DungeonRoom), "_dungeonSpaceGroup"); private static readonly FieldInfo? SpaceGroupField = AccessTools.Field(typeof(DungeonRoom), "_spaceGroup"); private static readonly FieldInfo? VSpaceTilesField = AccessTools.Field(typeof(VSpaceTileGroup), "m_tiles"); private static readonly FieldInfo? VSpaceAdjacencyField = AccessTools.Field(typeof(VSpaceTileGroup), "m_adjacency"); private static readonly MethodInfo? GetAllIdToTileMethod = typeof(RuntimeDungeon).GetMethod("GetAllIDToTile", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly MethodInfo? GetAllAdjacencyMethod = typeof(RuntimeDungeon).GetMethod("GetAllAdjacency", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static bool TryBuildGraph(GamePlayScene gps, out WebDashboardMinimapDungeonGraph graph) { graph = new WebDashboardMinimapDungeonGraph(); if (TryAddFromRuntimeDungeon(gps, graph) || TryAddFromDungeonRoom(graph) || TryAddFromLegacyGenerator(gps, graph)) { return graph.Tiles.Count > 0; } return false; } internal static bool TryBuildFromTileGroup(VSpaceTileGroup tileGroup, out WebDashboardMinimapDungeonGraph graph) { graph = new WebDashboardMinimapDungeonGraph(); if (!(VSpaceTilesField?.GetValue(tileGroup) is IDictionary dictionary)) { return false; } graph.Tiles.Clear(); graph.TileIds.Clear(); graph.Connections.Clear(); graph.MainPath.Clear(); foreach (DictionaryEntry item in dictionary) { object? value = item.Value; Tile val = (Tile)((value is Tile) ? value : null); if (val != null) { int value2 = Convert.ToInt32(item.Key); graph.Tiles.Add(val); graph.TileIds[val] = value2; TilePlacementData placement = val.Placement; if (placement != null && placement.IsOnMainPath) { graph.MainPath.Add(val); } } } if (VSpaceAdjacencyField?.GetValue(tileGroup) is IDictionary adjacencyDictionary) { AddVSpaceAdjacency(graph, adjacencyDictionary); } return graph.Tiles.Count > 0; } internal static bool HasTiles(GamePlayScene gps) { if (TryBuildGraph(gps, out WebDashboardMinimapDungeonGraph graph)) { return graph.Tiles.Count > 0; } return false; } private static bool TryAddFromRuntimeDungeon(GamePlayScene gps, WebDashboardMinimapDungeonGraph graph) { object? obj = RuntimeDungeonField?.GetValue(gps); RuntimeDungeon val = (RuntimeDungeon)((obj is RuntimeDungeon) ? obj : null); if (val == null) { return false; } if (!(GetAllIdToTileMethod?.Invoke(val, null) is Tile[] array) || array.Length == 0) { DungeonGenerator generator = val.Generator; return TryAddFromDungeonObject((generator != null) ? generator.CurrentDungeon : null, graph); } graph.Tiles.Clear(); graph.TileIds.Clear(); graph.Connections.Clear(); graph.MainPath.Clear(); for (int i = 0; i < array.Length; i++) { Tile val2 = array[i]; if (!((Object)(object)val2 == (Object)null)) { graph.Tiles.Add(val2); graph.TileIds[val2] = i; TilePlacementData placement = val2.Placement; if (placement != null && placement.IsOnMainPath) { graph.MainPath.Add(val2); } } } if (GetAllAdjacencyMethod?.Invoke(val, null) is IList adjacencyList) { AddRuntimeAdjacency(graph, adjacencyList); } return graph.Tiles.Count > 0; } private static bool TryAddFromLegacyGenerator(GamePlayScene gps, WebDashboardMinimapDungeonGraph graph) { object? obj = DungeonGeneratorField?.GetValue(gps); DungeonGenerator val = (DungeonGenerator)((obj is DungeonGenerator) ? obj : null); if (val == null) { return false; } return TryAddFromDungeonObject(val.CurrentDungeon, graph); } private static bool TryAddFromDungeonRoom(WebDashboardMinimapDungeonGraph graph) { IVroom? activeDungeonRoom = JoinAnytimeRoomTools.GetActiveDungeonRoom(); DungeonRoom val = (DungeonRoom)(object)((activeDungeonRoom is DungeonRoom) ? activeDungeonRoom : null); if (val == null) { return false; } VSpaceTileGroup val2 = null; object? obj = DungeonSpaceGroupField?.GetValue(val); VSpaceTileGroup val3 = (VSpaceTileGroup)((obj is VSpaceTileGroup) ? obj : null); if (val3 != null) { val2 = val3; } else { object? obj2 = SpaceGroupField?.GetValue(val); VSpaceTileGroup val4 = (VSpaceTileGroup)((obj2 is VSpaceTileGroup) ? obj2 : null); if (val4 != null) { val2 = val4; } } if (val2 == null || !(VSpaceTilesField?.GetValue(val2) is IDictionary dictionary)) { return false; } graph.Tiles.Clear(); graph.TileIds.Clear(); graph.Connections.Clear(); graph.MainPath.Clear(); foreach (DictionaryEntry item in dictionary) { object? value = item.Value; Tile val5 = (Tile)((value is Tile) ? value : null); if (val5 != null) { int value2 = Convert.ToInt32(item.Key); graph.Tiles.Add(val5); graph.TileIds[val5] = value2; TilePlacementData placement = val5.Placement; if (placement != null && placement.IsOnMainPath) { graph.MainPath.Add(val5); } } } if (VSpaceAdjacencyField?.GetValue(val2) is IDictionary adjacencyDictionary) { AddVSpaceAdjacency(graph, adjacencyDictionary); } return graph.Tiles.Count > 0; } private static bool TryAddFromDungeonObject(Dungeon? dungeon, WebDashboardMinimapDungeonGraph graph) { if (((dungeon != null) ? dungeon.AllTiles : null) == null || dungeon.AllTiles.Count == 0) { return false; } graph.Tiles.Clear(); graph.TileIds.Clear(); graph.Connections.Clear(); graph.MainPath.Clear(); int num = 0; foreach (Tile allTile in dungeon.AllTiles) { if (!((Object)(object)allTile == (Object)null)) { graph.Tiles.Add(allTile); graph.TileIds[allTile] = num++; TilePlacementData placement = allTile.Placement; if (placement != null && placement.IsOnMainPath) { graph.MainPath.Add(allTile); } } } if (dungeon.MainPathTiles != null) { foreach (Tile mainPathTile in dungeon.MainPathTiles) { if ((Object)(object)mainPathTile != (Object)null) { graph.MainPath.Add(mainPathTile); } } } if (dungeon.Connections != null) { AddDungeonConnections(dungeon, graph); } return graph.Tiles.Count > 0; } private static void AddRuntimeAdjacency(WebDashboardMinimapDungeonGraph graph, IList adjacencyList) { for (int i = 0; i < adjacencyList.Count; i++) { if (!(adjacencyList[i] is IEnumerable enumerable)) { continue; } foreach (object item in enumerable) { int toId = Convert.ToInt32(item); AddConnectionPair(graph, i, toId); } } } private static void AddVSpaceAdjacency(WebDashboardMinimapDungeonGraph graph, IDictionary adjacencyDictionary) { foreach (DictionaryEntry item in adjacencyDictionary) { int fromId = Convert.ToInt32(item.Key); if (!(item.Value is IEnumerable enumerable)) { continue; } foreach (object item2 in enumerable) { int toId = Convert.ToInt32(item2); AddConnectionPair(graph, fromId, toId); } } } private static void AddDungeonConnections(Dungeon dungeon, WebDashboardMinimapDungeonGraph graph) { Dictionary tileIds = graph.TileIds; foreach (DoorwayConnection connection in dungeon.Connections) { DungeonGraphConnection val = (DungeonGraphConnection)(object)((connection is DungeonGraphConnection) ? connection : null); if (val != null) { DungeonGraphNode a = val.A; object obj = ((a != null) ? a.Tile : null); if (obj == null) { Doorway doorwayA = val.DoorwayA; obj = ((doorwayA != null) ? doorwayA.Tile : null); } Tile val2 = (Tile)obj; DungeonGraphNode b = val.B; object obj2 = ((b != null) ? b.Tile : null); if (obj2 == null) { Doorway doorwayB = val.DoorwayB; obj2 = ((doorwayB != null) ? doorwayB.Tile : null); } Tile val3 = (Tile)obj2; if (!((Object)(object)val2 == (Object)null) && !((Object)(object)val3 == (Object)null) && tileIds.TryGetValue(val2, out var value) && tileIds.TryGetValue(val3, out var value2) && value != value2) { AddConnectionPair(graph, value, value2); } } } } private static void AddConnectionPair(WebDashboardMinimapDungeonGraph graph, int fromId, int toId) { if (fromId != toId) { graph.Connections.Add((fromId <= toId) ? (fromId, toId) : (toId, fromId)); } } } internal sealed class WebDashboardMinimapGridCell { internal IVSpace Space; internal GridCoordinate Coordinate; internal int Id; internal float CenterX; internal float CenterZ; internal float Size; } internal sealed class WebDashboardMinimapGridGraph { internal List Cells = new List(); internal Dictionary SpaceIds = new Dictionary(); internal List<(int From, int To)> Connections = new List<(int, int)>(); } internal static class WebDashboardMinimapGridSource { private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo? VSpaceSizeField = AccessTools.Field(typeof(VSpaceGridGroup), "m_VSpaceSize"); internal static bool TryBuildFromGridGroup(VSpaceGridGroup gridGroup, out WebDashboardMinimapGridGraph graph) { //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Unknown result type (might be due to invalid IL or missing references) graph = new WebDashboardMinimapGridGraph(); if (gridGroup == null) { return false; } float size = ResolveCellSize(gridGroup); IReadOnlyList allSpaces = gridGroup.GetAllSpaces(); if (allSpaces == null || allSpaces.Count == 0) { return false; } graph.Cells.Clear(); graph.SpaceIds.Clear(); graph.Connections.Clear(); int num = 0; foreach (IVSpace item3 in allSpaces) { if (((item3 != null) ? item3.Coordinate : null) is GridCoordinate coordinate && TryGetCellCenter(gridGroup, coordinate, out var centerX, out var centerZ)) { int num2 = num++; WebDashboardMinimapGridCell item = new WebDashboardMinimapGridCell { Space = item3, Coordinate = coordinate, Id = num2, CenterX = centerX, CenterZ = centerZ, Size = size }; graph.Cells.Add(item); graph.SpaceIds[item3] = num2; } } if (graph.Cells.Count == 0) { return false; } HashSet<(int, int)> hashSet = new HashSet<(int, int)>(); foreach (WebDashboardMinimapGridCell cell in graph.Cells) { IReadOnlyList aroundSpaces = gridGroup.GetAroundSpaces(cell.Space); if (aroundSpaces == null) { continue; } foreach (IVSpace item4 in aroundSpaces) { if (graph.SpaceIds.TryGetValue(item4, out var value) && value != cell.Id) { int id = cell.Id; (int, int) item2 = ((id <= value) ? (id, value) : (value, id)); if (hashSet.Add(item2)) { graph.Connections.Add(item2); } } } } return true; } internal static string BuildCellId(GridCoordinate coordinate) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) return $"grid-{coordinate.X}-{coordinate.Y}"; } internal static string BuildCellId(int gridX, int gridY) { return $"grid-{gridX}-{gridY}"; } private static float ResolveCellSize(VSpaceGridGroup gridGroup) { if (VSpaceSizeField?.GetValue(gridGroup) is float num && num > 0f) { return num; } return 1f; } private static bool TryGetCellCenter(VSpaceGridGroup gridGroup, GridCoordinate coordinate, out float centerX, out float centerZ) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) centerX = 0f; centerZ = 0f; try { float num = ResolveCellSize(gridGroup); SPoint center = gridGroup.GetCenter(); centerX = (float)center.X + (float)coordinate.X * num + num * 0.5f; centerZ = (float)center.Z + (float)coordinate.Y * num + num * 0.5f; return true; } catch { return false; } } } internal static class WebDashboardMinimapHeightLayers { private const float MinFloorCenterGap = 3.5f; internal static List> ClusterTilesByHeight(IReadOnlyList tiles) { //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) List<(Tile, float)> list = new List<(Tile, float)>(); foreach (Tile tile in tiles) { if (!((Object)(object)tile == (Object)null)) { TilePlacementData placement = tile.Placement; Bounds val = ((placement != null) ? placement.Bounds : tile.Bounds); list.Add((tile, ((Bounds)(ref val)).center.y)); } } if (list.Count == 0) { return new List>(); } list.Sort(((Tile tile, float centerY) left, (Tile tile, float centerY) right) => left.centerY.CompareTo(right.centerY)); List> list2 = new List>(); List list3 = new List(1) { list[0].Item1 }; float num = list[0].Item2; for (int num2 = 1; num2 < list.Count; num2++) { (Tile, float) tuple = list[num2]; float num3 = num / (float)list3.Count; if (tuple.Item2 - num3 >= 3.5f) { list2.Add(list3); list3 = new List(1) { tuple.Item1 }; num = tuple.Item2; } else { list3.Add(tuple.Item1); num += tuple.Item2; } } list2.Add(list3); return list2; } internal static WebDashboardMinimapDungeonGraph ExtractSubgraph(WebDashboardMinimapDungeonGraph source, IReadOnlyList layerTiles) { WebDashboardMinimapDungeonGraph webDashboardMinimapDungeonGraph = new WebDashboardMinimapDungeonGraph(); HashSet hashSet = new HashSet(); foreach (Tile layerTile in layerTiles) { if (!((Object)(object)layerTile == (Object)null) && source.TileIds.TryGetValue(layerTile, out var value)) { webDashboardMinimapDungeonGraph.Tiles.Add(layerTile); webDashboardMinimapDungeonGraph.TileIds[layerTile] = value; hashSet.Add(value); if (source.MainPath.Contains(layerTile)) { webDashboardMinimapDungeonGraph.MainPath.Add(layerTile); } } } foreach (var (num, num2) in source.Connections) { if (hashSet.Contains(num) && hashSet.Contains(num2)) { webDashboardMinimapDungeonGraph.Connections.Add((num, num2)); } } return webDashboardMinimapDungeonGraph; } internal static string BuildIndoorLayerAreaId(int layerIndex, int layerCount) { if (layerCount > 1) { return "indoor-" + layerIndex; } return "indoor"; } internal static string BuildIndoorLayerLabel(int layerIndex, int layerCount) { if (layerCount > 1) { return "Indoor · Floor " + (layerIndex + 1); } return "Indoor"; } } internal static class WebDashboardMinimapLayoutBuilder { private const string Feature = "WebDashboard"; private const float BoundsPadding = 0.05f; private const float HubMinSpan = 40f; private const float HubMaxSpan = 120f; private const float HubFallbackHalfSpan = 75f; private const float EdgeTouchEpsilon = 0.002f; private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo? BgRootField = typeof(GameMainBase).GetField("BGRoot", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? MaintenanceRoomRootField = typeof(MaintenanceScene).GetField("maintenanceRoomRoot", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static string _cachedRunKey = ""; private static bool _rebuildRequested; internal static WebDashboardMinimapLayoutDto Current { get; private set; } = new WebDashboardMinimapLayoutDto(); internal static int LayoutVersion { get; private set; } internal static void RequestRebuild() { _rebuildRequested = true; _cachedRunKey = ""; WebDashboardSnapshotCache.MarkDirty(); } internal static void EnsureLayout() { GameMainBase val = JoinAnytimeHub.GetPdata()?.main; string text = BuildRunKey(val); if (!_rebuildRequested && text == _cachedRunKey) { if (Current.DisplayMode == "map" && HasAnyAreaTiles(Current)) { return; } string displayMode = Current.DisplayMode; bool flag = ((displayMode == "hidden" || displayMode == "markers-only") ? true : false); if (flag && text.StartsWith("hub:", StringComparison.Ordinal)) { return; } GamePlayScene val2 = (GamePlayScene)(object)((val is GamePlayScene) ? val : null); if (val2 == null || !(Current.LayoutKind == "dungeon") || !WebDashboardMinimapDungeonSource.HasTiles(val2)) { return; } } _rebuildRequested = false; _cachedRunKey = text; GamePlayScene val3 = (GamePlayScene)(object)((val is GamePlayScene) ? val : null); WebDashboardMinimapLayoutDto current = ((val3 != null) ? TryBuildDungeonLayout(val3, text) : ((val is InTramWaitingScene) ? BuildHiddenLayout("Tram waiting room") : ((val is MaintenanceScene) ? BuildHiddenLayout("Maintenance bay") : ((!(val is DeathMatchScene)) ? (WebDashboardMinimapAreaResolver.ShouldHideMap(val) ? BuildHiddenLayout(ResolveSceneLabel(val)) : BuildHubLayout(val, ResolveSceneLabel(val))) : BuildHiddenLayout("Death match"))))); Current = current; Current.LayoutVersion = ++LayoutVersion; } private static string BuildRunKey(GameMainBase? main) { GamePlayScene val = (GamePlayScene)(object)((main is GamePlayScene) ? main : null); if (val != null) { long num = ResolveRoomUid(val); return $"dungeon:{val.DungeonMasterID}:{val.RandDungeonSeed}:{num}"; } if (!(main is InTramWaitingScene)) { if (!(main is MaintenanceScene)) { return "hub:" + (((object)main)?.GetType().Name ?? "unknown"); } return "hub:maintenance"; } return "hub:waiting"; } private static WebDashboardMinimapLayoutDto TryBuildDungeonLayout(GamePlayScene gps, string runKey) { string sceneLabel = ResolveDungeonLabel(gps); WebDashboardMinimapLayoutDto webDashboardMinimapLayoutDto = new WebDashboardMinimapLayoutDto { LayoutKind = "dungeon", DisplayMode = "map", SceneLabel = sceneLabel }; WebDashboardMinimapTileRegistry.Clear(); bool flag = false; IVroom? activeDungeonRoom = JoinAnytimeRoomTools.GetActiveDungeonRoom(); DungeonRoom val = (DungeonRoom)(object)((activeDungeonRoom is DungeonRoom) ? activeDungeonRoom : null); if (val != null) { WebDashboardMinimapAreaDto webDashboardMinimapAreaDto = WebDashboardMinimapOutdoorSource.TryBuildOutdoorArea(gps); if (webDashboardMinimapAreaDto != null) { webDashboardMinimapLayoutDto.Areas.Add(webDashboardMinimapAreaDto); flag = true; } else { VSpaceGridGroup val2 = WebDashboardMinimapAreaResolver.TryGetOutdoorGridGroup(val); if (val2 != null && WebDashboardMinimapGridSource.TryBuildFromGridGroup(val2, out WebDashboardMinimapGridGraph graph)) { webDashboardMinimapAreaDto = BuildAreaFromGridGraph(graph, "outdoor", "Outdoor", "outdoor"); WebDashboardMinimapTileRegistry.RegisterGridGraph(graph, webDashboardMinimapAreaDto.Id); webDashboardMinimapLayoutDto.Areas.Add(webDashboardMinimapAreaDto); flag = true; } else { ISpaceGroup? obj = WebDashboardMinimapAreaResolver.TryGetOutdoorSpaceGroup(val); VSpaceTileGroup val3 = (VSpaceTileGroup)(object)((obj is VSpaceTileGroup) ? obj : null); if (val3 != null && WebDashboardMinimapDungeonSource.TryBuildFromTileGroup(val3, out WebDashboardMinimapDungeonGraph graph2)) { webDashboardMinimapAreaDto = BuildAreaFromGraph(graph2, "outdoor", "Outdoor", "outdoor"); WebDashboardMinimapTileRegistry.RegisterGraph(graph2, webDashboardMinimapAreaDto.Id); webDashboardMinimapLayoutDto.Areas.Add(webDashboardMinimapAreaDto); flag = true; } } } VSpaceTileGroup val4 = WebDashboardMinimapAreaResolver.TryGetIndoorTileGroup(val); if (val4 != null && WebDashboardMinimapDungeonSource.TryBuildFromTileGroup(val4, out WebDashboardMinimapDungeonGraph graph3)) { flag = AddIndoorHeightLayerAreas(webDashboardMinimapLayoutDto, graph3) || flag; AppendCrossAreaConnections(webDashboardMinimapLayoutDto, graph3); } } if (!flag && WebDashboardMinimapDungeonSource.TryBuildGraph(gps, out WebDashboardMinimapDungeonGraph graph4)) { flag = AddIndoorHeightLayerAreas(webDashboardMinimapLayoutDto, graph4); AppendCrossAreaConnections(webDashboardMinimapLayoutDto, graph4); } if (!flag) { WebDashboardMinimapTileRegistry.Clear(); return BuildMarkersOnlyLayout((GameMainBase?)(object)gps, sceneLabel, "dungeon"); } webDashboardMinimapLayoutDto.DefaultAreaId = ResolveDefaultAreaId(webDashboardMinimapLayoutDto.Areas); MirrorLegacyFields(webDashboardMinimapLayoutDto); return webDashboardMinimapLayoutDto; } private static bool AddIndoorHeightLayerAreas(WebDashboardMinimapLayoutDto layout, WebDashboardMinimapDungeonGraph indoorGraph) { List> list = WebDashboardMinimapHeightLayers.ClusterTilesByHeight(indoorGraph.Tiles); if (list.Count == 0) { return false; } for (int i = 0; i < list.Count; i++) { WebDashboardMinimapDungeonGraph webDashboardMinimapDungeonGraph = WebDashboardMinimapHeightLayers.ExtractSubgraph(indoorGraph, list[i]); if (webDashboardMinimapDungeonGraph.Tiles.Count != 0) { string areaId = WebDashboardMinimapHeightLayers.BuildIndoorLayerAreaId(i, list.Count); string label = WebDashboardMinimapHeightLayers.BuildIndoorLayerLabel(i, list.Count); WebDashboardMinimapAreaDto item = BuildAreaFromGraph(webDashboardMinimapDungeonGraph, areaId, label, "indoor"); WebDashboardMinimapTileRegistry.RegisterGraph(webDashboardMinimapDungeonGraph, areaId); layout.Areas.Add(item); } } return layout.Areas.Exists((WebDashboardMinimapAreaDto area) => area.Kind == "indoor"); } private static WebDashboardMinimapAreaDto BuildAreaFromGraph(WebDashboardMinimapDungeonGraph graph, string areaId, string label, string kind) { //IL_005b: 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_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Unknown result type (might be due to invalid IL or missing references) //IL_00c7: Unknown result type (might be due to invalid IL or missing references) //IL_00d8: Unknown result type (might be due to invalid IL or missing references) //IL_00e9: Unknown result type (might be due to invalid IL or missing references) //IL_00fb: Unknown result type (might be due to invalid IL or missing references) List<(Tile, float, float, float, float)> list = new List<(Tile, float, float, float, float)>(); float num = float.PositiveInfinity; float num2 = float.NegativeInfinity; float num3 = float.PositiveInfinity; float num4 = float.NegativeInfinity; foreach (Tile tile in graph.Tiles) { if (!((Object)(object)tile == (Object)null)) { TilePlacementData placement = tile.Placement; Bounds val = ((placement != null) ? placement.Bounds : tile.Bounds); Vector3 center = ((Bounds)(ref val)).center; float num5 = Mathf.Max(((Bounds)(ref val)).extents.x, 0.5f); float num6 = Mathf.Max(((Bounds)(ref val)).extents.z, 0.5f); list.Add((tile, center.x, center.z, num5 * 2f, num6 * 2f)); num = Mathf.Min(num, center.x - num5); num2 = Mathf.Max(num2, center.x + num5); num3 = Mathf.Min(num3, center.z - num6); num4 = Mathf.Max(num4, center.z + num6); } } WebDashboardMinimapAreaDto webDashboardMinimapAreaDto = new WebDashboardMinimapAreaDto { Id = areaId, Label = label, Kind = kind }; if (list.Count == 0) { return webDashboardMinimapAreaDto; } float num7 = Mathf.Max(num2 - num, 1f); float num8 = Mathf.Max(num4 - num3, 1f); float num9 = num7 * 0.05f; float num10 = num8 * 0.05f; num -= num9; num2 += num9; num3 -= num10; num4 += num10; num7 = num2 - num; num8 = num4 - num3; webDashboardMinimapAreaDto.Bounds = new WebDashboardMinimapBoundsDto { MinX = num, MinZ = num3, MaxX = num2, MaxZ = num4 }; Dictionary dictionary = new Dictionary(); foreach (var (val2, num11, num12, num13, num14) in list) { if (graph.TileIds.TryGetValue(val2, out var value)) { string text = $"tile-{value}"; WebDashboardMinimapTileDto webDashboardMinimapTileDto = new WebDashboardMinimapTileDto { Id = text, Label = ResolveTileLabel(val2), X = Normalize(num11 - num13 * 0.5f, num, num7), Z = Normalize(num12 - num14 * 0.5f, num3, num8), W = Mathf.Clamp01(num13 / num7), H = Mathf.Clamp01(num14 / num8), IsMainPath = graph.MainPath.Contains(val2) }; webDashboardMinimapAreaDto.Tiles.Add(webDashboardMinimapTileDto); dictionary[text] = webDashboardMinimapTileDto; } } HashSet hashSet = new HashSet(); foreach (var connection in graph.Connections) { int item = connection.From; int item2 = connection.To; string text2 = $"tile-{item}"; string text3 = $"tile-{item2}"; if (!(text2 == text3)) { string item3 = ((string.CompareOrdinal(text2, text3) < 0) ? (text2 + "|" + text3) : (text3 + "|" + text2)); if (hashSet.Add(item3) && dictionary.TryGetValue(text2, out var value2) && dictionary.TryGetValue(text3, out var value3) && TryComputeConnectionPoint(value2, value3, out var x, out var z) && TryComputeConnectionDirection(value2, value3, out var dirX, out var dirZ)) { webDashboardMinimapAreaDto.ConnectionPoints.Add(new WebDashboardMinimapConnectionPointDto { X = x, Z = z, DirX = dirX, DirZ = dirZ, FromTileId = text2, ToTileId = text3, CrossArea = false }); } } } return webDashboardMinimapAreaDto; } private static WebDashboardMinimapAreaDto BuildAreaFromGridGraph(WebDashboardMinimapGridGraph graph, string areaId, string label, string kind) { //IL_01bc: Unknown result type (might be due to invalid IL or missing references) //IL_02ac: Unknown result type (might be due to invalid IL or missing references) //IL_02c5: Unknown result type (might be due to invalid IL or missing references) List<(WebDashboardMinimapGridCell, float, float, float, float)> list = new List<(WebDashboardMinimapGridCell, float, float, float, float)>(); float num = float.PositiveInfinity; float num2 = float.NegativeInfinity; float num3 = float.PositiveInfinity; float num4 = float.NegativeInfinity; foreach (WebDashboardMinimapGridCell cell in graph.Cells) { float num5 = cell.Size * 0.5f; list.Add((cell, cell.CenterX, cell.CenterZ, cell.Size, cell.Size)); num = Mathf.Min(num, cell.CenterX - num5); num2 = Mathf.Max(num2, cell.CenterX + num5); num3 = Mathf.Min(num3, cell.CenterZ - num5); num4 = Mathf.Max(num4, cell.CenterZ + num5); } WebDashboardMinimapAreaDto webDashboardMinimapAreaDto = new WebDashboardMinimapAreaDto { Id = areaId, Label = label, Kind = kind }; if (list.Count == 0) { return webDashboardMinimapAreaDto; } float num6 = Mathf.Max(num2 - num, 1f); float num7 = Mathf.Max(num4 - num3, 1f); float num8 = num6 * 0.05f; float num9 = num7 * 0.05f; num -= num8; num2 += num8; num3 -= num9; num4 += num9; num6 = num2 - num; num7 = num4 - num3; webDashboardMinimapAreaDto.Bounds = new WebDashboardMinimapBoundsDto { MinX = num, MinZ = num3, MaxX = num2, MaxZ = num4 }; Dictionary dictionary = new Dictionary(); foreach (var item9 in list) { WebDashboardMinimapGridCell item = item9.Item1; float item2 = item9.Item2; float item3 = item9.Item3; float item4 = item9.Item4; float item5 = item9.Item5; string text = WebDashboardMinimapGridSource.BuildCellId(item.Coordinate); WebDashboardMinimapTileDto webDashboardMinimapTileDto = new WebDashboardMinimapTileDto { Id = text, Label = "Sector", X = Normalize(item2 - item4 * 0.5f, num, num6), Z = Normalize(item3 - item5 * 0.5f, num3, num7), W = Mathf.Clamp01(item4 / num6), H = Mathf.Clamp01(item5 / num7), IsMainPath = false }; webDashboardMinimapAreaDto.Tiles.Add(webDashboardMinimapTileDto); dictionary[text] = webDashboardMinimapTileDto; } HashSet hashSet = new HashSet(); foreach (var connection in graph.Connections) { int item6 = connection.From; int item7 = connection.To; string text2 = WebDashboardMinimapGridSource.BuildCellId(graph.Cells[item6].Coordinate); string text3 = WebDashboardMinimapGridSource.BuildCellId(graph.Cells[item7].Coordinate); if (!(text2 == text3)) { string item8 = ((string.CompareOrdinal(text2, text3) < 0) ? (text2 + "|" + text3) : (text3 + "|" + text2)); if (hashSet.Add(item8) && dictionary.TryGetValue(text2, out var value) && dictionary.TryGetValue(text3, out var value2) && TryComputeConnectionPoint(value, value2, out var x, out var z) && TryComputeConnectionDirection(value, value2, out var dirX, out var dirZ)) { webDashboardMinimapAreaDto.ConnectionPoints.Add(new WebDashboardMinimapConnectionPointDto { X = x, Z = z, DirX = dirX, DirZ = dirZ, FromTileId = text2, ToTileId = text3, CrossArea = false }); } } } return webDashboardMinimapAreaDto; } private static void AppendCrossAreaConnections(WebDashboardMinimapLayoutDto layout, WebDashboardMinimapDungeonGraph fullGraph) { Dictionary dictionary = new Dictionary(); foreach (WebDashboardMinimapAreaDto area in layout.Areas) { dictionary[area.Id] = area; } HashSet seen = new HashSet(); foreach (var connection in fullGraph.Connections) { int item = connection.From; int item2 = connection.To; string text = WebDashboardMinimapTileRegistry.TryGetAreaId(item); string text2 = WebDashboardMinimapTileRegistry.TryGetAreaId(item2); if (!string.IsNullOrWhiteSpace(text) && !string.IsNullOrWhiteSpace(text2) && !(text == text2)) { TryAppendCrossAreaConnection(fullGraph, dictionary, seen, text, item, item2, text2); TryAppendCrossAreaConnection(fullGraph, dictionary, seen, text2, item2, item, text); } } } private static void TryAppendCrossAreaConnection(WebDashboardMinimapDungeonGraph fullGraph, Dictionary areasById, HashSet seen, string areaId, int localTileId, int remoteTileId, string targetAreaId) { string item = areaId + "|" + localTileId + "|" + remoteTileId; if (!seen.Add(item) || !areasById.TryGetValue(areaId, out WebDashboardMinimapAreaDto value)) { return; } Tile val = FindTileById(fullGraph, localTileId); if (val != null) { Tile val2 = FindTileById(fullGraph, remoteTileId); if (val2 != null && TryComputeCrossAreaConnectionPoint(val, val2, value.Bounds, out var pointX, out var pointZ, out var dirX, out var dirZ)) { value.ConnectionPoints.Add(new WebDashboardMinimapConnectionPointDto { X = pointX, Z = pointZ, DirX = dirX, DirZ = dirZ, FromTileId = "tile-" + localTileId, ToTileId = "tile-" + remoteTileId, TargetAreaId = targetAreaId, CrossArea = true }); } } } private static Tile? FindTileById(WebDashboardMinimapDungeonGraph graph, int tileId) { foreach (KeyValuePair tileId2 in graph.TileIds) { if (tileId2.Value == tileId) { return tileId2.Key; } } return null; } private static bool TryComputeCrossAreaConnectionPoint(Tile localTile, Tile remoteTile, WebDashboardMinimapBoundsDto areaBounds, out float pointX, out float pointZ, out float dirX, out float dirZ) { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_0042: 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) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Unknown result type (might be due to invalid IL or missing references) pointX = 0f; pointZ = 0f; dirX = 0f; dirZ = 0f; TilePlacementData placement = localTile.Placement; Bounds val = ((placement != null) ? placement.Bounds : localTile.Bounds); TilePlacementData placement2 = remoteTile.Placement; Bounds val2 = ((placement2 != null) ? placement2.Bounds : remoteTile.Bounds); float x = ((Bounds)(ref val)).center.x; float z = ((Bounds)(ref val)).center.z; float x2 = ((Bounds)(ref val2)).center.x; float z2 = ((Bounds)(ref val2)).center.z; float num = x2 - x; float num2 = z2 - z; float num3 = Mathf.Sqrt(num * num + num2 * num2); if (num3 <= 0.001f) { return false; } num /= num3; num2 /= num3; float num4 = Mathf.Max(((Bounds)(ref val)).extents.x, 0.5f); float num5 = Mathf.Max(((Bounds)(ref val)).extents.z, 0.5f); float value = x + num * num4; float value2 = z + num2 * num5; float num6 = Mathf.Max(areaBounds.MaxX - areaBounds.MinX, 1f); float num7 = Mathf.Max(areaBounds.MaxZ - areaBounds.MinZ, 1f); pointX = Normalize(value, areaBounds.MinX, num6); pointZ = Normalize(value2, areaBounds.MinZ, num7); float num8 = num / num6; float num9 = num2 / num7; float num10 = Mathf.Sqrt(num8 * num8 + num9 * num9); if (num10 <= 0.001f) { dirX = num; dirZ = num2; } else { dirX = num8 / num10; dirZ = num9 / num10; } return true; } private static bool TryComputeConnectionDirection(WebDashboardMinimapTileDto from, WebDashboardMinimapTileDto to, out float dirX, out float dirZ) { float num = from.X + from.W * 0.5f; float num2 = from.Z + from.H * 0.5f; float num3 = to.X + to.W * 0.5f; float num4 = to.Z + to.H * 0.5f; dirX = num3 - num; dirZ = num4 - num2; float num5 = Mathf.Sqrt(dirX * dirX + dirZ * dirZ); if (num5 <= 0.0001f) { return false; } dirX /= num5; dirZ /= num5; return true; } private static bool TryComputeConnectionPoint(WebDashboardMinimapTileDto from, WebDashboardMinimapTileDto to, out float x, out float z) { float x2 = from.X; float num = from.X + from.W; float z2 = from.Z; float num2 = from.Z + from.H; float x3 = to.X; float num3 = to.X + to.W; float z3 = to.Z; float num4 = to.Z + to.H; float num5 = Mathf.Max(x2, x3); float num6 = Mathf.Min(num, num3); float num7 = Mathf.Max(z2, z3); float num8 = Mathf.Min(num2, num4); if (Mathf.Abs(num - x3) <= 0.002f && num7 < num8) { x = num; z = (num7 + num8) * 0.5f; return true; } if (Mathf.Abs(num3 - x2) <= 0.002f && num7 < num8) { x = x2; z = (num7 + num8) * 0.5f; return true; } if (Mathf.Abs(num2 - z3) <= 0.002f && num5 < num6) { x = (num5 + num6) * 0.5f; z = num2; return true; } if (Mathf.Abs(num4 - z2) <= 0.002f && num5 < num6) { x = (num5 + num6) * 0.5f; z = z2; return true; } x = ((x2 + num) * 0.5f + (x3 + num3) * 0.5f) * 0.5f; z = ((z2 + num2) * 0.5f + (z3 + num4) * 0.5f) * 0.5f; return true; } private static WebDashboardMinimapLayoutDto BuildHiddenLayout(string sceneLabel) { return new WebDashboardMinimapLayoutDto { LayoutKind = "none", DisplayMode = "hidden", SceneLabel = sceneLabel }; } private static WebDashboardMinimapLayoutDto BuildMarkersOnlyLayout(GameMainBase? main, string sceneLabel, string layoutKind) { WebDashboardMinimapLayoutDto webDashboardMinimapLayoutDto = new WebDashboardMinimapLayoutDto { LayoutKind = layoutKind, DisplayMode = "markers-only", SceneLabel = sceneLabel, Bounds = TryBuildHubBounds(main), DefaultAreaId = "hub" }; webDashboardMinimapLayoutDto.Areas.Add(new WebDashboardMinimapAreaDto { Id = "hub", Label = sceneLabel, Kind = layoutKind, Bounds = webDashboardMinimapLayoutDto.Bounds }); return webDashboardMinimapLayoutDto; } private static WebDashboardMinimapLayoutDto BuildHubLayout(GameMainBase? main, string sceneLabel) { WebDashboardMinimapBoundsDto bounds = TryBuildHubBounds(main); return new WebDashboardMinimapLayoutDto { LayoutKind = "hub", DisplayMode = "markers-only", SceneLabel = sceneLabel, Bounds = bounds, DefaultAreaId = "hub", Areas = { new WebDashboardMinimapAreaDto { Id = "hub", Label = sceneLabel, Kind = "hub", Bounds = bounds } } }; } private static string ResolveDefaultAreaId(List areas) { foreach (WebDashboardMinimapAreaDto area in areas) { if (area.Id == "outdoor" && area.Tiles.Count > 0) { return area.Id; } } WebDashboardMinimapAreaDto webDashboardMinimapAreaDto = null; foreach (WebDashboardMinimapAreaDto area2 in areas) { if (!(area2.Kind != "indoor") && area2.Tiles.Count != 0 && (webDashboardMinimapAreaDto == null || string.CompareOrdinal(area2.Id, webDashboardMinimapAreaDto.Id) < 0)) { webDashboardMinimapAreaDto = area2; } } if (webDashboardMinimapAreaDto != null) { return webDashboardMinimapAreaDto.Id; } foreach (WebDashboardMinimapAreaDto area3 in areas) { if (area3.Tiles.Count > 0) { return area3.Id; } } if (areas.Count <= 0) { return ""; } return areas[0].Id; } private static bool HasAnyAreaTiles(WebDashboardMinimapLayoutDto layout) { foreach (WebDashboardMinimapAreaDto area in layout.Areas) { if (area.Tiles.Count > 0) { return true; } } return layout.Tiles.Count > 0; } private static void MirrorLegacyFields(WebDashboardMinimapLayoutDto layout) { layout.Tiles.Clear(); layout.Connections.Clear(); WebDashboardMinimapAreaDto webDashboardMinimapAreaDto = null; foreach (WebDashboardMinimapAreaDto area in layout.Areas) { if (area.Id == layout.DefaultAreaId || webDashboardMinimapAreaDto == null) { webDashboardMinimapAreaDto = area; } } if (webDashboardMinimapAreaDto == null) { return; } layout.Bounds = webDashboardMinimapAreaDto.Bounds; layout.Tiles.AddRange(webDashboardMinimapAreaDto.Tiles); foreach (WebDashboardMinimapConnectionPointDto connectionPoint in webDashboardMinimapAreaDto.ConnectionPoints) { layout.Connections.Add(new WebDashboardMinimapConnectionDto { From = connectionPoint.FromTileId, To = connectionPoint.ToTileId }); } } private static WebDashboardMinimapBoundsDto TryBuildHubBounds(GameMainBase? main) { //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) Transform val = (Transform)(((Object)(object)main != (Object)null) ? /*isinst with value type is only supported in some contexts*/: null); float minX = float.PositiveInfinity; float maxX = float.NegativeInfinity; float minZ = float.PositiveInfinity; float maxZ = float.NegativeInfinity; if ((Object)(object)main != (Object)null) { ExpandBoundsFromTransform(val, ref minX, ref maxX, ref minZ, ref maxZ); MaintenanceScene val2 = (MaintenanceScene)(object)((main is MaintenanceScene) ? main : null); if (val2 != null) { object? obj = MaintenanceRoomRootField?.GetValue(val2); ExpandBoundsFromTransform((Transform?)((obj is Transform) ? obj : null), ref minX, ref maxX, ref minZ, ref maxZ); } } if (float.IsPositiveInfinity(minX)) { if (!((Object)(object)val != (Object)null)) { return PlaceholderBounds(); } return CenteredBounds(val.position.x, val.position.z, 75f); } return FinalizeHubBounds(minX, maxX, minZ, maxZ, val); } private static WebDashboardMinimapBoundsDto FinalizeHubBounds(float minX, float maxX, float minZ, float maxZ, Transform? anchor) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) float num = (((Object)(object)anchor != (Object)null) ? anchor.position.x : ((minX + maxX) * 0.5f)); float num2 = (((Object)(object)anchor != (Object)null) ? anchor.position.z : ((minZ + maxZ) * 0.5f)); float num3 = Mathf.Clamp(maxX - minX, 40f, 120f); float num4 = Mathf.Clamp(maxZ - minZ, 40f, 120f); if (maxX - minX > 120f || maxZ - minZ > 120f) { minX = num - num3 * 0.5f; maxX = num + num3 * 0.5f; minZ = num2 - num4 * 0.5f; maxZ = num2 + num4 * 0.5f; } float num5 = num3 * 0.05f; float num6 = num4 * 0.05f; return new WebDashboardMinimapBoundsDto { MinX = minX - num5, MinZ = minZ - num6, MaxX = maxX + num5, MaxZ = maxZ + num6 }; } private static WebDashboardMinimapBoundsDto CenteredBounds(float centerX, float centerZ, float halfSpan) { float num = halfSpan * 0.05f; return new WebDashboardMinimapBoundsDto { MinX = centerX - halfSpan - num, MinZ = centerZ - halfSpan - num, MaxX = centerX + halfSpan + num, MaxZ = centerZ + halfSpan + num }; } private static void ExpandBoundsFromTransform(Transform? root, ref float minX, ref float maxX, ref float minZ, ref float maxZ) { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002b: 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_0047: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)root == (Object)null) { return; } bool flag = false; Renderer[] componentsInChildren = ((Component)root).GetComponentsInChildren(true); foreach (Renderer val in componentsInChildren) { if (!((Object)(object)val == (Object)null)) { Bounds bounds = val.bounds; minX = Mathf.Min(minX, ((Bounds)(ref bounds)).min.x); maxX = Mathf.Max(maxX, ((Bounds)(ref bounds)).max.x); minZ = Mathf.Min(minZ, ((Bounds)(ref bounds)).min.z); maxZ = Mathf.Max(maxZ, ((Bounds)(ref bounds)).max.z); flag = true; } } if (!flag) { Vector3 position = root.position; minX = Mathf.Min(minX, position.x - 75f); maxX = Mathf.Max(maxX, position.x + 75f); minZ = Mathf.Min(minZ, position.z - 75f); maxZ = Mathf.Max(maxZ, position.z + 75f); } } private static WebDashboardMinimapBoundsDto PlaceholderBounds() { return new WebDashboardMinimapBoundsDto { MinX = 0f, MinZ = 0f, MaxX = 1f, MaxZ = 1f }; } private static string ResolveDungeonLabel(GamePlayScene gps) { try { if ((Object)(object)Hub.s == (Object)null) { return "Dungeon"; } object? obj = typeof(Hub).GetProperty("dataman", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(Hub.s); DataManager val = (DataManager)((obj is DataManager) ? obj : null); if (val == null) { return "Dungeon"; } DungeonMasterInfo dungeonInfo = val.ExcelDataManager.GetDungeonInfo(gps.DungeonMasterID); if (dungeonInfo == null) { return "Dungeon"; } string text = dungeonInfo.ID.ToString(); int num = JoinAnytimeRoomTools.ResolvePickedMapId(null); if (num == 0 && !dungeonInfo.MapIDs.IsDefaultOrEmpty) { num = dungeonInfo.MapIDs[0]; } if (num != 0) { MapMasterInfo mapInfo = val.ExcelDataManager.GetMapInfo(num); if (!string.IsNullOrWhiteSpace((mapInfo != null) ? mapInfo.SceneName : null)) { return text + " / " + mapInfo.SceneName; } } return text; } catch { return "Dungeon"; } } private static string ResolveSceneLabel(GameMainBase? main) { string text = ((object)main)?.GetType().Name; if (!(text == "DeathMatchScene")) { if (text == null) { return "Session"; } return ((object)main).GetType().Name; } return "Death match"; } private static long ResolveRoomUid(GamePlayScene gps) { FieldInfo fieldInfo = typeof(GamePlayScene).GetField("RoomUID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? typeof(GamePlayScene).GetField("roomUID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (!(fieldInfo != null)) { return 0L; } return Convert.ToInt64(fieldInfo.GetValue(gps)); } private static string ResolveTileLabel(Tile tile) { TilePlacementData placement = tile.Placement; object obj; if (placement == null) { obj = null; } else { TileSet tileSet = placement.TileSet; obj = ((tileSet != null) ? ((Object)tileSet).name : null); } string name = (string)obj; object obj2 = SanitizeRoomName(((Object)tile).name); if (obj2 == null) { GameObject gameObject = ((Component)tile).gameObject; obj2 = SanitizeRoomName((gameObject != null) ? ((Object)gameObject).name : null) ?? SanitizeRoomName(name); } string text = (string)obj2; if (!string.IsNullOrWhiteSpace(text)) { return text; } return "Room"; } private static string? SanitizeRoomName(string? name) { if (string.IsNullOrWhiteSpace(name)) { return null; } string text = name.Trim(); if (!text.Equals("GameObject", StringComparison.OrdinalIgnoreCase)) { return text; } return null; } private static float Normalize(float value, float min, float span) { if (!(span <= 0f)) { return Mathf.Clamp01((value - min) / span); } return 0.5f; } } internal static class WebDashboardMinimapOutdoorSource { private const float BoundsPadding = 0.05f; private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo? OutdoorColliderField = typeof(GamePlayScene).GetField("outdoorCollider", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static bool TryGetOutdoorWorldBounds(GamePlayScene gps, out WebDashboardMinimapBoundsDto bounds) { bounds = new WebDashboardMinimapBoundsDto(); Component val = TryGetOutdoorCollider(gps); if (val == null) { return false; } return TryReadWorldBounds(val, out bounds); } internal static bool IsPositionOutdoor(GamePlayScene gps, Vector3 position) { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) Component val = TryGetOutdoorCollider(gps); if (val == null) { return false; } if (!(((object)val).GetType().GetProperty("bounds")?.GetValue(val) is Bounds val2)) { return false; } return ((Bounds)(ref val2)).Contains(position); } internal static WebDashboardMinimapAreaDto? TryBuildOutdoorArea(GamePlayScene gps) { if (!TryGetOutdoorWorldBounds(gps, out WebDashboardMinimapBoundsDto bounds)) { return null; } return new WebDashboardMinimapAreaDto { Id = "outdoor", Label = "Outdoor", Kind = "outdoor", Bounds = bounds, Tiles = { new WebDashboardMinimapTileDto { Id = "outdoor-zone", Label = "Outdoor", X = 0f, Z = 0f, W = 1f, H = 1f, IsMainPath = true } } }; } private static Component? TryGetOutdoorCollider(GamePlayScene gps) { object? obj = OutdoorColliderField?.GetValue(gps); return (Component?)((obj is Component) ? obj : null); } private static bool TryReadWorldBounds(Component collider, out WebDashboardMinimapBoundsDto bounds) { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00d0: Unknown result type (might be due to invalid IL or missing references) bounds = new WebDashboardMinimapBoundsDto(); if (!(((object)collider).GetType().GetProperty("bounds")?.GetValue(collider) is Bounds val)) { return false; } if (((Bounds)(ref val)).size.x <= 0.01f || ((Bounds)(ref val)).size.z <= 0.01f) { return false; } float x = ((Bounds)(ref val)).size.x; float z = ((Bounds)(ref val)).size.z; float num = x * 0.05f; float num2 = z * 0.05f; bounds = new WebDashboardMinimapBoundsDto { MinX = ((Bounds)(ref val)).min.x - num, MinZ = ((Bounds)(ref val)).min.z - num2, MaxX = ((Bounds)(ref val)).max.x + num, MaxZ = ((Bounds)(ref val)).max.z + num2 }; return true; } } internal static class WebDashboardMinimapPatches { [HarmonyPatch(typeof(DungeonGenerator), "ChangeStatus")] internal static class DungeonGenerationCompletePatch { private static void Postfix(GenerationStatus status) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Invalid comparison between Unknown and I4 if ((int)status == 8) { WebDashboardMinimapLayoutBuilder.RequestRebuild(); } } } [HarmonyPatch(typeof(RuntimeDungeon), "BuildDungeonInfo")] internal static class RuntimeDungeonBuiltPatch { private static void Postfix() { WebDashboardMinimapLayoutBuilder.RequestRebuild(); } } [HarmonyPatch(typeof(DungeonRoom), "InitSpawn")] internal static class DungeonRoomInitSpawnPatch { private static void Postfix() { WebDashboardMinimapLayoutBuilder.RequestRebuild(); } } internal static void Apply(Harmony harmony) { HarmonyPatchHelper.ApplyPatchTypes(harmony, "WebDashboard", HarmonyPatchHelper.GetNestedPatchTypes(typeof(WebDashboardMinimapPatches))); } } internal static class WebDashboardMinimapService { private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo? BgRootField = typeof(GameMainBase).GetField("BGRoot", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? TramConsoleField = typeof(GameMainBase).GetField("tramConsole", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static List CollectRawMarkers() { //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Invalid comparison between Unknown and I4 //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_00fc: Unknown result type (might be due to invalid IL or missing references) List list = new List(); try { GameMainBase val = JoinAnytimeHub.GetPdata()?.main; if ((Object)(object)val == (Object)null) { return list; } Dictionary protoActorMap = val.GetProtoActorMap(); if (protoActorMap == null) { return list; } IVroom? activeDungeonRoom = JoinAnytimeRoomTools.GetActiveDungeonRoom(); DungeonRoom val2 = (DungeonRoom)(object)((activeDungeonRoom is DungeonRoom) ? activeDungeonRoom : null); foreach (ProtoActor value in protoActorMap.Values) { if ((Object)(object)value == (Object)null || (int)value.ActorType != 1) { continue; } ulong num = StatisticsTracker.TryResolveSteamId(value); if (num != 0L) { Transform transform = ((Component)value).transform; Vector3 position = transform.position; float x = position.x; float z = position.z; float yaw = ResolveHorizontalYaw(transform); string areaId = "hub"; string tileId = string.Empty; string roomName = string.Empty; GamePlayScene val3 = (GamePlayScene)(object)((val is GamePlayScene) ? val : null); if (val3 != null && val2 != null) { areaId = WebDashboardMinimapAreaResolver.ResolvePlayerAreaId(val3, val2, position) ?? string.Empty; roomName = SpawnScalingRoomLookup.TryGetRoomName(val2, position); tileId = TryResolveTileId(val2, position, areaId); } else if (WebDashboardMinimapTramSpace.IsWaitingRoom(val)) { WebDashboardMinimapTramSpace.WorldToLocal(val, transform, out x, out z, out yaw); areaId = string.Empty; } list.Add(new WebDashboardMinimapMarkerDto { SteamId = num, DisplayName = (string.IsNullOrWhiteSpace(value.nickName) ? num.ToString() : value.nickName), X = x, Z = z, Yaw = yaw, RoomName = roomName, AreaId = areaId, TileId = tileId, IsAlive = !value.dead, IsHost = (MimesisSaveManager.IsHost() && LocalPlayerHelper.IsLocalSteamId(num)), IsLocal = LocalPlayerHelper.IsLocalSteamId(num) }); } } } catch { } return list; } internal static List CollectMarkers(IReadOnlyList players, out WebDashboardMinimapTrainDto? train) { List list = CollectRawMarkers(); WebDashboardMinimapLayoutDto current = WebDashboardMinimapLayoutBuilder.Current; WebDashboardMinimapTrainDto rawTrain = TryCollectTrain(JoinAnytimeHub.GetPdata()?.main, current); List list2 = new List(); foreach (WebDashboardMinimapMarkerDto item in list) { EnrichFromPlayers(item, players); WebDashboardMinimapBoundsDto bounds = ResolveMarkerBounds(current, item); list2.Add(NormalizeMarker(item, bounds)); } WebDashboardMinimapAreaDto outdoorArea = TryGetArea(current, "outdoor"); train = NormalizeTrainForLayout(rawTrain, current, outdoorArea); return list2; } private static WebDashboardMinimapTrainDto? NormalizeTrainForLayout(WebDashboardMinimapTrainDto? rawTrain, WebDashboardMinimapLayoutDto layout, WebDashboardMinimapAreaDto? outdoorArea) { if (rawTrain == null) { return null; } if (outdoorArea != null) { return NormalizeTrain(rawTrain, outdoorArea.Bounds, "outdoor"); } WebDashboardMinimapAreaDto webDashboardMinimapAreaDto = TryGetArea(layout, "hub"); if (webDashboardMinimapAreaDto != null) { return NormalizeTrain(rawTrain, webDashboardMinimapAreaDto.Bounds, "hub"); } return null; } internal static List FilterMarkers(IReadOnlyList markers, ulong focusSteamId, bool showAll, bool isHost) { if (showAll) { if (!isHost) { return new List(); } List list = new List(); { foreach (WebDashboardMinimapMarkerDto marker in markers) { if (marker.IsAlive) { list.Add(marker); } } return list; } } if (focusSteamId == 0L) { foreach (WebDashboardMinimapMarkerDto marker2 in markers) { if (marker2.IsLocal) { return new List(1) { marker2 }; } } if (markers.Count <= 0) { return new List(); } return new List(1) { markers[0] }; } foreach (WebDashboardMinimapMarkerDto marker3 in markers) { if (marker3.SteamId == focusSteamId) { return new List(1) { marker3 }; } } return new List(); } internal static WebDashboardMinimapAreaDto? TryGetArea(WebDashboardMinimapLayoutDto layout, string areaId) { if (string.IsNullOrWhiteSpace(areaId)) { return null; } foreach (WebDashboardMinimapAreaDto area in layout.Areas) { if (area.Id == areaId) { return area; } } return null; } internal static WebDashboardMinimapTrainDto? NormalizeTrain(WebDashboardMinimapTrainDto? train, WebDashboardMinimapBoundsDto bounds, string areaId) { if (train == null || string.IsNullOrWhiteSpace(areaId) || IsPlaceholderBounds(bounds)) { return null; } return new WebDashboardMinimapTrainDto { X = NormalizeCoord(train.X, bounds.MinX, bounds.MaxX), Z = NormalizeCoord(train.Z, bounds.MinZ, bounds.MaxZ), Yaw = train.Yaw, AreaId = areaId }; } internal static WebDashboardMinimapBoundsDto ResolveEffectiveBounds(WebDashboardMinimapLayoutDto layout, WebDashboardMinimapTrainDto? rawTrain) { if (!IsPlaceholderBounds(layout.Bounds)) { return layout.Bounds; } return BuildFallbackBounds(rawTrain); } internal static WebDashboardMinimapTrainDto? TryCollectTrain(GameMainBase? main, WebDashboardMinimapLayoutDto layout) { if ((main is InTramWaitingScene || main is DeathMatchScene || main is MaintenanceScene) ? true : false) { return null; } if (main is GamePlayScene) { if (TryGetArea(layout, "outdoor") == null) { return null; } return TryFindSceneTrainMarker(main); } return null; } private static WebDashboardMinimapBoundsDto ResolveMarkerBounds(WebDashboardMinimapLayoutDto layout, WebDashboardMinimapMarkerDto marker) { WebDashboardMinimapAreaDto webDashboardMinimapAreaDto = TryGetArea(layout, marker.AreaId); if (webDashboardMinimapAreaDto != null && !IsPlaceholderBounds(webDashboardMinimapAreaDto.Bounds)) { return webDashboardMinimapAreaDto.Bounds; } if (!string.IsNullOrWhiteSpace(marker.AreaId)) { return PlaceholderBounds(); } return ResolveEffectiveBounds(layout, null); } private static WebDashboardMinimapBoundsDto PlaceholderBounds() { return new WebDashboardMinimapBoundsDto { MinX = 0f, MinZ = 0f, MaxX = 1f, MaxZ = 1f }; } private static string TryResolveTileId(DungeonRoom room, Vector3 position, string areaId) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) if (string.IsNullOrWhiteSpace(areaId) || areaId == "outdoor" || !WebDashboardMinimapAreaResolver.IsIndoorAreaId(areaId)) { return string.Empty; } ISpaceGroup val = (ISpaceGroup)(object)WebDashboardMinimapAreaResolver.TryGetIndoorTileGroup(room); if (val == null) { return string.Empty; } try { IVSpace space = val.GetSpace(position); if (((space != null) ? space.Coordinate : null) is TileCoordinate val2) { return $"tile-{val2.TileID}"; } return string.Empty; } catch { return string.Empty; } } private static float ResolveHorizontalYaw(Transform transform) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) Vector3 forward = transform.forward; forward.y = 0f; if (((Vector3)(ref forward)).sqrMagnitude <= 0.0001f) { return transform.eulerAngles.y; } return Mathf.Atan2(forward.x, forward.z) * 57.29578f; } private static WebDashboardMinimapTrainDto? TryFindSceneTrainMarker(GameMainBase main) { //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) try { object? obj = BgRootField?.GetValue(main); Transform val = (Transform)((obj is Transform) ? obj : null); if ((Object)(object)val == (Object)null) { object? obj2 = TramConsoleField?.GetValue(main); Component val2 = (Component)((obj2 is Component) ? obj2 : null); if (val2 != null) { val = val2.transform; while ((Object)(object)val.parent != (Object)null && (Object)(object)val.parent != (Object)(object)val.root) { val = val.parent; } } } if ((Object)(object)val == (Object)null) { return null; } Vector3 position = val.position; return new WebDashboardMinimapTrainDto { X = position.x, Z = position.z, Yaw = ResolveHorizontalYaw(val) }; } catch { return null; } } private static WebDashboardMinimapBoundsDto BuildFallbackBounds(WebDashboardMinimapTrainDto? rawTrain) { float num = rawTrain?.X ?? 0f; float num2 = rawTrain?.Z ?? 0f; float num3 = 3.75f; return new WebDashboardMinimapBoundsDto { MinX = num - 75f - num3, MinZ = num2 - 75f - num3, MaxX = num + 75f + num3, MaxZ = num2 + 75f + num3 }; } private static bool IsPlaceholderBounds(WebDashboardMinimapBoundsDto bounds) { if (bounds.MinX == 0f && bounds.MinZ == 0f && bounds.MaxX == 1f) { return bounds.MaxZ == 1f; } return false; } private static void EnrichFromPlayers(WebDashboardMinimapMarkerDto marker, IReadOnlyList players) { foreach (WebDashboardPlayerDto player in players) { if (player.SteamId == marker.SteamId) { if (!string.IsNullOrWhiteSpace(player.DisplayName)) { marker.DisplayName = player.DisplayName; } marker.IsHost = player.IsHost; marker.IsLocal = player.IsLocal; break; } } } private static WebDashboardMinimapMarkerDto NormalizeMarker(WebDashboardMinimapMarkerDto marker, WebDashboardMinimapBoundsDto bounds) { return new WebDashboardMinimapMarkerDto { SteamId = marker.SteamId, DisplayName = marker.DisplayName, X = NormalizeCoord(marker.X, bounds.MinX, bounds.MaxX), Z = NormalizeCoord(marker.Z, bounds.MinZ, bounds.MaxZ), Yaw = marker.Yaw, RoomName = marker.RoomName, AreaId = marker.AreaId, TileId = marker.TileId, IsAlive = marker.IsAlive, IsHost = marker.IsHost, IsLocal = marker.IsLocal }; } private static float NormalizeCoord(float value, float min, float max) { float num = max - min; if (!(num <= 0f)) { return Mathf.Clamp01((value - min) / num); } return 0.5f; } } internal static class WebDashboardMinimapTileRegistry { private static readonly Dictionary TileIdToAreaId = new Dictionary(); private static readonly Dictionary<(int X, int Y), string> GridCoordToAreaId = new Dictionary<(int, int), string>(); internal static void Clear() { TileIdToAreaId.Clear(); GridCoordToAreaId.Clear(); } internal static void RegisterGraph(WebDashboardMinimapDungeonGraph graph, string areaId) { foreach (KeyValuePair tileId in graph.TileIds) { TileIdToAreaId[tileId.Value] = areaId; } } internal static void RegisterGridGraph(WebDashboardMinimapGridGraph graph, string areaId) { foreach (WebDashboardMinimapGridCell cell in graph.Cells) { GridCoordToAreaId[(cell.Coordinate.X, cell.Coordinate.Y)] = areaId; } } internal static string? TryGetAreaId(int tileId) { if (!TileIdToAreaId.TryGetValue(tileId, out string value)) { return null; } return value; } internal static string? TryGetGridAreaId(int gridX, int gridY) { if (!GridCoordToAreaId.TryGetValue((gridX, gridY), out string value)) { return null; } return value; } } internal static class WebDashboardMinimapTramSpace { private const float BoundsPadding = 0.05f; private const float WaitingRoomMinSpan = 8f; private const float WaitingRoomMaxSpan = 80f; private const float WaitingRoomFallbackHalfSpanX = 18f; private const float WaitingRoomFallbackHalfSpanZ = 30f; private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo? BgRootField = typeof(GameMainBase).GetField("BGRoot", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? TramConsoleField = typeof(GameMainBase).GetField("tramConsole", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static bool IsWaitingRoom(GameMainBase? main) { return main is InTramWaitingScene; } internal static Transform? TryGetAnchor(GameMainBase? main) { if (!((Object)(object)main != (Object)null)) { return null; } object? obj = BgRootField?.GetValue(main); return (Transform?)((obj is Transform) ? obj : null); } internal static Transform? TryGetInteriorScope(GameMainBase? main) { if ((Object)(object)main == (Object)null) { return null; } object? obj = TramConsoleField?.GetValue(main); Component val = (Component)((obj is Component) ? obj : null); if (val != null) { Transform parent = val.transform.parent; if (!((Object)(object)parent != (Object)null)) { return val.transform; } return parent; } return TryGetAnchor(main); } internal static void WorldToLocal(GameMainBase? main, Transform transform, out float x, out float z, out float yaw) { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) Transform val = TryGetAnchor(main); if ((Object)(object)val == (Object)null) { Vector3 position = transform.position; x = position.x; z = position.z; yaw = ResolveHorizontalYaw(transform); } else { Vector3 val2 = val.InverseTransformPoint(transform.position); x = val2.x; z = val2.z; Vector3 val3 = val.InverseTransformDirection(transform.forward); val3.y = 0f; yaw = ((((Vector3)(ref val3)).sqrMagnitude > 0.0001f) ? (Mathf.Atan2(val3.x, val3.z) * 57.29578f) : 0f); } } private static float ResolveHorizontalYaw(Transform transform) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) Vector3 forward = transform.forward; forward.y = 0f; if (((Vector3)(ref forward)).sqrMagnitude <= 0.0001f) { return transform.eulerAngles.y; } return Mathf.Atan2(forward.x, forward.z) * 57.29578f; } internal static WebDashboardMinimapBoundsDto BuildWaitingRoomBounds(GameMainBase? main) { //IL_0062: Unknown result type (might be due to invalid IL or missing references) Transform val = TryGetAnchor(main); Transform val2 = TryGetInteriorScope(main); if ((Object)(object)val == (Object)null || (Object)(object)val2 == (Object)null) { return WaitingRoomFallbackBounds(); } float minX = float.PositiveInfinity; float maxX = float.NegativeInfinity; float minZ = float.PositiveInfinity; float maxZ = float.NegativeInfinity; Renderer[] componentsInChildren = ((Component)val2).GetComponentsInChildren(true); foreach (Renderer val3 in componentsInChildren) { if (!((Object)(object)val3 == (Object)null)) { IncludeWorldBoundsInLocalSpace(val, val3.bounds, ref minX, ref maxX, ref minZ, ref maxZ); } } if (float.IsPositiveInfinity(minX)) { return WaitingRoomFallbackBounds(); } float num = maxX - minX; float num2 = maxZ - minZ; if (num > 80f || num2 > 80f) { return WaitingRoomFallbackBounds(); } num = Mathf.Max(num, 8f); num2 = Mathf.Max(num2, 8f); float num3 = num * 0.05f; float num4 = num2 * 0.05f; return new WebDashboardMinimapBoundsDto { MinX = minX - num3, MinZ = minZ - num4, MaxX = maxX + num3, MaxZ = maxZ + num4 }; } internal static WebDashboardMinimapTrainDto CreateWaitingRoomTrainMarker() { return new WebDashboardMinimapTrainDto { X = 0f, Z = 0f, Yaw = 0f }; } private static WebDashboardMinimapBoundsDto WaitingRoomFallbackBounds() { float num = 0.90000004f; float num2 = 1.5f; return new WebDashboardMinimapBoundsDto { MinX = -18f - num, MinZ = -30f - num2, MaxX = 18f + num, MaxZ = 30f + num2 }; } private static void IncludeWorldBoundsInLocalSpace(Transform anchor, Bounds worldBounds, ref float minX, ref float maxX, ref float minZ, ref float maxZ) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) Vector3 center = ((Bounds)(ref worldBounds)).center; Vector3 extents = ((Bounds)(ref worldBounds)).extents; for (int i = -1; i <= 1; i += 2) { for (int j = -1; j <= 1; j += 2) { for (int k = -1; k <= 1; k += 2) { Vector3 val = center + Vector3.Scale(extents, new Vector3((float)i, (float)j, (float)k)); Vector3 val2 = anchor.InverseTransformPoint(val); minX = Mathf.Min(minX, val2.x); maxX = Mathf.Max(maxX, val2.x); minZ = Mathf.Min(minZ, val2.z); maxZ = Mathf.Max(maxZ, val2.z); } } } } } internal static class WebDashboardModerationService { private const string Feature = "WebDashboard"; internal static WebDashboardActionResult Execute(WebDashboardPendingAction action) { if (!WebDashboardGameState.IsHost()) { return Fail("Host only."); } if (action.SteamId != 0L && LocalPlayerHelper.IsLocalSteamId(action.SteamId)) { return Fail("Cannot moderate the local host player."); } SessionManager sessionManager = WebDashboardSessionAccess.GetSessionManager(); if (sessionManager == null) { return Fail("Session manager unavailable."); } return action.Type switch { WebDashboardActionType.Kick => Kick(sessionManager, action), WebDashboardActionType.Ban => Ban(sessionManager, action), WebDashboardActionType.Unban => Unban(sessionManager, action), _ => Fail("Unknown action."), }; } private static WebDashboardActionResult Kick(SessionManager sessionManager, WebDashboardPendingAction action) { if (!TryResolveTarget(action, out SessionContext targetContext, out long playerUid)) { return Fail("Player not found."); } if (!TryGetHostKickContext(sessionManager, out VPlayer hostPlayer, out int hashCode)) { return Fail("Host player context unavailable."); } if (!WebDashboardSessionAccess.TryGetSessionId(targetContext, out var sessionId)) { return Fail("Session ID unavailable."); } try { return DisconnectPlayer(sessionManager, hostPlayer, playerUid, hashCode, sessionId, (DisconnectReason)3, "Kicked", "Player kicked."); } catch (Exception ex) { ModLog.Warn("WebDashboard", "Kick failed: " + ex.Message); return Fail("Kick failed."); } } private static WebDashboardActionResult Ban(SessionManager sessionManager, WebDashboardPendingAction action) { if (action.SteamId == 0L) { return Fail("Invalid Steam ID."); } if (!WebDashboardSessionAccess.TryAddBan(sessionManager, action.SteamId)) { if (!WebDashboardSessionAccess.IsBanned(sessionManager, action.SteamId)) { return Fail("Failed to add ban."); } return Ok("Player already banned."); } ModLog.Info("WebDashboard", $"Banned steam={action.SteamId}."); if (TryResolveTarget(action, out SessionContext targetContext, out long playerUid) && playerUid != 0L && TryGetHostKickContext(sessionManager, out VPlayer hostPlayer, out int hashCode) && WebDashboardSessionAccess.TryGetSessionId(targetContext, out var sessionId)) { try { WebDashboardActionResult webDashboardActionResult = DisconnectPlayer(sessionManager, hostPlayer, playerUid, hashCode, sessionId, (DisconnectReason)9, "Banned and kicked", "Player banned."); if (!webDashboardActionResult.Success) { return Ok("Player banned (disconnect may have failed if already offline)."); } return webDashboardActionResult; } catch (Exception ex) { ModLog.Warn("WebDashboard", "Ban disconnect failed: " + ex.Message); return Ok("Player banned (disconnect may have failed if already offline)."); } } return Ok("Player banned."); } private static WebDashboardActionResult DisconnectPlayer(SessionManager sessionManager, VPlayer hostPlayer, long playerUid, int hashCode, long sessionId, DisconnectReason reason, string logAction, string successMessage) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Expected O, but got Unknown //IL_000e: Unknown result type (might be due to invalid IL or missing references) //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_0026: Expected O, but got Unknown //IL_0029: Unknown result type (might be due to invalid IL or missing references) ((VActor)hostPlayer).SendToMe((IMsg)new KickPlayerRes(hashCode) { kickPlayerUID = playerUid }); sessionManager.BroadcastToAll((IMsg)new KickPlayerSig { kickPlayerUID = playerUid }); WebDashboardSessionAccess.DisconnectSession(sessionManager, sessionId, reason); ModLog.Info("WebDashboard", $"{logAction} player uid={playerUid}."); return Ok(successMessage); } private static WebDashboardActionResult Unban(SessionManager sessionManager, WebDashboardPendingAction action) { if (action.SteamId == 0L) { return Fail("Invalid Steam ID."); } if (!WebDashboardSessionAccess.TryRemoveBan(sessionManager, action.SteamId)) { return Fail("Player was not banned."); } ModLog.Info("WebDashboard", $"Unbanned steam={action.SteamId}."); return Ok("Ban removed."); } private static bool TryResolveTarget(WebDashboardPendingAction action, out SessionContext? targetContext, out long playerUid) { targetContext = null; playerUid = action.PlayerUid; SessionManager sessionManager = WebDashboardSessionAccess.GetSessionManager(); if (playerUid != 0L && sessionManager != null) { foreach (SessionContext item in WebDashboardSessionAccess.EnumerateSessionContexts(sessionManager)) { if (item.GetPlayerUID() == playerUid) { targetContext = item; return true; } } } if (action.SteamId == 0L) { return false; } SessionManager sessionManager2 = WebDashboardSessionAccess.GetSessionManager(); if (sessionManager2 == null) { return false; } foreach (SessionContext item2 in WebDashboardSessionAccess.EnumerateSessionContexts(sessionManager2)) { if (item2.SteamID == action.SteamId) { targetContext = item2; playerUid = item2.GetPlayerUID(); return playerUid != 0; } } return false; } private static bool TryGetHostKickContext(SessionManager sessionManager, out VPlayer? hostPlayer, out int hashCode) { hostPlayer = null; hashCode = 0; SessionContext val = WebDashboardSessionAccess.FindHostSessionContext(sessionManager); if (val == null) { return false; } hostPlayer = WebDashboardSessionAccess.GetVPlayer(val); hashCode = WebDashboardSessionAccess.GetEnterPktHashCode(val); return hostPlayer != null; } private static WebDashboardActionResult Ok(string message) { return new WebDashboardActionResult { Success = true, Message = message }; } private static WebDashboardActionResult Fail(string message) { return new WebDashboardActionResult { Success = false, Message = message }; } } internal static class WebDashboardPatches { [HarmonyPatch] internal static class NetworkGradeSigPatch { private static MethodBase TargetMethod() { return AccessTools.Method(typeof(GameMainBase), "OnPacket", new Type[1] { typeof(NetworkGradeSig) }, (Type[])null) ?? throw new InvalidOperationException("OnPacket(NetworkGradeSig) not found"); } private static void Postfix(NetworkGradeSig sig) { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Expected I4, but got Unknown if (((sig != null) ? sig.grades : null) == null) { return; } foreach (KeyValuePair grade in sig.grades) { GradeByPlayerUid[grade.Key] = (int)grade.Value; } WebDashboardSnapshotCache.MarkDirty(); } } [HarmonyPatch(typeof(UIPrefab_InGameMenu), "GetSteamAvatar")] internal static class SteamAvatarLoadedPatch { private static void Postfix(CSteamID steamID, Texture2D __result) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)__result == (Object)null) && WebDashboardGameAvatarSource.OnAvatarLoaded(steamID.m_SteamID, __result)) { WebDashboardSnapshotCache.MarkDirty(); } } } [HarmonyPatch(typeof(UIPrefab_InGameMenu), "SetRemoteVolumeController_v2")] internal static class VolumeControllerAvatarSyncPatch { private static void Postfix(UIPrefab_InGameMenu __instance) { if (WebDashboardGameAvatarSource.SyncFromInGameMenu(__instance)) { WebDashboardSnapshotCache.MarkDirty(); } } } private static readonly ConcurrentDictionary GradeByPlayerUid = new ConcurrentDictionary(); internal static void Apply(Harmony harmony) { HarmonyPatchHelper.ApplyPatchTypes(harmony, "WebDashboard", HarmonyPatchHelper.GetNestedPatchTypes(typeof(WebDashboardPatches))); } internal static bool TryGetCachedGrade(long playerUid, out int grade) { return GradeByPlayerUid.TryGetValue(playerUid, out grade); } } internal static class WebDashboardPlayerService { internal static List CollectPlayers() { Dictionary dictionary = new Dictionary(); SessionManager sessionManager = WebDashboardSessionAccess.GetSessionManager(); ulong num = LocalPlayerHelper.TryGetLocalSteamId(); Dictionary nameCache = TryGetSteamNameCache(); bool flag = WebDashboardGameState.IsHost(); if (sessionManager != null) { foreach (SessionContext item in WebDashboardSessionAccess.EnumerateSessionContexts(sessionManager)) { WebDashboardPlayerDto webDashboardPlayerDto = TryBuildPlayerDto(item, sessionManager, num, nameCache, flag); if (webDashboardPlayerDto != null) { dictionary[webDashboardPlayerDto.SteamId] = webDashboardPlayerDto; } } } if (ModConfig.EnableStatistics.Value) { foreach (ulong connectedSteamId in StatisticsTracker.GetConnectedSteamIds()) { if (connectedSteamId != 0L && !dictionary.ContainsKey(connectedSteamId)) { WebDashboardPlayerDto webDashboardPlayerDto2 = BuildFallbackPlayerDto(connectedSteamId, sessionManager, num, nameCache, flag); if (webDashboardPlayerDto2 != null) { dictionary[connectedSteamId] = webDashboardPlayerDto2; } } } } if (flag && num != 0L && !dictionary.ContainsKey(num)) { WebDashboardPlayerDto webDashboardPlayerDto3 = BuildFallbackPlayerDto(num, sessionManager, num, nameCache, flag, forceHost: true); if (webDashboardPlayerDto3 != null) { dictionary[num] = webDashboardPlayerDto3; } } if (flag && sessionManager != null) { foreach (ulong item2 in WebDashboardSessionAccess.EnumerateBannedSteamIds(sessionManager)) { if (item2 != 0L && !dictionary.ContainsKey(item2)) { WebDashboardPlayerDto webDashboardPlayerDto4 = BuildFallbackPlayerDto(item2, sessionManager, num, nameCache, flag); if (webDashboardPlayerDto4 != null) { webDashboardPlayerDto4.IsBanned = true; dictionary[item2] = webDashboardPlayerDto4; } } } } List list = dictionary.Values.ToList(); list.Sort(delegate(WebDashboardPlayerDto a, WebDashboardPlayerDto b) { int num2 = b.IsHost.CompareTo(a.IsHost); return (num2 == 0) ? string.Compare(a.DisplayName, b.DisplayName, StringComparison.OrdinalIgnoreCase) : num2; }); return list; } internal static bool TryFindPlayer(ulong steamId, out WebDashboardPlayerDto? player) { player = null; if (steamId == 0L) { return false; } foreach (WebDashboardPlayerDto item in CollectPlayers()) { if (item.SteamId == steamId) { player = item; return true; } } return false; } internal static string ResolveDisplayNameForSteamId(ulong steamId, int saveSlotId = -1) { if (steamId == 0L) { return ""; } if (TryFindPlayer(steamId, out WebDashboardPlayerDto player) && player != null && !string.IsNullOrWhiteSpace(player.DisplayName) && player.DisplayName != steamId.ToString()) { return player.DisplayName; } PlayerStatisticsDocument playerStatisticsDocument = StatisticsTracker.TryGetPlayerDocument(steamId); if (playerStatisticsDocument != null && !string.IsNullOrWhiteSpace(playerStatisticsDocument.DisplayName) && playerStatisticsDocument.DisplayName != steamId.ToString()) { return playerStatisticsDocument.DisplayName; } if (saveSlotId >= 0) { PlayerStatisticsDocument playerStatisticsDocument2 = StatisticsStore.LoadPlayer(saveSlotId, steamId); if (!string.IsNullOrWhiteSpace(playerStatisticsDocument2.DisplayName) && playerStatisticsDocument2.DisplayName != steamId.ToString()) { return playerStatisticsDocument2.DisplayName; } } string text = TryGetLeaderboardDisplayName(saveSlotId, steamId); if (!string.IsNullOrWhiteSpace(text)) { return text; } Dictionary dictionary = TryGetSteamNameCache(); if (dictionary != null && dictionary.TryGetValue(steamId, out var value) && !string.IsNullOrWhiteSpace(value)) { return value; } string text2 = ResolveNickNameFromActorMap(0L, steamId); if (!string.IsNullOrWhiteSpace(text2)) { return text2; } string text3 = TryGetLocalNickName(); if (text3 == null || !LocalPlayerHelper.IsLocalSteamId(steamId)) { return steamId.ToString(); } return text3; } private static WebDashboardPlayerDto? BuildFallbackPlayerDto(ulong steamId, SessionManager? sessionManager, ulong localSteamId, Dictionary? nameCache, bool dashboardIsHost, bool forceHost = false) { long num = 0L; SessionContext context = null; if (sessionManager != null) { foreach (SessionContext item in WebDashboardSessionAccess.EnumerateSessionContexts(sessionManager)) { if (item.SteamID == steamId) { context = item; try { num = item.GetPlayerUID(); } catch { } break; } } } bool flag = localSteamId != 0L && steamId == localSteamId; bool isHost = forceHost || (dashboardIsHost && flag); WebDashboardPlayerDto webDashboardPlayerDto = new WebDashboardPlayerDto { SteamId = steamId, PlayerUid = num, DisplayName = ResolveDisplayName(null, steamId, num, nameCache), IsHost = isHost, IsLocal = flag, IsBanned = (sessionManager != null && WebDashboardSessionAccess.IsBanned(sessionManager, steamId)) }; if (sessionManager != null && num != 0L && (WebDashboardSessionAccess.TryGetNetworkGrade(sessionManager, num, out var grade) || WebDashboardPatches.TryGetCachedGrade(num, out grade))) { webDashboardPlayerDto.NetworkGrade = grade; } EnrichPlayerDto(webDashboardPlayerDto, sessionManager, context, dashboardIsHost); return webDashboardPlayerDto; } private static WebDashboardPlayerDto? TryBuildPlayerDto(SessionContext context, SessionManager sessionManager, ulong localSteamId, Dictionary? nameCache, bool dashboardIsHost) { try { ulong num = context.SteamID; if (num == 0 && dashboardIsHost && LocalPlayerHelper.IsLocalSteamId(localSteamId)) { num = localSteamId; } if (num == 0L) { return null; } long num2 = 0L; try { num2 = context.GetPlayerUID(); } catch { } VPlayer? vPlayer = WebDashboardSessionAccess.GetVPlayer(context); bool flag = vPlayer != null && vPlayer.IsHost; if (!flag) { if (WebDashboardSessionAccess.IsHostSessionContext(sessionManager, context)) { flag = true; } else if (dashboardIsHost && localSteamId != 0L && num == localSteamId) { flag = true; } } WebDashboardPlayerDto webDashboardPlayerDto = new WebDashboardPlayerDto { SteamId = num, PlayerUid = num2, DisplayName = ResolveDisplayName(context, num, num2, nameCache), IsHost = flag, IsLocal = (localSteamId != 0L && num == localSteamId), IsBanned = WebDashboardSessionAccess.IsBanned(sessionManager, num) }; if (num2 != 0L && (WebDashboardSessionAccess.TryGetNetworkGrade(sessionManager, num2, out var grade) || WebDashboardPatches.TryGetCachedGrade(num2, out grade))) { webDashboardPlayerDto.NetworkGrade = grade; } EnrichPlayerDto(webDashboardPlayerDto, sessionManager, context, dashboardIsHost); return webDashboardPlayerDto; } catch { return null; } } private static string ResolveDisplayName(SessionContext? context, ulong steamId, long playerUid, Dictionary? nameCache) { if (!string.IsNullOrWhiteSpace((context != null) ? context.NickName : null)) { return context.NickName; } if (nameCache != null && nameCache.TryGetValue(steamId, out string value) && !string.IsNullOrWhiteSpace(value)) { return value; } string text = ResolveNickNameFromActorMap(playerUid, steamId); if (!string.IsNullOrWhiteSpace(text)) { return text; } PlayerStatisticsDocument playerStatisticsDocument = StatisticsTracker.TryGetPlayerDocument(steamId); if (playerStatisticsDocument != null && !string.IsNullOrWhiteSpace(playerStatisticsDocument.DisplayName) && playerStatisticsDocument.DisplayName != steamId.ToString()) { return playerStatisticsDocument.DisplayName; } string text2 = TryGetLocalNickName(); if (text2 == null || !LocalPlayerHelper.IsLocalSteamId(steamId)) { return steamId.ToString(); } return text2; } private static string? ResolveNickNameFromActorMap(long playerUid, ulong steamId = 0uL) { try { GameMainBase val = JoinAnytimeHub.GetPdata()?.main; if ((Object)(object)val == (Object)null) { return null; } Dictionary protoActorMap = val.GetProtoActorMap(); if (protoActorMap == null) { return null; } foreach (ProtoActor value in protoActorMap.Values) { if (!((Object)(object)value == (Object)null) && !string.IsNullOrWhiteSpace(value.nickName)) { if (playerUid != 0L && value.UID == playerUid) { return value.nickName; } if (steamId != 0L && StatisticsTracker.TryResolveSteamId(value) == steamId) { return value.nickName; } } } } catch { } return null; } private static void EnrichPlayerDto(WebDashboardPlayerDto dto, SessionManager? sessionManager, SessionContext? context, bool dashboardIsHost) { ApplyConnectionInfo(dto, context); if (dashboardIsHost && ModConfig.EnableStatistics.Value) { dto.CurrentSession = BuildSessionStats(dto.SteamId); } } private static void ApplyConnectionInfo(WebDashboardPlayerDto dto, SessionContext? context) { SpeechEventArchive val = FindArchive(dto.PlayerUid, dto.SteamId); PlayerConnectionInfo playerConnectionInfo = null; if ((Object)(object)val != (Object)null && VoiceEventStats.TryGetConnectionInfo(val, context, dto.SteamId, out PlayerConnectionInfo info)) { playerConnectionInfo = info; } if (context != null && VoiceEventStats.TryGetConnectionInfo(context, dto.PlayerUid, dto.SteamId, dto.IsLocal, out PlayerConnectionInfo info2)) { if (playerConnectionInfo == null) { playerConnectionInfo = info2; } else { if (IsUnavailableConnectionAddress(playerConnectionInfo.ConnectionAddress) && !IsUnavailableConnectionAddress(info2.ConnectionAddress)) { playerConnectionInfo.ConnectionAddress = info2.ConnectionAddress; } if (playerConnectionInfo.PlayerUid == 0L && info2.PlayerUid != 0L) { playerConnectionInfo.PlayerUid = info2.PlayerUid; } if (playerConnectionInfo.SteamId == 0L && info2.SteamId != 0L) { playerConnectionInfo.SteamId = info2.SteamId; } } } if (playerConnectionInfo != null) { ApplyConnectionFields(dto, playerConnectionInfo); return; } dto.ConnectionRole = (dto.IsLocal ? "host" : "client"); dto.ConnectionAddress = (dto.IsLocal ? "local" : "(unavailable)"); dto.VoiceEventCount = (((Object)(object)val != (Object)null) ? VoiceEventStats.GetEventCount(val) : 0); } private static bool IsUnavailableConnectionAddress(string address) { if (!string.IsNullOrEmpty(address)) { return address == "(unavailable)"; } return true; } private static void ApplyConnectionFields(WebDashboardPlayerDto dto, PlayerConnectionInfo info) { if (info.PlayerUid != 0L) { dto.PlayerUid = info.PlayerUid; } if (!string.IsNullOrWhiteSpace(info.DisplayName) && info.DisplayName != "(pending)") { dto.DisplayName = info.DisplayName; } dto.ConnectionRole = info.ConnectionRole; dto.ConnectionAddress = info.ConnectionAddress; dto.VoiceEventCount = info.VoiceEventCount; if (info.SteamId != 0L) { dto.SteamId = info.SteamId; } } private static SpeechEventArchive? FindArchive(long playerUid, ulong steamId) { foreach (SpeechEventArchive item in SpeechEventArchiveRegistry.EnumerateActive()) { if (!((Object)(object)item == (Object)null)) { long playerUID; try { playerUID = item.PlayerUID; } catch { continue; } if (playerUid != 0L && playerUID == playerUid) { return item; } if (steamId != 0L && VoiceEventStats.TryGetConnectionInfo(item, out PlayerConnectionInfo info) && info.SteamId == steamId) { return item; } } } return null; } private static WebDashboardSessionStatsDto? BuildSessionStats(ulong steamId) { if (steamId == 0L) { return null; } PlayerStatisticsDocument playerStatisticsDocument = StatisticsTracker.TryGetPlayerDocument(steamId); if (playerStatisticsDocument == null || playerStatisticsDocument.CurrentSession?.Counters == null) { return null; } StatCounters counters = playerStatisticsDocument.CurrentSession.Counters; return new WebDashboardSessionStatsDto { CurrencyEarned = counters.CurrencyEarned, Kills = counters.Kills, Deaths = counters.Deaths, Revives = counters.Revives, MimicEncounterCount = counters.MimicEncounterCount, ItemCarryCount = counters.ItemCarryCount, VoiceEvents = counters.VoiceEvents, DamageToAlly = counters.DamageToAlly, TotalConnectedSeconds = counters.TotalConnectedSeconds }; } private static string? TryGetLeaderboardDisplayName(int saveSlotId, ulong steamId) { if (saveSlotId < 0) { saveSlotId = WebDashboardGameState.GetSaveSlotId(); } if (saveSlotId >= 0) { return WebDashboardStatisticsBridge.TryGetDisplayNameFromLeaderboard(WebDashboardStatisticsBridge.GetLeaderboardDocument(saveSlotId), steamId); } return null; } private static string? TryGetLocalNickName() { try { PersistentData pdata = JoinAnytimeHub.GetPdata(); GameMainBase val = pdata?.main; if ((Object)(object)val != (Object)null && ((object)val).GetType().GetMethod("GetHostActorNickName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.Invoke(val, null) is string text && !string.IsNullOrWhiteSpace(text)) { return text; } if (typeof(PersistentData).GetField("MyNickName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(pdata) is string text2 && !string.IsNullOrWhiteSpace(text2)) { return text2; } } catch { } return null; } private static Dictionary? TryGetSteamNameCache() { try { GameMainBase val = JoinAnytimeHub.GetPdata()?.main; if ((Object)(object)val == (Object)null) { return null; } return ((object)val).GetType().GetField("steamIDToNameCache", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(val) as Dictionary; } catch { return null; } } } internal static class WebDashboardRouter { private static string _assetsRoot = ""; internal static void SetAssetsRoot(string path) { _assetsRoot = path; WebDashboardAvatarService.SetAssetsRoot(path); } internal static void Handle(HttpListenerContext context) { try { string method = context.Request.HttpMethod.ToUpperInvariant(); string text = NormalizePath(context.Request.Url?.AbsolutePath ?? "/"); if (text.StartsWith("/api/", StringComparison.Ordinal)) { HandleApi(context, method, text); } else { ServeStatic(context, text); } } catch (Exception ex) { ModLog.Warn("WebDashboard", "Request failed: " + ex.Message); TryWriteError(context, 500, "Internal server error."); } } private static void HandleApi(HttpListenerContext context, string method, string path) { if (path == "/api/events" && method == "GET") { WebDashboardSseHub.Subscribe(context); return; } WebDashboardSnapshot webDashboardSnapshot = WebDashboardSnapshotCache.Get(); if (path == "/api/status" && method == "GET") { WriteJson(context, 200, WebDashboardJson.SerializeStatus(webDashboardSnapshot.Status)); } else if (path == "/api/players" && method == "GET") { WriteJson(context, 200, WebDashboardJson.SerializePlayers(webDashboardSnapshot.Players)); } else if (path == "/api/minimap" && method == "GET") { HandleMinimapApi(context, webDashboardSnapshot); } else if (path == "/api/leaderboard" && method == "GET") { if (!webDashboardSnapshot.Status.IsHost) { WriteJson(context, 403, WebDashboardJson.SerializeError(403, "Host only.")); } else if (string.IsNullOrEmpty(webDashboardSnapshot.LeaderboardJson)) { WriteJson(context, 200, "{\"saveSlotId\":-1,\"connectedSteamIds\":[],\"entries\":[]}"); } else { WriteJson(context, 200, webDashboardSnapshot.LeaderboardJson); } } else if (path == "/api/settings" && method == "GET") { if (!webDashboardSnapshot.Status.IsHost) { WriteJson(context, 403, WebDashboardJson.SerializeError(403, "Host only.")); } else { WriteJson(context, 200, WebDashboardJson.SerializeSettings(WebDashboardConfigBridge.BuildSettings())); } } else if (path == "/api/settings" && method == "POST") { if (!webDashboardSnapshot.Status.IsHost) { WriteJson(context, 403, WebDashboardJson.SerializeError(403, "Host only.")); return; } WebDashboardConfigUpdateRequest webDashboardConfigUpdateRequest = ModJson.Deserialize(ReadRequestBody(context.Request)); if (webDashboardConfigUpdateRequest == null || string.IsNullOrWhiteSpace(webDashboardConfigUpdateRequest.SectionId) || string.IsNullOrWhiteSpace(webDashboardConfigUpdateRequest.Key)) { WriteJson(context, 400, WebDashboardJson.SerializeError(400, "Invalid settings update request.")); return; } WebDashboardConfigUpdateResult webDashboardConfigUpdateResult = WebDashboardConfigUpdateQueue.EnqueueAndWait(webDashboardConfigUpdateRequest.SectionId, webDashboardConfigUpdateRequest.Key, webDashboardConfigUpdateRequest.Value ?? ""); WriteJson(context, webDashboardConfigUpdateResult.Success ? 200 : 400, WebDashboardJson.SerializeConfigUpdateResult(webDashboardConfigUpdateResult)); } else if (path.StartsWith("/api/players/", StringComparison.Ordinal)) { HandlePlayerApi(context, method, path, webDashboardSnapshot); } else { WriteJson(context, 404, WebDashboardJson.SerializeError(404, "Not found.")); } } private static void HandleMinimapApi(HttpListenerContext context, WebDashboardSnapshot snapshot) { if (!snapshot.Status.InSession) { WriteJson(context, 404, WebDashboardJson.SerializeError(404, "No active session.")); return; } NameValueCollection queryString = context.Request.QueryString; bool flag = string.Equals(queryString["showAll"], "true", StringComparison.OrdinalIgnoreCase); if (flag && !snapshot.Status.IsHost) { WriteJson(context, 403, WebDashboardJson.SerializeError(403, "Host only.")); return; } ulong result = 0uL; string text = queryString["focusSteamId"]; if (!string.IsNullOrWhiteSpace(text) && !ulong.TryParse(text, out result)) { WriteJson(context, 400, WebDashboardJson.SerializeError(400, "Invalid focusSteamId.")); return; } List markers = WebDashboardMinimapService.FilterMarkers(snapshot.MinimapMarkers, result, flag, snapshot.Status.IsHost); WriteJson(context, 200, WebDashboardJson.SerializeMinimap(snapshot.MinimapLayout, markers, snapshot.MinimapTrain)); } private static void HandlePlayerApi(HttpListenerContext context, string method, string path, WebDashboardSnapshot snapshot) { string text = path; int length = "/api/players/".Length; string text2 = text.Substring(length, text.Length - length); int num = text2.IndexOf('/'); string s = ((num >= 0) ? text2.Substring(0, num) : text2); object obj; if (num < 0) { obj = ""; } else { text = text2; length = num + 1; obj = text.Substring(length, text.Length - length); } string text3 = (string)obj; if (!ulong.TryParse(s, out var result) || result == 0L) { WriteJson(context, 400, WebDashboardJson.SerializeError(400, "Invalid Steam ID.")); return; } if (text3 == "avatar" && method == "GET") { WebDashboardAvatarService.TryServe(context, result); return; } if (text3 == "stats" && method == "GET") { if (!snapshot.Status.IsHost) { WriteJson(context, 403, WebDashboardJson.SerializeError(403, "Host only.")); return; } if (snapshot.PlayerStatsJson.TryGetValue(result, out string value)) { WriteJson(context, 200, value); return; } int saveSlotId = snapshot.Status.SaveSlotId; if (saveSlotId < 0) { WriteJson(context, 404, WebDashboardJson.SerializeError(404, "No active save slot.")); return; } string text4 = WebDashboardStatisticsBridge.BuildPlayerStatsJson(saveSlotId, result); if (string.IsNullOrEmpty(text4)) { WriteJson(context, 404, WebDashboardJson.SerializeError(404, "Player statistics not found.")); } else { WriteJson(context, 200, text4); } return; } if (!snapshot.Status.IsHost) { WriteJson(context, 403, WebDashboardJson.SerializeError(403, "Host only.")); return; } if (method != "POST") { WriteJson(context, 405, WebDashboardJson.SerializeError(405, "Method not allowed.")); return; } WebDashboardActionType? webDashboardActionType = text3 switch { "kick" => WebDashboardActionType.Kick, "ban" => WebDashboardActionType.Ban, "unban" => WebDashboardActionType.Unban, _ => null, }; if (!webDashboardActionType.HasValue) { WriteJson(context, 404, WebDashboardJson.SerializeError(404, "Not found.")); return; } long playerUid = 0L; foreach (WebDashboardPlayerDto player in snapshot.Players) { if (player.SteamId == result) { playerUid = player.PlayerUid; break; } } WebDashboardActionQueue.Enqueue(new WebDashboardPendingAction { Type = webDashboardActionType.Value, SteamId = result, PlayerUid = playerUid }); WriteJson(context, 202, WebDashboardJson.SerializeActionResult(new WebDashboardActionResult { Success = true, Message = "Action queued." })); } private static void ServeStatic(HttpListenerContext context, string path) { if (string.IsNullOrEmpty(_assetsRoot) || !Directory.Exists(_assetsRoot)) { WriteText(context, 503, "text/plain", "Web dashboard assets not found."); return; } string path2 = ((path == "/") ? "index.html" : path.TrimStart('/')); string text = Path.GetFullPath(Path.Combine(_assetsRoot, path2)); string fullPath = Path.GetFullPath(_assetsRoot); if (!text.StartsWith(fullPath, StringComparison.OrdinalIgnoreCase)) { WriteText(context, 403, "text/plain", "Forbidden."); return; } if (!File.Exists(text)) { string text2 = Path.Combine(_assetsRoot, "index.html"); if (!File.Exists(text2)) { WriteText(context, 404, "text/plain", "Not found."); return; } text = text2; } string contentType = GetContentType(Path.GetExtension(text)); byte[] array = File.ReadAllBytes(text); context.Response.StatusCode = 200; context.Response.ContentType = contentType; context.Response.ContentLength64 = array.Length; context.Response.OutputStream.Write(array, 0, array.Length); context.Response.OutputStream.Close(); } private static string GetContentType(string extension) { switch (extension.ToLowerInvariant()) { case ".html": return "text/html; charset=utf-8"; case ".css": return "text/css; charset=utf-8"; case ".js": return "application/javascript; charset=utf-8"; case ".json": return "application/json; charset=utf-8"; case ".svg": return "image/svg+xml"; case ".png": return "image/png"; case ".jpeg": case ".jpg": return "image/jpeg"; case ".webp": return "image/webp"; case ".ico": return "image/x-icon"; default: return "application/octet-stream"; } } private static string NormalizePath(string path) { if (string.IsNullOrEmpty(path)) { return "/"; } int num = path.IndexOf('?'); if (num >= 0) { path = path.Substring(0, num); } if (!path.EndsWith('/') || path.Length <= 1) { return path; } string text = path; return text.Substring(0, text.Length - 1); } private static string ReadRequestBody(HttpListenerRequest request) { if (!request.HasEntityBody) { return ""; } using StreamReader streamReader = new StreamReader(request.InputStream, request.ContentEncoding ?? Encoding.UTF8); return streamReader.ReadToEnd(); } private static void WriteJson(HttpListenerContext context, int statusCode, string json) { WriteText(context, statusCode, "application/json; charset=utf-8", json); } private static void WriteText(HttpListenerContext context, int statusCode, string contentType, string body) { byte[] bytes = Encoding.UTF8.GetBytes(body); context.Response.StatusCode = statusCode; context.Response.ContentType = contentType; context.Response.ContentLength64 = bytes.Length; context.Response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; context.Response.OutputStream.Write(bytes, 0, bytes.Length); context.Response.OutputStream.Close(); } private static void TryWriteError(HttpListenerContext context, int statusCode, string message) { try { WriteJson(context, statusCode, WebDashboardJson.SerializeError(statusCode, message)); } catch { } } } internal static class WebDashboardServer { private const string Feature = "WebDashboard"; private static HttpListener? _listener; private static Thread? _listenerThread; private static volatile bool _running; private static string _listenUrl = ""; private static string _assetsRoot = ""; internal static void Apply(Harmony harmony) { ModConfig.Changed += OnConfigChanged; WebDashboardPatches.Apply(harmony); WebDashboardMinimapPatches.Apply(harmony); } private static void OnConfigChanged() { WebDashboardSnapshotCache.MarkDirty(); } internal static void SyncFromConfig() { if (!ModConfig.IsInitialized) { return; } if (!ModConfig.EnableWebDashboard.Value) { Stop(); return; } string text = ModConfig.WebDashboardListenAddress.Value?.Trim() ?? "127.0.0.1"; int value = ModConfig.WebDashboardListenPort.Value; if ((value < 1 || value > 65535) ? true : false) { ModLog.Warn("WebDashboard", $"Invalid port {value}; web dashboard not started."); Stop(); return; } if (!IsLoopback(text)) { ModLog.Warn("WebDashboard", $"Binding to {text}:{value} exposes the dashboard on the network. Use 127.0.0.1 unless you trust your LAN."); } string text2 = $"http://{text}:{value}/"; if (!string.Equals(text2, _listenUrl, StringComparison.OrdinalIgnoreCase) || !_running) { Stop(); Start(text2); } } internal static void OnUpdate() { if (_running) { WebDashboardActionQueue.Process(); WebDashboardConfigUpdateQueue.Process(); WebDashboardSnapshotCache.Tick(_listenUrl); } } internal static void StopOnDeinit() { ModConfig.Changed -= OnConfigChanged; Stop(); } private static void Start(string prefix) { _assetsRoot = ResolveAssetsRoot(); if (string.IsNullOrEmpty(_assetsRoot) || !Directory.Exists(_assetsRoot)) { ModLog.Error("WebDashboard", "Assets folder not found at expected path next to the mod DLL."); return; } WebDashboardRouter.SetAssetsRoot(_assetsRoot); WebDashboardSseHub.Start(); try { HttpListener httpListener = new HttpListener(); httpListener.Prefixes.Add(prefix); httpListener.Start(); _listener = httpListener; _listenUrl = prefix; _running = true; WebDashboardSnapshotCache.MarkDirty(); WebDashboardSnapshotCache.Refresh(_listenUrl); _listenerThread = new Thread(ListenLoop) { IsBackground = true, Name = "MimesisWebDashboard" }; _listenerThread.Start(); ModLog.Info("WebDashboard", "Listening at " + _listenUrl.TrimEnd('/')); } catch (Exception ex) { ModLog.Error("WebDashboard", "Failed to start HTTP listener at " + prefix + ": " + ex.Message); Stop(); } } private static void Stop() { _running = false; WebDashboardSseHub.Shutdown(); HttpListener listener = _listener; _listener = null; _listenUrl = ""; if (listener != null) { try { listener.Stop(); listener.Close(); } catch { } } Thread listenerThread = _listenerThread; _listenerThread = null; if (listenerThread != null && listenerThread.IsAlive && listenerThread != Thread.CurrentThread) { try { listenerThread.Join(500); } catch { } } } private static void ListenLoop() { while (_running && _listener != null) { try { HttpListenerContext context = _listener.GetContext(); ThreadPool.QueueUserWorkItem(delegate { WebDashboardRouter.Handle(context); }); } catch (HttpListenerException) when (!_running) { break; } catch (ObjectDisposedException) when (!_running) { break; } catch (Exception ex3) { if (_running) { ModLog.Warn("WebDashboard", "Accept loop error: " + ex3.Message); } } } } private static string ResolveAssetsRoot() { try { string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); return string.IsNullOrEmpty(directoryName) ? "" : Path.Combine(directoryName, "MimesisPlayerEnhancement", "assets"); } catch { return ""; } } private static bool IsLoopback(string address) { if (!string.Equals(address, "127.0.0.1", StringComparison.OrdinalIgnoreCase) && !string.Equals(address, "localhost", StringComparison.OrdinalIgnoreCase)) { return string.Equals(address, "::1", StringComparison.OrdinalIgnoreCase); } return true; } } internal static class WebDashboardSessionAccess { private const BindingFlags InstanceMemberFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo? HostSessionContextField = typeof(SessionManager).GetField("_hostSessionContext", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? ContextsField = typeof(SessionManager).GetField("m_Contexts", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? NetworkGradesField = typeof(SessionManager).GetField("_networkGrades", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? BannedSteamIdsField = typeof(SessionManager).GetField("_bannedSteamIDs", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? SessionVPlayerField = typeof(SessionContext).GetField("_vPlayer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? EnterPktHashCodeField = typeof(SessionContext).GetField("_enterPktHashCode", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static SessionManager? GetSessionManager() { try { if ((Object)(object)Hub.s == (Object)null) { return null; } object hubMember = MimesisSaveManager.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; } } internal static IEnumerable EnumerateSessionContexts(SessionManager sessionManager) { HashSet seen = new HashSet(); object? obj = HostSessionContextField?.GetValue(sessionManager); SessionContext val = (SessionContext)((obj is SessionContext) ? obj : null); if (val != null && val != null && seen.Add(val)) { yield return val; } if (!(ContextsField?.GetValue(sessionManager) is Dictionary dictionary)) { yield break; } foreach (SessionContext value in dictionary.Values) { if (value != null && seen.Add(value)) { yield return value; } } } internal static VPlayer? GetVPlayer(SessionContext context) { object? obj = SessionVPlayerField?.GetValue(context); return (VPlayer?)((obj is VPlayer) ? obj : null); } internal static int GetEnterPktHashCode(SessionContext context) { object obj = EnterPktHashCodeField?.GetValue(context); if (obj is int) { return (int)obj; } return context.EnterPktHashCode; } internal static SessionContext? FindHostSessionContext(SessionManager sessionManager) { foreach (SessionContext item in EnumerateSessionContexts(sessionManager)) { VPlayer vPlayer = GetVPlayer(item); if (vPlayer != null && vPlayer.IsHost) { return item; } } object? obj = HostSessionContextField?.GetValue(sessionManager); return (SessionContext?)((obj is SessionContext) ? obj : null); } internal static bool IsHostSessionContext(SessionManager sessionManager, SessionContext context) { if (context == null) { return false; } object? obj = HostSessionContextField?.GetValue(sessionManager); SessionContext val = (SessionContext)((obj is SessionContext) ? obj : null); if (val != null && val == context) { return true; } VPlayer vPlayer = GetVPlayer(context); if (vPlayer != null) { return vPlayer.IsHost; } return false; } internal static bool TryGetNetworkGrade(SessionManager sessionManager, long playerUid, out int grade) { grade = -1; if (playerUid == 0L || !(NetworkGradesField?.GetValue(sessionManager) is IDictionary dictionary)) { return false; } try { foreach (DictionaryEntry item in dictionary) { if (MatchesPlayerUid(item.Key, playerUid) && item.Value != null && TryConvertGradeValue(item.Value, out grade)) { return true; } } } catch { } return false; } private static bool MatchesPlayerUid(object key, long playerUid) { if (!(key is long num)) { if (key is int num2) { return num2 == playerUid; } return false; } return num == playerUid; } private static bool TryConvertGradeValue(object value, out int grade) { //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Expected I4, but got Unknown if (!(value is int num)) { if (!(value is long num2)) { if (value is NetworkGrade val) { grade = (int)val; return true; } try { grade = Convert.ToInt32(value); return true; } catch { grade = -1; return false; } } grade = (int)num2; return true; } grade = num; return true; } internal static bool IsBanned(SessionManager sessionManager, ulong steamId) { if (steamId == 0L) { return false; } try { return sessionManager.ExistBannedSteamID(steamId); } catch { return false; } } internal static bool TryAddBan(SessionManager sessionManager, ulong steamId) { if (steamId != 0L && BannedSteamIdsField?.GetValue(sessionManager) is HashSet hashSet) { return hashSet.Add(steamId); } return false; } internal static bool TryRemoveBan(SessionManager sessionManager, ulong steamId) { if (steamId != 0L && BannedSteamIdsField?.GetValue(sessionManager) is HashSet hashSet) { return hashSet.Remove(steamId); } return false; } internal static IEnumerable EnumerateBannedSteamIds(SessionManager sessionManager) { if (!(BannedSteamIdsField?.GetValue(sessionManager) is HashSet hashSet)) { yield break; } foreach (ulong item in hashSet) { if (item != 0L) { yield return item; } } } internal static bool TryGetSessionId(SessionContext context, out long sessionId) { sessionId = 0L; if (context == null) { return false; } try { sessionId = context.GetSessionID(); return sessionId != 0; } catch { return false; } } internal static void DisconnectSession(SessionManager sessionManager, long sessionId, DisconnectReason reason) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) sessionManager.Remove(sessionId, reason); } } internal static class WebDashboardSnapshotCache { private const int FullRefreshIntervalMs = 1000; private const int MinimapRefreshIntervalMs = 100; private static WebDashboardSnapshot _snapshot = new WebDashboardSnapshot(); private static int _version; private static volatile bool _dirty = true; private static bool _lastInSession; private static long _lastFullRefreshMs; private static long _lastMinimapRefreshMs; private static string _minimapFingerprint = ""; private static List _lastPlayers = new List(); private static string? _lastLeaderboardJson; private static List _lastConnectedSteamIds = new List(); internal static int Version => Volatile.Read(in _version); internal static WebDashboardSnapshot Get() { return _snapshot; } internal static void MarkDirty() { _dirty = true; } internal static void Tick(string listenUrl) { bool flag = WebDashboardGameState.IsInSession(); if (flag != _lastInSession) { _dirty = true; _lastInSession = flag; _minimapFingerprint = ""; } if (flag) { long num = UtcNowMs(); if (num - _lastMinimapRefreshMs >= 100) { RefreshMinimapLive(); _lastMinimapRefreshMs = num; } if (_dirty || num - _lastFullRefreshMs >= 1000) { Refresh(listenUrl); _dirty = false; _lastFullRefreshMs = num; } } else if (_dirty) { Refresh(listenUrl); _dirty = false; _lastFullRefreshMs = UtcNowMs(); _minimapFingerprint = ""; } } internal static void RefreshMinimapLive() { if (WebDashboardGameState.IsInSession()) { List list = _lastPlayers; if (list.Count == 0) { list = WebDashboardPlayerService.CollectPlayers(); } WebDashboardMinimapLayoutBuilder.EnsureLayout(); WebDashboardMinimapTrainDto train; List list2 = WebDashboardMinimapService.CollectMarkers(list, out train); string text = BuildMinimapFingerprint(list2, train); if (!(text == _minimapFingerprint) || WebDashboardMinimapLayoutBuilder.LayoutVersion != _snapshot.MinimapLayout.LayoutVersion) { _minimapFingerprint = text; WebDashboardMinimapLayoutDto current = WebDashboardMinimapLayoutBuilder.Current; WebDashboardSnapshot snapshot = _snapshot; WebDashboardSnapshot value = new WebDashboardSnapshot { Status = snapshot.Status, Players = snapshot.Players, LeaderboardJson = snapshot.LeaderboardJson, ConnectedSteamIds = snapshot.ConnectedSteamIds, PlayerStatsJson = snapshot.PlayerStatsJson, MinimapLayout = current, MinimapMarkers = list2, MinimapTrain = train }; Interlocked.Exchange(ref _snapshot, value); WebDashboardSseHub.NotifyMinimapChanged(); } } } internal static void Refresh(string listenUrl) { bool flag = WebDashboardGameState.IsInSession(); bool flag2 = WebDashboardGameState.IsHost(); int saveSlotId = WebDashboardGameState.GetSaveSlotId(); _lastInSession = flag; WebDashboardSnapshot webDashboardSnapshot = new WebDashboardSnapshot { Status = new WebDashboardStatusDto { InSession = flag, IsHost = flag2, SaveSlotId = saveSlotId, ModVersion = "26.6.3", ListenUrl = listenUrl, SnapshotVersion = Version, ConfigVersion = ModConfig.Version } }; if (!flag) { _lastPlayers = new List(); _lastLeaderboardJson = null; _lastConnectedSteamIds = new List(); _minimapFingerprint = ""; WebDashboardAvatarService.Clear(); } else { List list = WebDashboardPlayerService.CollectPlayers(); if (list.Count > 0) { _lastPlayers = list; } else if (_lastPlayers.Count > 0) { list = _lastPlayers; } webDashboardSnapshot.Players = list; HashSet hashSet = new HashSet(); foreach (WebDashboardPlayerDto item in list) { if (item.SteamId != 0L) { hashSet.Add(item.SteamId); } } if (flag2 && saveSlotId >= 0) { List list2 = WebDashboardStatisticsBridge.GetConnectedSteamIds(); if (list2.Count > 0) { _lastConnectedSteamIds = list2; } else if (_lastConnectedSteamIds.Count > 0) { list2 = _lastConnectedSteamIds; } webDashboardSnapshot.ConnectedSteamIds = list2; LeaderboardDocument leaderboardDocument = WebDashboardStatisticsBridge.GetLeaderboardDocument(saveSlotId); string text = ((leaderboardDocument == null) ? null : WebDashboardJson.SerializeLeaderboardResponse(leaderboardDocument, list2)); if (!string.IsNullOrEmpty(text)) { _lastLeaderboardJson = text; } else if (!string.IsNullOrEmpty(_lastLeaderboardJson)) { text = _lastLeaderboardJson; } webDashboardSnapshot.LeaderboardJson = text; foreach (ulong item2 in list2) { if (item2 != 0L) { hashSet.Add(item2); } } if (leaderboardDocument?.Entries != null) { foreach (LeaderboardEntry entry in leaderboardDocument.Entries) { if (entry.SteamId != 0L) { hashSet.Add(entry.SteamId); string value = WebDashboardStatisticsBridge.BuildPlayerStatsJson(saveSlotId, entry.SteamId, leaderboardDocument); if (!string.IsNullOrEmpty(value)) { webDashboardSnapshot.PlayerStatsJson[entry.SteamId] = value; } } } } } WebDashboardAvatarService.PrewarmForPlayers(new <>z__ReadOnlyArray(hashSet.ToArray())); WebDashboardMinimapLayoutBuilder.EnsureLayout(); webDashboardSnapshot.MinimapLayout = WebDashboardMinimapLayoutBuilder.Current; webDashboardSnapshot.MinimapMarkers = WebDashboardMinimapService.CollectMarkers(list, out WebDashboardMinimapTrainDto train); webDashboardSnapshot.MinimapTrain = train; _minimapFingerprint = BuildMinimapFingerprint(webDashboardSnapshot.MinimapMarkers, train); } Interlocked.Exchange(ref _snapshot, webDashboardSnapshot); Interlocked.Increment(ref _version); WebDashboardSseHub.NotifySnapshotChanged(); } private static string BuildMinimapFingerprint(IReadOnlyList markers, WebDashboardMinimapTrainDto? train) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(WebDashboardMinimapLayoutBuilder.LayoutVersion).Append('|'); if (train != null) { stringBuilder.Append(train.AreaId).Append('|').Append(train.X.ToString("F3")) .Append(',') .Append(train.Z.ToString("F3")) .Append(',') .Append(train.Yaw.ToString("F1")) .Append('|'); } foreach (WebDashboardMinimapMarkerDto marker in markers) { stringBuilder.Append(marker.SteamId).Append(':').Append(marker.X.ToString("F3")) .Append(',') .Append(marker.Z.ToString("F3")) .Append(',') .Append(marker.Yaw.ToString("F1")) .Append(',') .Append(marker.AreaId) .Append(',') .Append(marker.IsAlive ? '1' : '0') .Append(';'); } return stringBuilder.ToString(); } private static long UtcNowMs() { return DateTime.UtcNow.Ticks / 10000; } } internal static class WebDashboardSseHub { private sealed class SseClient { internal readonly HttpListenerContext Context; internal readonly AutoResetEvent Signal = new AutoResetEvent(initialState: false); internal volatile bool Active = true; private readonly object _writeLock = new object(); private Stream? _stream; internal SseClient(HttpListenerContext context) { Context = context; } internal bool TryWriteEvent(string eventName, string data) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("event: ").Append(eventName).Append('\n'); string[] array = data.Split('\n'); foreach (string value in array) { stringBuilder.Append("data: ").Append(value).Append('\n'); } stringBuilder.Append('\n'); return TryWrite(stringBuilder.ToString()); } internal bool TryWriteComment(string comment) { return TryWrite(": " + comment + "\n\n"); } private bool TryWrite(string text) { if (!Active || _shuttingDown) { return false; } lock (_writeLock) { try { if (_stream == null) { _stream = Context.Response.OutputStream; } byte[] bytes = Encoding.UTF8.GetBytes(text); _stream.Write(bytes, 0, bytes.Length); _stream.Flush(); return true; } catch { Active = false; return false; } } } } private const string Feature = "WebDashboard"; private const int SnapshotThrottleMs = 1000; private const int MinimapThrottleMs = 100; private const int LoopWaitMs = 50; private const int KeepaliveMs = 15000; private static readonly object Gate = new object(); private static readonly List Clients = new List(); private static readonly AutoResetEvent BroadcastSignal = new AutoResetEvent(initialState: false); private static Thread? _broadcastThread; private static volatile bool _shuttingDown; private static volatile bool _pendingSnapshotBroadcast; private static volatile bool _pendingMinimapBroadcast; private static int _clientCount; private static long _lastSnapshotPublishMs; private static long _lastMinimapPublishMs; private static int _lastPublishedVersion; internal static void Start() { lock (Gate) { if (_broadcastThread == null || !_broadcastThread.IsAlive) { _shuttingDown = false; _broadcastThread = new Thread(BroadcastLoop) { IsBackground = true, Name = "MimesisWebDashboardSse" }; _broadcastThread.Start(); } } } internal static void Subscribe(HttpListenerContext context) { HttpListenerResponse response = context.Response; response.StatusCode = 200; response.ContentType = "text/event-stream; charset=utf-8"; response.Headers["Cache-Control"] = "no-cache, no-store, must-revalidate"; response.Headers["Connection"] = "keep-alive"; response.SendChunked = true; SseClient sseClient = new SseClient(context); string data = WebDashboardJson.SerializeSnapshotEvent(WebDashboardSnapshotCache.Get()); lock (Gate) { if (_shuttingDown) { TryClose(sseClient); return; } Clients.Add(sseClient); Interlocked.Increment(ref _clientCount); } try { if (sseClient.TryWriteEvent("snapshot", data)) { while (!_shuttingDown && sseClient.Active && (sseClient.Signal.WaitOne(15000) || (!_shuttingDown && sseClient.Active && sseClient.TryWriteComment("ping")))) { } } } catch (Exception ex) { if (!_shuttingDown) { ModLog.Warn("WebDashboard", "SSE client disconnected: " + ex.Message); } } finally { RemoveClient(sseClient); TryClose(sseClient); } } internal static void NotifySnapshotChanged() { if (!_shuttingDown && Volatile.Read(in _clientCount) != 0) { _pendingSnapshotBroadcast = true; BroadcastSignal.Set(); } } internal static void NotifyMinimapChanged() { if (!_shuttingDown && Volatile.Read(in _clientCount) != 0) { _pendingMinimapBroadcast = true; BroadcastSignal.Set(); } } internal static void Shutdown() { _shuttingDown = true; _pendingSnapshotBroadcast = false; _pendingMinimapBroadcast = false; BroadcastSignal.Set(); List list; lock (Gate) { list = Clients.ToList(); Clients.Clear(); _clientCount = 0; } foreach (SseClient item in list) { item.Active = false; item.Signal.Set(); TryClose(item); } Thread broadcastThread = _broadcastThread; _broadcastThread = null; if (broadcastThread != null && broadcastThread.IsAlive && broadcastThread != Thread.CurrentThread) { try { broadcastThread.Join(500); } catch { } } } private static void BroadcastLoop() { while (!_shuttingDown) { BroadcastSignal.WaitOne(50); if (!_shuttingDown) { TryBroadcastMinimap(); TryBroadcastSnapshot(); continue; } break; } } private static void TryBroadcastMinimap() { if (!_pendingMinimapBroadcast || !IsThrottleReady(_lastMinimapPublishMs, 100)) { return; } SseClient[] array = CopyClientsAndClearPending(isMinimap: true); if (array != null) { WebDashboardSnapshot webDashboardSnapshot = WebDashboardSnapshotCache.Get(); string payload; try { payload = WebDashboardJson.SerializeMinimap(webDashboardSnapshot.MinimapLayout, webDashboardSnapshot.MinimapMarkers, webDashboardSnapshot.MinimapTrain); } catch (Exception ex) { ModLog.Warn("WebDashboard", "SSE minimap serialization failed: " + ex.Message); return; } _lastMinimapPublishMs = UtcNowMs(); BroadcastEvent(array, "minimap", payload); } } private static void TryBroadcastSnapshot() { if (!_pendingSnapshotBroadcast || !IsThrottleReady(_lastSnapshotPublishMs, 1000)) { return; } SseClient[] array = CopyClientsAndClearPending(isMinimap: false); if (array != null) { int version = WebDashboardSnapshotCache.Version; string payload; try { payload = WebDashboardJson.SerializeSnapshotEvent(WebDashboardSnapshotCache.Get()); } catch (Exception ex) { ModLog.Warn("WebDashboard", "SSE snapshot serialization failed: " + ex.Message); return; } _lastSnapshotPublishMs = UtcNowMs(); _lastPublishedVersion = version; BroadcastEvent(array, "snapshot", payload); if (WebDashboardSnapshotCache.Version != _lastPublishedVersion) { _pendingSnapshotBroadcast = true; BroadcastSignal.Set(); } } } private static SseClient[]? CopyClientsAndClearPending(bool isMinimap) { lock (Gate) { if (_shuttingDown || Clients.Count == 0) { if (isMinimap) { _pendingMinimapBroadcast = false; } else { _pendingSnapshotBroadcast = false; } return null; } if (isMinimap) { _pendingMinimapBroadcast = false; } else { _pendingSnapshotBroadcast = false; } return Clients.ToArray(); } } private static void BroadcastEvent(SseClient[] clients, string eventName, string payload) { foreach (SseClient sseClient in clients) { if (!sseClient.TryWriteEvent(eventName, payload)) { RemoveClient(sseClient); TryClose(sseClient); } else { sseClient.Signal.Set(); } } } private static bool IsThrottleReady(long lastPublishMs, int throttleMs) { if (lastPublishMs != 0L) { return UtcNowMs() - lastPublishMs >= throttleMs; } return true; } private static void RemoveClient(SseClient client) { lock (Gate) { if (Clients.Remove(client)) { Interlocked.Decrement(ref _clientCount); } } } private static void TryClose(SseClient client) { client.Active = false; try { client.Context.Response.OutputStream.Close(); } catch { } try { client.Context.Response.Close(); } catch { } } private static long UtcNowMs() { return DateTime.UtcNow.Ticks / 10000; } } internal static class WebDashboardStatisticsBridge { internal static string? BuildLeaderboardJson(int saveSlotId, IReadOnlyCollection connectedSteamIds) { LeaderboardDocument leaderboardDocument = GetLeaderboardDocument(saveSlotId); if (leaderboardDocument != null) { return WebDashboardJson.SerializeLeaderboardResponse(leaderboardDocument, connectedSteamIds); } return null; } internal static LeaderboardDocument? GetLeaderboardDocument(int saveSlotId) { if (!WebDashboardGameState.IsHost() || saveSlotId < 0) { return null; } return TryBuildLiveLeaderboard(saveSlotId) ?? TryLoadLeaderboardFromDisk(saveSlotId); } internal static string? TryGetDisplayNameFromLeaderboard(LeaderboardDocument? leaderboard, ulong steamId) { if (leaderboard?.Entries == null || steamId == 0L) { return null; } string b = steamId.ToString(); foreach (LeaderboardEntry entry in leaderboard.Entries) { if (entry.SteamId == steamId && !string.IsNullOrWhiteSpace(entry.DisplayName) && !string.Equals(entry.DisplayName, b, StringComparison.Ordinal)) { return entry.DisplayName; } } return null; } internal static string? BuildPlayerStatsJson(int saveSlotId, ulong steamId, LeaderboardDocument? leaderboard = null) { if (!WebDashboardGameState.IsHost() || steamId == 0L || saveSlotId < 0) { return null; } PlayerStatisticsDocument playerStatisticsDocument = StatisticsTracker.TryGetPlayerDocument(steamId); PlayerStatisticsDocument playerStatisticsDocument2 = ((playerStatisticsDocument != null) ? playerStatisticsDocument : StatisticsStore.LoadPlayer(saveSlotId, steamId)); if (leaderboard == null) { leaderboard = GetLeaderboardDocument(saveSlotId); } string text = TryGetDisplayNameFromLeaderboard(leaderboard, steamId) ?? WebDashboardPlayerService.ResolveDisplayNameForSteamId(steamId, saveSlotId); if (!string.IsNullOrWhiteSpace(text) && !string.Equals(text, steamId.ToString(), StringComparison.Ordinal)) { playerStatisticsDocument2.DisplayName = text; } return WebDashboardJson.SerializePlayerStats(playerStatisticsDocument2); } internal static List GetConnectedSteamIds() { if (ModConfig.EnableStatistics.Value) { return StatisticsTracker.GetConnectedSteamIds().ToList(); } return CollectSteamIdsFromPlayers(); } private static List CollectSteamIdsFromPlayers() { List list = new List(); foreach (WebDashboardPlayerDto item in WebDashboardPlayerService.CollectPlayers()) { if (item.SteamId != 0L) { list.Add(item.SteamId); } } return list; } private static LeaderboardDocument? TryBuildLiveLeaderboard(int saveSlotId) { if (!ModConfig.EnableStatistics.Value) { return null; } List list = StatisticsTracker.GetCachedPlayerDocuments().ToList(); if (list.Count != 0) { return LeaderboardBuilder.Build(saveSlotId, list); } return null; } private static LeaderboardDocument? TryLoadLeaderboardFromDisk(int saveSlotId) { string leaderboardFilePath = StatisticsStore.GetLeaderboardFilePath(saveSlotId); if (string.IsNullOrEmpty(leaderboardFilePath)) { return null; } string text = StatisticsStore.SafeReadText(leaderboardFilePath); if (string.IsNullOrEmpty(text)) { Dictionary dictionary = new Dictionary(); StatisticsStore.LoadAllPlayersForSlot(saveSlotId, dictionary); if (dictionary.Count != 0) { return LeaderboardBuilder.Build(saveSlotId, dictionary.Values); } return null; } return StatisticsJson.DeserializeLeaderboard(text); } } } namespace MimesisPlayerEnhancement.Features.WebDashboard.Models { internal sealed class WebDashboardStatusDto { public bool InSession; public bool IsHost; public int SaveSlotId = -1; public string ModVersion = ""; public string ListenUrl = ""; public int SnapshotVersion; public int ConfigVersion; } internal sealed class WebDashboardSessionStatsDto { public long CurrencyEarned; public long Kills; public long Deaths; public long Revives; public long MimicEncounterCount; public long ItemCarryCount; public long VoiceEvents; public long DamageToAlly; public long TotalConnectedSeconds; } internal sealed class WebDashboardPlayerDto { public ulong SteamId; public long PlayerUid; public string DisplayName = ""; public bool IsHost; public bool IsLocal; public bool IsBanned; public int NetworkGrade = -1; public string ConnectionRole = ""; public string ConnectionAddress = ""; public int VoiceEventCount; public WebDashboardSessionStatsDto? CurrentSession; } internal sealed class WebDashboardMinimapBoundsDto { public float MinX; public float MinZ; public float MaxX; public float MaxZ; } internal sealed class WebDashboardMinimapTileDto { public string Id = ""; public string Label = ""; public float X; public float Z; public float W; public float H; public bool IsMainPath; } internal sealed class WebDashboardMinimapConnectionDto { public string From = ""; public string To = ""; } internal sealed class WebDashboardMinimapConnectionPointDto { public float X; public float Z; public float DirX; public float DirZ; public string FromTileId = ""; public string ToTileId = ""; public string TargetAreaId = ""; public bool CrossArea; } internal sealed class WebDashboardMinimapAreaDto { public string Id = ""; public string Label = ""; public string Kind = ""; public WebDashboardMinimapBoundsDto Bounds = new WebDashboardMinimapBoundsDto(); public List Tiles = new List(); public List ConnectionPoints = new List(); } internal sealed class WebDashboardMinimapMarkerDto { public ulong SteamId; public string DisplayName = ""; public float X; public float Z; public float Yaw; public string RoomName = ""; public string AreaId = ""; public string TileId = ""; public bool IsAlive = true; public bool IsHost; public bool IsLocal; } internal sealed class WebDashboardMinimapTrainDto { public float X; public float Z; public float Yaw; public string AreaId = ""; } internal sealed class WebDashboardMinimapLayoutDto { public int LayoutVersion; public string LayoutKind = "none"; public string DisplayMode = "hidden"; public string SceneLabel = ""; public string DefaultAreaId = ""; public WebDashboardMinimapBoundsDto Bounds = new WebDashboardMinimapBoundsDto(); public List Areas = new List(); public List Tiles = new List(); public List Connections = new List(); } internal sealed class WebDashboardSnapshot { public WebDashboardStatusDto Status = new WebDashboardStatusDto(); public List Players = new List(); public string? LeaderboardJson; public List ConnectedSteamIds = new List(); public Dictionary PlayerStatsJson = new Dictionary(); public WebDashboardMinimapLayoutDto MinimapLayout = new WebDashboardMinimapLayoutDto(); public List MinimapMarkers = new List(); public WebDashboardMinimapTrainDto? MinimapTrain; } internal enum WebDashboardActionType { Kick, Ban, Unban } internal sealed class WebDashboardPendingAction { public WebDashboardActionType Type; public ulong SteamId; public long PlayerUid; } internal sealed class WebDashboardActionResult { public bool Success; public string Message = ""; } internal sealed class WebDashboardSettingsDto { public string ConfigPath = ""; public int ConfigVersion; public List Sections = new List(); } internal sealed class WebDashboardConfigSectionDto { public string Id = ""; public string Title = ""; public List Entries = new List(); } internal sealed class WebDashboardConfigEntryDto { public string Key = ""; public string Title = ""; public string Description = ""; public string Type = ""; public string Value = ""; public string DefaultValue = ""; public bool IsHidden; } internal sealed class WebDashboardConfigUpdateRequest { public string SectionId = ""; public string Key = ""; public string Value = ""; } internal sealed class WebDashboardConfigUpdateResult { public bool Success; public string Message = ""; public string SectionId = ""; public string Key = ""; public string Value = ""; public string Type = ""; } } namespace MimesisPlayerEnhancement.Features.Statistics { public static class InGameMessageHelper { internal const string MessagePrefix = "[PlayerEnhancements]"; private const string LocalOnlyRichTextColor = "#88CCFF"; private const BindingFlags InstanceMemberFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo EnterColor1Field = typeof(UIPrefab_PlayerEnterInfo).GetField("EnterColor1", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo EnterColor2Field = typeof(UIPrefab_PlayerEnterInfo).GetField("EnterColor2", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo ExitColor1Field = typeof(UIPrefab_PlayerEnterInfo).GetField("ExitColor1", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo ExitColor2Field = typeof(UIPrefab_PlayerEnterInfo).GetField("ExitColor2", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static PropertyInfo? _tmpTextProperty; private static PropertyInfo? _tmpColorGradientProperty; private static ConstructorInfo? _vertexGradientConstructor; private static PropertyInfo? _componentGameObjectProperty; public static void ShowModMessage(string message, bool isEntering = true, bool localOnly = false, bool ignoreFeatureToggles = false) { if ((!ignoreFeatureToggles && !ModConfig.ShowStatisticsToasts.Value && !ModConfig.ShowPlayerAnnouncements.Value) || string.IsNullOrWhiteSpace(message)) { return; } string message2 = FormatModMessage(message, localOnly); try { if (!TryEnqueueRawPlayerInfo(message2, isEntering)) { ModLog.Debug("Statistics", "Player enter info UI unavailable for mod toast."); } } catch (Exception ex) { ModLog.Debug("Statistics", "Mod toast failed: " + ex.Message); } } internal static string FormatModMessage(string message, bool localOnly) { string text = "[PlayerEnhancements] " + message.Trim(); if (localOnly) { text = "" + text + ""; } return text; } internal static IEnumerator RunExtendedPlayerEnterInfoUpdates(UIPrefab_PlayerEnterInfo ui) { IList currentDisplayed = GetListField(ui, "currentDisplayed"); IList displayStartTimeMilliSec = GetListField(ui, "displayStartTimeMilliSec"); IList isEnteringFlags = GetListField(ui, "isEnteringFlags"); IList playerInfos = GetListField(ui, "PlayerInfos"); long fadeOutMs = ui.fadeOutDisplayTimeMilliSec; Color enterColor1 = (Color)EnterColor1Field.GetValue(ui); Color enterColor2 = (Color)EnterColor2Field.GetValue(ui); Color exitColor1 = (Color)ExitColor1Field.GetValue(ui); Color exitColor2 = (Color)ExitColor2Field.GetValue(ui); while (true) { yield return (object)new WaitUntil((Func)(() => currentDisplayed.Count > 0)); long currentTickMilliSec = GetCurrentTickMilliSec(); while (displayStartTimeMilliSec.Count > 0) { long displayDurationMilliSec = GetDisplayDurationMilliSec(ui, 0); long num = Convert.ToInt64(displayStartTimeMilliSec[0]); if (currentTickMilliSec - num <= displayDurationMilliSec + fadeOutMs) { break; } currentDisplayed.RemoveAt(0); displayStartTimeMilliSec.RemoveAt(0); isEnteringFlags.RemoveAt(0); } for (int num2 = 0; num2 < playerInfos.Count; num2++) { object label = playerInfos[num2]; if (num2 < currentDisplayed.Count) { long displayDurationMilliSec2 = GetDisplayDurationMilliSec(ui, num2); long num3 = Convert.ToInt64(displayStartTimeMilliSec[num2]); float num4 = Mathf.Clamp01((float)(currentTickMilliSec - num3 - displayDurationMilliSec2) / (float)fadeOutMs); bool num5 = (bool)isEnteringFlags[num2]; Color c = (num5 ? enterColor1 : exitColor1); c.a = 1f - num4; Color c2 = (num5 ? enterColor2 : exitColor2); c2.a = 1f - num4; SetLabelText(label, (string)currentDisplayed[num2]); SetLabelGradient(label, c, c2); SetLabelActive(label, active: true); } else { SetLabelActive(label, active: false); SetLabelText(label, string.Empty); } } yield return null; } } internal static long GetDisplayDurationMilliSec(UIPrefab_PlayerEnterInfo ui, int index) { IList listField = GetListField(ui, "currentDisplayed"); if (listField == null || index >= listField.Count || !(listField[index] is string text) || !text.Contains("[PlayerEnhancements]")) { return ui.displayTimeSecMilliSec; } return (long)(Mathf.Max(1f, ModConfig.ModToastDurationSeconds.Value) * 1000f); } private static void EnsureTmpReflection(object label) { if (!(_tmpTextProperty != null)) { Type type = label.GetType(); _tmpTextProperty = type.GetProperty("text", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); _tmpColorGradientProperty = type.GetProperty("colorGradient", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); _componentGameObjectProperty = typeof(Component).GetProperty("gameObject", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); _vertexGradientConstructor = type.Assembly.GetType("TMPro.VertexGradient")?.GetConstructor(new Type[4] { typeof(Color), typeof(Color), typeof(Color), typeof(Color) }); } } private static void SetLabelText(object label, string text) { EnsureTmpReflection(label); _tmpTextProperty?.SetValue(label, text); } private static void SetLabelGradient(object label, Color c1, Color c2) { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) EnsureTmpReflection(label); if (!(_vertexGradientConstructor == null) && !(_tmpColorGradientProperty == null)) { object value = _vertexGradientConstructor.Invoke(new object[4] { c1, c1, c2, c2 }); _tmpColorGradientProperty.SetValue(label, value); } } private static void SetLabelActive(object label, bool active) { EnsureTmpReflection(label); object? obj = _componentGameObjectProperty?.GetValue(label); GameObject val = (GameObject)((obj is GameObject) ? obj : null); if (val != null) { val.SetActive(active); } } private static bool TryEnqueueRawPlayerInfo(string message, bool isEntering) { UIPrefab_PlayerEnterInfo playerEnterInfoUi = GetPlayerEnterInfoUi(); if (playerEnterInfoUi == null) { return false; } IList listField = GetListField(playerEnterInfoUi, "currentDisplayed"); IList listField2 = GetListField(playerEnterInfoUi, "displayStartTimeMilliSec"); IList listField3 = GetListField(playerEnterInfoUi, "isEnteringFlags"); IList listField4 = GetListField(playerEnterInfoUi, "PlayerInfos"); if (listField == null || listField2 == null || listField3 == null || listField4 == null) { return false; } if (listField4.Count <= 0) { return false; } TrimOldestIfFull(listField, listField2, listField3, listField4.Count); listField.Add(message); listField2.Add(GetCurrentTickMilliSec()); listField3.Add(isEntering); return true; } private static void TrimOldestIfFull(IList currentDisplayed, IList displayStartTime, IList isEnteringFlags, int maxVisible) { while (currentDisplayed.Count >= maxVisible && currentDisplayed.Count > 0) { currentDisplayed.RemoveAt(0); displayStartTime.RemoveAt(0); isEnteringFlags.RemoveAt(0); } } private static IList? GetListField(object target, string fieldName) { return target.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(target) as IList; } private static long GetCurrentTickMilliSec() { try { if ((Object)(object)Hub.s == (Object)null) { return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); } object obj = typeof(Hub).GetProperty("timeutil", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(Hub.s) ?? typeof(Hub).GetField("timeutil", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(Hub.s); if (obj == null) { return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); } MethodInfo method = obj.GetType().GetMethod("GetCurrentTickMilliSec", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, Type.EmptyTypes, null); return (method == null) ? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() : Convert.ToInt64(method.Invoke(obj, null)); } catch { return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); } } private static UIPrefab_PlayerEnterInfo? GetPlayerEnterInfoUi() { if ((Object)(object)Hub.s == (Object)null) { return null; } object obj = typeof(Hub).GetField("pdata", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(Hub.s); if (obj == null) { return null; } object obj2 = obj.GetType().GetField("main", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(obj); if (obj2 != null) { object? obj3 = obj2.GetType().GetField("playerEnterInfoUI", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(obj2); return (UIPrefab_PlayerEnterInfo?)((obj3 is UIPrefab_PlayerEnterInfo) ? obj3 : null); } return null; } } public static class LeaderboardBuilder { public static LeaderboardDocument Build(int slotId, IEnumerable players) { LeaderboardDocument leaderboardDocument = new LeaderboardDocument { SaveSlotId = slotId, UpdatedAtUtc = DateTime.UtcNow }; foreach (PlayerStatisticsDocument player in players) { if (player.SteamId != 0L) { StatCounters counters = player.Global.Counters; leaderboardDocument.Entries.Add(new LeaderboardEntry { SteamId = player.SteamId, DisplayName = player.DisplayName, ItemCarryCount = counters.ItemCarryCount, DamageToAlly = counters.DamageToAlly, MimicEncounterCount = counters.MimicEncounterCount, TimeInStartingVolumeMs = counters.TimeInStartingVolumeMs, CurrencyEarned = counters.CurrencyEarned, VoiceEvents = counters.VoiceEvents, Deaths = counters.Deaths, Revives = counters.Revives, Kills = counters.Kills, TotalConnectedSeconds = counters.TotalConnectedSeconds, SessionsCompleted = player.Global.SessionsCompleted }); } } leaderboardDocument.Entries = (from e in leaderboardDocument.Entries orderby e.CurrencyEarned descending, e.MimicEncounterCount descending, e.ItemCarryCount descending select e).ToList(); return leaderboardDocument; } } internal static class StatisticsJson { public static string SerializePlayer(PlayerStatisticsDocument doc) { return ModJson.Serialize(doc); } public static PlayerStatisticsDocument? DeserializePlayer(string json) { return ModJson.Deserialize(json); } public static string SerializeLeaderboard(LeaderboardDocument doc) { return ModJson.Serialize(doc); } public static LeaderboardDocument? DeserializeLeaderboard(string json) { return ModJson.Deserialize(json); } } internal static class StatisticsMessages { private const string Feature = "Statistics"; private const float LocalIntroDelaySeconds = 1f; private const float GlobalStatsJoinDelaySeconds = 3f; private const float GlobalStatsDedupSeconds = 3f; private const BindingFlags InstanceMemberFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; internal const string PluginDisplayName = "Mimesis Player Enhancement"; internal const string AuthorName = "Kandru"; internal const string DownloadUrl = "github.com/Kandru/mimesis-player-enhancements"; private static bool _localIntroScheduled; private static readonly HashSet PendingJoinStats = new HashSet(); private static readonly Dictionary<(ulong SteamId, bool IsJoin), DateTime> GlobalStatsShownAt = new Dictionary<(ulong, bool), DateTime>(); internal static void OnLocalPlayerArchiveStarted() { if (ShouldShow() && !MimesisSaveManager.IsHost()) { ScheduleLocalIntro(null, 0); } } internal static void OnPlayerJoinedSession(ulong steamId, string displayName, PlayerStatisticsDocument doc, bool isNewSession, int reconnectCount) { if (ShouldShow()) { ScheduleGlobalStatsOnJoin(steamId, displayName, doc); if (LocalPlayerHelper.IsLocalSteamId(steamId)) { ScheduleLocalIntro(isNewSession, reconnectCount); } } } internal static void OnPlayerLeftSession(ulong steamId, string displayName, PlayerStatisticsDocument doc) { if (ShouldShow()) { TryShowGlobalStats(steamId, displayName, doc, isJoin: false); } } internal static void OnCycleCompleted(int cycleNumber) { if (ShouldShow()) { InGameMessageHelper.ShowModMessage($"Cycle {cycleNumber} recorded."); } } internal static void OnGamePlayerInfoShown(string userName, bool isEntering) { if (!ShouldShow() || string.IsNullOrWhiteSpace(userName)) { return; } ulong num = ResolveSteamIdFromDisplayName(userName); if (num != 0L && MimesisSaveManager.TryGetActiveSaveSlotId(out var slotId)) { PlayerStatisticsDocument playerStatisticsDocument = StatisticsTracker.TryGetPlayerDocument(num) ?? StatisticsStore.LoadPlayer(slotId, num); playerStatisticsDocument.DisplayName = userName; if (isEntering) { ScheduleGlobalStatsOnJoin(num, userName, playerStatisticsDocument); } else { TryShowGlobalStats(num, userName, playerStatisticsDocument, isJoin: false); } } } private static bool ShouldShow() { if (ModConfig.EnableStatistics.Value) { return ModConfig.ShowStatisticsToasts.Value; } return false; } private static void ScheduleLocalIntro(bool? isNewSession, int reconnectCount) { if (!_localIntroScheduled) { _localIntroScheduled = true; MelonCoroutines.Start(ShowLocalIntroAfterDelay(isNewSession, reconnectCount)); } } private static IEnumerator ShowLocalIntroAfterDelay(bool? isNewSession, int reconnectCount) { yield return (object)new WaitForSeconds(1f); _localIntroScheduled = false; InGameMessageHelper.ShowModMessage(FormatLocalSessionIntro(isNewSession, reconnectCount), isEntering: true, localOnly: true); ModLog.Debug("Statistics", "Local session intro shown."); } private static void ScheduleGlobalStatsOnJoin(ulong steamId, string displayName, PlayerStatisticsDocument doc) { if (steamId != 0L && doc.Global != null && HasAnyGlobalStats(doc.Global) && PendingJoinStats.Add(steamId)) { MelonCoroutines.Start(ShowGlobalStatsOnJoinAfterDelay(steamId, displayName, doc)); } } private static IEnumerator ShowGlobalStatsOnJoinAfterDelay(ulong steamId, string displayName, PlayerStatisticsDocument doc) { yield return (object)new WaitForSeconds(3f); PendingJoinStats.Remove(steamId); TryShowGlobalStats(steamId, displayName, doc, isJoin: true); } private static string FormatLocalSessionIntro(bool? isNewSession, int reconnectCount) { List list = new List(1) { "v26.6.3, developed by Kandru, downloadable via github.com/Kandru/mimesis-player-enhancements" }; if (isNewSession == true) { list.Add("A new stats session has been started."); } else if (isNewSession == false) { list.Add((reconnectCount > 0) ? $"Stats session resumed (reconnect {reconnectCount})." : "Stats session resumed."); } return string.Join("\n", list); } private static void TryShowGlobalStats(ulong steamId, string displayName, PlayerStatisticsDocument doc, bool isJoin) { if (steamId != 0L && doc.Global != null && HasAnyGlobalStats(doc.Global)) { (ulong, bool) key = (steamId, isJoin); if (!GlobalStatsShownAt.TryGetValue(key, out var value) || !(DateTime.UtcNow - value < TimeSpan.FromSeconds(3.0))) { GlobalStatsShownAt[key] = DateTime.UtcNow; InGameMessageHelper.ShowModMessage(FormatGlobalStats(string.IsNullOrWhiteSpace(displayName) ? doc.DisplayName : displayName, doc.Global)); } } } internal static bool HasAnyGlobalStats(GlobalStats global) { if (global.SessionsCompleted > 0) { return true; } StatCounters counters = global.Counters; if (counters.CyclesCompleted <= 0 && counters.Deaths <= 0 && counters.Kills <= 0 && counters.Revives <= 0 && counters.VoiceEvents <= 0 && counters.CurrencyEarned <= 0 && counters.ItemCarryCount <= 0 && counters.DamageToAlly <= 0 && counters.MimicEncounterCount <= 0 && counters.TimeInStartingVolumeMs <= 0) { return counters.TotalConnectedSeconds > 0; } return true; } internal static string FormatGlobalStats(string displayName, GlobalStats global) { StatCounters counters = global.Counters; List list = new List(); if (global.SessionsCompleted > 0) { list.Add($"{global.SessionsCompleted} sessions"); } if (counters.CyclesCompleted > 0) { list.Add($"{counters.CyclesCompleted} cycles"); } if (counters.Kills > 0) { list.Add($"{counters.Kills} kills"); } if (counters.Deaths > 0) { list.Add($"{counters.Deaths} deaths"); } if (counters.Revives > 0) { list.Add($"{counters.Revives} revives"); } if (counters.VoiceEvents > 0) { list.Add($"{counters.VoiceEvents} voice events"); } if (counters.CurrencyEarned > 0) { list.Add($"{counters.CurrencyEarned} currency"); } if (counters.TotalConnectedSeconds > 0) { list.Add(FormatPlaytime(counters.TotalConnectedSeconds)); } string text = ((list.Count > 0) ? string.Join(", ", list) : "no recorded stats yet"); return displayName + " — " + text; } private static string FormatPlaytime(long totalSeconds) { if (totalSeconds < 60) { return $"{totalSeconds}s played"; } long num = totalSeconds / 3600; long num2 = totalSeconds % 3600 / 60; if (num <= 0) { return $"{num2}m played"; } if (num2 <= 0) { return $"{num}h played"; } return $"{num}h {num2}m played"; } private static ulong ResolveSteamIdFromDisplayName(string displayName) { try { if ((Object)(object)Hub.s == (Object)null) { return 0uL; } object obj = typeof(Hub).GetField("pdata", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(Hub.s); object obj2 = obj?.GetType().GetField("main", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(obj); if (obj2 == null) { return 0uL; } if (!(obj2.GetType().GetField("steamIDToNameCache", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(obj2) is Dictionary dictionary)) { return 0uL; } foreach (KeyValuePair item in dictionary) { if (string.Equals(item.Value, displayName, StringComparison.OrdinalIgnoreCase)) { return item.Key; } } } catch { } return 0uL; } } public static class StatisticsPatches { private const string Feature = "Statistics"; public static void Apply(Harmony harmony) { HarmonyPatchHelper.PatchApplyResult result = HarmonyPatchHelper.ApplyPatchTypes(harmony, "Statistics", HarmonyPatchHelper.GetNamespacePatchTypes(typeof(StatisticsPatches))); LogPatchAudit(harmony); HarmonyPatchHelper.LogPatchSummary("Statistics", result); } private static void LogPatchAudit(Harmony harmony) { HarmonyPatchHelper.LogPatchAudit("Statistics", harmony, new <>z__ReadOnlyArray<(string, MethodBase)>(new(string, MethodBase)[12] { ("IncreaseCycleCount/PlayReportManager", AccessTools.Method(typeof(PlayReportManager), "IncreaseCycleCount", (Type[])null, (Type[])null)), ("OnRegistPlayer/VRoomManager", AccessTools.Method(typeof(VRoomManager), "OnRegistPlayer", (Type[])null, (Type[])null)), ("ApplyLoadedGameData/GameSessionInfo", AccessTools.Method(typeof(GameSessionInfo), "ApplyLoadedGameData", (Type[])null, (Type[])null)), ("OnPlayerDeath/GameMainBase", AccessTools.Method(typeof(GameMainBase), "OnPlayerDeath", (Type[])null, (Type[])null)), ("OnPlayerRevive/GameMainBase", AccessTools.Method(typeof(GameMainBase), "OnPlayerRevive", (Type[])null, (Type[])null)), ("OnKillCountChanged/GameMainBase", AccessTools.Method(typeof(GameMainBase), "OnKillCountChanged", (Type[])null, (Type[])null)), ("Delete/PlatformMgr", AccessTools.Method(typeof(PlatformMgr), "Delete", (Type[])null, (Type[])null)), ("SaveGameData/MaintenanceRoom", AccessTools.Method(typeof(MaintenanceRoom), "SaveGameData", (Type[])null, (Type[])null)), ("OnUnregistPlayer/VWorld", AccessTools.Method(typeof(VWorld), "OnUnregistPlayer", (Type[])null, (Type[])null)), ("OnStartClient/SpeechEventArchive", AccessTools.Method(typeof(SpeechEventArchive), "OnStartClient", (Type[])null, (Type[])null)), ("AddPlayerInfo/UIPrefab_PlayerEnterInfo", AccessTools.Method(typeof(UIPrefab_PlayerEnterInfo), "AddPlayerInfo", (Type[])null, (Type[])null)), ("UpdatePlayerInfos/UIPrefab_PlayerEnterInfo", AccessTools.Method(typeof(UIPrefab_PlayerEnterInfo), "UpdatePlayerInfos", (Type[])null, (Type[])null)) })); } } public static class StatisticsStore { private const string Feature = "Statistics"; private const string StatisticsFolder = "statistics"; private const string PlayersFolder = "players"; private const string LeaderboardFile = "leaderboard.json"; private const string BackupSuffix = ".bak"; private const string TempSuffix = ".tmp"; private const int MaxRecentSessions = 20; public static int MaxRecentSessionsPerPlayer => 20; public static string? GetStatisticsRootPath(int slotId) { string mimesisSlotPath = MimesisSaveManager.GetMimesisSlotPath(slotId); if (!string.IsNullOrEmpty(mimesisSlotPath)) { return Path.Combine(mimesisSlotPath, "statistics"); } return null; } public static string? GetPlayerFilePath(int slotId, ulong steamId) { string statisticsRootPath = GetStatisticsRootPath(slotId); if (!string.IsNullOrEmpty(statisticsRootPath)) { return Path.Combine(statisticsRootPath, "players", steamId + ".json"); } return null; } public static string? GetLeaderboardFilePath(int slotId) { string statisticsRootPath = GetStatisticsRootPath(slotId); if (!string.IsNullOrEmpty(statisticsRootPath)) { return Path.Combine(statisticsRootPath, "leaderboard.json"); } return null; } public static void SafeWriteText(string filePath, string text) { string text2 = filePath + ".tmp"; string destFileName = filePath + ".bak"; Directory.CreateDirectory(Path.GetDirectoryName(filePath)); File.WriteAllText(text2, text); if (File.Exists(filePath)) { try { File.Copy(filePath, destFileName, overwrite: true); } catch (Exception ex) { ModLog.Warn("Statistics", "Backup failed for " + Path.GetFileName(filePath) + ": " + ex.Message); } } if (File.Exists(filePath)) { File.Delete(filePath); } File.Move(text2, filePath); } public static string? SafeReadText(string filePath) { if (File.Exists(filePath)) { try { string text = File.ReadAllText(filePath); if (!string.IsNullOrEmpty(text)) { return text; } } catch (Exception ex) { ModLog.Warn("Statistics", "Read failed (" + Path.GetFileName(filePath) + "): " + ex.Message); } } string path = filePath + ".bak"; if (File.Exists(path)) { try { string text2 = File.ReadAllText(path); if (!string.IsNullOrEmpty(text2)) { ModLog.Warn("Statistics", "Recovered from backup: " + Path.GetFileName(path)); return text2; } } catch (Exception ex2) { ModLog.Error("Statistics", "Backup read failed: " + ex2.Message); } } return null; } public static PlayerStatisticsDocument LoadPlayer(int slotId, ulong steamId) { string playerFilePath = GetPlayerFilePath(slotId, steamId); if (string.IsNullOrEmpty(playerFilePath)) { return NewPlayerDocument(steamId); } string text = SafeReadText(playerFilePath); if (string.IsNullOrEmpty(text)) { return NewPlayerDocument(steamId); } PlayerStatisticsDocument playerStatisticsDocument = StatisticsJson.DeserializePlayer(text); if (playerStatisticsDocument == null) { ModLog.Warn("Statistics", "Corrupt player statistics — replacing with fresh document: " + Path.GetFileName(playerFilePath)); return NewPlayerDocument(steamId); } playerStatisticsDocument.SteamId = steamId; PlayerStatisticsDocument playerStatisticsDocument2 = playerStatisticsDocument; if (playerStatisticsDocument2.RecentSessions == null) { playerStatisticsDocument2.RecentSessions = new List(); } playerStatisticsDocument2 = playerStatisticsDocument; if (playerStatisticsDocument2.Global == null) { playerStatisticsDocument2.Global = new GlobalStats(); } return playerStatisticsDocument; } public static void SavePlayer(int slotId, PlayerStatisticsDocument doc) { string playerFilePath = GetPlayerFilePath(slotId, doc.SteamId); if (!string.IsNullOrEmpty(playerFilePath)) { SafeWriteText(playerFilePath, SerializePreparedPlayer(doc)); } } internal static string SerializePreparedPlayer(PlayerStatisticsDocument doc) { TrimRecentSessions(doc); doc.Version = 1; return StatisticsJson.SerializePlayer(doc); } public static void SaveLeaderboard(int slotId, LeaderboardDocument leaderboard) { string leaderboardFilePath = GetLeaderboardFilePath(slotId); if (!string.IsNullOrEmpty(leaderboardFilePath)) { leaderboard.Version = 1; leaderboard.SaveSlotId = slotId; leaderboard.UpdatedAtUtc = DateTime.UtcNow; SafeWriteText(leaderboardFilePath, StatisticsJson.SerializeLeaderboard(leaderboard)); } } public static void LoadAllPlayersForSlot(int slotId, Dictionary cache) { string statisticsRootPath = GetStatisticsRootPath(slotId); if (string.IsNullOrEmpty(statisticsRootPath)) { return; } string path = Path.Combine(statisticsRootPath, "players"); if (!Directory.Exists(path)) { return; } string[] files = Directory.GetFiles(path, "*.json"); for (int i = 0; i < files.Length; i++) { if (ulong.TryParse(Path.GetFileNameWithoutExtension(files[i]), out var result)) { cache[result] = LoadPlayer(slotId, result); } } } public static void DeleteStatisticsData(int slotId) { string statisticsRootPath = GetStatisticsRootPath(slotId); if (string.IsNullOrEmpty(statisticsRootPath) || !Directory.Exists(statisticsRootPath)) { return; } try { Directory.Delete(statisticsRootPath, recursive: true); ModLog.Info("Statistics", $"Deleted statistics data for slot {slotId}."); } catch (Exception ex) { ModLog.Warn("Statistics", "DeleteStatisticsData: " + ex.Message); } } private static PlayerStatisticsDocument NewPlayerDocument(ulong steamId) { return new PlayerStatisticsDocument { SteamId = steamId, Global = new GlobalStats(), RecentSessions = new List() }; } private static void TrimRecentSessions(PlayerStatisticsDocument doc) { while (doc.RecentSessions.Count > 20) { doc.RecentSessions.RemoveAt(0); } } } public static class StatisticsTracker { private sealed class PlayReportSnapshot { public long ItemCarryCount; public long DamageToAlly; public long MimicEncounterCount; public long TimeInStartingVolumeMs; } private const string Feature = "Statistics"; private const BindingFlags InstanceMemberFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo? HubVworldField = typeof(Hub).GetField("vworld", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly PropertyInfo? HubVworldProperty = typeof(Hub).GetProperty("vworld", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static FieldInfo? _vRoomManagerField; private static FieldInfo? _gameSessionInfoField; private static PropertyInfo? _playReportManagerProperty; private static readonly Dictionary _players = new Dictionary(); private static readonly Dictionary _connectedSince = new Dictionary(); private static readonly Dictionary _cycleBaselines = new Dictionary(); private static readonly Dictionary _voiceEventBaselines = new Dictionary(); private static readonly Dictionary _lastKillCounts = new Dictionary(); private static readonly Dictionary _actorToSteam = new Dictionary(); private static int _loadedSlotId = -999; private static long _lastCurrencyBaseline; private static int _lastCycleCount; private static Dictionary? _voiceCountCache; internal static void ClearRuntimeState() { _players.Clear(); _connectedSince.Clear(); _cycleBaselines.Clear(); _voiceEventBaselines.Clear(); _lastKillCounts.Clear(); _actorToSteam.Clear(); _loadedSlotId = -999; _lastCurrencyBaseline = 0L; _lastCycleCount = 0; StatisticsWriteQueue.Clear(); } public static void LoadForSlot(int slotId) { if (MimesisSaveManager.IsValidSaveSlotId(slotId) && slotId != _loadedSlotId) { _loadedSlotId = slotId; _players.Clear(); _connectedSince.Clear(); _cycleBaselines.Clear(); _voiceEventBaselines.Clear(); _lastKillCounts.Clear(); _actorToSteam.Clear(); StatisticsStore.LoadAllPlayersForSlot(slotId, _players); ResetCycleBaselines(); StatisticsWriteQueue.Configure(slotId, (ulong steamId) => (!_players.TryGetValue(steamId, out PlayerStatisticsDocument value)) ? null : value, PersistLeaderboardDocument); ModLog.Info("Statistics", $"Loaded statistics for save slot {slotId} ({_players.Count} players)."); } } internal static void HandleArchiveStarted(SpeechEventArchive archive, int slotId) { if (!ModConfig.EnableStatistics.Value || !MimesisSaveManager.IsValidSaveSlotId(slotId)) { return; } if (slotId != _loadedSlotId) { LoadForSlot(slotId); } if (IsArchiveIdentityReady(archive)) { ulong num = ResolveSteamIdFromArchive(archive); if (num != 0L) { OnPlayerRegistered(num, slotId); } } } public static void OnPlayerRegistered(ulong steamId, int slotId) { if (!ModConfig.EnableStatistics.Value || steamId == 0L || !MimesisSaveManager.IsValidSaveSlotId(slotId) || _connectedSince.ContainsKey(steamId)) { return; } LoadForSlot(slotId); PlayerStatisticsDocument orCreatePlayer = GetOrCreatePlayer(steamId); orCreatePlayer.DisplayName = ResolveDisplayName(steamId, orCreatePlayer.DisplayName); DateTime utcNow = DateTime.UtcNow; int value = ModConfig.SessionReconnectGraceMinutes.Value; int num; if (orCreatePlayer.CurrentSession != null && orCreatePlayer.CurrentSession.IsOpen && orCreatePlayer.CurrentSession.LastDisconnectedAtUtc.HasValue) { num = ((utcNow - orCreatePlayer.CurrentSession.LastDisconnectedAtUtc.Value <= TimeSpan.FromMinutes(value)) ? 1 : 0); if (num != 0 && orCreatePlayer.CurrentSession != null) { orCreatePlayer.CurrentSession.ReconnectCount++; orCreatePlayer.CurrentSession.LastConnectedAtUtc = utcNow; orCreatePlayer.CurrentSession.LastDisconnectedAtUtc = null; ModLog.Info("Statistics", $"Player resumed session — steamId={steamId} session={orCreatePlayer.CurrentSession.SessionId} reconnects={orCreatePlayer.CurrentSession.ReconnectCount}"); goto IL_014e; } } else { num = 0; } FinalizeOpenSession(orCreatePlayer, countAsCompleted: true); orCreatePlayer.CurrentSession = NewSession(utcNow); ModLog.Info("Statistics", $"Player started session — steamId={steamId} session={orCreatePlayer.CurrentSession.SessionId}"); goto IL_014e; IL_014e: _connectedSince[steamId] = utcNow; EnsureCycleBaseline(steamId); EnsureVoiceBaseline(steamId); StatisticsWriteQueue.SavePlayerImmediate(slotId, orCreatePlayer); PersistLeaderboardImmediate(slotId); bool isNewSession = num == 0; int reconnectCount = orCreatePlayer.CurrentSession?.ReconnectCount ?? 0; StatisticsMessages.OnPlayerJoinedSession(steamId, orCreatePlayer.DisplayName, orCreatePlayer, isNewSession, reconnectCount); WebDashboardSnapshotCache.MarkDirty(); } public static void OnPlayerUnregistered(ulong steamId) { if (CanTrack() && steamId != 0L) { if (!_players.TryGetValue(steamId, out PlayerStatisticsDocument value)) { value = GetOrCreatePlayer(steamId); } FlushConnectedTime(steamId, value); _connectedSince.Remove(steamId); if (value.CurrentSession != null) { value.CurrentSession.LastDisconnectedAtUtc = DateTime.UtcNow; value.CurrentSession.IsOpen = true; } ModLog.Info("Statistics", $"Player disconnected — steamId={steamId} displayName={value.DisplayName}"); StatisticsWriteQueue.FlushAllSync(); StatisticsWriteQueue.SavePlayerImmediate(_loadedSlotId, value); PersistLeaderboardImmediate(_loadedSlotId); StatisticsMessages.OnPlayerLeftSession(steamId, value.DisplayName, value); WebDashboardSnapshotCache.MarkDirty(); } } public static void ProcessDeferred() { if (!CanTrack() || (_connectedSince.Count == 0 && !HasOpenDisconnectedSessions())) { return; } int value = ModConfig.SessionReconnectGraceMinutes.Value; DateTime utcNow = DateTime.UtcNow; int loadedSlotId = _loadedSlotId; bool flag = false; foreach (KeyValuePair player in _players) { PlayerStatisticsDocument value2 = player.Value; SessionStats currentSession = value2.CurrentSession; if (currentSession != null && currentSession.IsOpen && currentSession.LastDisconnectedAtUtc.HasValue && !(utcNow - currentSession.LastDisconnectedAtUtc.Value <= TimeSpan.FromMinutes(value))) { ModLog.Info("Statistics", $"Session finalized — steamId={player.Key} session={currentSession.SessionId} after grace period"); FinalizeOpenSession(value2, countAsCompleted: true); StatisticsWriteQueue.SavePlayerImmediate(loadedSlotId, value2); flag = true; } } foreach (ulong item in _connectedSince.Keys.ToList()) { if (_players.TryGetValue(item, out PlayerStatisticsDocument value3)) { SessionStats? currentSession2 = value3.CurrentSession; if (currentSession2 == null || !currentSession2.LastDisconnectedAtUtc.HasValue) { FlushConnectedTime(item, value3); } } } if (flag) { PersistLeaderboardImmediate(loadedSlotId); } } private static bool HasOpenDisconnectedSessions() { foreach (PlayerStatisticsDocument value in _players.Values) { SessionStats currentSession = value.CurrentSession; if (currentSession != null && currentSession.IsOpen && currentSession.LastDisconnectedAtUtc.HasValue) { return true; } } return false; } public static void OnCycleCompleted(PlayReportManager? manager) { if (!CanTrack()) { return; } int loadedSlotId = _loadedSlotId; long num = ((manager != null) ? manager.AccumulatedCurrency : 0); long num2 = Math.Max(0L, num - _lastCurrencyBaseline); _lastCurrencyBaseline = num; int num3 = (_lastCycleCount = ((manager != null) ? manager.AccumulatedCycleCount : (++_lastCycleCount))); if (num2 > 0) { ulong num4 = ResolveSteamIdFromUid(0L, isLocal: true); if (num4 != 0L) { PlayerStatisticsDocument orCreatePlayer; PlayerStatisticsDocument playerStatisticsDocument = (orCreatePlayer = GetOrCreatePlayer(num4)); if (orCreatePlayer.CurrentSession == null) { orCreatePlayer.CurrentSession = NewSession(DateTime.UtcNow); } playerStatisticsDocument.CurrentSession.Counters.CurrencyEarned += num2; playerStatisticsDocument.Global.Counters.CurrencyEarned += num2; } } Dictionary dictionary = ((manager != null) ? manager.CurrentReportDict : null); HashSet hashSet = new HashSet(); if (dictionary != null) { foreach (KeyValuePair item in dictionary) { ulong key = item.Key; if (key != 0L) { hashSet.Add(key); ApplyPlayReportDelta(key, item.Value, num3); } } } foreach (ulong key2 in _connectedSince.Keys) { hashSet.Add(key2); } foreach (ulong item2 in _connectedSince.Keys.ToList()) { if (_players.TryGetValue(item2, out PlayerStatisticsDocument value)) { FlushConnectedTime(item2, value); } } _voiceCountCache = BuildVoiceCountCache(); StatisticsWriteQueue.FlushAllSync(); foreach (ulong item3 in hashSet) { PlayerStatisticsDocument orCreatePlayer2 = GetOrCreatePlayer(item3); orCreatePlayer2.DisplayName = ResolveDisplayName(item3, orCreatePlayer2.DisplayName); ApplyVoiceDelta(item3, orCreatePlayer2); StatisticsWriteQueue.SavePlayerImmediate(loadedSlotId, orCreatePlayer2); } _voiceCountCache = null; ResetCycleBaselines(manager); PersistLeaderboardImmediate(loadedSlotId); if (ModConfig.ShowStatisticsToasts.Value) { StatisticsMessages.OnCycleCompleted(num3); } ModLog.Info("Statistics", $"Cycle {num3} statistics saved for slot {loadedSlotId} ({hashSet.Count} players)."); WebDashboardSnapshotCache.MarkDirty(); } public static void OnGameSaved(int slotId) { if (!MimesisSaveManager.IsHost() || !ModConfig.EnableStatistics.Value || !MimesisSaveManager.IsValidSaveSlotId(slotId)) { return; } LoadForSlot(slotId); StatisticsWriteQueue.FlushAllSync(); foreach (PlayerStatisticsDocument value in _players.Values) { StatisticsWriteQueue.SavePlayerImmediate(slotId, value); } PersistLeaderboardImmediate(slotId); ModLog.Debug("Statistics", $"Statistics persisted on game save for slot {slotId}."); WebDashboardSnapshotCache.MarkDirty(); } public static void OnPlayerDeath(ProtoActor actor) { if (!CanTrack() || (Object)(object)actor == (Object)null) { return; } ulong num = ResolveSteamIdFromActor(actor); if (num != 0L) { PlayerStatisticsDocument orCreatePlayer = GetOrCreatePlayer(num); PlayerStatisticsDocument playerStatisticsDocument = orCreatePlayer; if (playerStatisticsDocument.CurrentSession == null) { playerStatisticsDocument.CurrentSession = NewSession(DateTime.UtcNow); } orCreatePlayer.CurrentSession.Counters.Deaths++; orCreatePlayer.Global.Counters.Deaths++; RegisterActorMapping(actor, num); PersistCombatStats(num, orCreatePlayer); } } public static void OnPlayerRevive(ProtoActor actor) { if (!CanTrack() || (Object)(object)actor == (Object)null) { return; } ulong num = ResolveSteamIdFromActor(actor); if (num != 0L) { PlayerStatisticsDocument orCreatePlayer = GetOrCreatePlayer(num); PlayerStatisticsDocument playerStatisticsDocument = orCreatePlayer; if (playerStatisticsDocument.CurrentSession == null) { playerStatisticsDocument.CurrentSession = NewSession(DateTime.UtcNow); } orCreatePlayer.CurrentSession.Counters.Revives++; orCreatePlayer.Global.Counters.Revives++; RegisterActorMapping(actor, num); PersistCombatStats(num, orCreatePlayer); } } public static void OnKillCountChanged(ProtoActor actor, int killCount) { if (!CanTrack() || (Object)(object)actor == (Object)null) { return; } ulong num = ResolveSteamIdFromActor(actor); if (num == 0L) { return; } RegisterActorMapping(actor, num); PlayerStatisticsDocument orCreatePlayer = GetOrCreatePlayer(num); int value; int num2 = (_lastKillCounts.TryGetValue(num, out value) ? value : 0); if (num2 == 0 && killCount <= orCreatePlayer.Global.Counters.Kills) { _lastKillCounts[num] = killCount; return; } if (killCount <= num2) { _lastKillCounts[num] = killCount; return; } int num3 = killCount - num2; _lastKillCounts[num] = killCount; PlayerStatisticsDocument playerStatisticsDocument = orCreatePlayer; if (playerStatisticsDocument.CurrentSession == null) { playerStatisticsDocument.CurrentSession = NewSession(DateTime.UtcNow); } orCreatePlayer.CurrentSession.Counters.Kills += num3; orCreatePlayer.Global.Counters.Kills += num3; PersistCombatStats(num, orCreatePlayer); } public static void OnUpdate() { if (CanTrack()) { ProcessDeferred(); StatisticsWriteQueue.ProcessDebounced(); } } private static void PersistCombatStats(ulong steamId, PlayerStatisticsDocument doc) { StatisticsWriteQueue.MarkDirty(steamId); } private static bool CanTrack() { if (ModConfig.EnableStatistics.Value && _loadedSlotId >= 0 && MimesisSaveManager.IsValidSaveSlotId(_loadedSlotId)) { return MimesisSaveManager.IsHost(); } return false; } private static PlayerStatisticsDocument GetOrCreatePlayer(ulong steamId) { if (!_players.TryGetValue(steamId, out PlayerStatisticsDocument value)) { value = StatisticsStore.LoadPlayer(_loadedSlotId, steamId); value.SteamId = steamId; _players[steamId] = value; } return value; } internal static PlayerStatisticsDocument? TryGetPlayerDocument(ulong steamId) { if (!_players.TryGetValue(steamId, out PlayerStatisticsDocument value)) { return null; } return value; } internal static IReadOnlyList GetCachedPlayerDocuments() { return new <>z__ReadOnlyArray(_players.Values.ToArray()); } internal static IReadOnlyCollection GetConnectedSteamIds() { return _connectedSince.Keys; } internal static bool TryGetLoadedSlotId(out int slotId) { slotId = _loadedSlotId; return MimesisSaveManager.IsValidSaveSlotId(slotId); } internal static ulong TryResolveSteamId(ProtoActor actor) { return ResolveSteamIdFromActor(actor); } internal static bool TryGetCurrentPlayReport(ulong steamId, out PlayReportData report) { PlayReportData val = TryGetPlayReport(steamId); if (val != null) { report = val; return true; } report = null; return false; } internal static bool TryGetSessionCounters(ulong steamId, out StatCounters counters) { counters = new StatCounters(); if (steamId == 0L) { return false; } if (_players.TryGetValue(steamId, out PlayerStatisticsDocument value) && value.CurrentSession?.Counters != null) { counters = value.CurrentSession.Counters.Clone(); return true; } return false; } private static SessionStats NewSession(DateTime now) { return new SessionStats { SessionId = Guid.NewGuid().ToString("N"), StartedAtUtc = now, LastConnectedAtUtc = now, IsOpen = true, Counters = new StatCounters() }; } private static void FinalizeOpenSession(PlayerStatisticsDocument doc, bool countAsCompleted) { if (doc.CurrentSession != null && doc.CurrentSession.IsOpen) { doc.CurrentSession.IsOpen = false; doc.RecentSessions.Add(CloneSession(doc.CurrentSession)); while (doc.RecentSessions.Count > StatisticsStore.MaxRecentSessionsPerPlayer) { doc.RecentSessions.RemoveAt(0); } if (countAsCompleted) { doc.Global.SessionsCompleted++; } doc.CurrentSession = null; } } private static SessionStats CloneSession(SessionStats session) { return new SessionStats { SessionId = session.SessionId, StartedAtUtc = session.StartedAtUtc, LastConnectedAtUtc = session.LastConnectedAtUtc, LastDisconnectedAtUtc = session.LastDisconnectedAtUtc, ReconnectCount = session.ReconnectCount, IsOpen = false, Counters = session.Counters.Clone() }; } private static void FlushConnectedTime(ulong steamId, PlayerStatisticsDocument doc) { if (!_connectedSince.TryGetValue(steamId, out var value)) { return; } long num = (long)Math.Max(0.0, (DateTime.UtcNow - value).TotalSeconds); if (num > 0) { if (doc.CurrentSession == null) { doc.CurrentSession = NewSession(value); } doc.CurrentSession.Counters.TotalConnectedSeconds += num; doc.Global.Counters.TotalConnectedSeconds += num; _connectedSince[steamId] = DateTime.UtcNow; } } private static void ApplyPlayReportDelta(ulong steamId, PlayReportData report, int cycleNumber) { PlayerStatisticsDocument orCreatePlayer; PlayerStatisticsDocument doc = (orCreatePlayer = GetOrCreatePlayer(steamId)); if (orCreatePlayer.CurrentSession == null) { orCreatePlayer.CurrentSession = NewSession(DateTime.UtcNow); } if (!_cycleBaselines.TryGetValue(steamId, out PlayReportSnapshot value)) { value = SnapshotReport(report); _cycleBaselines[steamId] = value; } StatCounters delta = new StatCounters { ItemCarryCount = Math.Max(0L, report.TotalItemCarryCount - value.ItemCarryCount), DamageToAlly = Math.Max(0L, report.TotalDamageToAlly - value.DamageToAlly), MimicEncounterCount = Math.Max(0L, report.TotalMimicEncounterCount - value.MimicEncounterCount), TimeInStartingVolumeMs = Math.Max(0L, report.TotalTimeInStartingVolume - value.TimeInStartingVolumeMs), CyclesCompleted = 1 }; MergeDelta(doc, delta); _cycleBaselines[steamId] = SnapshotReport(report); } private static void ApplyVoiceDelta(ulong steamId, PlayerStatisticsDocument doc) { int num = CountVoiceEventsForSteamId(steamId); int value; int num2 = (_voiceEventBaselines.TryGetValue(steamId, out value) ? value : num); int num3 = Math.Max(0, num - num2); if (num3 != 0) { if (doc.CurrentSession == null) { doc.CurrentSession = NewSession(DateTime.UtcNow); } doc.CurrentSession.Counters.VoiceEvents += num3; doc.Global.Counters.VoiceEvents += num3; _voiceEventBaselines[steamId] = num; } } private static void MergeDelta(PlayerStatisticsDocument doc, StatCounters delta) { if (doc.CurrentSession == null) { doc.CurrentSession = NewSession(DateTime.UtcNow); } doc.CurrentSession.Counters.Add(delta); doc.Global.Counters.Add(delta); } private static PlayReportSnapshot SnapshotReport(PlayReportData report) { return new PlayReportSnapshot { ItemCarryCount = report.TotalItemCarryCount, DamageToAlly = report.TotalDamageToAlly, MimicEncounterCount = report.TotalMimicEncounterCount, TimeInStartingVolumeMs = report.TotalTimeInStartingVolume }; } private static void EnsureCycleBaseline(ulong steamId) { if (!_cycleBaselines.ContainsKey(steamId)) { PlayReportData val = TryGetPlayReport(steamId); if (val != null) { _cycleBaselines[steamId] = SnapshotReport(val); } } } private static void EnsureVoiceBaseline(ulong steamId) { if (!_voiceEventBaselines.ContainsKey(steamId)) { _voiceEventBaselines[steamId] = CountVoiceEventsForSteamId(steamId); } } private static void ResetCycleBaselines(PlayReportManager? manager = null) { _cycleBaselines.Clear(); _voiceEventBaselines.Clear(); if (manager == null) { manager = TryGetPlayReportManager(); } _lastCurrencyBaseline = ((manager != null) ? manager.AccumulatedCurrency : _lastCurrencyBaseline); _lastCycleCount = ((manager != null) ? manager.AccumulatedCycleCount : _lastCycleCount); Dictionary dictionary = ((manager != null) ? manager.CurrentReportDict : null); if (dictionary == null) { return; } foreach (KeyValuePair item in dictionary) { if (item.Key != 0L) { _cycleBaselines[item.Key] = SnapshotReport(item.Value); _voiceEventBaselines[item.Key] = CountVoiceEventsForSteamId(item.Key); } } } private static PlayReportData? TryGetPlayReport(ulong steamId) { PlayReportManager? obj = TryGetPlayReportManager(); Dictionary dictionary = ((obj != null) ? obj.CurrentReportDict : null); if (dictionary != null) { if (!dictionary.TryGetValue(steamId, out var value)) { return null; } return value; } return null; } private static PlayReportManager? TryGetPlayReportManager() { try { object obj = (((Object)(object)Hub.s == (Object)null) ? null : (HubVworldField?.GetValue(Hub.s) ?? HubVworldProperty?.GetValue(Hub.s))); if (obj == null) { return null; } EnsurePlayReportChainResolved(obj.GetType()); if (_vRoomManagerField == null || _gameSessionInfoField == null || _playReportManagerProperty == null) { return null; } object value = _vRoomManagerField.GetValue(obj); if (value == null) { return null; } object value2 = _gameSessionInfoField.GetValue(value); return (PlayReportManager?)((value2 == null) ? null : /*isinst with value type is only supported in some contexts*/); } catch { return null; } } private static void EnsurePlayReportChainResolved(Type vworldType) { if (_vRoomManagerField != null) { return; } _vRoomManagerField = vworldType.GetField("_vRoomManager", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (!(_vRoomManagerField == null)) { _gameSessionInfoField = _vRoomManagerField.FieldType.GetField("_gameSessionInfo", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (!(_gameSessionInfoField == null)) { _playReportManagerProperty = _gameSessionInfoField.FieldType.GetProperty("PlayReportManager", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } } } private static Dictionary BuildVoiceCountCache() { Dictionary dictionary = new Dictionary(); try { foreach (SpeechEventArchive item in SpeechEventArchiveRegistry.EnumerateActive()) { if (!((Object)(object)item == (Object)null)) { long playerUid = 0L; bool isLocal = false; try { playerUid = item.PlayerUID; isLocal = item.IsLocal; } catch { } ulong num = ResolveSteamIdFromUid(playerUid, isLocal); if (num != 0L) { dictionary.TryGetValue(num, out var value); dictionary[num] = value + VoiceEventStats.GetEventCount(item); } } } } catch { } return dictionary; } private static int CountVoiceEventsForSteamId(ulong steamId) { if (_voiceCountCache == null || !_voiceCountCache.TryGetValue(steamId, out var value)) { return 0; } return value; } private static ulong ResolveSteamIdFromArchive(SpeechEventArchive archive) { long playerUID; bool isLocal; try { playerUID = archive.PlayerUID; isLocal = archive.IsLocal; } catch { return 0uL; } if (!isLocal && playerUID == 0L) { try { if (string.IsNullOrEmpty(archive.PlayerId)) { return 0uL; } } catch { return 0uL; } } return ResolveSteamIdFromUid(playerUID, isLocal); } private static bool IsArchiveIdentityReady(SpeechEventArchive archive) { long playerUID; bool isLocal; try { playerUID = archive.PlayerUID; isLocal = archive.IsLocal; } catch { return false; } if (!isLocal && playerUID == 0L) { try { return !string.IsNullOrEmpty(archive.PlayerId); } catch { return false; } } return true; } private static ulong ResolveSteamIdFromActor(ProtoActor actor) { if (actor.steamID != 0L) { return actor.steamID; } if (actor.UID != 0L) { ulong num = ResolveSteamIdFromUid(actor.UID, actor.IsHost); if (num != 0L) { return num; } } if (actor.ActorID == 0 || !_actorToSteam.TryGetValue(actor.ActorID, out var value)) { return 0uL; } return value; } private static void RegisterActorMapping(ProtoActor actor, ulong steamId) { if (actor.ActorID != 0) { _actorToSteam[actor.ActorID] = steamId; } } private static ulong ResolveSteamIdFromUid(long playerUid, bool isLocal) { try { object obj = typeof(Hub).GetField("pdata", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(Hub.s); if ((obj?.GetType().GetField("actorUIDToSteamID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))?.GetValue(obj) is Dictionary dictionary && dictionary.TryGetValue(playerUid, out var value)) { return value; } } catch { } if (isLocal) { try { PlatformMgr instance = MonoSingleton.Instance; 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 ResolveDisplayName(ulong steamId, string fallback) { try { object obj = typeof(Hub).GetField("pdata", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(Hub.s); object obj2 = obj?.GetType().GetField("main", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(obj); if (obj2 != null && obj2.GetType().GetField("steamIDToNameCache", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(obj2) is Dictionary dictionary && dictionary.TryGetValue(steamId, out var value) && !string.IsNullOrWhiteSpace(value)) { return value; } if (obj?.GetType().GetField("MyNickName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(obj) is string text && !string.IsNullOrWhiteSpace(text) && ResolveSteamIdFromUid(0L, isLocal: true) == steamId) { return text; } } catch { } if (!string.IsNullOrWhiteSpace(fallback)) { return fallback; } return steamId.ToString(); } private static LeaderboardDocument PersistLeaderboardDocument(int slotId) { return LeaderboardBuilder.Build(slotId, _players.Values); } private static void PersistLeaderboardImmediate(int slotId) { LeaderboardDocument leaderboard = PersistLeaderboardDocument(slotId); StatisticsWriteQueue.SaveLeaderboardImmediate(slotId, leaderboard); } } internal static class StatisticsWriteQueue { private sealed class PendingWrite { internal string? LatestJson; internal Task? WriteTask; } private const string Feature = "Statistics"; private const float DebounceSeconds = 3f; private const int DirtyFlushThreshold = 8; private static readonly HashSet DirtyPlayers = new HashSet(); private static readonly HashSet DirtyLeaderboards = new HashSet(); private static readonly ConcurrentDictionary InFlight = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); private static readonly object DirtyLock = new object(); private static float _nextFlushTime; private static int _loadedSlotId = -999; private static Func? _documentResolver; private static Func? _leaderboardResolver; internal static void Configure(int slotId, Func documentResolver, Func leaderboardResolver) { _loadedSlotId = slotId; _documentResolver = documentResolver; _leaderboardResolver = leaderboardResolver; } internal static void Clear() { lock (DirtyLock) { DirtyPlayers.Clear(); DirtyLeaderboards.Clear(); } _nextFlushTime = 0f; _loadedSlotId = -999; _documentResolver = null; _leaderboardResolver = null; } internal static void MarkDirty(ulong steamId) { lock (DirtyLock) { DirtyPlayers.Add(steamId); if (_nextFlushTime <= Time.time) { _nextFlushTime = Time.time + 3f; } } } internal static void MarkLeaderboardDirty(int slotId) { lock (DirtyLock) { DirtyLeaderboards.Add(slotId); if (_nextFlushTime <= Time.time) { _nextFlushTime = Time.time + 3f; } } } internal static void SavePlayerImmediate(int slotId, PlayerStatisticsDocument doc) { lock (DirtyLock) { DirtyPlayers.Remove(doc.SteamId); } StatisticsStore.SavePlayer(slotId, doc); } internal static void SaveLeaderboardImmediate(int slotId, LeaderboardDocument leaderboard) { lock (DirtyLock) { DirtyLeaderboards.Remove(slotId); } StatisticsStore.SaveLeaderboard(slotId, leaderboard); } internal static void ProcessDebounced() { int num; lock (DirtyLock) { num = DirtyPlayers.Count + DirtyLeaderboards.Count; } if (num != 0 && (num >= 8 || !(Time.time < _nextFlushTime))) { FlushDirty(async: true); _nextFlushTime = Time.time + 3f; WebDashboardSnapshotCache.MarkDirty(); } } internal static void FlushAllSync() { FlushDirty(async: false); WaitForInFlightWrites(); } private static void FlushDirty(bool async) { ulong[] array; int[] array2; lock (DirtyLock) { array = new ulong[DirtyPlayers.Count]; DirtyPlayers.CopyTo(array); DirtyPlayers.Clear(); array2 = new int[DirtyLeaderboards.Count]; DirtyLeaderboards.CopyTo(array2); DirtyLeaderboards.Clear(); } int loadedSlotId = _loadedSlotId; if (loadedSlotId < 0 || _documentResolver == null) { return; } ulong[] array3 = array; foreach (ulong num in array3) { PlayerStatisticsDocument playerStatisticsDocument = _documentResolver(num); if (playerStatisticsDocument != null) { string playerFilePath = StatisticsStore.GetPlayerFilePath(loadedSlotId, num); if (!string.IsNullOrEmpty(playerFilePath)) { string json = StatisticsStore.SerializePreparedPlayer(playerStatisticsDocument); EnqueueWrite(playerFilePath, json, !async); } } } if (_leaderboardResolver == null) { return; } int[] array4 = array2; foreach (int num2 in array4) { LeaderboardDocument leaderboardDocument = _leaderboardResolver(num2); string leaderboardFilePath = StatisticsStore.GetLeaderboardFilePath(num2); if (!string.IsNullOrEmpty(leaderboardFilePath)) { leaderboardDocument.Version = 1; leaderboardDocument.SaveSlotId = num2; leaderboardDocument.UpdatedAtUtc = DateTime.UtcNow; string json2 = StatisticsJson.SerializeLeaderboard(leaderboardDocument); EnqueueWrite(leaderboardFilePath, json2, !async); } } } private static void EnqueueWrite(string path, string json, bool waitForCompletion) { PendingWrite orAdd = InFlight.GetOrAdd(path, (string _) => new PendingWrite()); lock (orAdd) { orAdd.LatestJson = json; Task writeTask = orAdd.WriteTask; if (writeTask != null && !writeTask.IsCompleted) { if (!waitForCompletion) { return; } WaitForTask(orAdd.WriteTask); } string snapshot = json; orAdd.WriteTask = Task.Run(delegate { WriteToDisk(path, snapshot); }); if (waitForCompletion) { WaitForTask(orAdd.WriteTask); } } } private static void WriteToDisk(string path, string json) { try { StatisticsStore.SafeWriteText(path, json); } catch (Exception ex) { ModLog.Warn("Statistics", "Background statistics write failed (" + Path.GetFileName(path) + "): " + ex.Message); } CompleteWrite(path, json); } private static void CompleteWrite(string path, string json) { if (!InFlight.TryGetValue(path, out PendingWrite value)) { return; } lock (value) { if (value.LatestJson != null && value.LatestJson != json) { string newer = value.LatestJson; value.LatestJson = null; value.WriteTask = Task.Run(delegate { WriteToDisk(path, newer); }); } else { value.LatestJson = null; InFlight.TryRemove(path, out PendingWrite _); } } } private static void WaitForInFlightWrites() { foreach (KeyValuePair item in InFlight) { Task writeTask; lock (item.Value) { writeTask = item.Value.WriteTask; } WaitForTask(writeTask); } } private static void WaitForTask(Task? task) { if (task == null || task.IsCompleted) { return; } try { task.Wait(TimeSpan.FromSeconds(10.0)); } catch (Exception ex) { ModLog.Warn("Statistics", "Statistics write wait failed: " + ex.Message); } } } } namespace MimesisPlayerEnhancement.Features.Statistics.Patches { [HarmonyPatch(typeof(GameSessionInfo), "ApplyLoadedGameData")] public static class GameSessionInfoLoadPatches { [HarmonyPostfix] public static void Postfix() { if (ModConfig.EnableStatistics.Value && MimesisSaveManager.IsHost()) { int currentSaveSlotId = MimesisSaveManager.GetCurrentSaveSlotId(); if (MimesisSaveManager.IsValidSaveSlotId(currentSaveSlotId)) { StatisticsTracker.LoadForSlot(currentSaveSlotId); } } } } [HarmonyPatch(typeof(GameMainBase), "OnPlayerDeath")] public static class GameMainDeathPatches { [HarmonyPostfix] public static void Postfix(ProtoActor actor) { if (ModConfig.EnableStatistics.Value) { StatisticsTracker.OnPlayerDeath(actor); } } } [HarmonyPatch(typeof(GameMainBase), "OnPlayerRevive")] public static class GameMainRevivePatches { [HarmonyPostfix] public static void Postfix(ProtoActor actor) { if (ModConfig.EnableStatistics.Value) { StatisticsTracker.OnPlayerRevive(actor); } } } [HarmonyPatch(typeof(GameMainBase), "OnKillCountChanged")] public static class GameMainKillPatches { [HarmonyPostfix] public static void Postfix(ProtoActor actor, int killCount) { if (ModConfig.EnableStatistics.Value) { StatisticsTracker.OnKillCountChanged(actor, killCount); } } } [HarmonyPatch(typeof(UIPrefab_PlayerEnterInfo), "UpdatePlayerInfos")] internal static class InGameMessageDurationPatch { [HarmonyPrefix] private static bool Prefix(UIPrefab_PlayerEnterInfo __instance, ref IEnumerator __result) { __result = InGameMessageHelper.RunExtendedPlayerEnterInfoUpdates(__instance); return false; } } [HarmonyPatch(typeof(PlayReportManager), "IncreaseCycleCount")] public static class PlayReportCyclePatches { [HarmonyPostfix] public static void Postfix(PlayReportManager __instance) { if (ModConfig.EnableStatistics.Value) { StatisticsTracker.OnCycleCompleted(__instance); } } } [HarmonyPatch(typeof(SpeechEventArchive), "OnStartClient")] internal static class StatisticsLocalJoinPatch { [HarmonyPostfix] private static void Postfix(SpeechEventArchive __instance) { bool isLocal; try { isLocal = __instance.IsLocal; } catch { return; } if (isLocal) { StatisticsMessages.OnLocalPlayerArchiveStarted(); } } } [HarmonyPatch(typeof(UIPrefab_PlayerEnterInfo), "AddPlayerInfo")] internal static class StatisticsGamePlayerInfoPatch { [HarmonyPostfix] private static void Postfix(string userName, bool isEntering) { StatisticsMessages.OnGamePlayerInfoShown(userName, isEntering); } } [HarmonyPatch(typeof(MaintenanceRoom), "SaveGameData")] public static class StatisticsMaintenanceRoomSavePatches { [HarmonyPostfix] public static void Postfix(int saveSlotID, List playerNames, bool isAutoSave, MsgErrorCode __result) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) if (ModConfig.EnableStatistics.Value && (int)__result == 0 && MimesisSaveManager.IsHost()) { StatisticsTracker.OnGameSaved((!isAutoSave) ? saveSlotID : 0); } } } [HarmonyPatch(typeof(PlatformMgr), "Delete")] public static class StatisticsPlatformMgrDeletePatches { private const string Feature = "Statistics"; [HarmonyPostfix] public static void Postfix(string fileName) { if (!ModConfig.EnableStatistics.Value) { return; } try { if (!string.IsNullOrEmpty(fileName) && fileName.StartsWith("MMGameData", StringComparison.OrdinalIgnoreCase) && int.TryParse(Path.GetFileNameWithoutExtension(fileName).Replace("MMGameData", ""), out var result) && MimesisSaveManager.IsValidSaveSlotId(result)) { StatisticsStore.DeleteStatisticsData(result); } } catch (Exception ex) { ModLog.Warn("Statistics", "PlatformMgr.Delete: " + ex.Message); } } } [HarmonyPatch(typeof(VRoomManager), "OnRegistPlayer")] public static class VRoomManagerRegisterPatches { [HarmonyPostfix] public static void Postfix(ulong steamID) { if (ModConfig.EnableStatistics.Value && (MimesisSaveManager.TryGetActiveSaveSlotId(out var slotId) || StatisticsTracker.TryGetLoadedSlotId(out slotId))) { StatisticsTracker.OnPlayerRegistered(steamID, slotId); } } } [HarmonyPatch(typeof(VWorld), "OnUnregistPlayer")] public static class VWorldUnregisterPatches { [HarmonyPostfix] public static void Postfix(ulong steamID) { if (ModConfig.EnableStatistics.Value) { StatisticsTracker.OnPlayerUnregistered(steamID); } } } } namespace MimesisPlayerEnhancement.Features.Statistics.Models { public sealed class StatCounters { public long ItemCarryCount; public long DamageToAlly; public long MimicEncounterCount; public long TimeInStartingVolumeMs; public long CurrencyEarned; public long VoiceEvents; public long Deaths; public long Revives; public long Kills; public int CyclesCompleted; public long TotalConnectedSeconds; public void Add(StatCounters other) { if (other != null) { ItemCarryCount += other.ItemCarryCount; DamageToAlly += other.DamageToAlly; MimicEncounterCount += other.MimicEncounterCount; TimeInStartingVolumeMs += other.TimeInStartingVolumeMs; CurrencyEarned += other.CurrencyEarned; VoiceEvents += other.VoiceEvents; Deaths += other.Deaths; Revives += other.Revives; Kills += other.Kills; CyclesCompleted += other.CyclesCompleted; TotalConnectedSeconds += other.TotalConnectedSeconds; } } public StatCounters Clone() { return new StatCounters { ItemCarryCount = ItemCarryCount, DamageToAlly = DamageToAlly, MimicEncounterCount = MimicEncounterCount, TimeInStartingVolumeMs = TimeInStartingVolumeMs, CurrencyEarned = CurrencyEarned, VoiceEvents = VoiceEvents, Deaths = Deaths, Revives = Revives, Kills = Kills, CyclesCompleted = CyclesCompleted, TotalConnectedSeconds = TotalConnectedSeconds }; } } public sealed class SessionStats { public string SessionId = ""; public DateTime StartedAtUtc; public DateTime LastConnectedAtUtc; public DateTime? LastDisconnectedAtUtc; public int ReconnectCount; public bool IsOpen = true; public StatCounters Counters = new StatCounters(); } public sealed class GlobalStats { public StatCounters Counters = new StatCounters(); public int SessionsCompleted; } public sealed class PlayerStatisticsDocument { public const int CurrentVersion = 1; public int Version = 1; public ulong SteamId; public string DisplayName = ""; public GlobalStats Global = new GlobalStats(); public SessionStats? CurrentSession; public List RecentSessions = new List(); } public sealed class LeaderboardEntry { public ulong SteamId; public string DisplayName = ""; public long ItemCarryCount; public long DamageToAlly; public long MimicEncounterCount; public long TimeInStartingVolumeMs; public long CurrencyEarned; public long VoiceEvents; public long Deaths; public long Revives; public long Kills; public long TotalConnectedSeconds; public int SessionsCompleted; } public sealed class LeaderboardDocument { public const int CurrentVersion = 1; public int Version = 1; public int SaveSlotId; public DateTime UpdatedAtUtc; public List Entries = new List(); } } namespace MimesisPlayerEnhancement.Features.SpectatorTransition { internal static class SpectatorTransitionApplier { private const long MinDyingWaitTimeMs = 1L; private const float MinDeadCameraSeconds = 0.01f; internal static bool IsEnabled => ModConfig.EnableSpectatorTransition.Value; internal static long ScaleDyingWaitTime(long vanillaMs) { if (!IsEnabled) { return vanillaMs; } long num = (long)Math.Round((float)vanillaMs * ModConfig.DyingWaitTimeMultiplier.Value); if (num >= 1 || vanillaMs <= 0) { return num; } return 1L; } internal static float ScaleDeadCameraDuration(float vanillaSeconds) { if (!IsEnabled) { return vanillaSeconds; } float num = vanillaSeconds * ModConfig.DeadCameraDurationMultiplier.Value; if (!(num < 0.01f) || !(vanillaSeconds > 0f)) { return num; } return 0.01f; } } public static class SpectatorTransitionPatches { [HarmonyPatch(/*Could not decode attribute arguments.*/)] public static class VCreatureDyingWaitTimePatch { [HarmonyPostfix] public static void Postfix(ref long __result) { try { if (SpectatorTransitionApplier.IsEnabled && HostApplyGate.ShouldApplyHostOnlyFeature()) { __result = SpectatorTransitionApplier.ScaleDyingWaitTime(__result); } } catch (Exception ex) { ModLog.Warn("SpectatorTransition", "get_DyingWaitTime postfix failed — " + ex.Message); } } } [HarmonyPatch] public static class PlayerActorDeadCameraTotalDurationPatch { private static MethodBase? TargetMethod() { return AccessTools.PropertyGetter(PlayerActorType, "deadCameraTotalDuration"); } [HarmonyPostfix] public static void Postfix(ref float __result) { try { if (SpectatorTransitionApplier.IsEnabled && !DeadCameraFieldRestore.FieldsAreScaled) { __result = SpectatorTransitionApplier.ScaleDeadCameraDuration(__result); } } catch (Exception ex) { ModLog.Warn("SpectatorTransition", "get_deadCameraTotalDuration postfix failed — " + ex.Message); } } } [HarmonyPatch(typeof(GameMainBase), "CorDying")] public static class GameMainBaseCorDyingPatch { private static readonly FieldInfo BlendTimeField = AccessTools.Field(PlayerActorType, "deadCameraBlendTime"); private static readonly FieldInfo DurationField = AccessTools.Field(PlayerActorType, "deadCameraDuration"); private static readonly FieldInfo PlayerActorField = AccessTools.Field(typeof(GameConfig), "playerActor"); [HarmonyPrefix] public static void Prefix(GameMainBase __instance) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Expected O, but got Unknown try { if (!SpectatorTransitionApplier.IsEnabled) { return; } GameConfig val = (GameConfig)GameConfigProperty.GetValue(__instance); if (!((Object)(object)val == (Object)null)) { object value = PlayerActorField.GetValue(val); if (value != null) { float num = (float)BlendTimeField.GetValue(value); float num2 = (float)DurationField.GetValue(value); DeadCameraFieldRestore.Save(__instance, num, num2); BlendTimeField.SetValue(value, SpectatorTransitionApplier.ScaleDeadCameraDuration(num)); DurationField.SetValue(value, SpectatorTransitionApplier.ScaleDeadCameraDuration(num2)); } } } catch (Exception ex) { ModLog.Warn("SpectatorTransition", "CorDying prefix failed — " + ex.Message); } } } [HarmonyPatch(typeof(GameMainBase), "OnPlayerDeathEnterSpectator")] public static class GameMainBaseOnPlayerDeathEnterSpectatorPatch { [HarmonyPostfix] public static void Postfix(GameMainBase __instance) { try { DeadCameraFieldRestore.Restore(__instance); } catch (Exception ex) { ModLog.Warn("SpectatorTransition", "OnPlayerDeathEnterSpectator postfix failed — " + ex.Message); } } } [HarmonyPatch(typeof(GameMainBase), "OnPlayerRevive")] public static class GameMainBaseOnPlayerRevivePatch { [HarmonyPostfix] public static void Postfix(GameMainBase __instance) { try { DeadCameraFieldRestore.Restore(__instance); } catch (Exception ex) { ModLog.Warn("SpectatorTransition", "OnPlayerRevive postfix failed — " + ex.Message); } } } private static class DeadCameraFieldRestore { private static readonly FieldInfo BlendTimeField = AccessTools.Field(PlayerActorType, "deadCameraBlendTime"); private static readonly FieldInfo DurationField = AccessTools.Field(PlayerActorType, "deadCameraDuration"); private static readonly FieldInfo PlayerActorField = AccessTools.Field(typeof(GameConfig), "playerActor"); private static GameMainBase? _instance; private static float _blendTime; private static float _duration; internal static bool FieldsAreScaled { get; private set; } internal static void Save(GameMainBase instance, float blendTime, float duration) { _instance = instance; _blendTime = blendTime; _duration = duration; FieldsAreScaled = true; } internal static void Restore(GameMainBase instance) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Expected O, but got Unknown if (!FieldsAreScaled || (Object)(object)_instance != (Object)(object)instance) { return; } try { GameConfig val = (GameConfig)GameConfigProperty.GetValue(instance); if (!((Object)(object)val == (Object)null)) { object value = PlayerActorField.GetValue(val); if (value != null) { BlendTimeField.SetValue(value, _blendTime); DurationField.SetValue(value, _duration); } } } finally { FieldsAreScaled = false; _instance = null; } } } private const string Feature = "SpectatorTransition"; private static readonly Type PlayerActorType = AccessTools.Inner(typeof(GameConfig), "PlayerActor"); private static readonly PropertyInfo GameConfigProperty = AccessTools.Property(typeof(GameMainBase), "gameConfig"); public static void Apply(Harmony harmony) { GameNetworkApi.GetGameAssembly(); HarmonyPatchHelper.PatchApplyResult result = HarmonyPatchHelper.ApplyPatchTypes(harmony, "SpectatorTransition", HarmonyPatchHelper.GetNestedPatchTypes(typeof(SpectatorTransitionPatches))); LogPatchAudit(harmony); HarmonyPatchHelper.LogPatchSummary("SpectatorTransition", result); } private static void LogPatchAudit(Harmony harmony) { HarmonyPatchHelper.LogPatchAudit("SpectatorTransition", harmony, new <>z__ReadOnlyArray<(string, MethodBase)>(new(string, MethodBase)[5] { ("get_DyingWaitTime/VCreature", AccessTools.PropertyGetter(typeof(VCreature), "DyingWaitTime")), ("get_deadCameraTotalDuration/PlayerActor", AccessTools.PropertyGetter(PlayerActorType, "deadCameraTotalDuration")), ("CorDying/GameMainBase", AccessTools.Method(typeof(GameMainBase), "CorDying", (Type[])null, (Type[])null)), ("OnPlayerDeathEnterSpectator/GameMainBase", AccessTools.Method(typeof(GameMainBase), "OnPlayerDeathEnterSpectator", (Type[])null, (Type[])null)), ("OnPlayerRevive/GameMainBase", AccessTools.Method(typeof(GameMainBase), "OnPlayerRevive", (Type[])null, (Type[])null)) })); } } } namespace MimesisPlayerEnhancement.Features.SpawnScaling { internal static class FixedSpawnCoordinator { private sealed class RoomState { private readonly Dictionary _remainingQuotaByMasterId = new Dictionary(); private readonly List _slots = new List(); private readonly Dictionary _spawnDataToRoom = new Dictionary(); internal DungeonRoom Room { get; } internal int SlotCount => _slots.Count; internal bool HasQuotas { get { foreach (int value in _remainingQuotaByMasterId.Values) { if (value > 0) { return true; } } return false; } } internal RoomState(DungeonRoom room) { Room = room; } internal void RegisterSlot(string key, FixedSpawnedActorData data) { _slots.Add(new SpawnSlot(key, data)); _spawnDataToRoom[(SpawnedActorData)(object)data] = Room; } internal bool TryGetRoomForSpawnData(SpawnedActorData data, out DungeonRoom room) { return _spawnDataToRoom.TryGetValue(data, out room); } internal IEnumerable>> GroupSlotsByMasterId() { Dictionary> dictionary = new Dictionary>(); foreach (SpawnSlot slot in _slots) { if (!dictionary.TryGetValue(((SpawnedActorData)slot.Data).MasterID, out var value)) { value = new List(); dictionary[((SpawnedActorData)slot.Data).MasterID] = value; } value.Add(slot); } return dictionary; } internal void SetRemainingQuota(int masterId, int quota) { if (quota <= 0) { _remainingQuotaByMasterId.Remove(masterId); } else { _remainingQuotaByMasterId[masterId] = quota; } } internal bool TryConsumeQuota(int masterId) { if (!_remainingQuotaByMasterId.TryGetValue(masterId, out var value) || value <= 0) { return false; } _remainingQuotaByMasterId[masterId] = value - 1; return true; } internal void RestoreQuota(int masterId) { if (_remainingQuotaByMasterId.TryGetValue(masterId, out var value)) { _remainingQuotaByMasterId[masterId] = value + 1; } } } private readonly struct SpawnSlot { internal string Key { get; } internal FixedSpawnedActorData Data { get; } internal SpawnSlot(string key, FixedSpawnedActorData data) { Key = key; Data = data; } } private readonly struct PendingRespawn { internal DungeonRoom Room { get; } internal SpawnedActorData Data { get; } internal int MasterId { get; } internal float ExecuteAt { get; } internal float NextAttemptAt { get; } internal bool QuotaConsumed { get; } internal PendingRespawn(DungeonRoom room, SpawnedActorData data, int masterId, float executeAt, bool quotaConsumed) { Room = room; Data = data; MasterId = masterId; ExecuteAt = executeAt; NextAttemptAt = executeAt; QuotaConsumed = quotaConsumed; } internal PendingRespawn WithNextAttemptAt(float nextAttemptAt) { return new PendingRespawn(Room, Data, MasterId, ExecuteAt, QuotaConsumed, nextAttemptAt); } private PendingRespawn(DungeonRoom room, SpawnedActorData data, int masterId, float executeAt, bool quotaConsumed, float nextAttemptAt) { Room = room; Data = data; MasterId = masterId; ExecuteAt = executeAt; NextAttemptAt = nextAttemptAt; QuotaConsumed = quotaConsumed; } } private const string Feature = "SpawnScaling"; private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo SpawnedActorDatasField = typeof(DungeonRoom).GetField("_spawnedActorDatas", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new InvalidOperationException("DungeonRoom._spawnedActorDatas not found"); private static readonly MethodInfo ExecuteCreatureSpawnMethod = AccessTools.Method(typeof(IVroom), "ExecuteCreatureSpawn", new Type[1] { typeof(SpawnedActorData) }, (Type[])null) ?? throw new InvalidOperationException("IVroom.ExecuteCreatureSpawn not found"); private static readonly MethodInfo SpawnMonsterMethod = AccessTools.Method(typeof(IVroom), "SpawnMonster", new Type[6] { typeof(int), typeof(SpawnedActorData), typeof(bool), typeof(string), typeof(string), typeof(ReasonOfSpawn) }, (Type[])null) ?? throw new InvalidOperationException("IVroom.SpawnMonster not found"); private static readonly MethodInfo MemberwiseCloneMethod = AccessTools.Method(typeof(object), "MemberwiseClone", (Type[])null, (Type[])null) ?? throw new InvalidOperationException("object.MemberwiseClone not found"); private static readonly FieldInfo CurrentSpawnCountBackingField = AccessToolsField(typeof(SpawnedActorData), "k__BackingField"); private static readonly FieldInfo SpawnDataIndexField = AccessToolsField(typeof(SpawnedActorData), "Index"); private static readonly HashSet AppliedRooms = new HashSet(); private static readonly Dictionary RoomStates = new Dictionary(); private static readonly List PendingRespawns = new List(); internal static void ApplyAfterInit(DungeonRoom room) { if (!ModConfig.EnableSpawnScaling.Value || AppliedRooms.Contains(room) || HostApplyGate.IsParticipantClient() || !HostApplyGate.ShouldApplyHostOnlyFeature()) { return; } AppliedRooms.Add(room); if (!(SpawnedActorDatasField.GetValue(room) is IDictionary { Count: not 0 } dictionary)) { return; } int memberCount = ((IVroom)room).GetMemberCount(); MapMarker_CreatureSpawnPoint[] array = CollectCreatureSpawnMarkers(); RoomState roomState = new RoomState(room); foreach (DictionaryEntry item in dictionary) { object? value = item.Value; FixedSpawnedActorData val = (FixedSpawnedActorData)((value is FixedSpawnedActorData) ? value : null); if (val != null && IsFixedCreatureSpawn((SpawnedActorData)(object)val)) { roomState.RegisterSlot((item.Key as string) ?? ((SpawnedActorData)val).Name, val); } } foreach (KeyValuePair> item2 in roomState.GroupSlotsByMasterId()) { int key = item2.Key; SpawnCategory category = SpawnCategoryLookup.GetCategory(key); float effectiveMultiplier = SpawnMultiplierResolver.GetEffectiveMultiplier(category, memberCount); int count = item2.Value.Count; int num = SpawnMultiplierResolver.ScaleCount(count, effectiveMultiplier); int num2 = num - count; if (num2 <= 0) { continue; } string displayName = MonsterTypeLookup.GetDisplayName(key); HashSet hashSet = new HashSet(); FixedSpawnedActorData val2 = null; foreach (SpawnSlot item3 in item2.Value) { hashSet.Add(((SpawnedActorData)item3.Data).Index); if (val2 == null) { val2 = item3.Data; } } if (val2 == null) { continue; } List list = new List(); MapMarker_CreatureSpawnPoint[] array2 = array; foreach (MapMarker_CreatureSpawnPoint val3 in array2) { if (((MapMarker_SpawnPoint)val3).masterID == key && !hashSet.Contains(((MapMarker_SpawnPoint)val3).ID)) { list.Add(val3); } } int num3 = ActivateUnusedMarkers(room, roomState, dictionary, val2, list, num2); int num4 = num2 - num3; roomState.SetRemainingQuota(key, num4); ModLog.Info("SpawnScaling", $"Fixed spawn scaling — category={SpawnCategoryLookup.Format(category)}, name={displayName}, master={key}, " + $"{effectiveMultiplier:0.##}× (vanilla={count}, target={num}, markers+={num3}, respawnQuota={num4})"); } if (roomState.HasQuotas || roomState.SlotCount > 0) { RoomStates[room] = roomState; } } internal static void OnActorDead(SpawnedActorData spawnData) { if (!ModConfig.EnableSpawnScaling.Value || spawnData == null || HostApplyGate.IsParticipantClient() || !HostApplyGate.ShouldApplyHostOnlyFeature() || !IsFixedCreatureSpawn(spawnData) || !TryFindRoomState(spawnData, out RoomState state, out DungeonRoom room)) { return; } bool flag = state.TryConsumeQuota(spawnData.MasterID); if (!ShouldScheduleRespawn(spawnData, flag)) { if (flag) { state.RestoreQuota(spawnData.MasterID); } } else { ScheduleRespawn(room, spawnData, spawnData.MasterID, flag); } } internal static void ProcessPendingRespawns() { //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0160: Unknown result type (might be due to invalid IL or missing references) if (PendingRespawns.Count == 0) { return; } float time = Time.time; for (int num = PendingRespawns.Count - 1; num >= 0; num--) { PendingRespawn pending = PendingRespawns[num]; if (!(time < pending.ExecuteAt) && !(time < pending.NextAttemptAt)) { RoomState value; if (pending.Room == null || pending.Data == null) { PendingRespawns.RemoveAt(num); } else if (FixedSpawnProximity.IsPlayerBlockingRespawn(pending.Room, pending.Data.PosVector)) { if (ModConfig.EnableDebugLogging.Value) { ModLog.Debug("SpawnScaling", $"Fixed spawn respawn waiting — master={pending.MasterId}, marker={pending.Data.Index}, " + $"players within {ModConfig.FixedSpawnRespawnMinPlayerDistanceMeters.Value:0.#}m"); } DeferNextAttempt(num, pending, time); } else if (!RoomStates.TryGetValue(pending.Room, out value)) { PendingRespawns.RemoveAt(num); } else { try { if (TryRespawnCreature(pending.Room, pending.Data)) { PendingRespawns.RemoveAt(num); } else if (pending.Data.ActorID != 0) { PendingRespawns.RemoveAt(num); } else if (FixedSpawnProximity.IsPlayerBlockingRespawn(pending.Room, pending.Data.PosVector)) { DeferNextAttempt(num, pending, time); } else { DeferNextAttempt(num, pending, time); } } catch (Exception ex) { ModLog.Warn("SpawnScaling", $"Fixed spawn respawn failed — master={pending.MasterId}: {ex.Message}"); DeferNextAttempt(num, pending, time); } } } } } private static int ActivateUnusedMarkers(DungeonRoom room, RoomState state, IDictionary spawnDatas, FixedSpawnedActorData template, List unusedMarkers, int need) { int num = 0; for (int i = 0; i < unusedMarkers.Count; i++) { if (num >= need) { break; } MapMarker_CreatureSpawnPoint val = unusedMarkers[i]; string text = ((MapMarker_SpawnPoint)val).Name; if (string.IsNullOrWhiteSpace(text) || spawnDatas.Contains(text)) { text = $"{((MapMarker_SpawnPoint)val).masterID}_{((MapMarker_SpawnPoint)val).ID}"; } if (!spawnDatas.Contains(text)) { FixedSpawnedActorData val2 = CreateSpawnDataFromMarker(template, val); spawnDatas.Add(text, val2); state.RegisterSlot(text, val2); try { ExecuteCreatureSpawn(room, (SpawnedActorData)(object)val2); num++; } catch (Exception ex) { spawnDatas.Remove(text); ModLog.Warn("SpawnScaling", $"Fixed spawn marker activation failed — master={((MapMarker_SpawnPoint)val).masterID}, marker={((MapMarker_SpawnPoint)val).ID}: {ex.Message}"); } } } return num; } private static FixedSpawnedActorData CreateSpawnDataFromMarker(FixedSpawnedActorData template, MapMarker_CreatureSpawnPoint marker) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Expected O, but got Unknown //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Expected O, but got Unknown //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Expected O, but got Unknown //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Expected O, but got Unknown //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Expected O, but got Unknown //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Expected O, but got Unknown //IL_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_00ee: Expected O, but got Unknown //IL_00ee: Unknown result type (might be due to invalid IL or missing references) //IL_0109: Unknown result type (might be due to invalid IL or missing references) //IL_0118: Expected O, but got Unknown //IL_0118: Unknown result type (might be due to invalid IL or missing references) //IL_0138: Expected O, but got Unknown //IL_0138: Unknown result type (might be due to invalid IL or missing references) //IL_0158: Expected O, but got Unknown //IL_0158: Unknown result type (might be due to invalid IL or missing references) //IL_017d: Expected O, but got Unknown //IL_017d: Unknown result type (might be due to invalid IL or missing references) //IL_019d: Expected O, but got Unknown //IL_019e: Expected O, but got Unknown FixedSpawnedActorData val = (FixedSpawnedActorData)MemberwiseCloneMethod.Invoke(template, null); ((SpawnedActorData)val).SetActorID(0); SetField((object)val, CurrentSpawnCountBackingField, 0); SetField((object)val, SpawnDataIndexField, ((MapMarker_SpawnPoint)marker).ID); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "IsIndoor"), ((MapMarker_SpawnPoint)marker).IsIndoor); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "MasterID"), ((MapMarker_SpawnPoint)marker).masterID); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "MaxRespawnCount"), ((MapMarker_SpawnPoint)marker).MaxRespawnCount); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "Name"), ((MapMarker_SpawnPoint)marker).Name); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "Pos"), ((MapMarker_SpawnPoint)marker).pos); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "PosVector"), ((MapMarker_SpawnPoint)marker).pos.pos); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "AIName"), marker.aiName); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "BTName"), marker.btName); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "SpawnWaitTime"), ((MapMarker_SpawnPoint)marker).spawnWaitTime); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "StackCount"), 1); return val; } private static void ExecuteCreatureSpawn(DungeonRoom room, SpawnedActorData spawnData) { if (room == null) { throw new InvalidOperationException("DungeonRoom is not IVroom"); } ExecuteCreatureSpawnMethod.Invoke(room, new object[1] { spawnData }); } private static void SetField(object target, FieldInfo field, object? value) { field.SetValue(target, value); } private static FieldInfo AccessToolsField(Type type, string name) { return AccessTools.Field(type, name) ?? throw new InvalidOperationException(type.Name + "." + name + " not found"); } private unsafe static bool IsFixedCreatureSpawn(SpawnedActorData spawnData) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000e: 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_002a: Unknown result type (might be due to invalid IL or missing references) if (spawnData is FixedSpawnedActorData) { MapMarkerType markerType = spawnData.MarkerType; if (!((object)(*(MapMarkerType*)(&markerType))/*cast due to .constrained prefix*/).Equals((object)(MapMarkerType)0)) { markerType = spawnData.MarkerType; return ((object)(*(MapMarkerType*)(&markerType))/*cast due to .constrained prefix*/).Equals((object)(MapMarkerType)3); } return true; } return false; } private static bool ShouldScheduleRespawn(SpawnedActorData spawnData, bool quotaConsumed) { if (!quotaConsumed && SpawnCategoryLookup.GetCategory(spawnData.MasterID) != SpawnCategory.Trap) { return HasRespawnBudget(spawnData); } return true; } private unsafe static bool HasRespawnBudget(SpawnedActorData spawnData) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) SpawnType spawnType = spawnData.SpawnType; if (!((object)(*(SpawnType*)(&spawnType))/*cast due to .constrained prefix*/).Equals((object)(SpawnType)0)) { if (spawnData.MaxRespawnCount != 0 && spawnData.CurrentSpawnCount > spawnData.MaxRespawnCount) { return spawnData.EnableReset; } return true; } return false; } private static bool TryRespawnCreature(DungeonRoom room, SpawnedActorData spawnData) { if (room == null) { return false; } if (spawnData.ActorID != 0) { return false; } PrepareSpawnCountForRespawn(spawnData); return (bool)SpawnMonsterMethod.Invoke(room, new object[6] { spawnData.MasterID, spawnData, spawnData.IsIndoor, spawnData.AIName, spawnData.BTName, (object)(ReasonOfSpawn)5 }); } private static void PrepareSpawnCountForRespawn(SpawnedActorData spawnData) { if (spawnData.EnableReset && spawnData.MaxRespawnCount > 0 && spawnData.CurrentSpawnCount > spawnData.MaxRespawnCount) { SetField(spawnData, CurrentSpawnCountBackingField, 0); } } private static void ScheduleRespawn(DungeonRoom room, SpawnedActorData spawnData, int masterId, bool quotaConsumed) { //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_00e0: Unknown result type (might be due to invalid IL or missing references) foreach (PendingRespawn pendingRespawn in PendingRespawns) { if (pendingRespawn.Room == room && pendingRespawn.Data == spawnData) { return; } } float value = ModConfig.FixedSpawnRespawnDelayMinSeconds.Value; float value2 = ModConfig.FixedSpawnRespawnDelayMaxSeconds.Value; float num = ((value >= value2) ? value : Random.Range(value, value2)); long spawnWaitTime = spawnData.SpawnWaitTime; if (spawnWaitTime > 0) { float num2 = (float)spawnWaitTime / 1000f; if (num2 > num) { num = num2; } } PendingRespawns.Add(new PendingRespawn(room, spawnData, masterId, Time.time + num, quotaConsumed)); if (ModConfig.EnableDebugLogging.Value) { Vector3 posVector = spawnData.PosVector; ModLog.Debug("SpawnScaling", $"Fixed spawn respawn scheduled — master={masterId}, marker={spawnData.Index}, " + $"pos={SpawnScalingLog.FormatLocation(room, posVector)}, delay={num:0.0}s"); } } private static void DeferNextAttempt(int index, PendingRespawn pending, float now) { PendingRespawns[index] = pending.WithNextAttemptAt(now + 1f); } private static bool TryFindRoomState(SpawnedActorData spawnData, out RoomState state, out DungeonRoom room) { foreach (KeyValuePair roomState in RoomStates) { if (roomState.Value.TryGetRoomForSpawnData(spawnData, out room)) { state = roomState.Value; return true; } } state = null; room = null; return false; } private static MapMarker_CreatureSpawnPoint[] CollectCreatureSpawnMarkers() { return Object.FindObjectsByType((FindObjectsSortMode)0); } } internal static class FixedSpawnProximity { private readonly struct CachedBlockResult { internal bool Blocked { get; } internal float NextCheckAt { get; } internal CachedBlockResult(bool blocked, float nextCheckAt) { Blocked = blocked; NextCheckAt = nextCheckAt; } } private static readonly Dictionary CreatureBlockCache = new Dictionary(); private static readonly Dictionary LootBlockCache = new Dictionary(); internal static bool ShouldBlockFixedCreatureRespawn(DungeonRoom? room, SpawnedActorData? spawnData, bool throttle = true) { if (ModConfig.FixedSpawnRespawnMinPlayerDistanceMeters.Value > 0f && room != null && spawnData != null && IsFixedCreatureRespawn(spawnData)) { return IsPlayerBlockingRespawnCached(room, spawnData, CreatureBlockCache, throttle); } return false; } internal static bool ShouldBlockFixedLootRespawn(DungeonRoom? room, SpawnedActorData? spawnData, bool throttle = true) { if (ModConfig.FixedSpawnRespawnMinPlayerDistanceMeters.Value > 0f && room != null && spawnData != null && IsFixedLootRespawn(spawnData)) { return IsPlayerBlockingRespawnCached(room, spawnData, LootBlockCache, throttle); } return false; } internal static bool IsPlayerBlockingRespawn(DungeonRoom room, Vector3 spawnPos) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) float value = ModConfig.FixedSpawnRespawnMinPlayerDistanceMeters.Value; if (value <= 0f) { return false; } List<(VActor, double)> playerActorsInRange = ((IVroom)room).GetPlayerActorsInRange(spawnPos, 0f, value, true); if (playerActorsInRange == null || playerActorsInRange.Count == 0) { return false; } foreach (var item3 in playerActorsInRange) { VActor item = item3.Item1; double item2 = item3.Item2; VPlayer val = (VPlayer)(object)((item is VPlayer) ? item : null); if (val != null && ((VActor)val).IsAliveStatus() && item2 <= (double)value) { return true; } } return false; } private static bool IsPlayerBlockingRespawnCached(DungeonRoom room, SpawnedActorData spawnData, Dictionary cache, bool throttle) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) float time = Time.time; if (throttle && cache.TryGetValue(spawnData, out var value) && time < value.NextCheckAt) { return value.Blocked; } bool flag = IsPlayerBlockingRespawn(room, spawnData.PosVector); if (throttle) { cache[spawnData] = new CachedBlockResult(flag, time + 1f); } return flag; } private unsafe static bool IsFixedCreatureRespawn(SpawnedActorData spawnData) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000e: 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_002a: Unknown result type (might be due to invalid IL or missing references) if (spawnData is FixedSpawnedActorData) { MapMarkerType markerType = spawnData.MarkerType; if (!((object)(*(MapMarkerType*)(&markerType))/*cast due to .constrained prefix*/).Equals((object)(MapMarkerType)0)) { markerType = spawnData.MarkerType; if (!((object)(*(MapMarkerType*)(&markerType))/*cast due to .constrained prefix*/).Equals((object)(MapMarkerType)3)) { goto IL_0052; } } if (spawnData.ActorID == 0) { return spawnData.CurrentSpawnCount > 0; } } goto IL_0052; IL_0052: return false; } private unsafe static bool IsFixedLootRespawn(SpawnedActorData spawnData) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) if (spawnData is FixedSpawnedActorData) { MapMarkerType markerType = spawnData.MarkerType; if (((object)(*(MapMarkerType*)(&markerType))/*cast due to .constrained prefix*/).Equals((object)(MapMarkerType)1) && spawnData.MasterID > 0 && spawnData.ActorID == 0) { return spawnData.CurrentSpawnCount > 0; } } return false; } } internal static class MonsterTypeLookup { private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly PropertyInfo? HubDatamanProperty = typeof(Hub).GetProperty("dataman", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static bool TryGetMonster(int masterId, out MonsterInfo info) { info = null; if (masterId <= 0 || (Object)(object)Hub.s == (Object)null) { return false; } object? obj = HubDatamanProperty?.GetValue(Hub.s); DataManager val = (DataManager)((obj is DataManager) ? obj : null); if (val == null) { return false; } MonsterInfo monsterInfo = val.ExcelDataManager.GetMonsterInfo(masterId); if (monsterInfo == null) { return false; } info = monsterInfo; return true; } internal static MonsterType GetMonsterType(int masterId) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) if (TryGetMonster(masterId, out MonsterInfo info)) { return info.MonsterType; } return (MonsterType)0; } internal static string GetDisplayName(int masterId, MonsterInfo? info = null) { if (info != null || TryGetMonster(masterId, out info)) { if (!string.IsNullOrWhiteSpace(info.Name)) { return info.Name; } return masterId.ToString(); } return masterId.ToString(); } } internal enum SpawnCategory { Mimic, Boss, Jako, Special, Trap, Other } internal static class SpawnCategoryLookup { internal unsafe static SpawnCategory GetCategory(MonsterInfo info) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) if (!info.IsMimic()) { if (!IsTrap(info)) { MonsterType monsterType = info.MonsterType; if (!((object)(*(MonsterType*)(&monsterType))/*cast due to .constrained prefix*/).Equals((object)(MonsterType)2)) { monsterType = info.MonsterType; if (!((object)(*(MonsterType*)(&monsterType))/*cast due to .constrained prefix*/).Equals((object)(MonsterType)3)) { monsterType = info.MonsterType; if (!((object)(*(MonsterType*)(&monsterType))/*cast due to .constrained prefix*/).Equals((object)(MonsterType)4)) { monsterType = info.MonsterType; if (!((object)(*(MonsterType*)(&monsterType))/*cast due to .constrained prefix*/).Equals((object)(MonsterType)1)) { return SpawnCategory.Other; } return SpawnCategory.Mimic; } return SpawnCategory.Special; } return SpawnCategory.Jako; } return SpawnCategory.Boss; } return SpawnCategory.Trap; } return SpawnCategory.Mimic; } internal static SpawnCategory GetCategory(int masterId) { if (MonsterTypeLookup.TryGetMonster(masterId, out MonsterInfo info)) { return GetCategory(info); } return SpawnCategory.Other; } internal static string Format(SpawnCategory category) { return category.ToString(); } private static bool IsTrap(MonsterInfo info) { if (!ContainsTrapHint(info.Name) && !ContainsTrapHint(info.PuppetName)) { return ContainsTrapHint(info.BTName); } return true; } private static bool ContainsTrapHint(string? value) { if (!string.IsNullOrWhiteSpace(value)) { return value.IndexOf("trap", StringComparison.OrdinalIgnoreCase) >= 0; } return false; } } internal static class SpawnMultiplierResolver { internal static bool IsAutoScaleEnabled(SpawnCategory category) { return category switch { SpawnCategory.Mimic => ModConfig.AutoScaleMimicSpawnsByPlayerCount.Value, SpawnCategory.Boss => ModConfig.AutoScaleBossSpawnsByPlayerCount.Value, SpawnCategory.Jako => ModConfig.AutoScaleJakoSpawnsByPlayerCount.Value, SpawnCategory.Special => ModConfig.AutoScaleSpecialSpawnsByPlayerCount.Value, SpawnCategory.Trap => ModConfig.AutoScaleTrapSpawnsByPlayerCount.Value, _ => ModConfig.AutoScaleOtherSpawnsByPlayerCount.Value, }; } internal static float GetPlayerScale(SpawnCategory category, int playerCount) { return ScalingMath.GetPlayerScale(playerCount, IsAutoScaleEnabled(category)); } internal static float GetPerCategoryMultiplier(SpawnCategory category) { return category switch { SpawnCategory.Mimic => ModConfig.MimicSpawnMultiplier.Value, SpawnCategory.Boss => ModConfig.BossSpawnMultiplier.Value, SpawnCategory.Jako => ModConfig.JakoSpawnMultiplier.Value, SpawnCategory.Special => ModConfig.SpecialSpawnMultiplier.Value, SpawnCategory.Trap => ModConfig.TrapSpawnMultiplier.Value, _ => ModConfig.OtherSpawnMultiplier.Value, }; } internal static float GetEffectiveMultiplier(SpawnCategory category, int playerCount) { return GetPerCategoryMultiplier(category) * GetPlayerScale(category, playerCount); } internal static float GetEffectiveMultiplier(int masterId, int playerCount) { return GetEffectiveMultiplier(SpawnCategoryLookup.GetCategory(masterId), playerCount); } internal static int ScaleCount(int vanilla, float multiplier) { return ScalingMath.ScaleCount(vanilla, multiplier); } internal static int ScaleCountWithImplicitBase(int vanilla, float multiplier, int implicitWhenZero) { return ScalingMath.ScaleCountWithImplicitBase(vanilla, multiplier, implicitWhenZero); } } internal static class SpawnScalingApplier { private const string Feature = "SpawnScaling"; private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly Type SpecialMonsterSpawnGroupType = typeof(DungeonRoom).GetNestedType("SpecialMonsterSpawnGroup", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new InvalidOperationException("DungeonRoom.SpecialMonsterSpawnGroup not found"); private static readonly FieldInfo MimicSpawnCountMaxField = AccessToolsField(typeof(DungeonRoom), "_mimicSpawnCountMax"); private static readonly FieldInfo MimicSpawnCountRemainField = AccessToolsField(typeof(DungeonRoom), "_mimicSpawnCountRemain"); private static readonly FieldInfo NormalMonsterThreatLimitField = AccessToolsField(typeof(DungeonRoom), "_normalMonsterThreatLimit"); private static readonly FieldInfo NormalMonsterThreatRemainField = AccessToolsField(typeof(DungeonRoom), "_normalMonsterThreatRemain"); private static readonly FieldInfo NormalMonsterSpawnThreatMinThresholdField = AccessToolsField(typeof(DungeonRoom), "_normalMonsterSpawnThreatMinThreshold"); private static readonly FieldInfo SpecialMonsterSpawnGroupsField = AccessToolsField(typeof(DungeonRoom), "_specialMonsterSpawnGroups"); private static readonly FieldInfo SpawnedActorDatasField = AccessToolsField(typeof(DungeonRoom), "_spawnedActorDatas"); private static readonly FieldInfo GroupSpawnDatasField = AccessToolsField(typeof(DungeonRoom), "_groupSpawnDatas"); private static readonly FieldInfo SpecialGroupInfoField = AccessToolsField(SpecialMonsterSpawnGroupType, "Info"); private static readonly FieldInfo SpecialGroupSpawnCountMaxField = AccessToolsField(SpecialMonsterSpawnGroupType, "SpawnCountMax"); private static readonly FieldInfo SpecialGroupSpawnCountRemainField = AccessToolsField(SpecialMonsterSpawnGroupType, "SpawnCountRemain"); private static readonly FieldInfo SpecialSpawnInfoSpawnCountMinField = AccessToolsField(typeof(SpecialMonsterSpawnInfo), "SpawnCountMin"); private static readonly FieldInfo SpecialSpawnInfoSpawnCountMaxField = AccessToolsField(typeof(SpecialMonsterSpawnInfo), "SpawnCountMax"); private static readonly FieldInfo GroupSpawnCountBackingField = AccessToolsField(typeof(GroupSpawnData), "k__BackingField"); private static readonly HashSet AppliedRooms = new HashSet(); private static readonly HashSet SkippedClientRooms = new HashSet(); internal static bool IsApplied(DungeonRoom room) { return AppliedRooms.Contains(room); } internal static void EnsureApplied(DungeonRoom room) { if (AppliedRooms.Contains(room)) { return; } if (HostApplyGate.IsParticipantClient()) { if (SkippedClientRooms.Add(room)) { ModLog.Debug("SpawnScaling", "Spawn scaling skipped — participant client"); } } else if (!HostApplyGate.ShouldApplyHostOnlyFeature()) { ModLog.Debug("SpawnScaling", "Spawn scaling deferred — waiting for host session"); } else { Apply(room); AppliedRooms.Add(room); } } internal static void Apply(DungeonRoom room) { if (!ModConfig.EnableSpawnScaling.Value) { ModLog.Debug("SpawnScaling", "Spawn scaling skipped — EnableSpawnScaling is off"); return; } if (!HostApplyGate.ShouldApplyHostOnlyFeature()) { ModLog.Debug("SpawnScaling", "Spawn scaling skipped — not host"); return; } int memberCount = ((IVroom)room).GetMemberCount(); SpawnScalingLog.InfoScalingApplied(memberCount); float effectiveMultiplier = SpawnMultiplierResolver.GetEffectiveMultiplier(SpawnCategory.Mimic, memberCount); float effectiveMultiplier2 = SpawnMultiplierResolver.GetEffectiveMultiplier(SpawnCategory.Jako, memberCount); int num = ScaleField(room, MimicSpawnCountMaxField, effectiveMultiplier, "mimicSpawnCountMax"); int num2 = ScaleField(room, MimicSpawnCountRemainField, effectiveMultiplier, "mimicSpawnCountRemain"); int num3 = ScaleField(room, NormalMonsterThreatLimitField, effectiveMultiplier2, "normalMonsterThreatLimit"); int num4 = ScaleField(room, NormalMonsterThreatRemainField, effectiveMultiplier2, "normalMonsterThreatRemain"); int num5 = ScaleField(room, NormalMonsterSpawnThreatMinThresholdField, effectiveMultiplier2, "normalMonsterSpawnThreatMinThreshold"); int num6 = ScaleSpecialGroups(room, memberCount); int num7 = ScaleSpawnPointDatas(room, memberCount); int num8 = ScaleGroupSpawnDatas(room, memberCount); ModLog.Info("SpawnScaling", $"Spawn budgets updated — mimic {effectiveMultiplier:0.##}× (max={num}, remain={num2}), " + $"jako {effectiveMultiplier2:0.##}× (limit={num3}, remain={num4}, min={num5}), " + $"specialGroups={num6}, spawnPoints={num7}, groupSpawns={num8}"); } private static int ScaleSpecialGroups(DungeonRoom room, int playerCount) { if (!(SpecialMonsterSpawnGroupsField.GetValue(room) is IList list)) { return 0; } int num = 0; foreach (object item in list) { if (item != null) { object? value = SpecialGroupInfoField.GetValue(item); SpecialMonsterSpawnInfo val = (SpecialMonsterSpawnInfo)((value is SpecialMonsterSpawnInfo) ? value : null); if (val != null) { float effectiveMultiplier = SpawnMultiplierResolver.GetEffectiveMultiplier(val.MasterID, playerCount); SpawnCategory category = SpawnCategoryLookup.GetCategory(val.MasterID); string displayName = MonsterTypeLookup.GetDisplayName(val.MasterID); int num2 = ScaleField(item, SpecialGroupSpawnCountMaxField, effectiveMultiplier, $"specialGroup[{val.MasterID}].spawnCountMax"); int num3 = ScaleField(item, SpecialGroupSpawnCountRemainField, effectiveMultiplier, $"specialGroup[{val.MasterID}].spawnCountRemain"); ScaleField(val, SpecialSpawnInfoSpawnCountMinField, effectiveMultiplier, $"specialGroup[{val.MasterID}].spawnCountMin"); ScaleField(val, SpecialSpawnInfoSpawnCountMaxField, effectiveMultiplier, $"specialGroup[{val.MasterID}].spawnCountMaxInfo"); num++; ModLog.Debug("SpawnScaling", "Special group configured — category=" + SpawnCategoryLookup.Format(category) + ", name=" + displayName + ", " + $"master={val.MasterID}, {effectiveMultiplier:0.##}× (max={num2}, remain={num3})"); } } } return num; } private static int ScaleSpawnPointDatas(DungeonRoom room, int playerCount) { if (!(SpawnedActorDatasField.GetValue(room) is IDictionary dictionary)) { return 0; } int num = 0; foreach (DictionaryEntry item in dictionary) { if (item.Value != null) { num += (ScaleSpawnDataObject(item.Value, playerCount) ? 1 : 0); } } return num; } private static int ScaleGroupSpawnDatas(DungeonRoom room, int playerCount) { if (!(GroupSpawnDatasField.GetValue(room) is IDictionary dictionary)) { return 0; } int num = 0; foreach (DictionaryEntry item in dictionary) { object? value = item.Value; GroupSpawnData val = (GroupSpawnData)((value is GroupSpawnData) ? value : null); if (val == null) { continue; } float? num2 = null; string text = null; SpawnCategory category = SpawnCategory.Other; if (val.Members != null) { using List.Enumerator enumerator = val.Members.GetEnumerator(); if (enumerator.MoveNext()) { GroupCreatureData current = enumerator.Current; category = SpawnCategoryLookup.GetCategory(current.MasterID); num2 = SpawnMultiplierResolver.GetEffectiveMultiplier(current.MasterID, playerCount); text = MonsterTypeLookup.GetDisplayName(current.MasterID); } } if (num2.HasValue) { int num3 = (int)(GroupSpawnCountBackingField.GetValue(val) ?? ((object)0)); int num4 = SpawnMultiplierResolver.ScaleCount(num3, num2.Value); GroupSpawnCountBackingField.SetValue(val, num4); num++; ModLog.Debug("SpawnScaling", "Group spawn configured — category=" + SpawnCategoryLookup.Format(category) + ", name=" + (text ?? "unknown") + ", " + $"id={val.GroupID}, {num2.Value:0.##}× (count {num3} -> {num4})"); } } return num; } private static bool ScaleSpawnDataObject(object spawnData, int playerCount) { FieldInfo field = ReflectionFieldCache.GetField(spawnData, "MasterID"); if (field == null) { return false; } if (spawnData is FixedSpawnedActorData) { return false; } int num = (int)(field.GetValue(spawnData) ?? ((object)0)); SpawnCategory category = SpawnCategoryLookup.GetCategory(num); float effectiveMultiplier = SpawnMultiplierResolver.GetEffectiveMultiplier(category, playerCount); string displayName = MonsterTypeLookup.GetDisplayName(num); bool flag = false; FieldInfo field2 = ReflectionFieldCache.GetField(spawnData, "StackCount"); if (field2 != null) { int num2 = (int)(field2.GetValue(spawnData) ?? ((object)0)); int num3 = SpawnMultiplierResolver.ScaleCountWithImplicitBase(num2, effectiveMultiplier, 1); field2.SetValue(spawnData, num3); SpawnScalingLog.DebugFieldScaled($"spawnPoint[{num}].stackCount ({displayName})", num2, num3, effectiveMultiplier); flag = true; } FieldInfo field3 = ReflectionFieldCache.GetField(spawnData, "MaxRespawnCount"); if (field3 != null) { int num4 = (int)(field3.GetValue(spawnData) ?? ((object)0)); if (num4 > 0) { int num5 = SpawnMultiplierResolver.ScaleCount(num4, effectiveMultiplier); field3.SetValue(spawnData, num5); SpawnScalingLog.DebugFieldScaled($"spawnPoint[{num}].maxRespawn ({displayName})", num4, num5, effectiveMultiplier); flag = true; } } if (flag) { ModLog.Debug("SpawnScaling", "Spawn point configured — category=" + SpawnCategoryLookup.Format(category) + ", name=" + displayName + ", " + $"master={num}, {effectiveMultiplier:0.##}×"); } return flag; } private static int ScaleField(object target, FieldInfo field, float multiplier, string label) { int num = (int)(field.GetValue(target) ?? ((object)0)); int num2 = SpawnMultiplierResolver.ScaleCount(num, multiplier); field.SetValue(target, num2); SpawnScalingLog.DebugFieldScaled(label, num, num2, multiplier); return num2; } private static FieldInfo AccessToolsField(Type type, string name) { return type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new InvalidOperationException(type.Name + "." + name + " not found"); } } internal static class SpawnScalingLog { private const string Feature = "SpawnScaling"; internal static string FormatPosition(Vector3 pos) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) return $"({pos.x:0.0}, {pos.y:0.0}, {pos.z:0.0})"; } internal static string FormatLocation(DungeonRoom? room, Vector3 pos) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) string text = FormatPosition(pos); if (room == null) { return text; } string text2 = SpawnScalingRoomLookup.TryGetRoomName(room, pos); if (!string.IsNullOrWhiteSpace(text2)) { return text + " room=" + text2; } return text; } internal static void InfoScalingApplied(int playerCount) { float num = ((playerCount > 4) ? ((float)playerCount / 4f) : 1f); ModLog.Info("SpawnScaling", $"Spawn scaling applied — players={playerCount} (shared playerScale={num:0.##}× when auto enabled), " + $"mimic={ModConfig.MimicSpawnMultiplier.Value:0.##}× auto={ModConfig.AutoScaleMimicSpawnsByPlayerCount.Value}, " + $"boss={ModConfig.BossSpawnMultiplier.Value:0.##}× auto={ModConfig.AutoScaleBossSpawnsByPlayerCount.Value}, " + $"jako={ModConfig.JakoSpawnMultiplier.Value:0.##}× auto={ModConfig.AutoScaleJakoSpawnsByPlayerCount.Value}, " + $"special={ModConfig.SpecialSpawnMultiplier.Value:0.##}× auto={ModConfig.AutoScaleSpecialSpawnsByPlayerCount.Value}, " + $"trap={ModConfig.TrapSpawnMultiplier.Value:0.##}× auto={ModConfig.AutoScaleTrapSpawnsByPlayerCount.Value}, " + $"other={ModConfig.OtherSpawnMultiplier.Value:0.##}× auto={ModConfig.AutoScaleOtherSpawnsByPlayerCount.Value}"); } internal static void DebugFieldScaled(string label, int before, int after, float multiplier) { if (before == after) { ModLog.Debug("SpawnScaling", $"{label} unchanged at {before} ({multiplier:0.##}×)"); return; } ModLog.Debug("SpawnScaling", $"{label} scaled {before} -> {after} ({multiplier:0.##}×)"); } internal static void DebugEntitySpawned(DungeonRoom room, int masterId, string entityName, SpawnCategory category, float effectiveMultiplier, bool scalingApplied, Vector3 position, bool isIndoor, ReasonOfSpawn reason, string spawnSource) { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) ModLog.Debug("SpawnScaling", $"Entity spawned — category={SpawnCategoryLookup.Format(category)}, name={entityName}, master={masterId}, " + $"multiplier={effectiveMultiplier:0.##}×, budgetsScaled={scalingApplied}, pos={FormatLocation(room, position)}, " + $"indoor={isIndoor}, reason={reason}, source={spawnSource}"); } internal static void DebugSpawnFailed(int masterId, string entityName, SpawnCategory category, bool scalingApplied, string spawnSource) { ModLog.Debug("SpawnScaling", "Entity spawn failed — category=" + SpawnCategoryLookup.Format(category) + ", name=" + entityName + ", " + $"master={masterId}, budgetsScaled={scalingApplied}, source={spawnSource}"); } } public static class SpawnScalingPatches { [HarmonyPatch(typeof(DungeonRoom), "InitSpawn")] public static class DungeonRoomInitSpawnPatch { [HarmonyPostfix] public static void Postfix(DungeonRoom __instance) { try { SpawnScalingApplier.EnsureApplied(__instance); FixedSpawnCoordinator.ApplyAfterInit(__instance); } catch (Exception ex) { ModLog.Warn("SpawnScaling", "InitSpawn postfix failed — " + ex.Message); } } } [HarmonyPatch(typeof(SpawnedActorData), "OnActorDead")] public static class SpawnedActorDataOnActorDeadPatch { [HarmonyPostfix] public static void Postfix(SpawnedActorData __instance) { try { FixedSpawnCoordinator.OnActorDead(__instance); } catch (Exception ex) { ModLog.Warn("SpawnScaling", "OnActorDead fixed spawn scaling failed — " + ex.Message); } } } [HarmonyPatch] public static class IVroomSpawnMonsterPatch { public static MethodBase? TargetMethod() { return ResolveSpawnMonsterMethod(); } [HarmonyPrefix] public static bool Prefix(IVroom __instance, SpawnedActorData spawnData, ref bool __result) { if (ModConfig.EnableSpawnScaling.Value) { DungeonRoom val = (DungeonRoom)(object)((__instance is DungeonRoom) ? __instance : null); if (val != null) { if (FixedSpawnProximity.ShouldBlockFixedCreatureRespawn(val, spawnData)) { __result = false; return false; } return true; } } return true; } [HarmonyPostfix] public static void Postfix(IVroom __instance, int masterID, SpawnedActorData spawnData, bool isIndoor, string aiName, string btName, ReasonOfSpawn reasonOfSpawn, bool __result) { //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) if (!ModConfig.EnableDebugLogging.Value || !ModConfig.EnableSpawnScaling.Value) { return; } DungeonRoom val = (DungeonRoom)(object)((__instance is DungeonRoom) ? __instance : null); if (val != null) { int memberCount = ((IVroom)val).GetMemberCount(); SpawnCategory category = SpawnCategoryLookup.GetCategory(masterID); float effectiveMultiplier = SpawnMultiplierResolver.GetEffectiveMultiplier(category, memberCount); string displayName = MonsterTypeLookup.GetDisplayName(masterID); bool scalingApplied = SpawnScalingApplier.IsApplied(val); if (__result) { SpawnScalingLog.DebugEntitySpawned(val, masterID, displayName, category, effectiveMultiplier, scalingApplied, ExtractSpawnPosition(spawnData), isIndoor, reasonOfSpawn, "SpawnMonster"); } else { SpawnScalingLog.DebugSpawnFailed(masterID, displayName, category, scalingApplied, "SpawnMonster"); } } } private static Vector3 ExtractSpawnPosition(SpawnedActorData spawnData) { //IL_0003: 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_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) if (spawnData == null) { return Vector3.zero; } object obj = ((object)spawnData).GetType().GetField("PosVector", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(spawnData); if (obj is Vector3) { return (Vector3)obj; } object? obj2 = ((object)spawnData).GetType().GetField("Pos", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(spawnData); PosWithRot val = (PosWithRot)((obj2 is PosWithRot) ? obj2 : null); if (val == null) { return Vector3.zero; } return val.pos; } } private const string Feature = "SpawnScaling"; private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly Type[] SpawnMonsterParameterTypes = new Type[6] { typeof(int), typeof(SpawnedActorData), typeof(bool), typeof(string), typeof(string), typeof(ReasonOfSpawn) }; public static void Apply(Harmony harmony) { GameNetworkApi.GetGameAssembly(); HarmonyPatchHelper.PatchApplyResult result = HarmonyPatchHelper.ApplyPatchTypes(harmony, "SpawnScaling", HarmonyPatchHelper.GetNestedPatchTypes(typeof(SpawnScalingPatches))); LogPatchAudit(harmony); HarmonyPatchHelper.LogPatchSummary("SpawnScaling", result); } private static MethodBase? ResolveSpawnMonsterMethod() { return AccessTools.Method(typeof(IVroom), "SpawnMonster", SpawnMonsterParameterTypes, (Type[])null); } private static void LogPatchAudit(Harmony harmony) { HarmonyPatchHelper.LogPatchAudit("SpawnScaling", harmony, new <>z__ReadOnlyArray<(string, MethodBase)>(new(string, MethodBase)[3] { ("InitSpawn/DungeonRoom", AccessTools.Method(typeof(DungeonRoom), "InitSpawn", (Type[])null, (Type[])null)), ("OnActorDead/SpawnedActorData", AccessTools.Method(typeof(SpawnedActorData), "OnActorDead", (Type[])null, (Type[])null)), ("SpawnMonster/IVroom", ResolveSpawnMonsterMethod()) })); } } internal static class SpawnScalingRoomLookup { private static readonly FieldInfo DungeonSpaceGroupField = AccessTools.Field(typeof(DungeonRoom), "_dungeonSpaceGroup") ?? throw new InvalidOperationException("DungeonRoom._dungeonSpaceGroup not found"); private static readonly FieldInfo SpaceGroupField = AccessTools.Field(typeof(DungeonRoom), "_spaceGroup") ?? throw new InvalidOperationException("DungeonRoom._spaceGroup not found"); private static readonly FieldInfo TilesField = AccessTools.Field(typeof(VSpaceTileGroup), "m_tiles") ?? throw new InvalidOperationException("VSpaceTileGroup.m_tiles not found"); internal static string TryGetRoomName(DungeonRoom room, Vector3 position) { //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) if (room == null) { return string.Empty; } if (!TryGetTileGroup(room, out VSpaceTileGroup tileGroup) || tileGroup == null) { return string.Empty; } try { ISpaceGroup val = (ISpaceGroup)(object)tileGroup; if (val == null) { return string.Empty; } IVSpace space = val.GetSpace(position); return (!(((space != null) ? space.Coordinate : null) is TileCoordinate val2)) ? string.Empty : TryGetTileName(tileGroup, val2.TileID); } catch { return string.Empty; } } private static bool TryGetTileGroup(DungeonRoom room, out VSpaceTileGroup? tileGroup) { tileGroup = null; object? value = DungeonSpaceGroupField.GetValue(room); VSpaceTileGroup val = (VSpaceTileGroup)((value is VSpaceTileGroup) ? value : null); if (val != null) { tileGroup = val; return true; } object? value2 = SpaceGroupField.GetValue(room); VSpaceTileGroup val2 = (VSpaceTileGroup)((value2 is VSpaceTileGroup) ? value2 : null); if (val2 != null) { tileGroup = val2; return true; } return false; } private static string TryGetTileName(VSpaceTileGroup tileGroup, int tileId) { string text; if (tileId > 0 && TilesField.GetValue(tileGroup) is IDictionary dictionary) { object? obj = dictionary[tileId]; Tile val = (Tile)((obj is Tile) ? obj : null); if (val == null) { return string.Empty; } text = SanitizeRoomName(((Object)val).name); if (text == null) { GameObject gameObject = ((Component)val).gameObject; text = SanitizeRoomName((gameObject != null) ? ((Object)gameObject).name : null); if (text == null) { return string.Empty; } } } else { text = string.Empty; } return text; } private static string? SanitizeRoomName(string? name) { if (string.IsNullOrWhiteSpace(name)) { return null; } string text = name.Trim(); if (!text.Equals("GameObject", StringComparison.OrdinalIgnoreCase)) { return text; } return null; } } } namespace MimesisPlayerEnhancement.Features.PlayerAnnouncements { internal static class BossSpawnAnnouncer { private const float DebounceSeconds = 1f; private const float InitialSpawnGraceSeconds = 3f; private static readonly Dictionary PendingSpawns = new Dictionary(); private static int _flushGeneration; private static float _suppressUntilTime; internal static void BeginDungeonRun() { _suppressUntilTime = Time.time + 3f; PendingSpawns.Clear(); _flushGeneration++; } internal static void RecordSpawn(int masterId) { if (ModConfig.ShowPlayerAnnouncements.Value && !(Time.time < _suppressUntilTime)) { SpawnCategory category = SpawnCategoryLookup.GetCategory(masterId); if ((category == SpawnCategory.Boss || category == SpawnCategory.Special) ? true : false) { PendingSpawns.TryGetValue(masterId, out var value); PendingSpawns[masterId] = value + 1; MelonCoroutines.Start(FlushAfterDelay(++_flushGeneration)); } } } private static IEnumerator FlushAfterDelay(int generation) { yield return (object)new WaitForSeconds(1f); if (generation == _flushGeneration && PendingSpawns.Count != 0) { string text = FormatSpawnMessage(PendingSpawns); PendingSpawns.Clear(); if (!string.IsNullOrWhiteSpace(text)) { PlayerAnnouncements.ShowToast(text, isEntering: false); } } } private static string FormatSpawnMessage(Dictionary spawns) { List list = new List(); foreach (KeyValuePair spawn in spawns) { string displayName = MonsterTypeLookup.GetDisplayName(spawn.Key); list.Add(FormatSegment(spawn.Value, displayName)); } if (list.Count == 0) { return ""; } string text; switch (list.Count) { case 1: text = list[0]; break; case 2: text = list[0] + " and " + list[1]; break; default: text = string.Join(", ", list.GetRange(0, list.Count - 1)) + ", and " + list[list.Count - 1]; break; } return text + " appeared. Be careful!"; } private static string FormatSegment(int count, string name) { if (count > 0) { if (count != 1) { return $"{count} {name}"; } return "A " + name; } return ""; } } internal static class DungeonSettingsFormatter { internal static string? FormatForDungeonEntry(DungeonRoom room) { int num = SessionPlayerCountHelper.ResolveFromRoom((IVroom?)(object)room); List list = new List(); if (num > 4) { list.Add($"{num} players"); } AppendSpawnSummary(list, num); AppendLootSummary(list, num); AppendMoneySummary(list, num); AppendDungeonTime(list, num); AppendDungeonRandomizer(list); if (list.Count != 0) { return "This run: " + string.Join(", ", list) + "."; } return null; } private static void AppendSpawnSummary(List parts, int playerCount) { if (ModConfig.EnableSpawnScaling.Value) { AppendMultiplier(parts, "boss spawns", SpawnMultiplierResolver.GetEffectiveMultiplier(SpawnCategory.Boss, playerCount)); AppendMultiplier(parts, "special spawns", SpawnMultiplierResolver.GetEffectiveMultiplier(SpawnCategory.Special, playerCount)); AppendMultiplier(parts, "monster spawns", SpawnMultiplierResolver.GetEffectiveMultiplier(SpawnCategory.Jako, playerCount)); } } private static void AppendLootSummary(List parts, int playerCount) { if (ModConfig.EnableLootMultiplicator.Value) { float multiplier = AverageNonDefaultMultiplier(LootMultiplierResolver.GetEffectiveMultiplier(LootSource.Map, (ItemType)0, playerCount), LootMultiplierResolver.GetEffectiveMultiplier(LootSource.Map, (ItemType)1, playerCount), LootMultiplierResolver.GetEffectiveMultiplier(LootSource.Map, (ItemType)2, playerCount)); AppendMultiplier(parts, "map loot", multiplier); float multiplier2 = AverageNonDefaultMultiplier(LootMultiplierResolver.GetEffectiveMultiplier(LootSource.Drop, (ItemType)0, playerCount), LootMultiplierResolver.GetEffectiveMultiplier(LootSource.Drop, (ItemType)1, playerCount), LootMultiplierResolver.GetEffectiveMultiplier(LootSource.Drop, (ItemType)2, playerCount)); AppendMultiplier(parts, "drop loot", multiplier2); } } private static void AppendMoneySummary(List parts, int playerCount) { if (ModConfig.EnableMoneyMultiplier.Value) { AppendMultiplier(parts, "quota", MoneyMultiplierResolver.GetEffectiveMultiplier(MoneyType.RoundGoal, playerCount)); AppendMultiplier(parts, "scrap value", MoneyMultiplierResolver.GetEffectiveMultiplier(MoneyType.ScrapSellValue, playerCount)); } } private static void AppendDungeonTime(List parts, int playerCount) { if (ModConfig.EnableDungeonTime.Value) { double bonusSeconds = DungeonTimeResolver.GetBonusSeconds(playerCount); if (!(bonusSeconds <= 0.0)) { parts.Add($"+{(int)bonusSeconds}s shift time"); } } } private static void AppendDungeonRandomizer(List parts) { if (ModConfig.EnableDungeonRandomizer.Value) { parts.Add("dungeon randomizer on"); } } private static void AppendMultiplier(List parts, string label, float multiplier) { if (!IsDefaultMultiplier(multiplier)) { parts.Add(label + " " + FormatMultiplier(multiplier)); } } private static float AverageNonDefaultMultiplier(params float[] multipliers) { float num = 0f; int num2 = 0; foreach (float num3 in multipliers) { if (!IsDefaultMultiplier(num3)) { num += num3; num2++; } } if (num2 != 0) { return num / (float)num2; } return 1f; } private static bool IsDefaultMultiplier(float multiplier) { if (multiplier >= 0.995f) { return multiplier <= 1.005f; } return false; } private static string FormatMultiplier(float multiplier) { return $"×{multiplier:0.##}"; } } internal static class MapRunStatsTracker { private sealed class MapRunSnapshot { public long ItemCarryCount; public long DamageToAlly; public long MimicEncounterCount; public long TimeInStartingVolumeMs; public long Kills; public long Deaths; public long Revives; } private static readonly Dictionary Baselines = new Dictionary(); internal static void ResetForDungeonEntry() { Baselines.Clear(); if (!ModConfig.EnableStatistics.Value) { return; } ulong num = LocalPlayerHelper.TryGetLocalSteamId(); if (num != 0L) { Baselines[num] = CaptureCurrent(num); } foreach (ulong connectedSteamId in StatisticsTracker.GetConnectedSteamIds()) { if (!Baselines.ContainsKey(connectedSteamId)) { Baselines[connectedSteamId] = CaptureCurrent(connectedSteamId); } } } internal static void OnLocalPlayerDeath(ProtoActor actor) { if (!ShouldShowDeathStats()) { return; } ulong num = StatisticsTracker.TryResolveSteamId(actor); if (num != 0L && LocalPlayerHelper.IsLocalSteamId(num)) { MapRunSnapshot value; MapRunSnapshot baseline = (Baselines.TryGetValue(num, out value) ? value : new MapRunSnapshot()); string text = FormatMapRunStats(Subtract(CaptureCurrent(num), baseline)); if (!string.IsNullOrWhiteSpace(text)) { PlayerAnnouncements.ShowToast(text, isEntering: false, localOnly: true); } } } private static bool ShouldShowDeathStats() { if (ModConfig.ShowPlayerAnnouncements.Value) { return ModConfig.EnableStatistics.Value; } return false; } private static MapRunSnapshot CaptureCurrent(ulong steamId) { MapRunSnapshot mapRunSnapshot = new MapRunSnapshot(); if (StatisticsTracker.TryGetCurrentPlayReport(steamId, out PlayReportData report)) { mapRunSnapshot.ItemCarryCount = report.TotalItemCarryCount; mapRunSnapshot.DamageToAlly = report.TotalDamageToAlly; mapRunSnapshot.MimicEncounterCount = report.TotalMimicEncounterCount; mapRunSnapshot.TimeInStartingVolumeMs = report.TotalTimeInStartingVolume; } if (StatisticsTracker.TryGetSessionCounters(steamId, out StatCounters counters)) { mapRunSnapshot.Kills = counters.Kills; mapRunSnapshot.Deaths = counters.Deaths; mapRunSnapshot.Revives = counters.Revives; } return mapRunSnapshot; } private static MapRunSnapshot Subtract(MapRunSnapshot current, MapRunSnapshot baseline) { return new MapRunSnapshot { ItemCarryCount = current.ItemCarryCount - baseline.ItemCarryCount, DamageToAlly = current.DamageToAlly - baseline.DamageToAlly, MimicEncounterCount = current.MimicEncounterCount - baseline.MimicEncounterCount, TimeInStartingVolumeMs = current.TimeInStartingVolumeMs - baseline.TimeInStartingVolumeMs, Kills = current.Kills - baseline.Kills, Deaths = current.Deaths - baseline.Deaths, Revives = current.Revives - baseline.Revives }; } private static string FormatMapRunStats(MapRunSnapshot stats) { List list = new List(); if (stats.Kills > 0) { list.Add(string.Format("{0} kill{1}", stats.Kills, (stats.Kills == 1) ? "" : "s")); } if (stats.Deaths > 0) { list.Add(string.Format("{0} death{1}", stats.Deaths, (stats.Deaths == 1) ? "" : "s")); } if (stats.Revives > 0) { list.Add(string.Format("{0} revive{1}", stats.Revives, (stats.Revives == 1) ? "" : "s")); } if (stats.ItemCarryCount > 0) { list.Add(string.Format("{0} item{1} carried", stats.ItemCarryCount, (stats.ItemCarryCount == 1) ? "" : "s")); } if (stats.MimicEncounterCount > 0) { list.Add(string.Format("{0} mimic encounter{1}", stats.MimicEncounterCount, (stats.MimicEncounterCount == 1) ? "" : "s")); } if (stats.DamageToAlly > 0) { list.Add($"{stats.DamageToAlly} ally damage"); } if (list.Count != 0) { return "Your run this map: " + string.Join(", ", list) + "."; } return "Your run this map: no recorded activity yet."; } } public static class PlayerAnnouncementPatches { [HarmonyPatch(typeof(DungeonRoom), "OnAllMemberEntered")] public static class DungeonRoomOnAllMemberEnteredAnnouncementPatch { [HarmonyPostfix] public static void Postfix(DungeonRoom __instance) { try { PlayerAnnouncements.OnAllMembersEnteredDungeon(__instance); } catch (Exception ex) { ModLog.Warn("Announcements", "OnAllMemberEntered announcement failed — " + ex.Message); } } } [HarmonyPatch(typeof(GameMainBase), "OnPlayerDeath")] public static class GameMainDeathAnnouncementPatch { [HarmonyPostfix] public static void Postfix(ProtoActor actor) { try { MapRunStatsTracker.OnLocalPlayerDeath(actor); } catch (Exception ex) { ModLog.Warn("Announcements", "Death announcement failed — " + ex.Message); } } } [HarmonyPatch(typeof(DungeonRoom), "OnActorEnter")] public static class DungeonRoomOnActorEnterAnnouncementPatch { [HarmonyPostfix] public unsafe static void Postfix(VActor actor) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) try { VMonster val = (VMonster)(object)((actor is VMonster) ? actor : null); if (val != null) { ActorType actorType = ((VActor)val).ActorType; if (((object)(*(ActorType*)(&actorType))/*cast due to .constrained prefix*/).Equals((object)(ActorType)2)) { BossSpawnAnnouncer.RecordSpawn(((VActor)val).MasterID); } } } catch (Exception ex) { ModLog.Warn("Announcements", "OnActorEnter announcement failed — " + ex.Message); } } } private const string Feature = "Announcements"; public static void Apply(Harmony harmony) { GameNetworkApi.GetGameAssembly(); HarmonyPatchHelper.PatchApplyResult result = HarmonyPatchHelper.ApplyPatchTypes(harmony, "Announcements", HarmonyPatchHelper.GetNestedPatchTypes(typeof(PlayerAnnouncementPatches))); LogPatchAudit(harmony); HarmonyPatchHelper.LogPatchSummary("Announcements", result); } private static void LogPatchAudit(Harmony harmony) { HarmonyPatchHelper.LogPatchAudit("Announcements", harmony, new <>z__ReadOnlyArray<(string, MethodBase)>(new(string, MethodBase)[3] { ("OnAllMemberEntered/DungeonRoom", AccessTools.Method(typeof(DungeonRoom), "OnAllMemberEntered", (Type[])null, (Type[])null)), ("OnPlayerDeath/GameMainBase", AccessTools.Method(typeof(GameMainBase), "OnPlayerDeath", (Type[])null, (Type[])null)), ("OnActorEnter/DungeonRoom", AccessTools.Method(typeof(DungeonRoom), "OnActorEnter", (Type[])null, (Type[])null)) })); } } internal static class PlayerAnnouncements { private const string Feature = "Announcements"; private static readonly HashSet EntryAnnouncedRooms = new HashSet(); internal static void OnAllMembersEnteredDungeon(DungeonRoom room) { if (!EntryAnnouncedRooms.Add(room)) { return; } MapRunStatsTracker.ResetForDungeonEntry(); BossSpawnAnnouncer.BeginDungeonRun(); if (ModConfig.ShowPlayerAnnouncements.Value) { string text = DungeonSettingsFormatter.FormatForDungeonEntry(room); if (!string.IsNullOrWhiteSpace(text)) { ShowToast(text); } } } internal static void ShowToast(string message, bool isEntering = true, bool localOnly = false) { if (ModConfig.ShowPlayerAnnouncements.Value) { InGameMessageHelper.ShowModMessage(message, isEntering, localOnly); ModLog.Debug("Announcements", localOnly ? ("Local toast: " + message) : ("Toast: " + message)); } } } } namespace MimesisPlayerEnhancement.Features.Persistence { public static class MimesisSaveManager { private const string MimesisDataFolder = "MimesisData"; private const string SlotPrefix = "Slot"; private const string SpeechEventsFile = "speech_events.bin"; private const string MetadataFile = "metadata.json"; private const int MetadataVersion = 2; private const string BackupSuffix = ".bak"; private const string TempSuffix = ".tmp"; private static readonly FieldInfo CompressedAudioDataField = typeof(SpeechEvent).GetField("CompressedAudioData", BindingFlags.Instance | BindingFlags.Public); private static void SafeWriteAllBytes(string filePath, byte[] data) { string text = filePath + ".tmp"; string destFileName = filePath + ".bak"; File.WriteAllBytes(text, data); if (File.Exists(filePath)) { try { File.Copy(filePath, destFileName, overwrite: true); } catch (Exception ex) { ModLog.Warn("Persistence", "Backup failed for " + Path.GetFileName(filePath) + ": " + ex.Message); } } if (File.Exists(filePath)) { File.Delete(filePath); } File.Move(text, filePath); } private static void SafeWriteAllText(string filePath, string text) { SafeWriteAllBytes(filePath, Encoding.UTF8.GetBytes(text)); } private static void SafeDeleteFile(string filePath) { string[] array = new string[3] { filePath, filePath + ".bak", filePath + ".tmp" }; foreach (string path in array) { if (File.Exists(path)) { try { File.Delete(path); ModLog.Debug("Persistence", "Deleted stale file: " + Path.GetFileName(path)); } catch (Exception ex) { ModLog.Warn("Persistence", "Failed to delete " + Path.GetFileName(path) + ": " + ex.Message); } } } } private static byte[]? SafeReadAllBytes(string filePath) { if (File.Exists(filePath)) { try { byte[] array = File.ReadAllBytes(filePath); if (array != null && array.Length != 0) { return array; } } catch (Exception ex) { ModLog.Warn("Persistence", "Main file read failed (" + Path.GetFileName(filePath) + "): " + ex.Message); } } string path = filePath + ".bak"; if (File.Exists(path)) { try { byte[] array2 = File.ReadAllBytes(path); if (array2 != null && array2.Length != 0) { ModLog.Warn("Persistence", "Recovered from backup: " + Path.GetFileName(path)); return array2; } } catch (Exception ex2) { ModLog.Error("Persistence", "Backup also failed (" + Path.GetFileName(path) + "): " + ex2.Message); } } return null; } public static void SafeWritePlayerMapping(string filePath, string json) { SafeWriteAllText(filePath, json); } public static string? SafeReadPlayerMapping(string filePath) { if (File.Exists(filePath)) { try { string text = File.ReadAllText(filePath); if (!string.IsNullOrEmpty(text)) { return text; } } catch (Exception ex) { ModLog.Warn("Persistence", "Player mapping read failed: " + ex.Message); } } string path = filePath + ".bak"; if (File.Exists(path)) { try { string text2 = File.ReadAllText(path); if (!string.IsNullOrEmpty(text2)) { ModLog.Warn("Persistence", "Recovered player mapping from backup"); return text2; } } catch (Exception ex2) { ModLog.Error("Persistence", "Player mapping backup also failed: " + ex2.Message); } } return null; } public static string? GetMimesisSlotPath(int slotId) { PlatformMgr instance = MonoSingleton.Instance; if ((Object)(object)instance == (Object)null) { return null; } string saveFileFolderPath = instance.GetSaveFileFolderPath(); if (!string.IsNullOrEmpty(saveFileFolderPath)) { return Path.Combine(saveFileFolderPath, "MimesisData", "Slot" + slotId); } return null; } public static bool IsHost() { return HostStatusCache.IsHostFast(); } public static List CollectAllSpeechEvents() { List list = new List(); try { HashSet hashSet = new HashSet(); foreach (SpeechEventArchive item in SpeechEventArchiveRegistry.EnumerateActive()) { SpeechEventSyncListHelper.CollectFromArchive(item, hashSet, list); } int count = list.Count; List disconnectedEvents = SpeechEventPoolManager.GetDisconnectedEvents(); int num = 0; foreach (SpeechEvent item2 in disconnectedEvents) { if (item2 != null && hashSet.Add(item2.Id)) { list.Add(item2); num++; } } List pendingEvents = SpeechEventPoolManager.GetPendingEvents(); int num2 = 0; foreach (SpeechEvent item3 in pendingEvents) { if (item3 != null && hashSet.Add(item3.Id)) { list.Add(item3); num2++; } } if (num > 0 || num2 > 0) { ModLog.Debug("Persistence", "CollectAllSpeechEvents: " + $"{count} live + {num} disconnected + {num2} pending (absent players) = {list.Count} total"); } } catch (Exception ex) { ModLog.Warn("Persistence", "CollectAllSpeechEvents: " + ex.Message); } return list; } public static int GetCurrentSaveSlotId() { try { object hubMember = GetHubMember("vworld"); if (hubMember == null) { return -1; } PropertyInfo property = hubMember.GetType().GetProperty("SaveSlotID", BindingFlags.Instance | BindingFlags.Public); return (property == null) ? (-1) : ((int)property.GetValue(hubMember)); } catch { return -1; } } public static bool IsValidSaveSlotId(int slotId) { return MMSaveGameData.CheckSaveSlotID(slotId, true); } public static bool TryGetActiveSaveSlotId(out int slotId) { slotId = GetCurrentSaveSlotId(); if (IsHost()) { return IsValidSaveSlotId(slotId); } return false; } public 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 static void SaveMimesisData(int slotId) { if (!IsHost()) { ModLog.Debug("Persistence", "Skip save: not host."); return; } string mimesisSlotPath = GetMimesisSlotPath(slotId); if (string.IsNullOrEmpty(mimesisSlotPath)) { ModLog.Warn("Persistence", "Skip save: slot path unavailable."); return; } try { Directory.CreateDirectory(mimesisSlotPath); List list = CollectAllSpeechEvents(); string filePath = Path.Combine(mimesisSlotPath, "speech_events.bin"); int num = 0; if (list != null && list.Count > 0) { using MemoryStream memoryStream = new MemoryStream(); using BinaryWriter binaryWriter = new BinaryWriter(memoryStream); List<(byte[], byte[])> list2 = new List<(byte[], byte[])>(); foreach (SpeechEvent item2 in list) { byte[] dataFromSndEvent = ReplayableSndEvent.GetDataFromSndEvent(item2); if (dataFromSndEvent != null && dataFromSndEvent.Length != 0) { byte[] item = item2.CompressedAudioData ?? Array.Empty(); list2.Add((dataFromSndEvent, item)); } } num = list2.Count; binaryWriter.Write(num); long num2 = 0L; foreach (var (array, array2) in list2) { binaryWriter.Write(array.Length); binaryWriter.Write(array); binaryWriter.Write(array2.Length); binaryWriter.Write(array2); num2 += array2.Length; } SafeWriteAllBytes(filePath, memoryStream.ToArray()); ModLog.Debug("Persistence", $"Serialized {num} SpeechEvents, audio={num2 / 1024}KB"); } else { SafeDeleteFile(filePath); } SpeechEventPoolManager.SavePlayerMapping(slotId); SafeWriteAllText(Path.Combine(mimesisSlotPath, "metadata.json"), $"{{\"version\":{2},\"timestamp\":\"{DateTime.UtcNow:O}\",\"speechCount\":{num}}}"); ModLog.Info("Persistence", $"Saved slot {slotId} — speechEvents={num}"); } catch (Exception arg) { ModLog.Error("Persistence", $"SaveMimesisData: {arg}"); } } public static List? LoadSpeechEvents(int slotId) { string mimesisSlotPath = GetMimesisSlotPath(slotId); if (string.IsNullOrEmpty(mimesisSlotPath)) { return null; } string text = Path.Combine(mimesisSlotPath, "speech_events.bin"); if (!File.Exists(text) && !File.Exists(text + ".bak")) { return null; } try { byte[] array = SafeReadAllBytes(text); if (array == null || array.Length < 4) { return null; } List list = new List(); int num = 0; using (MemoryStream memoryStream = new MemoryStream(array)) { using BinaryReader binaryReader = new BinaryReader(memoryStream); num = binaryReader.ReadInt32(); if ((num <= 0 || num > 100000) ? true : false) { return LoadSpeechEventsOldFormat(array); } long num2 = 0L; for (int i = 0; i < num; i++) { if (memoryStream.Position >= array.Length) { break; } int num3 = binaryReader.ReadInt32(); if (num3 <= 0 || memoryStream.Position + num3 > array.Length) { continue; } byte[] metaData = binaryReader.ReadBytes(num3); byte[] audioData = null; if (memoryStream.Position + 4 <= array.Length) { int num4 = binaryReader.ReadInt32(); if (num4 >= 0 && memoryStream.Position + num4 <= array.Length) { audioData = ((num4 > 0) ? binaryReader.ReadBytes(num4) : null); num2 += num4; } else { memoryStream.Position -= 4L; } } SpeechEvent val = DeserializeSingleSpeechEvent(metaData, audioData); if (val != null) { list.Add(val); } } ModLog.Debug("Persistence", $"Loaded {list.Count}/{num} SpeechEvents from slot {slotId}, audio={num2 / 1024}KB"); } return (list.Count > 0) ? list : null; } catch (Exception ex) { ModLog.Warn("Persistence", "LoadSpeechEvents: " + ex.Message); return null; } } private static List? LoadSpeechEventsOldFormat(byte[] data) { try { Type type = FindMemoryPackSerializerType(); if (type == null) { return null; } MethodInfo methodInfo = type.GetMethods(BindingFlags.Static | BindingFlags.Public).FirstOrDefault(delegate(MethodInfo m) { if (m.Name != "Deserialize") { return false; } ParameterInfo[] parameters = m.GetParameters(); return parameters.Length >= 2 && parameters[0].ParameterType == typeof(Type) && parameters[1].ParameterType == typeof(byte[]); }); if (methodInfo == null) { return null; } List list = (List)methodInfo.Invoke(null, new object[3] { typeof(List), data, null }); if (list != null && list.Count > 0) { ModLog.Debug("Persistence", $"Loaded {list.Count} SpeechEvents (legacy format)"); return list; } } catch { } return null; } private static SpeechEvent? DeserializeSingleSpeechEvent(byte[] metaData, byte[]? audioData = null) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) if (metaData == null || metaData.Length == 0) { return null; } try { SpeechEvent sndEvent = new ReplayableSndEvent((SndEventType)1, 0, 0L, 0L, metaData, (byte[])null).GetSndEvent((REPLAY_HEADER_VERSION)3); if (sndEvent != null && audioData != null && audioData.Length != 0 && CompressedAudioDataField != null) { CompressedAudioDataField.SetValue(sndEvent, audioData); } return sndEvent; } catch { return null; } } private static Type? FindMemoryPackSerializerType() { Type type = Type.GetType("MemoryPack.MemoryPackSerializer, MemoryPack"); if (type != null) { return type; } type = Type.GetType("MemoryPack.MemoryPackSerializer, MemoryPack.Core"); if (type != null) { return type; } type = Type.GetType("MemoryPack.MemoryPackSerializer, MemoryPack.Runtime"); if (type != null) { return type; } Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { try { type = assembly.GetType("MemoryPack.MemoryPackSerializer"); if (type != null) { return type; } } catch { } } return null; } public static bool HasMimesisData(int slotId) { string mimesisSlotPath = GetMimesisSlotPath(slotId); if (string.IsNullOrEmpty(mimesisSlotPath)) { return false; } if (!File.Exists(Path.Combine(mimesisSlotPath, "speech_events.bin"))) { return File.Exists(Path.Combine(mimesisSlotPath, "speech_events.bin.bak")); } return true; } public static void DeleteMimesisData(int slotId) { string mimesisSlotPath = GetMimesisSlotPath(slotId); if (string.IsNullOrEmpty(mimesisSlotPath) || !Directory.Exists(mimesisSlotPath)) { return; } try { Directory.Delete(mimesisSlotPath, recursive: true); } catch (Exception ex) { ModLog.Warn("Persistence", "DeleteMimesisData: " + ex.Message); } } } public static class PersistencePatches { private const string Feature = "Persistence"; public static void Apply(Harmony harmony) { HarmonyPatchHelper.PatchApplyResult result = HarmonyPatchHelper.ApplyPatchTypes(harmony, "Persistence", HarmonyPatchHelper.GetNamespacePatchTypes(typeof(PersistencePatches))); LogPatchAudit(harmony); HarmonyPatchHelper.LogPatchSummary("Persistence", result); } private static void LogPatchAudit(Harmony harmony) { HarmonyPatchHelper.LogPatchAudit("Persistence", harmony, new <>z__ReadOnlyArray<(string, MethodBase)>(new(string, MethodBase)[5] { ("Delete/PlatformMgr", AccessTools.Method(typeof(PlatformMgr), "Delete", (Type[])null, (Type[])null)), ("SaveGameData/MaintenanceRoom", AccessTools.Method(typeof(MaintenanceRoom), "SaveGameData", (Type[])null, (Type[])null)), ("OnStartClient/SpeechEventArchive", AccessTools.Method(typeof(SpeechEventArchive), "OnStartClient", (Type[])null, (Type[])null)), ("OnStopClient/SpeechEventArchive", AccessTools.Method(typeof(SpeechEventArchive), "OnStopClient", (Type[])null, (Type[])null)), ("GetRandomOtherSpeechEventArchive/VoiceManager", AccessTools.Method(typeof(VoiceManager), "GetRandomOtherSpeechEventArchive", (Type[])null, (Type[])null)) })); } } internal static class SpeechEventArchiveRegistry { private static readonly List Archives = new List(); internal static void Register(SpeechEventArchive archive) { if (!((Object)(object)archive == (Object)null) && !Archives.Contains(archive)) { Archives.Add(archive); } } internal static void Unregister(SpeechEventArchive archive) { if (!((Object)(object)archive == (Object)null)) { Archives.Remove(archive); } } internal static void Clear() { Archives.Clear(); } internal static IEnumerable EnumerateActive() { for (int i = Archives.Count - 1; i >= 0; i--) { SpeechEventArchive val = Archives[i]; if ((Object)(object)val == (Object)null) { Archives.RemoveAt(i); } else { yield return val; } } } } public static class SpeechEventPoolManager { public enum EventState { Pending, Injected } private static readonly Dictionary _pool = new Dictionary(); private static readonly Dictionary> _byPlayerName = new Dictionary>(); private static readonly Dictionary _steamToDissonance = new Dictionary(); private static int _loadedSlotId = -1; private static SpeechEventArchive? _localArchive; private static readonly List<(SpeechEventArchive archive, List events)> _deferredNameUpdates = new List<(SpeechEventArchive, List)>(); private static readonly List _deferredInjectionArchives = new List(); private static readonly Dictionary _disconnectedCache = new Dictionary(); private static readonly Dictionary _disconnectedPlayerMappings = new Dictionary(); private static readonly FieldInfo RecordedTimeField = typeof(SpeechEvent).GetField("RecordedTime", BindingFlags.Instance | BindingFlags.Public); private static readonly FieldInfo LastPlayedTimeField = typeof(SpeechEvent).GetField("LastPlayedTime", BindingFlags.Instance | BindingFlags.Public); private const string PlayerMappingFile = "player_mapping.json"; private static readonly HashSet DeferredInjectionSeenIds = new HashSet(); public static int TotalCount => _pool.Count; public static bool IsLoaded { get { if (_loadedSlotId >= 0) { return _pool.Count > 0; } return false; } } public static int DisconnectedCacheCount => _disconnectedCache.Count; public static void LoadForSlot(int slotId) { if (slotId == _loadedSlotId && _pool.Count > 0) { return; } Reset(); _loadedSlotId = slotId; List list = MimesisSaveManager.LoadSpeechEvents(slotId); if (list == null || list.Count == 0) { ModLog.Debug("Persistence", "No events to load for slot " + slotId); return; } foreach (SpeechEvent item in list) { if (item != null && !_pool.ContainsKey(item.Id)) { string text = item.PlayerName ?? ""; _pool[item.Id] = (item, EventState.Pending, text); if (!_byPlayerName.TryGetValue(text, out List value)) { value = new List(); _byPlayerName[text] = value; } value.Add(item.Id); } } LoadPlayerMapping(slotId); ModLog.Info("Persistence", $"Loaded {_pool.Count} events for slot {slotId} " + $"({_steamToDissonance.Count} SteamID mappings)"); } public static List ClaimEventsForArchive(string? playerId, long playerUID, bool isLocal = false, SpeechEventArchive? archive = null) { List list = new List(); if (_pool.Count == 0) { return list; } ModLog.Debug("Persistence", "ClaimEventsForArchive: PlayerId='" + playerId + "', " + $"PlayerUID={playerUID}, isLocal={isLocal}, " + string.Format("pool has {0} events, playerNames in pool: [{1}]", _pool.Count, string.Join(", ", _byPlayerName.Keys))); HashSet hashSet = new HashSet(); if (!string.IsNullOrEmpty(playerId) && _byPlayerName.ContainsKey(playerId)) { hashSet.Add(playerId); ModLog.Debug("Persistence", "Level 1 match: DissonanceID '" + playerId + "' found directly"); } ulong steamIdForPlayerUID = GetSteamIdForPlayerUID(playerUID, isLocal); if (steamIdForPlayerUID != 0L) { if (_steamToDissonance.TryGetValue(steamIdForPlayerUID, out string value)) { if (_byPlayerName.ContainsKey(value)) { hashSet.Add(value); ModLog.Debug("Persistence", $"Level 2 match: SteamID {steamIdForPlayerUID} " + "-> old DissonanceID '" + value + "' (new: '" + playerId + "')"); } } else { ModLog.Debug("Persistence", $"Level 2: SteamID {steamIdForPlayerUID} not in mapping " + $"({_steamToDissonance.Count} entries)"); } } else { ModLog.Debug("Persistence", "Level 2: Could not resolve SteamID for " + $"PlayerUID={playerUID}"); } foreach (string item in hashSet) { if (!_byPlayerName.TryGetValue(item, out List value2)) { continue; } foreach (long item2 in value2) { if (_pool.TryGetValue(item2, out (SpeechEvent, EventState, string) value3) && value3.Item2 == EventState.Pending) { _pool[item2] = (value3.Item1, EventState.Injected, value3.Item3); list.Add(value3.Item1); } } } if (list.Count > 0) { if (!string.IsNullOrEmpty(playerId)) { foreach (SpeechEvent item3 in list) { item3.PlayerName = playerId; } ModLog.Debug("Persistence", $"Claimed {list.Count} events, " + "PlayerName updated to '" + playerId + "'"); } else if ((Object)(object)archive != (Object)null) { _deferredNameUpdates.Add((archive, new List(list))); ModLog.Debug("Persistence", $"Claimed {list.Count} events, " + "PlayerId empty -> deferred PlayerName update registered"); } } else { var (num, num2, num3) = GetCounts(); ModLog.Debug("Persistence", "No events claimed for PlayerId='" + playerId + "' (matched names: [" + string.Join(", ", hashSet) + "], " + $"pool: {num}P/{num2}I/{num3}F)"); } return list; } private static ulong GetSteamIdForPlayerUID(long playerUID, bool isLocal = false) { if (playerUID != 0L) { try { object hubMember = MimesisSaveManager.GetHubMember("pdata"); if (hubMember != null) { FieldInfo field = hubMember.GetType().GetField("actorUIDToSteamID", BindingFlags.Instance | BindingFlags.Public); if (field != null && field.GetValue(hubMember) is Dictionary dictionary && dictionary.TryGetValue(playerUID, out var value)) { ModLog.Debug("Persistence", $"Resolved PlayerUID {playerUID} -> SteamID {value} (from actorUIDToSteamID)"); return value; } } } catch (Exception ex) { ModLog.Warn("Persistence", "actorUIDToSteamID lookup error: " + ex.Message); } } if (isLocal) { ulong localSteamId = GetLocalSteamId(); if (localSteamId != 0L) { ModLog.Debug("Persistence", $"Resolved host SteamID={localSteamId} (from PlatformMgr, PlayerUID was {playerUID})"); return localSteamId; } } ModLog.Warn("Persistence", $"Could not resolve SteamID for PlayerUID={playerUID} (isLocal={isLocal})"); return 0uL; } private static ulong GetLocalSteamId() { try { PlatformMgr instance = MonoSingleton.Instance; if ((Object)(object)instance == (Object)null) { return 0uL; } FieldInfo field = typeof(PlatformMgr).GetField("_uniqueUserPath", BindingFlags.Instance | BindingFlags.NonPublic); if (field == null) { return 0uL; } string text = field.GetValue(instance) as string; if (!string.IsNullOrEmpty(text) && ulong.TryParse(text, out var result)) { return result; } } catch (Exception ex) { ModLog.Warn("Persistence", "GetLocalSteamId error: " + ex.Message); } return 0uL; } public static void RegisterDeferredInjection(SpeechEventArchive archive) { if ((Object)(object)archive == (Object)null) { return; } foreach (SpeechEventArchive deferredInjectionArchive in _deferredInjectionArchives) { if ((Object)(object)deferredInjectionArchive == (Object)(object)archive) { return; } } _deferredInjectionArchives.Add(archive); ModLog.Debug("Persistence", "Registered archive for deferred injection " + $"(waiting for SyncVars, {_deferredInjectionArchives.Count} pending)"); } public static void ProcessDeferredUpdates() { if (_deferredInjectionArchives.Count == 0 && _deferredNameUpdates.Count == 0) { return; } if (_deferredInjectionArchives.Count > 0) { for (int num = _deferredInjectionArchives.Count - 1; num >= 0; num--) { SpeechEventArchive val = _deferredInjectionArchives[num]; if ((Object)(object)val == (Object)null) { _deferredInjectionArchives.RemoveAt(num); continue; } string playerId; long playerUID; bool isLocal; try { playerId = val.PlayerId; playerUID = val.PlayerUID; isLocal = val.IsLocal; } catch { continue; } if (string.IsNullOrEmpty(playerId) && playerUID == 0L) { continue; } _deferredInjectionArchives.RemoveAt(num); ModLog.Debug("Persistence", "Deferred injection: SyncVars ready for " + $"PlayerId='{playerId}', PlayerUID={playerUID}"); SyncList events = val.events; if (events == null) { continue; } float currentSessionTime = GetCurrentSessionTime(); DeferredInjectionSeenIds.Clear(); for (int i = 0; i < events.Count; i++) { DeferredInjectionSeenIds.Add(events[i].Id); } int num2 = 0; if (HasPending()) { List list = ClaimEventsForArchive(playerId, playerUID, isLocal, val); if (list != null) { foreach (SpeechEvent item in list) { if (item != null && !DeferredInjectionSeenIds.Contains(item.Id)) { FixEventTiming(item, currentSessionTime); events.Add(item); DeferredInjectionSeenIds.Add(item.Id); num2++; } } } } if (_disconnectedCache.Count > 0) { List list2 = ClaimDisconnectedEventsForArchive(playerId, playerUID, isLocal); if (list2 != null) { foreach (SpeechEvent item2 in list2) { if (item2 != null && !DeferredInjectionSeenIds.Contains(item2.Id)) { FixEventTiming(item2, currentSessionTime); events.Add(item2); DeferredInjectionSeenIds.Add(item2.Id); num2++; } } } } if (num2 > 0) { ModLog.Info("Persistence", "Player connected — " + VoiceEventStats.DescribePlayer(val) + " — " + $"restored {num2} voice events (deferred injection)"); } else { ModLog.Info("Persistence", "Player connected — " + VoiceEventStats.DescribePlayer(val) + " — no matching saved voices (deferred injection)"); } } } if (_deferredNameUpdates.Count == 0) { return; } for (int num3 = _deferredNameUpdates.Count - 1; num3 >= 0; num3--) { var (val2, list3) = _deferredNameUpdates[num3]; if ((Object)(object)val2 == (Object)null) { _deferredNameUpdates.RemoveAt(num3); continue; } string playerId2; try { playerId2 = val2.PlayerId; } catch { continue; } if (string.IsNullOrEmpty(playerId2)) { continue; } foreach (SpeechEvent item3 in list3) { if (item3 != null) { item3.PlayerName = playerId2; } } ModLog.Debug("Persistence", $"Deferred update: {list3.Count} events " + "PlayerName updated to '" + playerId2 + "'"); _deferredNameUpdates.RemoveAt(num3); } } public static SpeechEventArchive? GetLocalArchive() { return _localArchive; } public static void SetLocalArchive(SpeechEventArchive archive) { _localArchive = archive; } public static (int pending, int injected, int fallback) GetCounts() { int num = 0; int num2 = 0; using (Dictionary.ValueCollection.Enumerator enumerator = _pool.Values.GetEnumerator()) { while (enumerator.MoveNext()) { switch (enumerator.Current.Item2) { case EventState.Pending: num++; break; case EventState.Injected: num2++; break; } } } return (pending: num, injected: num2, fallback: 0); } public static List GetPendingEvents() { List list = new List(); foreach (var value in _pool.Values) { var (item, _, _) = value; if (value.state == EventState.Pending) { list.Add(item); } } return list; } public static bool HasPending() { foreach (var value in _pool.Values) { if (value.state == EventState.Pending) { return true; } } return false; } public static void FixEventTiming(SpeechEvent ev, float currentTime) { RecordedTimeField?.SetValue(ev, currentTime); LastPlayedTimeField?.SetValue(ev, currentTime); } public static float GetCurrentSessionTime() { try { object hubMember = MimesisSaveManager.GetHubMember("timeutil"); if (hubMember != null) { MethodInfo method = hubMember.GetType().GetMethod("GetCurrentTickSec", BindingFlags.Instance | BindingFlags.Public); if (method != null) { return (int)method.Invoke(hubMember, null); } } } catch { } return 0f; } public static int CacheEventsFromArchive(SpeechEventArchive archive) { if ((Object)(object)archive == (Object)null) { return 0; } try { string text = null; long playerUID = 0L; bool flag = false; try { text = archive.PlayerId; playerUID = archive.PlayerUID; flag = archive.IsLocal; } catch { } if (flag) { return 0; } List list = new List(); HashSet seenIds = new HashSet(); SpeechEventSyncListHelper.CollectFromArchive(archive, seenIds, list); int num = 0; foreach (SpeechEvent item in list) { if (!_disconnectedCache.ContainsKey(item.Id)) { _disconnectedCache[item.Id] = item; num++; } } ulong steamIdForPlayerUID = GetSteamIdForPlayerUID(playerUID); if (steamIdForPlayerUID != 0L && !string.IsNullOrEmpty(text)) { _disconnectedPlayerMappings[steamIdForPlayerUID] = text; ModLog.Debug("Persistence", $"Cached player mapping: SteamID {steamIdForPlayerUID} -> '{text}'"); } ModLog.Debug("Persistence", $"Disconnect cache — {VoiceEventStats.DescribePlayerVerbose(archive)} — cached {num} events (totalCache={_disconnectedCache.Count})"); return num; } catch (Exception ex) { ModLog.Warn("Persistence", "CacheEventsFromArchive error: " + ex.Message); return 0; } } public static List ClaimDisconnectedEventsForArchive(string? playerId, long playerUID, bool isLocal) { List list = new List(); if (_disconnectedCache.Count == 0) { return list; } HashSet hashSet = new HashSet(); if (!string.IsNullOrEmpty(playerId)) { hashSet.Add(playerId); } ulong steamIdForPlayerUID = GetSteamIdForPlayerUID(playerUID, isLocal); if (steamIdForPlayerUID != 0L && _disconnectedPlayerMappings.TryGetValue(steamIdForPlayerUID, out string value)) { hashSet.Add(value); ModLog.Debug("Persistence", $"Disconnected cache match: SteamID {steamIdForPlayerUID} -> old DissonanceID '{value}'"); } if (hashSet.Count == 0) { return list; } List list2 = new List(); foreach (KeyValuePair item2 in _disconnectedCache) { string item = item2.Value.PlayerName ?? ""; if (hashSet.Contains(item)) { if (!string.IsNullOrEmpty(playerId)) { item2.Value.PlayerName = playerId; } list.Add(item2.Value); list2.Add(item2.Key); } } foreach (long item3 in list2) { _disconnectedCache.Remove(item3); } if (steamIdForPlayerUID != 0L && list.Count > 0) { _disconnectedPlayerMappings.Remove(steamIdForPlayerUID); } if (list.Count > 0) { ModLog.Debug("Persistence", $"Reclaimed {list.Count} events from disconnected cache " + $"for PlayerId='{playerId}' (remaining cache: {_disconnectedCache.Count})"); } return list; } public static List GetDisconnectedEvents() { return _disconnectedCache.Values.ToList(); } public static Dictionary GetDisconnectedPlayerMappings() { return new Dictionary(_disconnectedPlayerMappings); } public static void Reset() { _pool.Clear(); _byPlayerName.Clear(); _steamToDissonance.Clear(); _deferredNameUpdates.Clear(); _deferredInjectionArchives.Clear(); _disconnectedCache.Clear(); _disconnectedPlayerMappings.Clear(); _loadedSlotId = -1; _localArchive = null; } public static void SavePlayerMapping(int slotId) { string mimesisSlotPath = MimesisSaveManager.GetMimesisSlotPath(slotId); if (string.IsNullOrEmpty(mimesisSlotPath)) { ModLog.Warn("Persistence", "SavePlayerMapping: slot path is null/empty!"); return; } try { Dictionary dictionary = new Dictionary(); int num = 0; foreach (SpeechEventArchive item in SpeechEventArchiveRegistry.EnumerateActive()) { num++; try { string playerId = item.PlayerId; long playerUID = item.PlayerUID; bool isLocal = item.IsLocal; if (string.IsNullOrEmpty(playerId)) { ModLog.Debug("Persistence", $" Archive skipped: empty PlayerId (UID={playerUID})"); continue; } ulong steamIdForPlayerUID = GetSteamIdForPlayerUID(playerUID, isLocal); ModLog.Debug("Persistence", $" Archive: PlayerId='{playerId}', PlayerUID={playerUID}, IsLocal={isLocal}, SteamID={steamIdForPlayerUID}"); if (steamIdForPlayerUID != 0L) { dictionary[steamIdForPlayerUID] = playerId; } else { ModLog.Warn("Persistence", $" Could not resolve SteamID for PlayerUID={playerUID}"); } } catch (Exception ex) { ModLog.Warn("Persistence", " Archive error: " + ex.Message); } } ModLog.Debug("Persistence", $"SavePlayerMapping: found {num} live archives"); Dictionary disconnectedPlayerMappings = GetDisconnectedPlayerMappings(); int num2 = 0; foreach (KeyValuePair item2 in disconnectedPlayerMappings) { if (!dictionary.ContainsKey(item2.Key)) { dictionary[item2.Key] = item2.Value; num2++; } } if (num2 > 0) { ModLog.Debug("Persistence", $"Added {num2} disconnected player mappings"); } List list = new List(); foreach (KeyValuePair item3 in dictionary) { string arg = EscapeJsonString(item3.Value); list.Add($"\"{item3.Key}\":\"{arg}\""); } string json = "{" + string.Join(",", list) + "}"; Directory.CreateDirectory(mimesisSlotPath); string text = Path.Combine(mimesisSlotPath, "player_mapping.json"); MimesisSaveManager.SafeWritePlayerMapping(text, json); ModLog.Debug("Persistence", $"Saved player mapping: {dictionary.Count} entries ({num2} from cache) -> {text}"); } catch (Exception arg2) { ModLog.Error("Persistence", $"SavePlayerMapping FAILED: {arg2}"); } } private static void LoadPlayerMapping(int slotId) { _steamToDissonance.Clear(); string mimesisSlotPath = MimesisSaveManager.GetMimesisSlotPath(slotId); if (string.IsNullOrEmpty(mimesisSlotPath)) { return; } string text = Path.Combine(mimesisSlotPath, "player_mapping.json"); string text2 = MimesisSaveManager.SafeReadPlayerMapping(text); if (string.IsNullOrEmpty(text2)) { ModLog.Debug("Persistence", "No player_mapping.json at " + text); return; } try { foreach (KeyValuePair item in ParseSteamToDissonanceJson(text2)) { _steamToDissonance[item.Key] = item.Value; } ModLog.Debug("Persistence", $"Loaded player mapping: {_steamToDissonance.Count} entries"); foreach (KeyValuePair item2 in _steamToDissonance) { ModLog.Debug("Persistence", $" SteamID {item2.Key} -> DissonanceID '{item2.Value}'"); } } catch (Exception ex) { ModLog.Warn("Persistence", "LoadPlayerMapping: " + ex.Message); } } private static Dictionary ParseSteamToDissonanceJson(string json) { Dictionary dictionary = new Dictionary(); if (string.IsNullOrEmpty(json)) { return dictionary; } json = json.Trim(); if (json.StartsWith("{")) { string text = json; json = text.Substring(1, text.Length - 1); } if (json.EndsWith("}")) { string text = json; json = text.Substring(0, text.Length - 1); } json = json.Trim(); if (string.IsNullOrEmpty(json)) { return dictionary; } int num = 0; while (num < json.Length) { int num2 = json.IndexOf('"', num); if (num2 < 0) { break; } int num3 = json.IndexOf('"', num2 + 1); if (num3 < 0) { break; } string s = json.Substring(num2 + 1, num3 - num2 - 1); int num4 = json.IndexOf(':', num3 + 1); if (num4 < 0) { break; } int num5 = json.IndexOf('"', num4 + 1); if (num5 < 0) { break; } int num6 = json.IndexOf('"', num5 + 1); if (num6 < 0) { break; } string value = json.Substring(num5 + 1, num6 - num5 - 1); if (ulong.TryParse(s, out var result)) { dictionary[result] = value; } num = num6 + 1; int num7 = json.IndexOf(',', num); num = ((num7 >= 0) ? (num7 + 1) : json.Length); } return dictionary; } private static string EscapeJsonString(string s) { if (s != null) { return s.Replace("\\", "\\\\").Replace("\"", "\\\""); } return ""; } } internal static class SpeechEventSyncListHelper { private sealed class SyncListAccessor { private readonly PropertyInfo _countProp; private readonly PropertyInfo _indexer; private SyncListAccessor(PropertyInfo countProp, PropertyInfo indexer) { _countProp = countProp; _indexer = indexer; } internal static SyncListAccessor? TryCreate(Type syncListType) { PropertyInfo property = syncListType.GetProperty("Count", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); PropertyInfo property2 = syncListType.GetProperty("Item", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, null, new Type[1] { typeof(int) }, null); if (!(property == null) && !(property2 == null)) { return new SyncListAccessor(property, property2); } return null; } internal int Collect(object syncList, HashSet seenIds, List output) { int num = 0; int num2 = (int)_countProp.GetValue(syncList); for (int i = 0; i < num2; i++) { object? value = _indexer.GetValue(syncList, new object[1] { i }); SpeechEvent val = (SpeechEvent)((value is SpeechEvent) ? value : null); if (val != null && seenIds.Add(val.Id)) { output.Add(val); num++; } } return num; } internal int Visit(object syncList, HashSet seenIds, Action onEvent) { int num = 0; int num2 = (int)_countProp.GetValue(syncList); for (int i = 0; i < num2; i++) { object? value = _indexer.GetValue(syncList, new object[1] { i }); SpeechEvent val = (SpeechEvent)((value is SpeechEvent) ? value : null); if (val != null && seenIds.Add(val.Id)) { onEvent(val); num++; } } return num; } } private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo? EventsField = typeof(SpeechEventArchive).GetField("events", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly ConcurrentDictionary Accessors = new ConcurrentDictionary(); internal static int CollectFromArchive(SpeechEventArchive archive, HashSet seenIds, List output) { if (EventsField == null) { return 0; } object value = EventsField.GetValue(archive); if (value == null) { return 0; } if (!Accessors.TryGetValue(value.GetType(), out SyncListAccessor value2)) { value2 = SyncListAccessor.TryCreate(value.GetType()); if (value2 == null) { return 0; } Accessors[value.GetType()] = value2; } return value2.Collect(value, seenIds, output); } internal static int CollectFromSyncList(object syncList, HashSet seenIds, Action onEvent) { if (!Accessors.TryGetValue(syncList.GetType(), out SyncListAccessor value)) { value = SyncListAccessor.TryCreate(syncList.GetType()); if (value == null) { return 0; } Accessors[syncList.GetType()] = value; } return value.Visit(syncList, seenIds, onEvent); } } } namespace MimesisPlayerEnhancement.Features.Persistence.Patches { [HarmonyPatch(typeof(MaintenanceRoom), "SaveGameData")] public static class MaintenanceRoomPatches { private const string Feature = "Persistence"; [HarmonyPostfix] public static void Postfix(int saveSlotID, List playerNames, bool isAutoSave, MsgErrorCode __result) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) if (ModConfig.EnablePersistence.Value && (int)__result == 0 && MimesisSaveManager.IsHost()) { int num = ((!isAutoSave) ? saveSlotID : 0); ModLog.Info("Persistence", $"Game save triggered — persisting mimic voices for slot {num}."); MimesisSaveManager.SaveMimesisData(num); } } } [HarmonyPatch(typeof(PlatformMgr), "Delete")] public static class PlatformMgrPatches { private const string Feature = "Persistence"; [HarmonyPostfix] public static void Postfix(string fileName) { if (!ModConfig.EnablePersistence.Value) { return; } try { if (!string.IsNullOrEmpty(fileName) && fileName.StartsWith("MMGameData", StringComparison.OrdinalIgnoreCase) && int.TryParse(Path.GetFileNameWithoutExtension(fileName).Replace("MMGameData", ""), out var result) && MMSaveGameData.CheckSaveSlotID(result, true)) { MimesisSaveManager.DeleteMimesisData(result); ModLog.Info("Persistence", $"Deleted persisted voice data for slot {result}."); } } catch (Exception ex) { ModLog.Warn("Persistence", "PlatformMgr.Delete: " + ex.Message); } } } [HarmonyPatch(typeof(SpeechEventArchive), "OnStopClient")] public static class SpeechEventArchiveDisconnectPatches { private const string Feature = "Persistence"; [HarmonyPrefix] public static void Prefix(SpeechEventArchive __instance) { SpeechEventArchiveRegistry.Unregister(__instance); if (!ModConfig.EnablePersistence.Value) { return; } try { if (MimesisSaveManager.IsHost()) { bool flag = false; try { flag = __instance.IsLocal; } catch { } if (!flag) { int num = SpeechEventPoolManager.CacheEventsFromArchive(__instance); ModLog.Info("Persistence", $"Player disconnecting — {VoiceEventStats.DescribePlayer(__instance)} — cached {num} voice events"); } } } catch (Exception ex) { ModLog.Warn("Persistence", "Disconnect cache error: " + ex.Message); } } } [HarmonyPatch(typeof(SpeechEventArchive), "OnStartClient")] public static class SpeechEventArchivePatches { private const string Feature = "Persistence"; private static int _poolLoadedForSlot = -999; [HarmonyPostfix] public static void Postfix(SpeechEventArchive __instance) { try { SpeechEventArchiveRegistry.Register(__instance); if (!MimesisSaveManager.IsHost()) { if (ModConfig.EnablePersistence.Value) { ModLog.Debug("Persistence", "Archive started (non-host) — " + VoiceEventStats.DescribePlayer(__instance)); } return; } int currentSaveSlotId = MimesisSaveManager.GetCurrentSaveSlotId(); if (!MimesisSaveManager.IsValidSaveSlotId(currentSaveSlotId)) { if (ModConfig.EnablePersistence.Value) { ModLog.Debug("Persistence", "Archive started outside save slot — " + VoiceEventStats.DescribePlayer(__instance)); } return; } if (ModConfig.EnablePersistence.Value) { HandlePersistence(__instance, currentSaveSlotId); } if (ModConfig.EnableStatistics.Value) { StatisticsTracker.HandleArchiveStarted(__instance, currentSaveSlotId); } } catch (Exception ex) { ModLog.Warn("Persistence", "SpeechEventArchive inject failed: " + ex.Message); } } private static void HandlePersistence(SpeechEventArchive __instance, int slotId) { if (slotId != _poolLoadedForSlot) { _poolLoadedForSlot = slotId; SpeechEventPoolManager.Reset(); if (MimesisSaveManager.HasMimesisData(slotId)) { SpeechEventPoolManager.LoadForSlot(slotId); ModLog.Info("Persistence", $"Loaded persisted voice pool for save slot {slotId} ({SpeechEventPoolManager.TotalCount} events)."); } else { ModLog.Debug("Persistence", $"No persisted voice data for save slot {slotId}."); } } if (__instance.IsLocal) { SpeechEventPoolManager.SetLocalArchive(__instance); } string text = null; long num = 0L; bool flag = false; try { text = __instance.PlayerId; num = __instance.PlayerUID; flag = __instance.IsLocal; } catch { } int eventCount = VoiceEventStats.GetEventCount(__instance); if (!flag && string.IsNullOrEmpty(text) && num == 0L) { if (SpeechEventPoolManager.HasPending() || SpeechEventPoolManager.DisconnectedCacheCount > 0) { SpeechEventPoolManager.RegisterDeferredInjection(__instance); ModLog.Info("Persistence", "Player connecting — " + VoiceEventStats.DescribePlayer(__instance) + " — " + $"voice injection deferred (pendingPool={SpeechEventPoolManager.GetCounts().pending}, " + $"disconnectedCache={SpeechEventPoolManager.DisconnectedCacheCount})"); } else { ModLog.Info("Persistence", "Player connecting — " + VoiceEventStats.DescribePlayer(__instance) + " — awaiting identity sync"); } return; } SyncList events = __instance.events; if (events == null) { ModLog.Warn("Persistence", "Player archive has no event list — " + VoiceEventStats.DescribePlayer(__instance)); return; } float currentSessionTime = SpeechEventPoolManager.GetCurrentSessionTime(); HashSet hashSet = new HashSet(); for (int i = 0; i < events.Count; i++) { hashSet.Add(events[i].Id); } int num2 = 0; int num3 = 0; if (SpeechEventPoolManager.HasPending()) { List list = SpeechEventPoolManager.ClaimEventsForArchive(text, num, flag, __instance); if (list != null && list.Count > 0) { foreach (SpeechEvent item in list) { if (item != null && !hashSet.Contains(item.Id)) { SpeechEventPoolManager.FixEventTiming(item, currentSessionTime); events.Add(item); hashSet.Add(item.Id); num2++; } } } } if (SpeechEventPoolManager.DisconnectedCacheCount > 0) { List list2 = SpeechEventPoolManager.ClaimDisconnectedEventsForArchive(text, num, flag); if (list2 != null && list2.Count > 0) { foreach (SpeechEvent item2 in list2) { if (item2 != null && !hashSet.Contains(item2.Id)) { SpeechEventPoolManager.FixEventTiming(item2, currentSessionTime); events.Add(item2); hashSet.Add(item2.Id); num3++; } } } } int num4 = num2 + num3; int eventCount2 = VoiceEventStats.GetEventCount(__instance); if (num4 > 0) { ModLog.Info("Persistence", "Player connected — " + VoiceEventStats.DescribePlayer(__instance) + " — " + $"restored {num4} voice events (pool={num2}, reconnect={num3}, " + $"before={eventCount}, after={eventCount2})"); } else if (SpeechEventPoolManager.HasPending() || SpeechEventPoolManager.DisconnectedCacheCount > 0) { ModLog.Info("Persistence", "Player connected — " + VoiceEventStats.DescribePlayer(__instance) + " — no matching saved voices"); } else { ModLog.Info("Persistence", "Player connected — " + VoiceEventStats.DescribePlayer(__instance) + " — no persistence data"); } var (num5, num6, _) = SpeechEventPoolManager.GetCounts(); ModLog.Debug("Persistence", $"Archive detail — slot={slotId} time={currentSessionTime:F1} poolState={num5}P/{num6}I/0F " + $"disconnectedCache={SpeechEventPoolManager.DisconnectedCacheCount}"); } } [HarmonyPatch(typeof(VoiceManager), "GetRandomOtherSpeechEventArchive")] public static class VoiceManagerHallucinationPatch { private const string Feature = "Persistence"; [HarmonyPostfix] public static void Postfix(ref SpeechEventArchive __result) { if (!ModConfig.EnablePersistence.Value) { return; } try { if (!((Object)(object)__result != (Object)null)) { SpeechEventArchive localArchive = SpeechEventPoolManager.GetLocalArchive(); if (!((Object)(object)localArchive == (Object)null) && localArchive.WarmedUpCount > 0) { __result = localArchive; } } } catch (Exception ex) { ModLog.Warn("Persistence", "Hallucination fallback: " + ex.Message); } } } } namespace MimesisPlayerEnhancement.Features.MoreVoices { public static class MoreVoicesPatches { [HarmonyPatch(typeof(SpeechEventArchive), "OnStartClient")] public static class SpeechEventArchiveLimitsPatch { [HarmonyPostfix] public static void Postfix(SpeechEventArchive __instance) { if (!ModConfig.EnableMoreVoices.Value || (Object)(object)__instance == (Object)null) { return; } try { int value = ModConfig.MaxIndoorVoiceEvents.Value; int value2 = ModConfig.MaxDeathMatchVoiceEvents.Value; int value3 = ModConfig.MaxOutdoorVoiceEvents.Value; if (ApplyLimitsToArchive(__instance, value, value2, value3)) { ModLog.Info("MoreVoices", "Voice archive started — " + FormatLimits(value, value2, value3) + ", " + VoiceEventStats.DescribePlayer(__instance)); } ModLog.Debug("MoreVoices", $"Voice archive detail — maxEvents={GetFieldValue(__instance, MaxEventsField)}, " + $"maxDeathMatch={GetFieldValue(__instance, MaxDeathMatchEventsField)}, " + $"maxOutdoor={GetFieldValue(__instance, MaxOutDoorEventsField)}, " + VoiceEventStats.DescribePlayerVerbose(__instance)); } catch (Exception ex) { ModLog.Warn("MoreVoices", "Voice archive postfix failed: " + ex.Message); } } } private const string Feature = "MoreVoices"; private const BindingFlags InstanceFieldFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo? MaxEventsField = typeof(SpeechEventArchive).GetField("maxEvents", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? MaxDeathMatchEventsField = typeof(SpeechEventArchive).GetField("maxDeathMatchEvents", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? MaxOutDoorEventsField = typeof(SpeechEventArchive).GetField("maxOutDoorEvents", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); public static void Apply(Harmony harmony) { if (MaxEventsField == null || MaxDeathMatchEventsField == null || MaxOutDoorEventsField == null) { ModLog.Warn("MoreVoices", "One or more SpeechEventArchive limit fields not found — voice cap patches may not apply"); } HarmonyPatchHelper.PatchApplyResult result = HarmonyPatchHelper.ApplyPatchTypes(harmony, "MoreVoices", HarmonyPatchHelper.GetNestedPatchTypes(typeof(MoreVoicesPatches))); LogPatchAudit(harmony); HarmonyPatchHelper.LogPatchSummary("MoreVoices", result); } public static void RefreshFromConfig() { if (!ModConfig.EnableMoreVoices.Value) { return; } int value = ModConfig.MaxIndoorVoiceEvents.Value; int value2 = ModConfig.MaxDeathMatchVoiceEvents.Value; int value3 = ModConfig.MaxOutdoorVoiceEvents.Value; int num = 0; foreach (SpeechEventArchive item in SpeechEventArchiveRegistry.EnumerateActive()) { if (!((Object)(object)item == (Object)null) && ApplyLimitsToArchive(item, value, value2, value3)) { num++; } } string text = FormatLimits(value, value2, value3); if (num > 0) { ModLog.Info("MoreVoices", $"Refreshed voice limits on {num} archive(s) — {text}."); } else { ModLog.Debug("MoreVoices", "Voice limit refresh complete — " + text + ", no active archives."); } } private static void LogPatchAudit(Harmony harmony) { HarmonyPatchHelper.LogPatchAudit("MoreVoices", harmony, new <>z__ReadOnlySingleElementList<(string, MethodBase)>(("OnStartClient/SpeechEventArchive", AccessTools.Method(typeof(SpeechEventArchive), "OnStartClient", (Type[])null, (Type[])null)))); } private static bool ApplyLimitsToArchive(SpeechEventArchive archive, int maxIndoor, int maxDeathMatch, int maxOutdoor) { try { int fieldValue = GetFieldValue(archive, MaxEventsField); int fieldValue2 = GetFieldValue(archive, MaxDeathMatchEventsField); int fieldValue3 = GetFieldValue(archive, MaxOutDoorEventsField); SetFieldValue(archive, MaxEventsField, maxIndoor); SetFieldValue(archive, MaxDeathMatchEventsField, maxDeathMatch); SetFieldValue(archive, MaxOutDoorEventsField, maxOutdoor); ModLog.Debug("MoreVoices", "Applied instance limits -> " + FormatLimits(maxIndoor, maxDeathMatch, maxOutdoor) + " " + $"(was maxEvents={fieldValue}, maxDeathMatch={fieldValue2}, maxOutdoor={fieldValue3})."); return true; } catch (Exception ex) { ModLog.Warn("MoreVoices", "Failed to set voice limit fields: " + ex.Message); return false; } } private static string FormatLimits(int maxIndoor, int maxDeathMatch, int maxOutdoor) { return $"indoor={maxIndoor}, deathmatch={maxDeathMatch}, outdoor={maxOutdoor}"; } private static int GetFieldValue(SpeechEventArchive archive, FieldInfo? field) { if (!(field == null)) { return (int)field.GetValue(archive); } throw new MissingFieldException(typeof(SpeechEventArchive).FullName, "voice limit field"); } private static void SetFieldValue(SpeechEventArchive archive, FieldInfo? field, int value) { if (field == null) { throw new MissingFieldException(typeof(SpeechEventArchive).FullName, "voice limit field"); } field.SetValue(archive, value); } } } namespace MimesisPlayerEnhancement.Features.MorePlayers { public static class MorePlayersPatches { [HarmonyPatch] public static class IVroomCanEnterChannelTranspiler { public static IEnumerable TargetMethods() { MethodInfo methodInfo = GameNetworkApi.GetIVroomType()?.GetMethod("CanEnterChannel", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (!(methodInfo != null)) { return Array.Empty(); } return new MethodBase[1] { methodInfo }; } [HarmonyTranspiler] public static IEnumerable Transpiler(IEnumerable instructions) { //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Expected O, but got Unknown List list = instructions.ToList(); for (int i = 0; i < list.Count; i++) { if (list[i].opcode == OpCodes.Ldfld && list[i].operand is FieldInfo { Name: "C_MaxPlayerCount" }) { list[i] = new CodeInstruction(OpCodes.Pop, (object)null); list.Insert(i + 1, LoadMaxPlayers()); break; } } return list; } } [HarmonyPatch] public static class GetMaximumClientsPatch { public static IEnumerable TargetMethods() { return FindSocketMethods("GetMaximumClients"); } [HarmonyPrefix] public static bool Prefix(ref int __result) { if (!ModConfig.EnableMorePlayers.Value) { return true; } __result = MaxClientConnections; return false; } } [HarmonyPatch] public static class SetMaximumClientsPatch { public static IEnumerable TargetMethods() { return FindSocketMethods("SetMaximumClients"); } [HarmonyPrefix] public static bool Prefix(ref int value) { if (!ModConfig.EnableMorePlayers.Value) { return true; } value = MaxClientConnections; return true; } } [HarmonyPatch] public static class ServerSocketConstructorPatch { public static MethodBase? TargetMethod() { return ResolveServerSocketConstructor(); } [HarmonyPostfix] public static void Postfix(object __instance) { if (!ModConfig.EnableMorePlayers.Value) { return; } try { GameNetworkApi.SetMaximumClients(__instance, MaxClientConnections); } catch (Exception ex) { ModLog.Warn("MorePlayers", "Server socket ctor postfix failed: " + ex.Message); } } } [HarmonyPatch] public static class GetMemberCountSmartPatch { public static IEnumerable TargetMethods() { MethodInfo methodInfo = GameNetworkApi.GetIVroomType()?.GetMethod("GetMemberCount", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (!(methodInfo != null)) { return Array.Empty(); } return new MethodBase[1] { methodInfo }; } [HarmonyPrefix] public static bool Prefix(ref int __result, object __instance) { if (!ModConfig.EnableMorePlayers.Value) { return true; } try { int roomPlayerCount = GameNetworkApi.GetRoomPlayerCount(__instance); StackTrace stackTrace = new StackTrace(); bool flag = false; bool flag2 = false; for (int i = 0; i < Math.Min(stackTrace.FrameCount, 10); i++) { MethodBase methodBase = stackTrace.GetFrame(i)?.GetMethod(); if (!(methodBase == null)) { string name = methodBase.Name; if (name.Contains("EnterWaitingRoom") || name.Contains("EnterMaintenenceRoom") || name.Contains("EnterMaintenanceRoom") || name.Contains("CanEnter")) { flag = true; break; } if (name.Contains("GetSessionCount") || name.Contains("GetRoomMemberCount")) { flag2 = true; break; } } } if (flag) { __result = 0; ModLog.Debug("MorePlayers", "GetMemberCount returning 0 for enter check in " + __instance.GetType().Name); return false; } __result = roomPlayerCount; return false; } catch (Exception ex) { ModLog.Warn("MorePlayers", "GetMemberCount patch error: " + ex.Message); return true; } } } [HarmonyPatch] public static class AllRoomsCanEnterChannelPatch { public static IEnumerable TargetMethods() { Assembly gameAssembly = GameNetworkApi.GetGameAssembly(); if (gameAssembly == null) { return Array.Empty(); } List list = new List(); string[] array = new string[3] { "VWaitingRoom", "MaintenanceRoom", "IVroom" }; foreach (string name in array) { MethodInfo methodInfo = gameAssembly.GetType(name)?.GetMethod("CanEnterChannel", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (methodInfo != null && !list.Contains(methodInfo)) { list.Add(methodInfo); } } return list; } [HarmonyPrefix] public static bool Prefix(ref object __result, object __instance, long playerUID) { if (!ModConfig.EnableMorePlayers.Value) { return true; } try { Type type = GameNetworkApi.GetGameAssembly()?.GetTypes().FirstOrDefault((Type t) => t.Name == "MsgErrorCode"); if (type == null || !type.IsEnum) { return true; } if (ReflectionHelper.GetFieldValue(__instance, "_vPlayerDict") is IDictionary dictionary) { foreach (object value in dictionary.Values) { if (value == null) { continue; } long num = ReflectionHelper.GetFieldValue(value, "UID"); if (num == 0L) { PropertyInfo property = value.GetType().GetProperty("UID", BindingFlags.Instance | BindingFlags.Public); if (property != null) { num = Convert.ToInt64(property.GetValue(value)); } } if (num == playerUID) { __result = Enum.Parse(type, "DuplicatePlayer"); ModLog.Info("MorePlayers", $"Join denied — duplicate player uid={playerUID} in {__instance.GetType().Name}."); return false; } } if (PlayersInRoom(dictionary) >= MaxPlayers) { __result = Enum.Parse(type, "PlayerCountExceeded"); ModLog.Info("MorePlayers", $"Join denied — session full ({PlayersInRoom(dictionary)}/{MaxPlayers} players) in {__instance.GetType().Name}, uid={playerUID}."); return false; } __result = Enum.Parse(type, "Success"); int num2 = PlayersInRoom(dictionary) + 1; ModLog.Debug("MorePlayers", $"Join allowed — uid={playerUID} in {__instance.GetType().Name} ({num2}/{MaxPlayers} players)."); return false; } ModLog.Warn("MorePlayers", $"Join denied — player dictionary unavailable in {__instance.GetType().Name}, uid={playerUID}."); __result = Enum.Parse(type, "PlayerCountExceeded"); return false; } catch (Exception ex) { ModLog.Warn("MorePlayers", "CanEnterChannel patch error: " + ex.Message); return true; } } } [HarmonyPatch] public static class VRoomManagerEnterRoomPatches { public static IEnumerable TargetMethods() { Type type = GameNetworkApi.GetGameAssembly()?.GetType("VRoomManager"); if (type == null) { return Array.Empty(); } List list = new List(); string[] array = new string[3] { "EnterWaitingRoom", "EnterMaintenenceRoom", "EnterMaintenanceRoom" }; foreach (string name in array) { MethodInfo method = type.GetMethod(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (method != null) { list.Add(method); } } return list; } [HarmonyPrefix] public static void Prefix(object __instance, MethodBase __originalMethod) { if (!ModConfig.EnableMorePlayers.Value) { return; } try { UpdateRoomMaxPlayers(__instance); string text; switch (__originalMethod.Name) { case "EnterWaitingRoom": text = "WaitingRoom"; break; case "EnterMaintenenceRoom": case "EnterMaintenanceRoom": text = "MaintenanceRoom"; break; default: text = __originalMethod.Name; break; } string arg = text; ModLog.Info("MorePlayers", $"Enter room — {arg} (session cap {MaxPlayers})."); } catch (Exception ex) { ModLog.Warn("MorePlayers", "VRoomManager enter room patch error: " + ex.Message); } } } [HarmonyPatch] public static class GameSessionInfoAddPlayerSteamIdPatch { public static MethodBase? TargetMethod() { return ResolveGameSessionInfoMethod("AddPlayerSteamID"); } [HarmonyTranspiler] public static IEnumerable Transpiler(IEnumerable instructions) { //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Expected O, but got Unknown List list = instructions.ToList(); for (int i = 0; i < list.Count; i++) { if (list[i].opcode == OpCodes.Ldc_I4_4) { list[i] = LoadMaxPlayers(); } else if (list[i].opcode == OpCodes.Ldfld && list[i].operand is FieldInfo { Name: "C_MaxPlayerCount" }) { list[i] = new CodeInstruction(OpCodes.Pop, (object)null); list.Insert(i + 1, LoadMaxPlayers()); i++; } } return list; } } [HarmonyPatch] public static class PublicRoomListSetRoomListTranspiler { public static MethodBase? TargetMethod() { return ResolveGameTypeMethod("UIPrefab_PublicRoomList", "SetRoomList"); } [HarmonyTranspiler] public static IEnumerable Transpiler(IEnumerable instructions) { return ReplaceVanillaLobbyCap(instructions); } } [HarmonyPatch] public static class RoomCardSetRoomDataTranspiler { public static MethodBase? TargetMethod() { return ResolveGameTypeMethod("UiPrefab_RoomCard", "SetRoomData"); } [HarmonyTranspiler] public static IEnumerable Transpiler(IEnumerable instructions) { return ReplaceLobbyPlayerCountSuffix(instructions); } } [HarmonyPatch] public static class UpdatePlayerGroupSizeTranspiler { public static MethodBase? TargetMethod() { return ResolveSteamInviteDispatcherMethod("UpdatePlayerGroupSize"); } [HarmonyTranspiler] public static IEnumerable Transpiler(IEnumerable instructions) { return ReplaceVanillaLobbyCap(instructions); } } [HarmonyPatch] public static class SteamLobbyCreationPatch { public static MethodBase? TargetMethod() { return ResolveSteamInviteDispatcherMethod("CreateLobby"); } [HarmonyPrefix] public static bool Prefix(bool isOpenForRandomMatch) { if (!ModConfig.EnableMorePlayers.Value) { return true; } try { Type type = Type.GetType("Steamworks.SteamMatchmaking, com.rlabrecque.steamworks.net"); Type type2 = Type.GetType("Steamworks.ELobbyType, com.rlabrecque.steamworks.net"); Type type3 = Type.GetType("UnityEngine.PlayerPrefs, UnityEngine.CoreModule"); if (type == null || type2 == null || type3 == null) { ModLog.Warn("MorePlayers", "Steam lobby creation skipped — Steamworks or PlayerPrefs types not found."); return true; } MethodInfo method = type.GetMethod("CreateLobby", BindingFlags.Static | BindingFlags.Public); MethodInfo method2 = type3.GetMethod("SetInt", BindingFlags.Static | BindingFlags.Public); if (method == null || method2 == null) { ModLog.Warn("MorePlayers", "Steam lobby creation skipped — CreateLobby or PlayerPrefs.SetInt not found."); return true; } object obj = Enum.ToObject(type2, 2); method.Invoke(null, new object[2] { obj, MaxPlayers }); method2.Invoke(null, new object[2] { "TempLobbyIsOpen", isOpenForRandomMatch ? 1 : 0 }); ModLog.Info("MorePlayers", $"Steam lobby created — maxPlayers={MaxPlayers}, openForMatchmaking={isOpenForRandomMatch}."); return false; } catch (Exception ex) { ModLog.Warn("MorePlayers", "Steam lobby creation patch error: " + ex.Message); return true; } } } private const string Feature = "MorePlayers"; private const int VanillaMaxPlayers = 4; private const BindingFlags InstanceMethodFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly MethodInfo GetMaxPlayersMethod = AccessTools.Method(typeof(MorePlayersPatches), "GetMaxPlayers", (Type[])null, (Type[])null); private static readonly MethodInfo GetLobbyPlayerCountSuffixMethod = AccessTools.Method(typeof(MorePlayersPatches), "GetLobbyPlayerCountSuffix", (Type[])null, (Type[])null); internal static int MaxPlayers => GetMaxPlayers(); internal static int MaxClientConnections => MaxPlayers; public static void Apply(Harmony harmony) { GameNetworkApi.GetGameAssembly(); HarmonyPatchHelper.PatchApplyResult result = HarmonyPatchHelper.ApplyPatchTypes(harmony, "MorePlayers", HarmonyPatchHelper.GetNestedPatchTypes(typeof(MorePlayersPatches))); LogPatchAudit(harmony); HarmonyPatchHelper.LogPatchSummary("MorePlayers", result); } private static void LogPatchAudit(Harmony harmony) { List<(string label, MethodBase? method)> checks = new List<(string, MethodBase)>(); Check("CanEnterChannel/IVroom", ResolveRoomMethod("IVroom", "CanEnterChannel")); Check("CanEnterChannel/VWaitingRoom", ResolveRoomMethod("VWaitingRoom", "CanEnterChannel")); Check("CanEnterChannel/MaintenanceRoom", ResolveRoomMethod("MaintenanceRoom", "CanEnterChannel")); Check("GetMemberCount/IVroom", GameNetworkApi.GetIVroomType()?.GetMethod("GetMemberCount", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)); foreach (MethodBase item in FindSocketMethods("GetMaximumClients")) { Check("GetMaximumClients/ServerSocket", item); } foreach (MethodBase item2 in FindSocketMethods("SetMaximumClients")) { Check("SetMaximumClients/ServerSocket", item2); } Check("ServerSocket.ctor", ResolveServerSocketConstructor()); Check("AddPlayerSteamID/GameSessionInfo", ResolveGameSessionInfoMethod("AddPlayerSteamID")); Check("CreateLobby/SteamInviteDispatcher", ResolveSteamInviteDispatcherMethod("CreateLobby")); Check("UpdatePlayerGroupSize/SteamInviteDispatcher", ResolveSteamInviteDispatcherMethod("UpdatePlayerGroupSize")); Check("SetRoomList/UIPrefab_PublicRoomList", ResolveGameTypeMethod("UIPrefab_PublicRoomList", "SetRoomList")); Check("SetRoomData/UiPrefab_RoomCard", ResolveGameTypeMethod("UiPrefab_RoomCard", "SetRoomData")); Check("EnterWaitingRoom/VRoomManager", ResolveVRoomManagerMethod("EnterWaitingRoom")); Check("EnterMaintenenceRoom/VRoomManager", ResolveVRoomManagerMethod("EnterMaintenenceRoom")); HarmonyPatchHelper.LogPatchAudit("MorePlayers", harmony, checks); void Check(string label, MethodBase? method) { checks.Add((label, method)); } } public static void RefreshFromConfig() { int num = (ModConfig.EnableMorePlayers.Value ? MaxClientConnections : 4); try { object serverSocket = GameNetworkApi.GetServerSocket(); if (serverSocket != null) { GameNetworkApi.SetMaximumClients(serverSocket, num); ModLog.Debug("MorePlayers", $"Server socket max clients refreshed to {num} (session cap {(ModConfig.EnableMorePlayers.Value ? MaxPlayers : 4)})."); } } catch (Exception ex) { ModLog.Warn("MorePlayers", "Server socket refresh: " + ex.Message); } try { UpdateRoomMaxPlayers(GameNetworkApi.GetVRoomManager(), logRefresh: true); } catch (Exception ex2) { ModLog.Warn("MorePlayers", "Room max players refresh: " + ex2.Message); } } private static void UpdateRoomMaxPlayers(object? vroomManager, bool logRefresh = false) { if (vroomManager == null || !(ReflectionHelper.GetFieldValue(vroomManager, "_vrooms") is IDictionary dictionary)) { return; } foreach (object value in dictionary.Values) { if (value == null) { continue; } Type type = value.GetType(); while (type != null) { FieldInfo field = type.GetField("_maxPlayers", BindingFlags.Instance | BindingFlags.NonPublic); if (field != null) { int num = (ModConfig.EnableMorePlayers.Value ? MaxPlayers : 4); field.SetValue(value, num); if (logRefresh) { ModLog.Debug("MorePlayers", $"Room {value.GetType().Name} max players refreshed to {num}."); } break; } type = type.BaseType; } } } public static int GetMaxPlayers() { if (!ModConfig.EnableMorePlayers.Value) { return 4; } return ModConfig.MaxPlayers.Value; } public static string GetLobbyPlayerCountSuffix() { return "/" + GetMaxPlayers(); } private static int PlayersInRoom(IDictionary? vPlayerDict) { return vPlayerDict?.Count ?? 0; } private static CodeInstruction LoadMaxPlayers() { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected O, but got Unknown return new CodeInstruction(OpCodes.Call, (object)GetMaxPlayersMethod); } private static IEnumerable ReplaceVanillaLobbyCap(IEnumerable instructions) { List list = instructions.ToList(); for (int i = 0; i < list.Count; i++) { if (list[i].opcode == OpCodes.Ldc_I4_4) { list[i] = LoadMaxPlayers(); } } return list; } private static IEnumerable ReplaceLobbyPlayerCountSuffix(IEnumerable instructions) { //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Expected O, but got Unknown List list = instructions.ToList(); for (int i = 0; i < list.Count; i++) { if (list[i].opcode == OpCodes.Ldstr && list[i].operand is string text && text == "/4") { list[i] = new CodeInstruction(OpCodes.Call, (object)GetLobbyPlayerCountSuffixMethod); } } return list; } private static MethodBase? ResolveGameTypeMethod(string typeName, string methodName) { return GameNetworkApi.GetGameAssembly()?.GetType(typeName)?.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } private static MethodBase? ResolveRoomMethod(string typeName, string methodName) { return GameNetworkApi.GetGameAssembly()?.GetType(typeName)?.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } private static MethodBase? ResolveGameSessionInfoMethod(string methodName) { return GameNetworkApi.GetGameSessionInfoType()?.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } private static MethodBase? ResolveVRoomManagerMethod(string methodName) { return GameNetworkApi.GetGameAssembly()?.GetType("VRoomManager")?.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } private static MethodBase? ResolveSteamInviteDispatcherMethod(string methodName) { return GameNetworkApi.GetGameAssembly()?.GetTypes().FirstOrDefault((Type t) => t.Name == "SteamInviteDispatcher")?.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); } private static MethodBase? ResolveServerSocketConstructor() { Assembly gameAssembly = GameNetworkApi.GetGameAssembly(); string[] array = new string[2] { "FishySteamworks.Server.ServerSocket", "FishyNet.Transporting.Server.ServerSocket" }; foreach (string name in array) { ConstructorInfo constructorInfo = (gameAssembly?.GetType(name))?.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault(); if (constructorInfo != null) { return constructorInfo; } } return null; } private static IEnumerable FindSocketMethods(string methodName) { Assembly assembly = GameNetworkApi.GetGameAssembly(); if (assembly == null) { yield break; } string[] array = new string[2] { "FishySteamworks.Server.ServerSocket", "FishyNet.Transporting.Server.ServerSocket" }; foreach (string name in array) { MethodInfo methodInfo = assembly.GetType(name)?.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (methodInfo != null) { yield return methodInfo; } } } } } namespace MimesisPlayerEnhancement.Features.MoneyMultiplier { internal static class MaintenanceShopPriceSync { private static readonly FieldInfo LevelObjectsField = AccessTools.Field(typeof(IVroom), "_levelObjects") ?? throw new InvalidOperationException("IVroom._levelObjects not found"); internal static void SyncVendingMachineLevelObjects(MaintenanceRoom room, Dictionary priceForItems) { //IL_003b: Unknown result type (might be due to invalid IL or missing references) if (!(LevelObjectsField.GetValue(room) is IDictionary dictionary)) { return; } foreach (DictionaryEntry item in dictionary) { object? value = item.Value; InsertLevelObjectInfo val = (InsertLevelObjectInfo)((value is InsertLevelObjectInfo) ? value : null); if (val != null && (int)val.InsertLevelObjectType == 0 && priceForItems.TryGetValue(val.OutputItemMasterID, out ShopItemPriceInfo value2) && value2 != null) { val.InputAmount = value2.Price; val.DiscountRate = value2.DiscountRate; } } } } internal static class MoneyMultiplierApplier { private const string Feature = "MoneyMultiplier"; private const int MaxShopItemRollAttemptsPerSlot = 32; private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly PropertyInfo? HubDatamanProperty = typeof(Hub).GetProperty("dataman", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly PropertyInfo? HubDynamicDataManProperty = typeof(Hub).GetProperty("dynamicDataMan", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly MethodInfo? GetVendingMachineLevelObjectsMethod = typeof(Hub).Assembly.GetType("DynamicDataManager")?.GetMethod("GetVendingMachineLevelObjects", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly MethodInfo? GetShopGroupInfoMethod = typeof(Hub).Assembly.GetType("ExcelDataManager")?.GetMethod("GetShopGroupInfo", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo TargetCurrencyField = AccessTools.Field(typeof(GameSessionInfo), "_targetCurrency") ?? throw new InvalidOperationException("GameSessionInfo._targetCurrency not found"); private static readonly FieldInfo PriceForItemsField = AccessTools.Field(typeof(MaintenanceRoom), "_priceForItems") ?? throw new InvalidOperationException("MaintenanceRoom._priceForItems not found"); internal static bool IsEnabled() { if (ModConfig.EnableMoneyMultiplier.Value) { return HostApplyGate.ShouldApplyHostOnlyFeature(); } return false; } internal static int ScaleShopPrice(MaintenanceRoom room, int vanilla) { return ShopBuyPriceApplier.ScalePrice(room, vanilla); } internal static bool TryGetVanillaInitialMoney(out int value) { value = 0; if ((Object)(object)Hub.s == (Object)null) { return false; } object? obj = HubDatamanProperty?.GetValue(Hub.s); DataManager val = (DataManager)((obj is DataManager) ? obj : null); if (val == null) { return false; } try { value = val.ExcelDataManager.Consts.C_InitialMoney; return true; } catch { return false; } } internal static int ScaleForType(MoneyType type, int vanilla, int playerCount) { float effectiveMultiplier = MoneyMultiplierResolver.GetEffectiveMultiplier(type, playerCount); int num = MoneyMultiplierResolver.ScaleAmount(vanilla, effectiveMultiplier); MoneyMultiplierLog.DebugScaled(type, vanilla, num, playerCount, effectiveMultiplier); return num; } internal static void ApplyStartupMoney(MaintenanceRoom room, ref int currency) { if (IsEnabled() && !StartupMoneyLoadGuard.IsActive && TryGetVanillaInitialMoney(out var value) && currency == value) { int playerCount = MoneyPlayerCountHelper.ResolveFromRoom(room); currency = ScaleForType(MoneyType.Startup, currency, playerCount); } } internal static void ApplyRoundGoal(GameSessionInfo info) { if (IsEnabled()) { int num = (int)(TargetCurrencyField.GetValue(info) ?? ((object)0)); if (num > 0) { int playerCount = MoneyPlayerCountHelper.ResolveFromSession(info); int num2 = ScaleForType(MoneyType.RoundGoal, num, playerCount); TargetCurrencyField.SetValue(info, num2); } } } internal static int ScaleScrapValue(int vanilla) { if (!IsEnabled()) { return vanilla; } return ScaleForType(MoneyType.ScrapSellValue, vanilla, MoneyPlayerCountHelper.ResolveForItemPrices()); } internal static int ScaleReinforcePrice(MaintenanceRoom room, int vanilla) { if (!IsEnabled()) { return vanilla; } int playerCount = MoneyPlayerCountHelper.ResolveFromRoom(room); return ScaleForType(MoneyType.ReinforcePrice, vanilla, playerCount); } internal static void ApplyShopItems(MaintenanceRoom room) { //IL_01ac: Unknown result type (might be due to invalid IL or missing references) //IL_01b1: Unknown result type (might be due to invalid IL or missing references) //IL_01b9: Unknown result type (might be due to invalid IL or missing references) //IL_01c6: Expected O, but got Unknown if (!IsEnabled() || !(PriceForItemsField.GetValue(room) is Dictionary { Count: var count } dictionary) || count == 0) { return; } int num = MoneyPlayerCountHelper.ResolveFromRoom(room); int num2 = ScaleForType(MoneyType.ShopItems, count, num); if (num2 <= count || (Object)(object)Hub.s == (Object)null) { return; } object? obj = HubDatamanProperty?.GetValue(Hub.s); DataManager val = (DataManager)((obj is DataManager) ? obj : null); if (val == null || HubDynamicDataManProperty?.GetValue(Hub.s) == null || GetVendingMachineLevelObjectsMethod == null || GetShopGroupInfoMethod == null) { return; } object value = HubDynamicDataManProperty.GetValue(Hub.s); if (!(GetVendingMachineLevelObjectsMethod.Invoke(value, null) is IList list)) { return; } List list2 = new List(); foreach (object item in list) { VendingMachineLevelObject val2 = (VendingMachineLevelObject)((item is VendingMachineLevelObject) ? item : null); if (val2 != null && val2.shopGroupID > 0) { list2.Add(val2.shopGroupID); } } if (list2.Count == 0) { ModLog.Debug("MoneyMultiplier", "Shop items scaling skipped — no vending-machine shop groups on map"); return; } object excelDataManager = val.ExcelDataManager; int num3 = 0; int num4 = 0; int num5 = (num2 - count) * 32; while (dictionary.Count < num2 && num4 < num5) { num4++; int num6 = list2[num4 % list2.Count]; if (TryParseShopGroupInfo(GetShopGroupInfoMethod.Invoke(excelDataManager, new object[1] { num6 }), out var masterId, out var price, out var discountRate) && masterId > 0 && !dictionary.ContainsKey(masterId)) { dictionary.Add(masterId, new ShopItemPriceInfo { Price = price, DiscountRate = discountRate }); num3++; } } if (num3 > 0) { float effectiveMultiplier = MoneyMultiplierResolver.GetEffectiveMultiplier(MoneyType.ShopItems, num); ModLog.Info("MoneyMultiplier", $"Shop items scaled — {count} -> {dictionary.Count} unique items " + $"(target={num2}, added={num3}, players={num}, effective={effectiveMultiplier:0.##}×)"); } else if (ModConfig.EnableDebugLogging.Value) { ModLog.Debug("MoneyMultiplier", $"Shop items unchanged at {count} — could not roll {num2 - count} additional unique items"); } } private static bool TryParseShopGroupInfo(object? result, out int masterId, out int price, out float discountRate) { masterId = 0; price = 0; discountRate = 0f; if (result == null) { return false; } Type type = result.GetType(); masterId = (int)(type.GetField("Item1")?.GetValue(result) ?? ((object)0)); price = (int)(type.GetField("Item2")?.GetValue(result) ?? ((object)0)); discountRate = (float)(type.GetField("Item3")?.GetValue(result) ?? ((object)0f)); return masterId > 0; } } internal static class MoneyMultiplierLog { private const string Feature = "MoneyMultiplier"; internal static void DebugScaled(MoneyType type, int vanilla, int scaled, int playerCount, float effectiveMultiplier) { if (ModConfig.EnableDebugLogging.Value) { ModLog.Debug("MoneyMultiplier", $"{FormatType(type)} scaled {vanilla} -> {scaled} (players={playerCount}, effective={effectiveMultiplier:0.##}×)"); } } internal static string FormatType(MoneyType type) { return type switch { MoneyType.Startup => "Startup money", MoneyType.RoundGoal => "Round goal", MoneyType.ScrapSellValue => "Scrap/sell value", MoneyType.ShopBuyPrice => "Shop buy price", MoneyType.ShopItems => "Shop items", MoneyType.ReinforcePrice => "Reinforce price", _ => type.ToString(), }; } } public static class MoneyMultiplierPatches { [HarmonyPatch(/*Could not decode attribute arguments.*/)] public static class IVroomSetCurrencyPatch { [HarmonyPrefix] public static void Prefix(IVroom __instance, ref int value) { try { MaintenanceRoom val = (MaintenanceRoom)(object)((__instance is MaintenanceRoom) ? __instance : null); if (val != null) { MoneyMultiplierApplier.ApplyStartupMoney(val, ref value); } } catch (Exception ex) { ModLog.Warn("MoneyMultiplier", "set_Currency prefix failed — " + ex.Message); } } } [HarmonyPatch(typeof(GameSessionInfo), "RefreshTargetCurrency")] public static class GameSessionInfoRefreshTargetCurrencyPatch { [HarmonyPostfix] public static void Postfix(GameSessionInfo __instance) { try { MoneyMultiplierApplier.ApplyRoundGoal(__instance); } catch (Exception ex) { ModLog.Warn("MoneyMultiplier", "RefreshTargetCurrency postfix failed — " + ex.Message); } } } [HarmonyPatch(typeof(GameSessionInfo), "ClampTargetCurrencyToMin")] public static class GameSessionInfoClampTargetCurrencyToMinPatch { [HarmonyPostfix] public static void Postfix(GameSessionInfo __instance) { try { MoneyMultiplierApplier.ApplyRoundGoal(__instance); } catch (Exception ex) { ModLog.Warn("MoneyMultiplier", "ClampTargetCurrencyToMin postfix failed — " + ex.Message); } } } [HarmonyPatch(typeof(VRoomManager), "InitMaintenenceRoom")] public static class VRoomManagerInitMaintenenceRoomStartupMoneyPatch { [HarmonyPrefix] public static void Prefix(int saveSlotID, ref bool __state) { __state = StartupMoneyLoadGuard.TryEnterForSaveSlot(saveSlotID); } [HarmonyFinalizer] public static void Finalizer(bool __state) { if (__state) { StartupMoneyLoadGuard.Exit(); } } } [HarmonyPatch(typeof(ItemMasterInfo), "GetPrice")] public static class ItemMasterInfoGetPricePatch { [HarmonyPostfix] public static void Postfix(ref int __result) { if (!ModConfig.EnableMoneyMultiplier.Value) { return; } try { __result = MoneyMultiplierApplier.ScaleScrapValue(__result); } catch (Exception ex) { ModLog.Warn("MoneyMultiplier", "GetPrice postfix failed — " + ex.Message); } } } [HarmonyPatch(typeof(ItemMasterInfo), "GetMeanPrice")] public static class ItemMasterInfoGetMeanPricePatch { [HarmonyPostfix] public static void Postfix(ref int __result) { if (!ModConfig.EnableMoneyMultiplier.Value) { return; } try { __result = MoneyMultiplierApplier.ScaleScrapValue(__result); } catch (Exception ex) { ModLog.Warn("MoneyMultiplier", "GetMeanPrice postfix failed — " + ex.Message); } } } [HarmonyPatch(typeof(MaintenanceRoom), "TryGetShopItemPrice")] public static class MaintenanceRoomTryGetShopItemPricePatch { [HarmonyPrefix] public static void Prefix(MaintenanceRoom __instance) { try { ShopBuyPriceState.EnsureApplied(__instance); } catch (Exception ex) { ModLog.Warn("MoneyMultiplier", "TryGetShopItemPrice prefix failed — " + ex.Message); } } } [HarmonyPatch(typeof(MaintenanceRoom), "InitShopItems")] public static class MaintenanceRoomInitShopItemsPatch { [HarmonyPrefix] public static void Prefix(MaintenanceRoom __instance) { try { ShopBuyPriceState.MarkDirty(__instance); ShopBuyPriceApplier.ClearVanillaPrices(__instance); } catch (Exception ex) { ModLog.Warn("MoneyMultiplier", "InitShopItems prefix failed — " + ex.Message); } } [HarmonyPostfix] public static void Postfix(MaintenanceRoom __instance) { try { MoneyMultiplierApplier.ApplyShopItems(__instance); ShopDiscountApplier.Apply(__instance); ShopBuyPriceApplier.ApplyInPlace(__instance); ShopBuyPriceState.MarkApplied(__instance); } catch (Exception ex) { ModLog.Warn("MoneyMultiplier", "InitShopItems postfix failed — " + ex.Message); } } } [HarmonyPatch(typeof(MaintenanceRoom), "ApplyLoadedGameData")] public static class MaintenanceRoomApplyLoadedGameDataPatch { [HarmonyPostfix] public static void Postfix(MaintenanceRoom __instance) { try { ShopBuyPriceState.MarkDirty(__instance); ShopBuyPriceApplier.ClearVanillaPrices(__instance); ShopDiscountApplier.Apply(__instance); ShopBuyPriceApplier.ApplyInPlace(__instance); ShopBuyPriceState.MarkApplied(__instance); } catch (Exception ex) { ModLog.Warn("MoneyMultiplier", "ApplyLoadedGameData postfix failed — " + ex.Message); } } } [HarmonyPatch(typeof(MaintenanceRoom), "OnEnterChannel")] public static class MaintenanceRoomOnEnterChannelPatch { [HarmonyPrefix] public static void Prefix(MaintenanceRoom __instance) { try { ShopBuyPriceState.EnsureApplied(__instance); } catch (Exception ex) { ModLog.Warn("MoneyMultiplier", "OnEnterChannel prefix failed — " + ex.Message); } } } [HarmonyPatch(typeof(MaintenanceRoom), "ReinforceItem")] public static class MaintenanceRoomReinforceItemPatch { [HarmonyPrefix] public static void Prefix(MaintenanceRoom __instance, ref int price) { try { price = MoneyMultiplierApplier.ScaleReinforcePrice(__instance, price); } catch (Exception ex) { ModLog.Warn("MoneyMultiplier", "ReinforceItem prefix failed — " + ex.Message); } } } private const string Feature = "MoneyMultiplier"; public static void Apply(Harmony harmony) { GameNetworkApi.GetGameAssembly(); ModConfig.Changed += ShopBuyPriceState.NotifyConfigChanged; HarmonyPatchHelper.PatchApplyResult result = HarmonyPatchHelper.ApplyPatchTypes(harmony, "MoneyMultiplier", HarmonyPatchHelper.GetNestedPatchTypes(typeof(MoneyMultiplierPatches))); LogPatchAudit(harmony); HarmonyPatchHelper.LogPatchSummary("MoneyMultiplier", result); } private static void LogPatchAudit(Harmony harmony) { HarmonyPatchHelper.LogPatchAudit("MoneyMultiplier", harmony, new <>z__ReadOnlyArray<(string, MethodBase)>(new(string, MethodBase)[11] { ("set_Currency/IVroom", AccessTools.PropertySetter(typeof(IVroom), "Currency")), ("RefreshTargetCurrency/GameSessionInfo", AccessTools.Method(typeof(GameSessionInfo), "RefreshTargetCurrency", (Type[])null, (Type[])null)), ("ClampTargetCurrencyToMin/GameSessionInfo", AccessTools.Method(typeof(GameSessionInfo), "ClampTargetCurrencyToMin", (Type[])null, (Type[])null)), ("InitMaintenenceRoom/VRoomManager", AccessTools.Method(typeof(VRoomManager), "InitMaintenenceRoom", (Type[])null, (Type[])null)), ("GetPrice/ItemMasterInfo", AccessTools.Method(typeof(ItemMasterInfo), "GetPrice", (Type[])null, (Type[])null)), ("GetMeanPrice/ItemMasterInfo", AccessTools.Method(typeof(ItemMasterInfo), "GetMeanPrice", (Type[])null, (Type[])null)), ("TryGetShopItemPrice/MaintenanceRoom", AccessTools.Method(typeof(MaintenanceRoom), "TryGetShopItemPrice", (Type[])null, (Type[])null)), ("InitShopItems/MaintenanceRoom", AccessTools.Method(typeof(MaintenanceRoom), "InitShopItems", (Type[])null, (Type[])null)), ("ApplyLoadedGameData/MaintenanceRoom", AccessTools.Method(typeof(MaintenanceRoom), "ApplyLoadedGameData", (Type[])null, (Type[])null)), ("OnEnterChannel/MaintenanceRoom", AccessTools.Method(typeof(MaintenanceRoom), "OnEnterChannel", (Type[])null, (Type[])null)), ("ReinforceItem/MaintenanceRoom", AccessTools.Method(typeof(MaintenanceRoom), "ReinforceItem", (Type[])null, (Type[])null)) })); } } internal static class MoneyMultiplierResolver { internal static bool IsAutoScaleEnabled(MoneyType type) { return type switch { MoneyType.Startup => ModConfig.AutoScaleStartupMoneyByPlayerCount.Value, MoneyType.RoundGoal => ModConfig.AutoScaleRoundGoalMoneyByPlayerCount.Value, MoneyType.ScrapSellValue => ModConfig.AutoScaleScrapSellValueByPlayerCount.Value, MoneyType.ShopBuyPrice => ModConfig.AutoScaleShopBuyPriceByPlayerCount.Value, MoneyType.ShopItems => ModConfig.AutoScaleShopItemsByPlayerCount.Value, MoneyType.ReinforcePrice => ModConfig.AutoScaleReinforcePriceByPlayerCount.Value, _ => false, }; } internal static float GetPerTypeMultiplier(MoneyType type) { return type switch { MoneyType.Startup => ModConfig.StartupMoneyMultiplier.Value, MoneyType.RoundGoal => ModConfig.RoundGoalMoneyMultiplier.Value, MoneyType.ScrapSellValue => ModConfig.ScrapSellValueMultiplier.Value, MoneyType.ShopBuyPrice => ModConfig.ShopBuyPriceMultiplier.Value, MoneyType.ShopItems => ModConfig.ShopItemsMultiplier.Value, MoneyType.ReinforcePrice => ModConfig.ReinforcePriceMultiplier.Value, _ => 1f, }; } internal static float GetPlayerScale(MoneyType type, int playerCount) { return ScalingMath.GetPlayerScale(playerCount, IsAutoScaleEnabled(type)); } internal static float GetEffectiveMultiplier(MoneyType type, int playerCount) { return GetPerTypeMultiplier(type) * GetPlayerScale(type, playerCount); } internal static int ScaleAmount(int vanilla, float multiplier) { return ScalingMath.ScaleCount(vanilla, multiplier); } } internal static class MoneyPlayerCountHelper { internal static int ResolveFromRoom(MaintenanceRoom? room) { return SessionPlayerCountHelper.ResolveFromRoom((IVroom?)(object)room); } internal static int ResolveFromSession(GameSessionInfo? info) { return SessionPlayerCountHelper.ResolveFromSession(info); } internal static int ResolveForItemPrices() { return SessionPlayerCountHelper.ResolveFromSession(); } } internal enum MoneyType { Startup, RoundGoal, ScrapSellValue, ShopBuyPrice, ShopItems, ReinforcePrice } internal static class ShopBuyPriceApplier { private const string Feature = "MoneyMultiplier"; private static readonly FieldInfo PriceForItemsField = AccessTools.Field(typeof(MaintenanceRoom), "_priceForItems") ?? throw new InvalidOperationException("MaintenanceRoom._priceForItems not found"); private static readonly ConditionalWeakTable> VanillaPricesByRoom = new ConditionalWeakTable>(); internal static bool ShouldApplyShopPrices() { if (ModConfig.EnableMoneyMultiplier.Value) { return HostApplyGate.ShouldApplyHostOnlyFeature(); } return false; } internal static void ClearVanillaPrices(MaintenanceRoom room) { if (room != null) { VanillaPricesByRoom.Remove(room); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Dictionary GetVanillaPrices(MaintenanceRoom room) { return VanillaPricesByRoom.GetOrCreateValue(room); } internal static int ScalePrice(MaintenanceRoom room, int vanilla) { if (!ShouldApplyShopPrices() || vanilla <= 0) { return vanilla; } int playerCount = MoneyPlayerCountHelper.ResolveFromRoom(room); float effectiveMultiplier = MoneyMultiplierResolver.GetEffectiveMultiplier(MoneyType.ShopBuyPrice, playerCount); return MoneyMultiplierResolver.ScaleAmount(vanilla, effectiveMultiplier); } internal static void ApplyInPlace(MaintenanceRoom room) { if (!ShouldApplyShopPrices() || !(PriceForItemsField.GetValue(room) is Dictionary { Count: not 0 } dictionary)) { return; } int num = MoneyPlayerCountHelper.ResolveFromRoom(room); float effectiveMultiplier = MoneyMultiplierResolver.GetEffectiveMultiplier(MoneyType.ShopBuyPrice, num); Dictionary vanillaPrices = GetVanillaPrices(room); int num2 = 0; foreach (KeyValuePair item in dictionary) { ShopItemPriceInfo value = item.Value; if (value != null && value.Price > 0) { if (!vanillaPrices.TryGetValue(item.Key, out var value2)) { value2 = value.Price; vanillaPrices[item.Key] = value2; } int num3 = ((effectiveMultiplier == 1f) ? value2 : MoneyMultiplierResolver.ScaleAmount(value2, effectiveMultiplier)); if (value.Price != num3) { value.Price = num3; num2++; } } } if (num2 != 0 || effectiveMultiplier != 1f) { MaintenanceShopPriceSync.SyncVendingMachineLevelObjects(room, dictionary); if (num2 > 0 || ModConfig.EnableDebugLogging.Value) { ModLog.Info("MoneyMultiplier", $"Shop buy prices scaled — {num2}/{dictionary.Count} items " + $"(players={num}, effective={effectiveMultiplier:0.##}×)"); } } } } internal static class ShopBuyPriceState { private sealed class RoomState { internal int AppliedConfigGeneration = -1; } private static int _configGeneration; private static readonly ConditionalWeakTable States = new ConditionalWeakTable(); internal static void NotifyConfigChanged() { Interlocked.Increment(ref _configGeneration); } internal static void MarkDirty(MaintenanceRoom room) { if (room != null) { GetState(room).AppliedConfigGeneration = -1; } } internal static void MarkApplied(MaintenanceRoom room) { if (room != null) { GetState(room).AppliedConfigGeneration = Volatile.Read(in _configGeneration); } } internal static void EnsureApplied(MaintenanceRoom room) { if (room != null) { RoomState state = GetState(room); if (state.AppliedConfigGeneration != _configGeneration) { ShopBuyPriceApplier.ApplyInPlace(room); state.AppliedConfigGeneration = _configGeneration; } } } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static RoomState GetState(MaintenanceRoom room) { return States.GetOrCreateValue(room); } } internal static class ShopDiscountApplier { private const string Feature = "MoneyMultiplier"; private static readonly FieldInfo PriceForItemsField = AccessTools.Field(typeof(MaintenanceRoom), "_priceForItems") ?? throw new InvalidOperationException("MaintenanceRoom._priceForItems not found"); internal static void Apply(MaintenanceRoom room) { if (!MoneyMultiplierApplier.IsEnabled() || ModConfig.ShopDiscountChancePercent.Value <= 0 || !(PriceForItemsField.GetValue(room) is Dictionary { Count: not 0 } dictionary)) { return; } int value = ModConfig.ShopDiscountMinPercent.Value; int num = ModConfig.ShopDiscountMaxPercent.Value; int value2 = ModConfig.ShopDiscountChancePercent.Value; if (num < value) { num = value; } int num2 = 0; foreach (ShopItemPriceInfo value3 in dictionary.Values) { if (value3 == null) { continue; } int basePrice = GetBasePrice(value3.Price, value3.DiscountRate); if (basePrice > 0) { if (!RollDiscount(value2)) { value3.DiscountRate = 0f; value3.Price = basePrice; continue; } int num3 = RollDiscountPercent(value, num); value3.DiscountRate = (float)num3 / 100f; value3.Price = Math.Max(1, (int)Math.Round((float)basePrice * (1f - value3.DiscountRate))); num2++; } } MaintenanceShopPriceSync.SyncVendingMachineLevelObjects(room, dictionary); if (num2 > 0 || ModConfig.EnableDebugLogging.Value) { ModLog.Debug("MoneyMultiplier", $"Shop discounts applied — {num2}/{dictionary.Count} items discounted " + $"(chance={value2}%, range={value}-{num}%)"); } } private static int GetBasePrice(int price, float discountRate) { if (price <= 0) { return 0; } bool flag = ((discountRate <= 0f || discountRate >= 1f) ? true : false); return flag ? price : Math.Max(1, (int)Math.Round((float)price / (1f - discountRate))); } private static bool RollDiscount(int chancePercent) { if (chancePercent < 100) { if (chancePercent > 0) { return SimpleRandUtil.Next(0, 10000) < chancePercent * 100; } return false; } return true; } private static int RollDiscountPercent(int minPercent, int maxPercent) { if (maxPercent > minPercent) { return SimpleRandUtil.Next(minPercent, maxPercent + 1); } return minPercent; } } internal static class StartupMoneyLoadGuard { private static int _depth; internal static bool IsActive => _depth > 0; internal static bool TryEnterForSaveSlot(int saveSlotId) { if (saveSlotId == -1) { return false; } try { if (!MonoSingleton.Instance.IsSaveFileExist(MMSaveGameData.GetSaveFileName(saveSlotId))) { return false; } } catch { return false; } _depth++; return true; } internal static void Exit() { if (_depth > 0) { _depth--; } } } } namespace MimesisPlayerEnhancement.Features.LootMultiplicator { internal static class FixedLootSpawnCoordinator { private sealed class RoomState { private readonly Dictionary _remainingQuotaByMasterId = new Dictionary(); private readonly List _slots = new List(); private readonly Dictionary _spawnDataToRoom = new Dictionary(); internal DungeonRoom Room { get; } internal int SlotCount => _slots.Count; internal bool HasQuotas { get { foreach (int value in _remainingQuotaByMasterId.Values) { if (value > 0) { return true; } } return false; } } internal RoomState(DungeonRoom room) { Room = room; } internal void RegisterSlot(string key, FixedSpawnedActorData data) { _slots.Add(new SpawnSlot(key, data)); _spawnDataToRoom[(SpawnedActorData)(object)data] = Room; } internal bool TryGetRoomForSpawnData(SpawnedActorData data, out DungeonRoom room) { return _spawnDataToRoom.TryGetValue(data, out room); } internal IEnumerable>> GroupSlotsByMasterId() { Dictionary> dictionary = new Dictionary>(); foreach (SpawnSlot slot in _slots) { if (!dictionary.TryGetValue(((SpawnedActorData)slot.Data).MasterID, out var value)) { value = new List(); dictionary[((SpawnedActorData)slot.Data).MasterID] = value; } value.Add(slot); } return dictionary; } internal void SetRemainingQuota(int masterId, int quota) { if (quota <= 0) { _remainingQuotaByMasterId.Remove(masterId); } else { _remainingQuotaByMasterId[masterId] = quota; } } internal bool TryConsumeQuota(int masterId) { if (!_remainingQuotaByMasterId.TryGetValue(masterId, out var value) || value <= 0) { return false; } _remainingQuotaByMasterId[masterId] = value - 1; return true; } internal void RestoreQuota(int masterId) { if (_remainingQuotaByMasterId.TryGetValue(masterId, out var value)) { _remainingQuotaByMasterId[masterId] = value + 1; } } } private readonly struct SpawnSlot { internal string Key { get; } internal FixedSpawnedActorData Data { get; } internal SpawnSlot(string key, FixedSpawnedActorData data) { Key = key; Data = data; } } private readonly struct PendingRespawn { internal DungeonRoom Room { get; } internal SpawnedActorData Data { get; } internal int MasterId { get; } internal float ExecuteAt { get; } internal float NextAttemptAt { get; } internal PendingRespawn(DungeonRoom room, SpawnedActorData data, int masterId, float executeAt) { Room = room; Data = data; MasterId = masterId; ExecuteAt = executeAt; NextAttemptAt = executeAt; } internal PendingRespawn WithNextAttemptAt(float nextAttemptAt) { return new PendingRespawn(Room, Data, MasterId, ExecuteAt, nextAttemptAt); } private PendingRespawn(DungeonRoom room, SpawnedActorData data, int masterId, float executeAt, float nextAttemptAt) { Room = room; Data = data; MasterId = masterId; ExecuteAt = executeAt; NextAttemptAt = nextAttemptAt; } } private const string Feature = "LootMultiplicator"; private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly Type[] SpawnLootingObjectParameterTypes = new Type[9] { typeof(ItemElement), typeof(PosWithRot), typeof(bool), typeof(ReasonOfSpawn), typeof(int), typeof(int), typeof(long), typeof(bool), typeof(bool) }; private static readonly FieldInfo SpawnedActorDatasField = typeof(DungeonRoom).GetField("_spawnedActorDatas", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new InvalidOperationException("DungeonRoom._spawnedActorDatas not found"); private static readonly MethodInfo GetNewItemElementMethod = AccessTools.Method(typeof(IVroom), "GetNewItemElement", new Type[6] { typeof(int), typeof(bool), typeof(int), typeof(int), typeof(int), typeof(int) }, (Type[])null) ?? throw new InvalidOperationException("IVroom.GetNewItemElement not found"); private static readonly MethodInfo ExecuteLootingObjectSpawnMethod = AccessTools.Method(typeof(IVroom), "ExecuteLootingObjectSpawn", new Type[1] { typeof(SpawnedActorData) }, (Type[])null) ?? throw new InvalidOperationException("IVroom.ExecuteLootingObjectSpawn not found"); private static readonly MethodInfo MemberwiseCloneMethod = AccessTools.Method(typeof(object), "MemberwiseClone", (Type[])null, (Type[])null) ?? throw new InvalidOperationException("object.MemberwiseClone not found"); private static readonly MethodInfo SpawnLootingObjectMethod = AccessTools.Method(typeof(IVroom), "SpawnLootingObject", SpawnLootingObjectParameterTypes, (Type[])null) ?? throw new InvalidOperationException("IVroom.SpawnLootingObject not found"); private static readonly FieldInfo CurrentSpawnCountBackingField = AccessToolsField(typeof(SpawnedActorData), "k__BackingField"); private static readonly FieldInfo SpawnDataIndexField = AccessToolsField(typeof(SpawnedActorData), "Index"); private static readonly HashSet AppliedRooms = new HashSet(); private static readonly Dictionary RoomStates = new Dictionary(); private static readonly List PendingRespawns = new List(); internal unsafe static void ApplyAfterInit(DungeonRoom room) { //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0112: Unknown result type (might be due to invalid IL or missing references) //IL_0117: Unknown result type (might be due to invalid IL or missing references) //IL_011a: Unknown result type (might be due to invalid IL or missing references) //IL_01ed: Unknown result type (might be due to invalid IL or missing references) if (!ModConfig.EnableLootMultiplicator.Value || AppliedRooms.Contains(room) || HostApplyGate.IsParticipantClient() || !HostApplyGate.ShouldApplyHostOnlyFeature()) { return; } AppliedRooms.Add(room); if (!(SpawnedActorDatasField.GetValue(room) is IDictionary { Count: not 0 } dictionary)) { return; } int memberCount = ((IVroom)room).GetMemberCount(); RoomState roomState = new RoomState(room); foreach (DictionaryEntry item in dictionary) { object? value = item.Value; FixedSpawnedActorData val = (FixedSpawnedActorData)((value is FixedSpawnedActorData) ? value : null); if (val != null) { MapMarkerType markerType = ((SpawnedActorData)val).MarkerType; if (((object)(*(MapMarkerType*)(&markerType))/*cast due to .constrained prefix*/).Equals((object)(MapMarkerType)1)) { roomState.RegisterSlot((item.Key as string) ?? ((SpawnedActorData)val).Name, val); } } } foreach (KeyValuePair> item2 in roomState.GroupSlotsByMasterId()) { int key = item2.Key; if (key <= 0) { continue; } ItemType itemType = ItemTypeLookup.GetItemType(key); float effectiveMultiplier = LootMultiplierResolver.GetEffectiveMultiplier(LootSource.Map, itemType, memberCount); int count = item2.Value.Count; int num = LootMultiplierResolver.ScaleCount(count, effectiveMultiplier); int num2 = num - count; if (num2 <= 0) { continue; } HashSet hashSet = new HashSet(); FixedSpawnedActorData val2 = null; foreach (SpawnSlot item3 in item2.Value) { hashSet.Add(((SpawnedActorData)item3.Data).Index); if (val2 == null) { val2 = item3.Data; } } if (val2 != null) { List list = MapLootMarkerSpreader.CollectUnusedMarkers(key, hashSet); MapLootMarkerSpreader.ShuffleMarkers(list); int num3 = ActivateUnusedMarkers(room, roomState, dictionary, val2, list, num2); int num4 = num2 - num3; roomState.SetRemainingQuota(key, num4); ModLog.Info("LootMultiplicator", $"Fixed loot scaling — type={itemType}, master={key}, " + $"{effectiveMultiplier:0.##}× (vanilla={count}, target={num}, markers+={num3}, respawnQuota={num4})"); } } if (roomState.HasQuotas || roomState.SlotCount > 0) { RoomStates[room] = roomState; } LootSpawnDataLookup.RebuildIndex(room); } internal unsafe static void OnActorDead(SpawnedActorData spawnData) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) if (!ModConfig.EnableLootMultiplicator.Value || spawnData == null || HostApplyGate.IsParticipantClient() || !HostApplyGate.ShouldApplyHostOnlyFeature()) { return; } FixedSpawnedActorData val = (FixedSpawnedActorData)(object)((spawnData is FixedSpawnedActorData) ? spawnData : null); if (val != null) { MapMarkerType markerType = ((SpawnedActorData)val).MarkerType; if (((object)(*(MapMarkerType*)(&markerType))/*cast due to .constrained prefix*/).Equals((object)(MapMarkerType)1) && TryFindRoomState(spawnData, out RoomState state, out DungeonRoom room) && state.TryConsumeQuota(spawnData.MasterID)) { ScheduleRespawn(room, spawnData, spawnData.MasterID); } } } internal static void ProcessPendingRespawns() { if (PendingRespawns.Count == 0) { return; } float time = Time.time; for (int num = PendingRespawns.Count - 1; num >= 0; num--) { PendingRespawn pending = PendingRespawns[num]; if (!(time < pending.ExecuteAt) && !(time < pending.NextAttemptAt)) { RoomState value; if (pending.Room == null || pending.Data == null) { PendingRespawns.RemoveAt(num); } else if (FixedSpawnProximity.ShouldBlockFixedLootRespawn(pending.Room, pending.Data, throttle: false)) { if (ModConfig.EnableDebugLogging.Value) { ModLog.Debug("LootMultiplicator", $"Fixed loot respawn waiting — master={pending.MasterId}, marker={pending.Data.Index}, " + $"players within {ModConfig.FixedSpawnRespawnMinPlayerDistanceMeters.Value:0.#}m"); } DeferNextAttempt(num, pending, time); } else if (!RoomStates.TryGetValue(pending.Room, out value)) { PendingRespawns.RemoveAt(num); } else { try { if (TrySpawnFixedLoot(pending.Room, pending.Data, isRespawn: true)) { PendingRespawns.RemoveAt(num); } else if (pending.Data.ActorID != 0) { PendingRespawns.RemoveAt(num); } else if (FixedSpawnProximity.ShouldBlockFixedLootRespawn(pending.Room, pending.Data, throttle: false)) { DeferNextAttempt(num, pending, time); } else { DeferNextAttempt(num, pending, time); } } catch (Exception ex) { ModLog.Warn("LootMultiplicator", $"Fixed loot respawn failed — master={pending.MasterId}: {ex.Message}"); DeferNextAttempt(num, pending, time); } } } } } private static int ActivateUnusedMarkers(DungeonRoom room, RoomState state, IDictionary spawnDatas, FixedSpawnedActorData template, List unusedMarkers, int need) { int num = 0; for (int i = 0; i < unusedMarkers.Count; i++) { if (num >= need) { break; } MapMarker_LootingObjectSpawnPoint val = unusedMarkers[i]; string text = ((MapMarker_SpawnPoint)val).Name; if (string.IsNullOrWhiteSpace(text) || spawnDatas.Contains(text)) { text = $"{((MapMarker_SpawnPoint)val).masterID}_{((MapMarker_SpawnPoint)val).ID}"; } if (!spawnDatas.Contains(text)) { FixedSpawnedActorData val2 = CreateSpawnDataFromMarker(template, val); spawnDatas.Add(text, val2); state.RegisterSlot(text, val2); try { ExecuteLootingObjectSpawn(room, (SpawnedActorData)(object)val2); num++; } catch (Exception ex) { spawnDatas.Remove(text); ModLog.Warn("LootMultiplicator", $"Fixed loot marker activation failed — master={((MapMarker_SpawnPoint)val).masterID}, marker={((MapMarker_SpawnPoint)val).ID}: {ex.Message}"); } } } return num; } private static FixedSpawnedActorData CreateSpawnDataFromMarker(FixedSpawnedActorData template, MapMarker_LootingObjectSpawnPoint marker) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Expected O, but got Unknown //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Expected O, but got Unknown //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Expected O, but got Unknown //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Expected O, but got Unknown //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Expected O, but got Unknown //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Expected O, but got Unknown //IL_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_00ee: Expected O, but got Unknown //IL_00ee: Unknown result type (might be due to invalid IL or missing references) //IL_0109: Unknown result type (might be due to invalid IL or missing references) //IL_0118: Expected O, but got Unknown //IL_0118: Unknown result type (might be due to invalid IL or missing references) //IL_013d: Expected O, but got Unknown //IL_013d: Unknown result type (might be due to invalid IL or missing references) //IL_016e: Expected O, but got Unknown //IL_016e: Unknown result type (might be due to invalid IL or missing references) //IL_0193: Expected O, but got Unknown //IL_0193: Unknown result type (might be due to invalid IL or missing references) //IL_01b8: Expected O, but got Unknown //IL_01b8: Unknown result type (might be due to invalid IL or missing references) //IL_01dd: Expected O, but got Unknown //IL_01dd: Unknown result type (might be due to invalid IL or missing references) //IL_0202: Expected O, but got Unknown //IL_0202: Unknown result type (might be due to invalid IL or missing references) //IL_0218: Unknown result type (might be due to invalid IL or missing references) //IL_0227: Expected O, but got Unknown //IL_0228: Expected O, but got Unknown FixedSpawnedActorData val = (FixedSpawnedActorData)MemberwiseCloneMethod.Invoke(template, null); ((SpawnedActorData)val).SetActorID(0); SetField((object)val, CurrentSpawnCountBackingField, 0); SetField((object)val, SpawnDataIndexField, ((MapMarker_SpawnPoint)marker).ID); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "IsIndoor"), ((MapMarker_SpawnPoint)marker).IsIndoor); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "MasterID"), ((MapMarker_SpawnPoint)marker).masterID); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "MaxRespawnCount"), ((MapMarker_SpawnPoint)marker).MaxRespawnCount); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "Name"), ((MapMarker_SpawnPoint)marker).Name); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "Pos"), ((MapMarker_SpawnPoint)marker).pos); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "PosVector"), ((MapMarker_SpawnPoint)marker).pos.pos); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "SpawnWaitTime"), ((MapMarker_SpawnPoint)marker).spawnWaitTime); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "StackCount"), (marker.stackCount <= 0) ? 1 : marker.stackCount); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "Durability"), marker.durability); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "DefaultGauge"), marker.defaultGauge); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "IgnoreNav"), marker.ignoreNav); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "EnableReset"), ((MapMarker_SpawnPoint)marker).enableReset); SetField((object)val, AccessToolsField(typeof(SpawnedActorData), "SpawnType"), ((MapMarker_SpawnPoint)marker).spawnType); return val; } private static void ExecuteLootingObjectSpawn(DungeonRoom room, SpawnedActorData spawnData) { if (room == null) { throw new InvalidOperationException("DungeonRoom is not IVroom"); } ExecuteLootingObjectSpawnMethod.Invoke(room, new object[1] { spawnData }); } private static bool TrySpawnFixedLoot(DungeonRoom room, SpawnedActorData spawnData, bool isRespawn) { if (room == null) { return false; } if (spawnData.ActorID != 0 || spawnData.MasterID <= 0) { return false; } if (isRespawn && FixedSpawnProximity.ShouldBlockFixedLootRespawn(room, spawnData, throttle: false)) { return false; } if (isRespawn) { PrepareSpawnCountForRespawn(spawnData); } if (!ItemTypeLookup.TryGetItem(spawnData.MasterID, out ItemMasterInfo _)) { return false; } int num = ((spawnData.StackCount <= 0) ? 1 : spawnData.StackCount); object? obj = GetNewItemElementMethod.Invoke(room, new object[6] { spawnData.MasterID, false, num, spawnData.Durability, spawnData.DefaultGauge, 0 }); ItemElement val = (ItemElement)((obj is ItemElement) ? obj : null); if (val == null) { return false; } int num2 = (int)SpawnLootingObjectMethod.Invoke(room, new object[9] { val, spawnData.Pos, spawnData.IsIndoor, (object)(ReasonOfSpawn)1, spawnData.Index, 0, 0L, spawnData.IgnoreNav, false }); if (num2 == 0) { return false; } spawnData.SetActorID(num2); return true; } private static void PrepareSpawnCountForRespawn(SpawnedActorData spawnData) { if (spawnData.EnableReset && spawnData.MaxRespawnCount > 0 && spawnData.CurrentSpawnCount > spawnData.MaxRespawnCount) { SetField(spawnData, CurrentSpawnCountBackingField, 0); } } private static void SetField(object target, FieldInfo field, object? value) { field.SetValue(target, value); } private static FieldInfo AccessToolsField(Type type, string name) { return AccessTools.Field(type, name) ?? throw new InvalidOperationException(type.Name + "." + name + " not found"); } private static void ScheduleRespawn(DungeonRoom room, SpawnedActorData spawnData, int masterId) { foreach (PendingRespawn pendingRespawn in PendingRespawns) { if (pendingRespawn.Room == room && pendingRespawn.Data == spawnData) { return; } } float value = ModConfig.FixedSpawnRespawnDelayMinSeconds.Value; float value2 = ModConfig.FixedSpawnRespawnDelayMaxSeconds.Value; float num = ((value >= value2) ? value : Random.Range(value, value2)); long spawnWaitTime = spawnData.SpawnWaitTime; if (spawnWaitTime > 0) { float num2 = (float)spawnWaitTime / 1000f; if (num2 > num) { num = num2; } } PendingRespawns.Add(new PendingRespawn(room, spawnData, masterId, Time.time + num)); if (ModConfig.EnableDebugLogging.Value) { ModLog.Debug("LootMultiplicator", $"Fixed loot respawn scheduled — master={masterId}, marker={spawnData.Index}, delay={num:0.0}s"); } } private static void DeferNextAttempt(int index, PendingRespawn pending, float now) { PendingRespawns[index] = pending.WithNextAttemptAt(now + 1f); } private static bool TryFindRoomState(SpawnedActorData spawnData, out RoomState state, out DungeonRoom room) { foreach (KeyValuePair roomState in RoomStates) { if (roomState.Value.TryGetRoomForSpawnData(spawnData, out room)) { state = roomState.Value; return true; } } state = null; room = null; return false; } } internal static class ItemElementStackHelper { private static readonly MethodInfo? SetConsumableRemainCountMethod = AccessTools.PropertySetter(typeof(ConsumableItemElement), "RemainCount"); internal static int GetStackCount(ItemElement element) { if (element == null) { return 1; } ConsumableItemElement val = (ConsumableItemElement)(object)((element is ConsumableItemElement) ? element : null); if (val != null) { if (val.RemainCount <= 0) { return 1; } return val.RemainCount; } try { ItemInfo val2 = element.toItemInfo(); return (val2.stackCount <= 0) ? 1 : val2.stackCount; } catch { return 1; } } internal static void SetStackCount(ItemElement element, int stackCount) { if (element != null && stackCount > 0) { ConsumableItemElement val = (ConsumableItemElement)(object)((element is ConsumableItemElement) ? element : null); if (val != null && SetConsumableRemainCountMethod != null) { SetConsumableRemainCountMethod.Invoke(val, new object[1] { stackCount }); } } } internal static ItemType GetItemType(ItemElement element) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) if (element != null) { if (element.ItemMasterID <= 0) { return ItemTypeLookup.NormalizeItemType(element.ItemType); } return ItemTypeLookup.GetItemType(element.ItemMasterID); } return (ItemType)2; } } internal static class ItemTypeLookup { private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly PropertyInfo? HubDatamanProperty = typeof(Hub).GetProperty("dataman", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static bool TryGetItem(int masterId, out ItemMasterInfo info) { info = null; if (masterId <= 0 || (Object)(object)Hub.s == (Object)null) { return false; } object? obj = HubDatamanProperty?.GetValue(Hub.s); DataManager val = (DataManager)((obj is DataManager) ? obj : null); if (val == null) { return false; } ItemMasterInfo itemInfo = val.ExcelDataManager.GetItemInfo(masterId); if (itemInfo == null) { return false; } info = itemInfo; return true; } internal static ItemType GetItemType(int masterId) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) if (TryGetItem(masterId, out ItemMasterInfo info)) { return NormalizeItemType(info.ItemType); } return (ItemType)2; } internal static ItemType GetDominantItemType(ImmutableDictionary? candidates) { //IL_0060: Unknown result type (might be due to invalid IL or missing references) if (candidates == null || candidates.Count == 0) { return (ItemType)2; } int num = 0; int num2 = -1; foreach (KeyValuePair candidate in candidates) { int item = candidate.Value.Item1; if (item > num2) { num2 = item; num = candidate.Key; } } if (num <= 0) { return (ItemType)2; } return GetItemType(num); } internal static string GetDisplayName(int masterId, ItemMasterInfo? info = null) { if (info != null || TryGetItem(masterId, out info)) { if (!string.IsNullOrWhiteSpace(info.Name)) { return info.Name; } return masterId.ToString(); } return masterId.ToString(); } internal unsafe static ItemType NormalizeItemType(ItemType itemType) { object obj = (object)(ItemType)0; if (!((object)(*(ItemType*)(&itemType))/*cast due to .constrained prefix*/).Equals(obj)) { object obj2 = (object)(ItemType)1; if (((object)(*(ItemType*)(&itemType))/*cast due to .constrained prefix*/).Equals(obj2)) { return (ItemType)1; } return (ItemType)2; } return (ItemType)0; } } internal static class LootItemCountScaler { internal static bool TryScaleElementStack(IVroom? room, ItemElement element, LootSource source) { if (element == null || element.ItemMasterID <= 0) { return false; } int stackCount = ItemElementStackHelper.GetStackCount(element); int itemCount = stackCount; if (!TryScaleItemCount(room, source, element.ItemMasterID, ref itemCount)) { return false; } if (itemCount == stackCount) { return false; } ItemElementStackHelper.SetStackCount(element, itemCount); return true; } private unsafe static bool TryScaleItemCount(IVroom? room, LootSource source, int masterId, ref int itemCount) { //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) if (!ModConfig.EnableLootMultiplicator.Value || masterId <= 0) { return false; } if (source.Equals(LootSource.Map)) { return false; } if (HostApplyGate.IsParticipantClient() || !HostApplyGate.ShouldApplyHostOnlyFeature()) { return false; } ItemType itemType = ItemTypeLookup.GetItemType(masterId); if (!((object)(*(ItemType*)(&itemType))/*cast due to .constrained prefix*/).Equals((object)(ItemType)0)) { return false; } int playerCount = SessionPlayerCountHelper.ResolveFromRoom(room); float effectiveMultiplier = LootMultiplierResolver.GetEffectiveMultiplier(source, itemType, playerCount); if (effectiveMultiplier <= 1f) { return false; } int num = LootMultiplierResolver.ScaleCount((itemCount <= 0) ? 1 : itemCount, effectiveMultiplier); if (num == itemCount) { return false; } itemCount = num; return true; } } internal static class LootMultiplicatorApplier { private const string Feature = "LootMultiplicator"; private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo SpawnedActorDatasField = AccessToolsField(typeof(DungeonRoom), "_spawnedActorDatas"); private static readonly HashSet AppliedRooms = new HashSet(); private static readonly HashSet SkippedClientRooms = new HashSet(); internal static bool IsApplied(DungeonRoom room) { return AppliedRooms.Contains(room); } internal static void EnsureApplied(DungeonRoom room) { if (AppliedRooms.Contains(room)) { return; } if (HostApplyGate.IsParticipantClient()) { if (SkippedClientRooms.Add(room)) { ModLog.Debug("LootMultiplicator", "Loot scaling skipped — participant client"); } } else if (!HostApplyGate.ShouldApplyHostOnlyFeature()) { ModLog.Debug("LootMultiplicator", "Loot scaling deferred — waiting for host session"); } else { Apply(room); AppliedRooms.Add(room); FixedLootSpawnCoordinator.ApplyAfterInit(room); } } internal static void Apply(DungeonRoom room) { if (!ModConfig.EnableLootMultiplicator.Value) { ModLog.Debug("LootMultiplicator", "Loot scaling skipped — EnableLootMultiplicator is off"); return; } if (!HostApplyGate.ShouldApplyHostOnlyFeature()) { ModLog.Debug("LootMultiplicator", "Loot scaling skipped — not host"); return; } int memberCount = ((IVroom)room).GetMemberCount(); LootMultiplicatorLog.InfoScalingApplied(memberCount); int num = ScaleSpawnPointDatas(room, memberCount); ModLog.Info("LootMultiplicator", $"Map loot spawn data updated — scaledSlots={num}, players={memberCount}"); } private static int ScaleSpawnPointDatas(DungeonRoom room, int playerCount) { if (!(SpawnedActorDatasField.GetValue(room) is IDictionary dictionary)) { return 0; } int num = 0; foreach (DictionaryEntry item in dictionary) { if (item.Value != null) { num += (ScaleLootSpawnData(item.Value, playerCount) ? 1 : 0); } } return num; } private static bool ScaleLootSpawnData(object spawnData, int playerCount) { RandomSpawnedItemActorData val = (RandomSpawnedItemActorData)((spawnData is RandomSpawnedItemActorData) ? spawnData : null); if (val == null) { FixedSpawnedActorData val2 = (FixedSpawnedActorData)((spawnData is FixedSpawnedActorData) ? spawnData : null); if (val2 != null && IsLootFixedSpawn(val2)) { return ScaleFixedLootSpawnData(val2, playerCount); } return false; } return ScaleRandomLootSpawnData(val, playerCount); } private unsafe static bool IsLootFixedSpawn(FixedSpawnedActorData spawnData) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) MapMarkerType markerType = ((SpawnedActorData)spawnData).MarkerType; return ((object)(*(MapMarkerType*)(&markerType))/*cast due to .constrained prefix*/).Equals((object)(MapMarkerType)1); } private static bool ScaleRandomLootSpawnData(RandomSpawnedItemActorData spawnData, int playerCount) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) ItemType dominantItemType = ItemTypeLookup.GetDominantItemType(spawnData.Candidates); float effectiveMultiplier = LootMultiplierResolver.GetEffectiveMultiplier(LootSource.Map, dominantItemType, playerCount); return ScaleCommonSpawnFields(spawnData, ((SpawnedActorData)spawnData).MasterID, dominantItemType, effectiveMultiplier, "randomLoot", scaleStackCount: false); } private static bool ScaleFixedLootSpawnData(FixedSpawnedActorData spawnData, int playerCount) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) ItemType itemType = ItemTypeLookup.GetItemType(((SpawnedActorData)spawnData).MasterID); float effectiveMultiplier = LootMultiplierResolver.GetEffectiveMultiplier(LootSource.Map, itemType, playerCount); return ScaleCommonSpawnFields(spawnData, ((SpawnedActorData)spawnData).MasterID, itemType, effectiveMultiplier, "fixedLoot", scaleStackCount: false); } private static bool ScaleCommonSpawnFields(object spawnData, int masterId, ItemType itemType, float multiplier, string context, bool scaleStackCount) { //IL_00e2: Unknown result type (might be due to invalid IL or missing references) bool flag = false; int num = 0; int num2 = 0; bool flag2 = false; FieldInfo field = ReflectionFieldCache.GetField(spawnData, "StackCount"); if (scaleStackCount && field != null) { num = (int)(field.GetValue(spawnData) ?? ((object)0)); num2 = LootMultiplierResolver.ScaleCountWithImplicitBase(num, multiplier, 1); field.SetValue(spawnData, num2); flag2 = true; LootMultiplicatorLog.DebugFieldScaled($"{context}[{masterId}].stackCount", num, num2, multiplier); flag = true; } FieldInfo field2 = ReflectionFieldCache.GetField(spawnData, "MaxRespawnCount"); if (field2 != null) { int num3 = (int)(field2.GetValue(spawnData) ?? ((object)0)); if (num3 > 0) { int num4 = LootMultiplierResolver.ScaleCount(num3, multiplier); field2.SetValue(spawnData, num4); LootMultiplicatorLog.DebugFieldScaled($"{context}[{masterId}].maxRespawn", num3, num4, multiplier); flag = true; } } if (flag && flag2) { LootMultiplicatorLog.DebugMapSlotConfigured(LootSource.Map, itemType, masterId, num, num2, multiplier, context); } return flag; } private static FieldInfo AccessToolsField(Type type, string name) { return type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new InvalidOperationException(type.Name + "." + name + " not found"); } } internal static class LootMultiplicatorLog { private const string Feature = "LootMultiplicator"; internal static void InfoScalingApplied(int playerCount) { float num = ((playerCount > 4) ? ((float)playerCount / 4f) : 1f); ModLog.Info("LootMultiplicator", $"Loot scaling applied — players={playerCount} (shared playerScale={num:0.##}× when auto enabled), " + $"mapConsumable={ModConfig.MapConsumableLootMultiplier.Value:0.##}× auto={ModConfig.AutoScaleMapConsumableLootByPlayerCount.Value}, " + $"mapEquipment={ModConfig.MapEquipmentLootMultiplier.Value:0.##}× auto={ModConfig.AutoScaleMapEquipmentLootByPlayerCount.Value}, " + $"mapMiscellany={ModConfig.MapMiscellanyLootMultiplier.Value:0.##}× auto={ModConfig.AutoScaleMapMiscellanyLootByPlayerCount.Value}, " + $"dropConsumable={ModConfig.DropConsumableLootMultiplier.Value:0.##}× auto={ModConfig.AutoScaleDropConsumableLootByPlayerCount.Value}, " + $"dropEquipment={ModConfig.DropEquipmentLootMultiplier.Value:0.##}× auto={ModConfig.AutoScaleDropEquipmentLootByPlayerCount.Value}, " + $"dropMiscellany={ModConfig.DropMiscellanyLootMultiplier.Value:0.##}× auto={ModConfig.AutoScaleDropMiscellanyLootByPlayerCount.Value}, " + $"triggerConsumable={ModConfig.TriggerConsumableLootMultiplier.Value:0.##}× auto={ModConfig.AutoScaleTriggerConsumableLootByPlayerCount.Value}, " + $"triggerEquipment={ModConfig.TriggerEquipmentLootMultiplier.Value:0.##}× auto={ModConfig.AutoScaleTriggerEquipmentLootByPlayerCount.Value}, " + $"triggerMiscellany={ModConfig.TriggerMiscellanyLootMultiplier.Value:0.##}× auto={ModConfig.AutoScaleTriggerMiscellanyLootByPlayerCount.Value}"); } internal static void InfoRuntimeScaled(LootSource source, ItemType itemType, int masterId, int before, int after, float multiplier, string context) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) string displayName = ItemTypeLookup.GetDisplayName(masterId); ModLog.Info("LootMultiplicator", $"{context} — source={source}, type={itemType}, item={displayName}, master={masterId}, " + $"{multiplier:0.##}× (count {before} -> {after})"); } internal static void InfoDropTableScaled(int entriesChanged, int entriesAdded, int playerCount) { ModLog.Info("LootMultiplicator", $"Drop table scaled — entriesChanged={entriesChanged}, entriesAdded={entriesAdded}, players={playerCount}"); } internal static void DebugFieldScaled(string label, int before, int after, float multiplier) { if (ModConfig.EnableDebugLogging.Value) { if (before == after) { ModLog.Debug("LootMultiplicator", $"{label} unchanged at {before} ({multiplier:0.##}×)"); return; } ModLog.Debug("LootMultiplicator", $"{label} scaled {before} -> {after} ({multiplier:0.##}×)"); } } internal static void DebugLootScaled(LootSource source, ItemType itemType, int masterId, int before, int after, float multiplier, string context) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) if (ModConfig.EnableDebugLogging.Value) { string displayName = ItemTypeLookup.GetDisplayName(masterId); ModLog.Debug("LootMultiplicator", $"{context} — source={source}, type={itemType}, item={displayName}, master={masterId}, " + $"{multiplier:0.##}× (count {before} -> {after})"); } } internal static void DebugMapSlotConfigured(LootSource source, ItemType itemType, int masterId, int stackBefore, int stackAfter, float multiplier, string context) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) if (ModConfig.EnableDebugLogging.Value) { string displayName = ItemTypeLookup.GetDisplayName(masterId); ModLog.Debug("LootMultiplicator", $"Map loot slot configured — {context}, source={source}, type={itemType}, item={displayName}, master={masterId}, " + $"{multiplier:0.##}× (stack {stackBefore} -> {stackAfter})"); } } internal static void DebugLootSpawned(DungeonRoom? room, int masterId, string itemName, ItemType itemType, LootSource source, float effectiveMultiplier, bool scalingApplied, Vector3 position, bool isIndoor, ReasonOfSpawn reason, int stackCount, string spawnSource) { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) if (ModConfig.EnableDebugLogging.Value) { ModLog.Debug("LootMultiplicator", $"Loot spawned — source={source}, type={itemType}, item={itemName}, master={masterId}, stack={stackCount}, " + $"multiplier={effectiveMultiplier:0.##}×, budgetsScaled={scalingApplied}, " + $"pos={SpawnScalingLog.FormatLocation(room, position)}, indoor={isIndoor}, reason={reason}, via={spawnSource}"); } } internal static void DebugLootSpawnFailed(int masterId, string itemName, ItemType itemType, LootSource source, bool scalingApplied, ReasonOfSpawn reason, string spawnSource) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) if (ModConfig.EnableDebugLogging.Value) { ModLog.Debug("LootMultiplicator", $"Loot spawn failed — source={source}, type={itemType}, item={itemName}, master={masterId}, " + $"budgetsScaled={scalingApplied}, reason={reason}, via={spawnSource}"); } } internal static void DebugExecuteLootSpawn(DungeonRoom room, SpawnedActorData spawnData, ItemType itemType, float multiplier, bool scalingApplied) { //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) if (ModConfig.EnableDebugLogging.Value) { int num = ExtractMasterId(spawnData); int num2 = ExtractStackCount(spawnData); string text = ((num > 0) ? ItemTypeLookup.GetDisplayName(num) : spawnData.Name); ModLog.Debug("LootMultiplicator", $"ExecuteLootingObjectSpawn — type={itemType}, item={text}, master={num}, stack={num2}, " + $"multiplier={multiplier:0.##}×, budgetsScaled={scalingApplied}, pos={SpawnScalingLog.FormatLocation(room, spawnData.PosVector)}"); } } private static int ExtractMasterId(SpawnedActorData spawnData) { return ((spawnData is FixedSpawnedActorData) ? spawnData : null)?.MasterID ?? ((spawnData is RandomSpawnedItemActorData) ? spawnData : null)?.MasterID ?? 0; } private static int ExtractStackCount(SpawnedActorData spawnData) { return ((spawnData is FixedSpawnedActorData) ? spawnData : null)?.StackCount ?? ((spawnData is RandomSpawnedItemActorData) ? spawnData : null)?.StackCount ?? 0; } } public static class LootMultiplicatorPatches { [HarmonyPatch(typeof(DungeonRoom), "InitSpawn")] public static class DungeonRoomInitSpawnPatch { [HarmonyPostfix] public static void Postfix(DungeonRoom __instance) { try { LootMultiplicatorApplier.EnsureApplied(__instance); FixedLootSpawnCoordinator.ApplyAfterInit(__instance); } catch (Exception ex) { ModLog.Warn("LootMultiplicator", "InitSpawn postfix failed — " + ex.Message); } } } [HarmonyPatch] public static class IVroomExecuteLootingObjectSpawnPatch { public static MethodBase? TargetMethod() { return ResolveExecuteLootingObjectSpawnMethod(); } [HarmonyPrefix] public static void Prefix(IVroom __instance, SpawnedActorData spawnedActorData) { if (!ModConfig.EnableLootMultiplicator.Value) { return; } DungeonRoom val = (DungeonRoom)(object)((__instance is DungeonRoom) ? __instance : null); if (val == null) { return; } try { LootMultiplicatorApplier.EnsureApplied(val); if (IsMapFixedLootSpawn(spawnedActorData)) { MapLootSpawnContext.Enter(); } } catch (Exception ex) { ModLog.Warn("LootMultiplicator", "ExecuteLootingObjectSpawn prefix failed — " + ex.Message); } } [HarmonyPostfix] public static void Postfix(IVroom __instance, SpawnedActorData spawnedActorData) { //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) if (IsMapFixedLootSpawn(spawnedActorData)) { MapLootSpawnContext.Exit(); } if (!ModConfig.EnableDebugLogging.Value || !ModConfig.EnableLootMultiplicator.Value) { return; } DungeonRoom val = (DungeonRoom)(object)((__instance is DungeonRoom) ? __instance : null); if (val == null || spawnedActorData == null || !IsLootSpawnData(spawnedActorData, out var itemType, out var masterId)) { return; } try { int memberCount = ((IVroom)val).GetMemberCount(); float effectiveMultiplier = LootMultiplierResolver.GetEffectiveMultiplier(LootSource.Map, itemType, memberCount); bool scalingApplied = LootMultiplicatorApplier.IsApplied(val); string itemName = ((masterId > 0) ? ItemTypeLookup.GetDisplayName(masterId) : spawnedActorData.Name); int stackCount = ExtractStackCount(spawnedActorData); LootMultiplicatorLog.DebugExecuteLootSpawn(val, spawnedActorData, itemType, effectiveMultiplier, scalingApplied); LootMultiplicatorLog.DebugLootSpawned(val, masterId, itemName, itemType, LootSource.Map, effectiveMultiplier, scalingApplied, spawnedActorData.PosVector, spawnedActorData.IsIndoor, (ReasonOfSpawn)1, stackCount, "ExecuteLootingObjectSpawn"); } catch (Exception ex) { ModLog.Warn("LootMultiplicator", "ExecuteLootingObjectSpawn postfix logging failed — " + ex.Message); } } } [HarmonyPatch] public static class IVroomSpawnLootingObjectPatch { public static MethodBase? TargetMethod() { return ResolveSpawnLootingObjectMethod(); } [HarmonyPrefix] public static bool Prefix(IVroom __instance, ItemElement element, ReasonOfSpawn reasonOfSpawn, int spawnPointIndex, bool isRestored, ref int __result) { //IL_0052: Unknown result type (might be due to invalid IL or missing references) if (!ModConfig.EnableLootMultiplicator.Value) { return true; } try { if (!LootSpawnScalingContext.IsDuplicating) { DungeonRoom val = (DungeonRoom)(object)((__instance is DungeonRoom) ? __instance : null); if (val != null && spawnPointIndex != 0 && LootSpawnDataLookup.TryFindByMarkerIndex(val, spawnPointIndex, out SpawnedActorData spawnData) && FixedSpawnProximity.ShouldBlockFixedLootRespawn(val, spawnData)) { __result = 0; return false; } } DungeonRoom val2 = (DungeonRoom)(object)((__instance is DungeonRoom) ? __instance : null); if (val2 != null) { LootMultiplicatorApplier.EnsureApplied(val2); } RuntimeLootScaler.ScaleSpawnedItem(__instance, element, reasonOfSpawn, spawnPointIndex, isRestored); } catch (Exception ex) { ModLog.Warn("LootMultiplicator", "SpawnLootingObject prefix failed — " + ex.Message); } return true; } [HarmonyPostfix] public static void Postfix(IVroom __instance, ItemElement element, PosWithRot pos, bool isIndoor, ReasonOfSpawn reasonOfSpawn, int spawnPointIndex, int prevProjectileActorID, long projectileDropTime, bool ignoreNav, bool isRestored, int __result) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: 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) //IL_00c0: Unknown result type (might be due to invalid IL or missing references) if (__result > 0) { try { LootPileDuplicator.TrySpawnExtraPiles(__instance, element, pos, isIndoor, reasonOfSpawn, spawnPointIndex, prevProjectileActorID, projectileDropTime, ignoreNav, isRestored); } catch (Exception ex) { ModLog.Warn("LootMultiplicator", "SpawnLootingObject pile duplication failed — " + ex.Message); } } if (ModConfig.EnableDebugLogging.Value && ModConfig.EnableLootMultiplicator.Value && element != null && RuntimeLootScaler.TryMapReasonToSource(reasonOfSpawn, out var source)) { DungeonRoom val = (DungeonRoom)(object)((__instance is DungeonRoom) ? __instance : null); ItemType itemType = ItemElementStackHelper.GetItemType(element); int itemMasterID = element.ItemMasterID; string displayName = ItemTypeLookup.GetDisplayName(itemMasterID); int playerCount = SessionPlayerCountHelper.ResolveFromRoom(__instance); float effectiveMultiplier = LootMultiplierResolver.GetEffectiveMultiplier(source, itemType, playerCount); bool scalingApplied = val != null && LootMultiplicatorApplier.IsApplied(val); int stackCount = ItemElementStackHelper.GetStackCount(element); if (__result > 0) { LootMultiplicatorLog.DebugLootSpawned(val, itemMasterID, displayName, itemType, source, effectiveMultiplier, scalingApplied, pos.pos, isIndoor, reasonOfSpawn, stackCount, "SpawnLootingObject"); } else { LootMultiplicatorLog.DebugLootSpawnFailed(itemMasterID, displayName, itemType, source, scalingApplied, reasonOfSpawn, "SpawnLootingObject"); } } } } [HarmonyPatch(typeof(SpawnedActorData), "OnActorDead")] public static class SpawnedActorDataOnActorDeadPatch { [HarmonyPostfix] public static void Postfix(SpawnedActorData __instance) { try { FixedLootSpawnCoordinator.OnActorDead(__instance); } catch (Exception ex) { ModLog.Warn("LootMultiplicator", "OnActorDead fixed loot scaling failed — " + ex.Message); } } } private const string Feature = "LootMultiplicator"; private static readonly Type[] SpawnLootingObjectParameterTypes = new Type[9] { typeof(ItemElement), typeof(PosWithRot), typeof(bool), typeof(ReasonOfSpawn), typeof(int), typeof(int), typeof(long), typeof(bool), typeof(bool) }; public static void Apply(Harmony harmony) { GameNetworkApi.GetGameAssembly(); HarmonyPatchHelper.PatchApplyResult result = HarmonyPatchHelper.ApplyPatchTypes(harmony, "LootMultiplicator", HarmonyPatchHelper.GetNestedPatchTypes(typeof(LootMultiplicatorPatches))); LogPatchAudit(harmony); HarmonyPatchHelper.LogPatchSummary("LootMultiplicator", result); } private static MethodBase? ResolveSpawnLootingObjectMethod() { return AccessTools.Method(typeof(IVroom), "SpawnLootingObject", SpawnLootingObjectParameterTypes, (Type[])null); } private static MethodBase? ResolveExecuteLootingObjectSpawnMethod() { return AccessTools.Method(typeof(IVroom), "ExecuteLootingObjectSpawn", new Type[1] { typeof(SpawnedActorData) }, (Type[])null); } private static void LogPatchAudit(Harmony harmony) { HarmonyPatchHelper.LogPatchAudit("LootMultiplicator", harmony, new <>z__ReadOnlyArray<(string, MethodBase)>(new(string, MethodBase)[4] { ("InitSpawn/DungeonRoom", AccessTools.Method(typeof(DungeonRoom), "InitSpawn", (Type[])null, (Type[])null)), ("ExecuteLootingObjectSpawn/IVroom", ResolveExecuteLootingObjectSpawnMethod()), ("SpawnLootingObject/IVroom", ResolveSpawnLootingObjectMethod()), ("OnActorDead/SpawnedActorData", AccessTools.Method(typeof(SpawnedActorData), "OnActorDead", (Type[])null, (Type[])null)) })); } private unsafe static bool IsMapFixedLootSpawn(SpawnedActorData? spawnData) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) FixedSpawnedActorData val = (FixedSpawnedActorData)(object)((spawnData is FixedSpawnedActorData) ? spawnData : null); if (val != null) { MapMarkerType markerType = ((SpawnedActorData)val).MarkerType; if (((object)(*(MapMarkerType*)(&markerType))/*cast due to .constrained prefix*/).Equals((object)(MapMarkerType)1)) { return ((SpawnedActorData)val).MasterID > 0; } } return false; } private unsafe static bool IsLootSpawnData(SpawnedActorData spawnData, out ItemType itemType, out int masterId) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Expected I4, but got Unknown //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Expected I4, but got Unknown RandomSpawnedItemActorData val = (RandomSpawnedItemActorData)(object)((spawnData is RandomSpawnedItemActorData) ? spawnData : null); if (val == null) { FixedSpawnedActorData val2 = (FixedSpawnedActorData)(object)((spawnData is FixedSpawnedActorData) ? spawnData : null); if (val2 != null) { FixedSpawnedActorData val3 = val2; MapMarkerType markerType = ((SpawnedActorData)val3).MarkerType; if (((object)(*(MapMarkerType*)(&markerType))/*cast due to .constrained prefix*/).Equals((object)(MapMarkerType)1) && ItemTypeLookup.TryGetItem(((SpawnedActorData)val3).MasterID, out ItemMasterInfo _)) { itemType = (ItemType)(int)ItemTypeLookup.GetItemType(((SpawnedActorData)val3).MasterID); masterId = ((SpawnedActorData)val3).MasterID; return true; } } itemType = (ItemType)0; masterId = 0; return false; } RandomSpawnedItemActorData val4 = val; itemType = (ItemType)(int)ItemTypeLookup.GetDominantItemType(val4.Candidates); masterId = ((SpawnedActorData)val4).MasterID; return true; } private static int ExtractStackCount(SpawnedActorData spawnData) { return ((spawnData is FixedSpawnedActorData) ? spawnData : null)?.StackCount ?? ((spawnData is RandomSpawnedItemActorData) ? spawnData : null)?.StackCount ?? 0; } } internal static class LootMultiplierResolver { internal unsafe static bool IsAutoScaleEnabled(LootSource source, ItemType itemType) { //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_003c: 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_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_0113: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Unknown result type (might be due to invalid IL or missing references) //IL_013a: Unknown result type (might be due to invalid IL or missing references) //IL_013c: Unknown result type (might be due to invalid IL or missing references) (LootSource, ItemType) tuple = (source, ItemTypeLookup.NormalizeItemType(itemType)); switch (tuple.Item1) { case LootSource.Map: { ItemType item = tuple.Item2; ItemType val3 = item; if (((object)(*(ItemType*)(&val3))/*cast due to .constrained prefix*/).Equals((object)(ItemType)0)) { return ModConfig.AutoScaleMapConsumableLootByPlayerCount.Value; } ItemType val4 = item; if (((object)(*(ItemType*)(&val4))/*cast due to .constrained prefix*/).Equals((object)(ItemType)1)) { return ModConfig.AutoScaleMapEquipmentLootByPlayerCount.Value; } return ModConfig.AutoScaleMapMiscellanyLootByPlayerCount.Value; } case LootSource.Drop: { ItemType item = tuple.Item2; ItemType val5 = item; if (((object)(*(ItemType*)(&val5))/*cast due to .constrained prefix*/).Equals((object)(ItemType)0)) { return ModConfig.AutoScaleDropConsumableLootByPlayerCount.Value; } ItemType val6 = item; if (((object)(*(ItemType*)(&val6))/*cast due to .constrained prefix*/).Equals((object)(ItemType)1)) { return ModConfig.AutoScaleDropEquipmentLootByPlayerCount.Value; } return ModConfig.AutoScaleDropMiscellanyLootByPlayerCount.Value; } case LootSource.Trigger: { ItemType item = tuple.Item2; ItemType val = item; if (((object)(*(ItemType*)(&val))/*cast due to .constrained prefix*/).Equals((object)(ItemType)0)) { return ModConfig.AutoScaleTriggerConsumableLootByPlayerCount.Value; } ItemType val2 = item; if (((object)(*(ItemType*)(&val2))/*cast due to .constrained prefix*/).Equals((object)(ItemType)1)) { return ModConfig.AutoScaleTriggerEquipmentLootByPlayerCount.Value; } break; } } return ModConfig.AutoScaleTriggerMiscellanyLootByPlayerCount.Value; } internal static float GetPlayerScale(LootSource source, ItemType itemType, int playerCount) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) return ScalingMath.GetPlayerScale(playerCount, IsAutoScaleEnabled(source, itemType)); } internal unsafe static float GetBaseMultiplier(LootSource source, ItemType itemType) { //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_003c: 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_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_0113: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Unknown result type (might be due to invalid IL or missing references) //IL_013a: Unknown result type (might be due to invalid IL or missing references) //IL_013c: Unknown result type (might be due to invalid IL or missing references) (LootSource, ItemType) tuple = (source, ItemTypeLookup.NormalizeItemType(itemType)); switch (tuple.Item1) { case LootSource.Map: { ItemType item = tuple.Item2; ItemType val3 = item; if (((object)(*(ItemType*)(&val3))/*cast due to .constrained prefix*/).Equals((object)(ItemType)0)) { return ModConfig.MapConsumableLootMultiplier.Value; } ItemType val4 = item; if (((object)(*(ItemType*)(&val4))/*cast due to .constrained prefix*/).Equals((object)(ItemType)1)) { return ModConfig.MapEquipmentLootMultiplier.Value; } return ModConfig.MapMiscellanyLootMultiplier.Value; } case LootSource.Drop: { ItemType item = tuple.Item2; ItemType val5 = item; if (((object)(*(ItemType*)(&val5))/*cast due to .constrained prefix*/).Equals((object)(ItemType)0)) { return ModConfig.DropConsumableLootMultiplier.Value; } ItemType val6 = item; if (((object)(*(ItemType*)(&val6))/*cast due to .constrained prefix*/).Equals((object)(ItemType)1)) { return ModConfig.DropEquipmentLootMultiplier.Value; } return ModConfig.DropMiscellanyLootMultiplier.Value; } case LootSource.Trigger: { ItemType item = tuple.Item2; ItemType val = item; if (((object)(*(ItemType*)(&val))/*cast due to .constrained prefix*/).Equals((object)(ItemType)0)) { return ModConfig.TriggerConsumableLootMultiplier.Value; } ItemType val2 = item; if (((object)(*(ItemType*)(&val2))/*cast due to .constrained prefix*/).Equals((object)(ItemType)1)) { return ModConfig.TriggerEquipmentLootMultiplier.Value; } break; } } return ModConfig.TriggerMiscellanyLootMultiplier.Value; } internal static float GetEffectiveMultiplier(LootSource source, ItemType itemType, int playerCount) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) return GetBaseMultiplier(source, itemType) * GetPlayerScale(source, itemType, playerCount); } internal static float GetEffectiveMultiplier(LootSource source, int masterId, int playerCount) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) return GetEffectiveMultiplier(source, ItemTypeLookup.GetItemType(masterId), playerCount); } internal static int ScaleCount(int vanilla, float multiplier) { return ScalingMath.ScaleCount(vanilla, multiplier); } internal static int ScaleCountWithImplicitBase(int vanilla, float multiplier, int implicitWhenZero) { return ScalingMath.ScaleCountWithImplicitBase(vanilla, multiplier, implicitWhenZero); } } internal static class LootPileDuplicator { internal unsafe static void TrySpawnExtraPiles(IVroom vroom, ItemElement template, PosWithRot pos, bool isIndoor, ReasonOfSpawn reasonOfSpawn, int spawnPointIndex, int prevProjectileActorId, long projectileDropTime, bool ignoreNav, bool isRestored) { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_00d5: Unknown result type (might be due to invalid IL or missing references) //IL_00fb: Unknown result type (might be due to invalid IL or missing references) //IL_010b: Unknown result type (might be due to invalid IL or missing references) if (!ModConfig.EnableLootMultiplicator.Value || template == null || isRestored || LootSpawnScalingContext.IsDuplicating || HostApplyGate.IsParticipantClient() || !HostApplyGate.ShouldApplyHostOnlyFeature() || !LootSourceResolver.ShouldScaleSpawn(reasonOfSpawn, isRestored) || !LootSourceResolver.TryResolveLootSource(reasonOfSpawn, spawnPointIndex, out var source)) { return; } if (source.Equals(LootSource.Map)) { if (!MapLootSpawnContext.IsActive) { MapLootMarkerSpreader.TrySpreadToUnusedMarkers(vroom, template, reasonOfSpawn, prevProjectileActorId, projectileDropTime, ignoreNav, isRestored); } return; } ItemType itemType = ItemElementStackHelper.GetItemType(template); if (((object)(*(ItemType*)(&itemType))/*cast due to .constrained prefix*/).Equals((object)(ItemType)0)) { return; } int playerCount = SessionPlayerCountHelper.ResolveFromRoom(vroom); float effectiveMultiplier = LootMultiplierResolver.GetEffectiveMultiplier(source, itemType, playerCount); int num = LootMultiplierResolver.ScaleCount(1, effectiveMultiplier); int num2 = num - 1; if (num2 <= 0) { return; } LootSpawnScalingContext.BeginDuplicating(); try { for (int i = 0; i < num2; i++) { ItemElement val = CreateSpawnCopy(vroom, template); if (val != null) { vroom.SpawnLootingObject(val, pos, isIndoor, reasonOfSpawn, spawnPointIndex, prevProjectileActorId, projectileDropTime, ignoreNav, false); } } } finally { LootSpawnScalingContext.EndDuplicating(); } LootMultiplicatorLog.InfoRuntimeScaled(source, itemType, template.ItemMasterID, 1, num, effectiveMultiplier, $"SpawnLootingObject/{reasonOfSpawn}/piles"); } private static ItemElement? CreateSpawnCopy(IVroom vroom, ItemElement template) { try { ItemInfo val = template.toItemInfo(); return vroom.GetNewItemElement(val.itemMasterID, val.isFake, 1, val.durability, val.remainGauge, val.price); } catch { return null; } } } internal enum LootSource { Map, Drop, Trigger } internal static class LootSourceResolver { internal static bool ShouldScaleSpawn(ReasonOfSpawn reasonOfSpawn, bool isRestored) { //IL_0003: Unknown result type (might be due to invalid IL or missing references) LootSource source; if (!isRestored) { return TryResolveLootSource(reasonOfSpawn, 0, out source); } return false; } internal unsafe static bool TryResolveLootSource(ReasonOfSpawn reasonOfSpawn, int spawnPointIndex, out LootSource source) { if (MapLootSpawnContext.IsActive) { source = LootSource.Map; return true; } if (spawnPointIndex != 0) { source = LootSource.Map; return true; } object obj = (object)(ReasonOfSpawn)1; if (!((object)(*(ReasonOfSpawn*)(&reasonOfSpawn))/*cast due to .constrained prefix*/).Equals(obj)) { object obj2 = (object)(ReasonOfSpawn)9; if (!((object)(*(ReasonOfSpawn*)(&reasonOfSpawn))/*cast due to .constrained prefix*/).Equals(obj2)) { object obj3 = (object)(ReasonOfSpawn)4; if (((object)(*(ReasonOfSpawn*)(&reasonOfSpawn))/*cast due to .constrained prefix*/).Equals(obj3)) { source = LootSource.Drop; return true; } object obj4 = (object)(ReasonOfSpawn)5; if (!((object)(*(ReasonOfSpawn*)(&reasonOfSpawn))/*cast due to .constrained prefix*/).Equals(obj4)) { object obj5 = (object)(ReasonOfSpawn)11; if (!((object)(*(ReasonOfSpawn*)(&reasonOfSpawn))/*cast due to .constrained prefix*/).Equals(obj5)) { object obj6 = (object)(ReasonOfSpawn)12; if (!((object)(*(ReasonOfSpawn*)(&reasonOfSpawn))/*cast due to .constrained prefix*/).Equals(obj6)) { object obj7 = (object)(ReasonOfSpawn)6; if (!((object)(*(ReasonOfSpawn*)(&reasonOfSpawn))/*cast due to .constrained prefix*/).Equals(obj7)) { source = LootSource.Map; return false; } } } } source = LootSource.Trigger; return true; } } source = LootSource.Map; return true; } } internal static class LootSpawnDataLookup { private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo SpawnedActorDatasField = typeof(DungeonRoom).GetField("_spawnedActorDatas", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new InvalidOperationException("DungeonRoom._spawnedActorDatas not found"); private static readonly Dictionary> IndexByRoom = new Dictionary>(); internal static void RebuildIndex(DungeonRoom room) { Dictionary dictionary = new Dictionary(); if (SpawnedActorDatasField.GetValue(room) is IDictionary dictionary2) { foreach (DictionaryEntry item in dictionary2) { object? value = item.Value; SpawnedActorData val = (SpawnedActorData)((value is SpawnedActorData) ? value : null); if (val != null && val.Index != 0) { dictionary[val.Index] = val; } } } IndexByRoom[room] = dictionary; } internal static bool TryFindByMarkerIndex(DungeonRoom room, int markerIndex, out SpawnedActorData? spawnData) { spawnData = null; if (markerIndex == 0) { return false; } if (!IndexByRoom.TryGetValue(room, out Dictionary value)) { RebuildIndex(room); value = IndexByRoom[room]; } if (!value.TryGetValue(markerIndex, out var value2)) { return false; } spawnData = value2; return true; } } internal static class LootSpawnScalingContext { [ThreadStatic] private static int _duplicateDepth; internal static bool IsDuplicating => _duplicateDepth > 0; internal static void BeginDuplicating() { _duplicateDepth++; } internal static void EndDuplicating() { _duplicateDepth = Math.Max(0, _duplicateDepth - 1); } } internal static class MapLootMarkerSpreader { private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly Type[] SpawnLootingObjectParameterTypes = new Type[9] { typeof(ItemElement), typeof(PosWithRot), typeof(bool), typeof(ReasonOfSpawn), typeof(int), typeof(int), typeof(long), typeof(bool), typeof(bool) }; private static readonly FieldInfo SpawnedActorDatasField = typeof(DungeonRoom).GetField("_spawnedActorDatas", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new InvalidOperationException("DungeonRoom._spawnedActorDatas not found"); private static readonly MethodInfo SpawnLootingObjectMethod = AccessTools.Method(typeof(IVroom), "SpawnLootingObject", SpawnLootingObjectParameterTypes, (Type[])null) ?? throw new InvalidOperationException("IVroom.SpawnLootingObject not found"); internal static void ShuffleMarkers(IList list) { for (int num = list.Count - 1; num > 0; num--) { int num2 = Random.Range(0, num + 1); int index = num; int index2 = num2; T value = list[num2]; T value2 = list[num]; list[index] = value; list[index2] = value2; } } internal static void TrySpreadToUnusedMarkers(IVroom vroom, ItemElement template, ReasonOfSpawn reasonOfSpawn, int prevProjectileActorId, long projectileDropTime, bool ignoreNav, bool isRestored) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_013f: Unknown result type (might be due to invalid IL or missing references) //IL_0151: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) DungeonRoom val = (DungeonRoom)(object)((vroom is DungeonRoom) ? vroom : null); if (val == null || template == null || template.ItemMasterID <= 0) { return; } ItemType itemType = ItemElementStackHelper.GetItemType(template); int playerCount = SessionPlayerCountHelper.ResolveFromRoom(vroom); float effectiveMultiplier = LootMultiplierResolver.GetEffectiveMultiplier(LootSource.Map, itemType, playerCount); int num = LootMultiplierResolver.ScaleCount(1, effectiveMultiplier) - 1; if (num <= 0) { return; } HashSet usedMarkerIds = CollectUsedMarkerIds(val); List list = CollectUnusedMarkers(template.ItemMasterID, usedMarkerIds); if (list.Count == 0) { return; } ShuffleMarkers(list); int num2 = 0; LootSpawnScalingContext.BeginDuplicating(); try { for (int i = 0; i < list.Count; i++) { if (num2 >= num) { break; } MapMarker_LootingObjectSpawnPoint val2 = list[i]; ItemElement val3 = CreateSpawnCopy(vroom, template); if (val3 != null && (int)SpawnLootingObjectMethod.Invoke(vroom, new object[9] { val3, ((MapMarker_SpawnPoint)val2).pos, ((MapMarker_SpawnPoint)val2).IsIndoor, reasonOfSpawn, ((MapMarker_SpawnPoint)val2).ID, prevProjectileActorId, projectileDropTime, val2.ignoreNav || ignoreNav, isRestored }) != 0) { num2++; } } } finally { LootSpawnScalingContext.EndDuplicating(); } if (num2 > 0) { LootMultiplicatorLog.InfoRuntimeScaled(LootSource.Map, itemType, template.ItemMasterID, 1, 1 + num2, effectiveMultiplier, $"SpawnLootingObject/{reasonOfSpawn}/markers"); } } internal static List CollectUnusedMarkers(int masterId, HashSet usedMarkerIds) { List list = new List(); MapMarker_LootingObjectSpawnPoint[] array = CollectLootMarkers(); foreach (MapMarker_LootingObjectSpawnPoint val in array) { if (((MapMarker_SpawnPoint)val).masterID == masterId && !usedMarkerIds.Contains(((MapMarker_SpawnPoint)val).ID)) { list.Add(val); } } return list; } internal static HashSet CollectUsedMarkerIds(DungeonRoom room) { HashSet hashSet = new HashSet(); if (!(SpawnedActorDatasField.GetValue(room) is IDictionary dictionary)) { return hashSet; } foreach (DictionaryEntry item in dictionary) { object? value = item.Value; SpawnedActorData val = (SpawnedActorData)((value is SpawnedActorData) ? value : null); if (val != null && val.Index != 0) { hashSet.Add(val.Index); } } return hashSet; } internal static MapMarker_LootingObjectSpawnPoint[] CollectLootMarkers() { return Object.FindObjectsByType((FindObjectsSortMode)0); } private static ItemElement? CreateSpawnCopy(IVroom vroom, ItemElement template) { try { ItemInfo val = template.toItemInfo(); return vroom.GetNewItemElement(val.itemMasterID, val.isFake, 1, val.durability, val.remainGauge, val.price); } catch { return null; } } } internal static class MapLootSpawnContext { [ThreadStatic] private static int _depth; internal static bool IsActive => _depth > 0; internal static void Enter() { _depth++; } internal static void Exit() { _depth = Math.Max(0, _depth - 1); } } internal static class RuntimeLootScaler { internal static void ScaleSpawnedItem(IVroom? room, ItemElement element, ReasonOfSpawn reasonOfSpawn, int spawnPointIndex = 0, bool isRestored = false) { //IL_0028: 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_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) if (ModConfig.EnableLootMultiplicator.Value && element != null && !LootSpawnScalingContext.IsDuplicating && !HostApplyGate.IsParticipantClient() && HostApplyGate.ShouldApplyHostOnlyFeature() && LootSourceResolver.ShouldScaleSpawn(reasonOfSpawn, isRestored) && LootSourceResolver.TryResolveLootSource(reasonOfSpawn, spawnPointIndex, out var source)) { int stackCount = ItemElementStackHelper.GetStackCount(element); if (LootItemCountScaler.TryScaleElementStack(room, element, source)) { int stackCount2 = ItemElementStackHelper.GetStackCount(element); ItemType itemType = ItemElementStackHelper.GetItemType(element); float effectiveMultiplier = LootMultiplierResolver.GetEffectiveMultiplier(source, itemType, SessionPlayerCountHelper.ResolveFromRoom(room)); LootMultiplicatorLog.InfoRuntimeScaled(source, itemType, element.ItemMasterID, stackCount, stackCount2, effectiveMultiplier, $"SpawnLootingObject/{reasonOfSpawn}"); LootMultiplicatorLog.DebugLootScaled(source, itemType, element.ItemMasterID, stackCount, stackCount2, effectiveMultiplier, $"SpawnLootingObject/{reasonOfSpawn}"); } } } internal static bool TryMapReasonToSource(ReasonOfSpawn reasonOfSpawn, out LootSource source) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return LootSourceResolver.TryResolveLootSource(reasonOfSpawn, 0, out source); } } } namespace MimesisPlayerEnhancement.Features.JoinAnytime { internal static class JoinAnytimeHub { private const string Feature = "JoinAnytime"; private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo? PdataField = typeof(Hub).GetField("pdata", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? SteamInviteField = typeof(Hub).GetField("steamInviteDispatcher", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly FieldInfo? IsPublicRoomField = typeof(SteamInviteDispatcher).GetField("isPublicRoom", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static bool _warnedMissingFields; internal static PersistentData? GetPdata() { if ((Object)(object)Hub.s == (Object)null) { return null; } if (PdataField == null) { WarnMissingFieldsOnce(); return null; } object? value = PdataField.GetValue(Hub.s); return (PersistentData?)((value is PersistentData) ? value : null); } internal static SteamInviteDispatcher? GetSteamInviteDispatcher() { if ((Object)(object)Hub.s == (Object)null) { return null; } if (SteamInviteField == null) { WarnMissingFieldsOnce(); return null; } object? value = SteamInviteField.GetValue(Hub.s); return (SteamInviteDispatcher?)((value is SteamInviteDispatcher) ? value : null); } internal static bool IsHostLobbyPublic(SteamInviteDispatcher? dispatcher) { if ((Object)(object)dispatcher != (Object)null && IsPublicRoomField != null) { object value = IsPublicRoomField.GetValue(dispatcher); if (value is bool) { return (bool)value; } return false; } return false; } private static void WarnMissingFieldsOnce() { if (!_warnedMissingFields) { _warnedMissingFields = true; ModLog.Warn("JoinAnytime", "Hub reflection fields missing — pdata or steamInviteDispatcher not found"); } } } internal static class JoinAnytimeNetworkTools { internal static void SendPreGameTramStateToClient(VPlayer player) { SendPreGameTramState(((VActor)player).UID, delegate(IMsg msg) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) ((VActor)player).SendToMe(msg); }); } internal static void SendPreGameTramStateToClient(SessionContext context) { SendPreGameTramState(context.GetPlayerUID(), delegate(IMsg msg) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) context.Send(msg); }); } private static void SendPreGameTramState(long uid, Action send) { //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Expected O, but got Unknown //IL_0095: Expected O, but got Unknown //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Expected O, but got Unknown if (LateJoinManager.HasPreGameStateBeenSent(uid)) { ModLog.Debug("JoinAnytime", $"Skipping duplicate pre-game tram state send for uid={uid}"); return; } if (!JoinAnytimeRoomTools.TryEnsureWaitingRoom(out IVroom waitingRoom)) { ModLog.Warn("JoinAnytime", $"SendPreGameTramState failed — waiting room unavailable for uid={uid}"); return; } ModLog.Info("JoinAnytime", $"Sending pre-game tram state to uid={uid} — waitingRoomUID={waitingRoom.RoomID}"); send((IMsg)new MakeRoomCompleteSig { nextRoomInfo = new RoomInfo { roomType = (VRoomType)1, roomUID = waitingRoom.RoomID } }); send((IMsg)new MoveToWaitingRoomSig()); LateJoinManager.MarkPreGameStateSent(uid); } } public static class JoinAnytimePatches { private const string Feature = "JoinAnytime"; public static void Apply(Harmony harmony) { HarmonyPatchHelper.PatchApplyResult result = HarmonyPatchHelper.ApplyPatchTypes(harmony, "JoinAnytime", HarmonyPatchHelper.GetNamespacePatchTypes(typeof(JoinAnytimePatches))); LogPatchAudit(harmony); HarmonyPatchHelper.LogPatchSummary("JoinAnytime", result); } private static void LogPatchAudit(Harmony harmony) { HarmonyPatchHelper.LogPatchAudit("JoinAnytime", harmony, new <>z__ReadOnlyArray<(string, MethodBase)>(new(string, MethodBase)[11] { ("CanEnterSession/GameSessionInfo", AccessTools.Method(typeof(GameSessionInfo), "CanEnterSession", (Type[])null, (Type[])null)), ("Login/SessionContext", AccessTools.Method(typeof(SessionContext), "Login", (Type[])null, (Type[])null)), ("EnterWaitingRoom/VRoomManager", AccessTools.Method(typeof(VRoomManager), "EnterWaitingRoom", (Type[])null, (Type[])null)), ("EnterMaintenenceRoom/VRoomManager", AccessTools.Method(typeof(VRoomManager), "EnterMaintenenceRoom", (Type[])null, (Type[])null)), ("OnRequestStartGame/VWaitingRoom", AccessTools.Method(typeof(IVroom), "RunEventActionInternal", (Type[])null, (Type[])null)), ("OnChangeLevelObjectStateSig/NewTramLeverLevelObject", AccessTools.Method(typeof(NewTramLeverLevelObject), "OnChangeLevelObjectStateSig", (Type[])null, (Type[])null)), ("SetLobbyPublic/SteamInviteDispatcher", AccessTools.Method(typeof(SteamInviteDispatcher), "SetLobbyPublic", (Type[])null, (Type[])null)), ("CreateLobby/SteamInviteDispatcher", AccessTools.Method(typeof(SteamInviteDispatcher), "CreateLobby", (Type[])null, (Type[])null)), ("SetPresenceInLobby/SteamInviteDispatcher", AccessTools.Method(typeof(SteamInviteDispatcher), "SetPresenceInLobby", (Type[])null, (Type[])null)), ("CorRefreshSteamLobbyData/GameMainBase", AccessTools.Method(typeof(GameMainBase), "CorRefreshSteamLobbyData", new Type[1] { typeof(Action) }, (Type[])null)), (".ctor/VPlayer", AccessTools.Constructor(typeof(VPlayer), new Type[10] { typeof(SessionContext), typeof(int), typeof(int), typeof(bool), typeof(string), typeof(string), typeof(PosWithRot), typeof(bool), typeof(IVroom), typeof(ReasonOfSpawn) }, false)) })); } } internal static class JoinAnytimeRoomTools { private const string Feature = "JoinAnytime"; private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly PropertyInfo? HubDatamanProperty = typeof(Hub).GetProperty("dataman", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); private static readonly PropertyInfo? HubVworldProperty = typeof(Hub).GetProperty("vworld", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static void MoveCurrentPlayerToSnapshot(SessionContext context) { object? obj = typeof(SessionContext).GetField("_vPlayer", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(context); VPlayer val = (VPlayer)((obj is VPlayer) ? obj : null); if (val == null) { ModLog.Warn("JoinAnytime", "MoveCurrentPlayerToSnapshot skipped — _vPlayer not found"); return; } IVroom vRoom = ((VActor)val).VRoom; int objectID = ((VActor)val).ObjectID; ModLog.Debug("JoinAnytime", string.Format("Removing old player actor={0} room={1}", objectID, ((object)vRoom)?.GetType().Name ?? "null")); if (vRoom != null) { vRoom.PendRemovePlayer(objectID, false, false); } context.CreatePlayerSnapshot(true); } internal static string GetSceneNameFromMapId(int mapMasterId) { if (mapMasterId == 0) { return string.Empty; } if (!TryGetDataman(out DataManager dataman)) { ModLog.Warn("JoinAnytime", "GetSceneNameFromMapId failed — dataman unavailable"); return string.Empty; } MapMasterInfo mapInfo = dataman.ExcelDataManager.GetMapInfo(mapMasterId); return ((mapInfo != null) ? mapInfo.SceneName : null) ?? string.Empty; } internal static string GetSceneNameFromDungeon(int dungeonMasterId, int pickedMapId = 0) { int num = ((pickedMapId != 0) ? pickedMapId : ResolvePickedMapId(null)); if (num != 0) { return GetSceneNameFromMapId(num); } if (!TryGetDataman(out DataManager dataman)) { ModLog.Warn("JoinAnytime", "GetSceneNameFromDungeon failed — dataman unavailable"); return string.Empty; } DungeonMasterInfo dungeonInfo = dataman.ExcelDataManager.GetDungeonInfo(dungeonMasterId); if (dungeonInfo == null) { return string.Empty; } if (dungeonInfo.MapIDs.IsDefaultOrEmpty) { return string.Empty; } return GetSceneNameFromMapId(dungeonInfo.MapIDs[0]); } internal static int ResolvePickedMapId(IVroom? room) { DungeonRoom val = (DungeonRoom)(object)((room is DungeonRoom) ? room : null); if (val != null && val.PickedMapID != 0) { return val.PickedMapID; } if (TryGetVRoomManager(out VRoomManager vroomManager) && vroomManager != null) { GameSessionInfo gameSessionInfo = vroomManager.GetGameSessionInfo(); if (gameSessionInfo.PickedMapID != 0) { return gameSessionInfo.PickedMapID; } } return JoinAnytimeHub.GetPdata()?.PickedMapID ?? 0; } internal static bool TryEnsureWaitingRoom(out IVroom? waitingRoom) { waitingRoom = null; if (!TryGetVRoomManager(out VRoomManager vroomManager) || vroomManager == null) { ModLog.Warn("JoinAnytime", "TryEnsureWaitingRoom failed — VRoomManager unavailable"); return false; } waitingRoom = TryGetWaitingRoom(vroomManager); if (waitingRoom != null) { return true; } if ((Object)(object)Hub.s == (Object)null) { ModLog.Warn("JoinAnytime", "TryEnsureWaitingRoom failed — Hub.s unavailable"); return false; } object? obj = typeof(Hub).GetProperty("vworld", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(Hub.s); VWorld val = (VWorld)((obj is VWorld) ? obj : null); if (val == null) { ModLog.Warn("JoinAnytime", "TryEnsureWaitingRoom failed — VWorld unavailable"); return false; } ModLog.Info("JoinAnytime", "Creating waiting room for late joiner"); val.InitWaitingRoom(); waitingRoom = TryGetWaitingRoom(vroomManager); if (waitingRoom == null) { ModLog.Warn("JoinAnytime", "TryEnsureWaitingRoom failed — room still missing after InitWaitingRoom"); } return waitingRoom != null; } internal static void RefreshWaitingRoomDisplaysForOccupants(VRoomManager? vroomManager = null) { if (!ModConfig.EnableJoinAnytime.Value || (vroomManager == null && !TryGetVRoomManager(out vroomManager))) { return; } IVroom? obj = TryGetWaitingRoom(vroomManager); VWaitingRoom val = (VWaitingRoom)(object)((obj is VWaitingRoom) ? obj : null); if (val == null) { return; } bool hasLoadedOccupant = false; ((IVroom)val).IterateAllPlayer((Action)delegate(VPlayer player) { if (player.LevelLoadCompleted) { hasLoadedOccupant = true; } }); if (hasLoadedOccupant) { val.SendRollDungeonSig(); ModLog.Info("JoinAnytime", "Broadcast RollDungeonSig to refresh tram displays for players already in waiting room"); } } internal static bool ShouldBlockWaitingRoomStartGame() { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Invalid comparison between Unknown and I4 if (!TryGetVRoomManager(out VRoomManager vroomManager) || vroomManager == null) { return false; } if ((int)vroomManager.GetGameSessionInfo().GameSessionState == 4) { return true; } if (CountDungeonPlayers(vroomManager) > 0) { return true; } int playerCountInSession = vroomManager.GetPlayerCountInSession(); if (playerCountInSession <= 0) { return false; } return vroomManager.GetRoomMemberCount((VRoomType)1) < playerCountInSession; } internal static IVroom? GetActiveDungeonRoom() { if (!TryGetVRoomManager(out VRoomManager vroomManager) || vroomManager == null) { ModLog.Warn("JoinAnytime", "GetActiveDungeonRoom failed — VRoomManager unavailable"); return null; } if (!(ReflectionHelper.GetFieldValue(vroomManager, "_vrooms") is Dictionary dictionary)) { return null; } IVroom val = null; int num = -1; IVroom val2 = null; long num2 = long.MinValue; foreach (IVroom value in dictionary.Values) { if (value is DungeonRoom) { if (value.RoomID > num2) { val2 = value; num2 = value.RoomID; } int memberCount = value.GetMemberCount(); if (memberCount > 0 && (val == null || memberCount > num || (memberCount == num && value.RoomID > val.RoomID))) { val = value; num = memberCount; } } } return val ?? val2; } private static IVroom? TryGetWaitingRoom(VRoomManager vroomManager) { if (!(ReflectionHelper.GetFieldValue(vroomManager, "_vrooms") is Dictionary dictionary)) { return null; } foreach (IVroom value in dictionary.Values) { if (value is VWaitingRoom) { return value; } } return null; } private static int CountDungeonPlayers(VRoomManager vroomManager) { if (!(ReflectionHelper.GetFieldValue(vroomManager, "_vrooms") is Dictionary dictionary)) { return 0; } int num = 0; foreach (IVroom value in dictionary.Values) { if (value is DungeonRoom) { num += value.GetMemberCount(); } } return num; } private static bool TryGetDataman(out DataManager dataman) { dataman = null; if (!((Object)(object)Hub.s == (Object)null)) { object? obj = HubDatamanProperty?.GetValue(Hub.s); DataManager val = (DataManager)((obj is DataManager) ? obj : null); if (val != null) { dataman = val; return true; } } return false; } private static bool TryGetVRoomManager(out VRoomManager? vroomManager) { vroomManager = null; if (!((Object)(object)Hub.s == (Object)null)) { object? obj = HubVworldProperty?.GetValue(Hub.s); VWorld val = (VWorld)((obj is VWorld) ? obj : null); if (val != null) { vroomManager = val.VRoomManager; return vroomManager != null; } } return false; } } internal static class JoinAnytimeUserMessages { private const string Feature = "JoinAnytime"; private const string LeverBlockedMessage = "Can't depart yet — other players are still in the dungeon."; private const float LeverFeedbackDelaySeconds = 0.5f; private const float LeverFeedbackDedupSeconds = 5f; private static readonly FieldInfo? StartGameSigField = typeof(InTramWaitingScene).GetField("startGameSig", BindingFlags.Instance | BindingFlags.NonPublic); private static DateTime _lastLeverBlockedShownUtc; internal static void OnWaitingRoomStartBlocked(IVroom room, int actorId) { if (LateJoinManager.IsEnabled && actorId != 0) { VPlayer val = room.FindPlayerByObjectID(actorId); if (val != null && LocalPlayerHelper.IsLocalSteamId(val.SteamID)) { ShowLeverBlockedLocal(immediate: true); } } } internal static void OnLocalTramLeverOpened(int actorId) { if (!LateJoinManager.IsEnabled) { return; } PersistentData pdata = JoinAnytimeHub.GetPdata(); if (pdata?.main is InTramWaitingScene) { ProtoActor myAvatar = pdata.main.GetMyAvatar(); if (!((Object)(object)myAvatar == (Object)null) && myAvatar.ActorID == actorId) { ScheduleLocalLeverPullFeedback(); } } } internal static void ScheduleLocalLeverPullFeedback() { if (LateJoinManager.IsEnabled) { MelonCoroutines.Start(ShowLeverBlockedAfterPullIfNeeded()); } } private static IEnumerator ShowLeverBlockedAfterPullIfNeeded() { yield return (object)new WaitForSeconds(3.5f); if (LateJoinManager.IsEnabled) { GameMainBase obj = JoinAnytimeHub.GetPdata()?.main; InTramWaitingScene val = (InTramWaitingScene)(object)((obj is InTramWaitingScene) ? obj : null); if (val != null && !HasPendingDungeonStart(val)) { ShowLeverBlockedLocal(immediate: false); } } } private static void ShowLeverBlockedLocal(bool immediate) { DateTime utcNow = DateTime.UtcNow; if (!((utcNow - _lastLeverBlockedShownUtc).TotalSeconds < 5.0)) { _lastLeverBlockedShownUtc = utcNow; InGameMessageHelper.ShowModMessage("Can't depart yet — other players are still in the dungeon.", isEntering: false, localOnly: true, ignoreFeatureToggles: true); ModLog.Debug("JoinAnytime", immediate ? "Showed tram lever blocked toast (server)" : "Showed tram lever blocked toast (client feedback)"); } } private static bool HasPendingDungeonStart(InTramWaitingScene scene) { if (StartGameSigField?.GetValue(scene) is MoveToDungeonSig) { return true; } return false; } } internal static class LateJoinManager { private const string Feature = "JoinAnytime"; private static readonly HashSet SentPreGameStateUids = new HashSet(); internal static bool IsEnabled => ModConfig.EnableJoinAnytime.Value; internal static void OnServerLogin(SessionContext context) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Invalid comparison between Unknown and I4 if (IsEnabled) { PersistentData pdata = JoinAnytimeHub.GetPdata(); if (pdata != null && (int)pdata.ClientMode <= 0 && (pdata.main is GamePlayScene || pdata.main is InTramWaitingScene)) { ModLog.Debug("JoinAnytime", $"Login while session active — uid={context.GetPlayerUID()} main={((object)pdata.main).GetType().Name}"); JoinAnytimeNetworkTools.SendPreGameTramStateToClient(context); } } } internal static void OnServerPlayerCreated(VPlayer player) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Invalid comparison between Unknown and I4 if (IsEnabled) { PersistentData pdata = JoinAnytimeHub.GetPdata(); if (pdata != null && (int)pdata.ClientMode <= 0 && !player.IsHost && ((VActor)player).VRoom is MaintenanceRoom) { ModLog.Info("JoinAnytime", string.Format("Late joiner in maintenance — uid={0} hostScene={1}", ((VActor)player).UID, ((object)pdata.main)?.GetType().Name ?? "null")); JoinAnytimeNetworkTools.SendPreGameTramStateToClient(player); } } } internal static void OnServerEnterWaitingRoom(SessionContext context) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Invalid comparison between Unknown and I4 if (IsEnabled && context != null && context.ExistPlayer() && (int)context.GetVRoomType() == 3) { GameMainBase val = JoinAnytimeHub.GetPdata()?.main; if (val is InTramWaitingScene || val is GamePlayScene) { ModLog.Debug("JoinAnytime", "Moving player snapshot Maintenance -> Waiting"); JoinAnytimeRoomTools.MoveCurrentPlayerToSnapshot(context); } } } internal static void OnServerEnterMaintenance(SessionContext context) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Invalid comparison between Unknown and I4 if (IsEnabled && context != null && context.ExistPlayer()) { PersistentData pdata = JoinAnytimeHub.GetPdata(); if ((int)context.GetVRoomType() == 2 && pdata?.main is MaintenanceScene) { ModLog.Debug("JoinAnytime", "Moving player snapshot Dungeon -> Maintenance"); JoinAnytimeRoomTools.MoveCurrentPlayerToSnapshot(context); } } } internal static bool HasPreGameStateBeenSent(long uid) { return SentPreGameStateUids.Contains(uid); } internal static void MarkPreGameStateSent(long uid) { SentPreGameStateUids.Add(uid); } internal static void RefreshLobbyVisibilityAfterSteamUpdate() { LobbyVisibilityHelper.RefreshAfterLobbyDataUpdate(); } } internal static class LobbyVisibilityHelper { private const string Feature = "JoinAnytime"; private static bool? _lastLoggedHostPublic; internal static void SyncHostPreference(bool wantsPublic, string source) { if (_lastLoggedHostPublic != wantsPublic) { _lastLoggedHostPublic = wantsPublic; ModLog.Debug("JoinAnytime", "Host lobby visibility -> " + (wantsPublic ? "public" : "private") + " (" + source + ")"); } } internal static void OnLobbyCreated(SteamInviteDispatcher dispatcher, bool isOpenForRandomMatch) { if (ModConfig.EnableJoinAnytime.Value) { SyncHostPreference(isOpenForRandomMatch, "CreateLobby"); SyncHostPreference(JoinAnytimeHub.IsHostLobbyPublic(dispatcher), "CreateLobby state"); } } internal static void OnSetLobbyPublicCompleted(SteamInviteDispatcher dispatcher, bool requestedPublic) { if (ModConfig.EnableJoinAnytime.Value) { bool flag = JoinAnytimeHub.IsHostLobbyPublic(dispatcher); SyncHostPreference(flag, "SetLobbyPublic"); ApplyPresence(dispatcher, flag, requestedPublic); } } internal static void RefreshAfterLobbyDataUpdate() { if (!ModConfig.EnableJoinAnytime.Value) { return; } SteamInviteDispatcher steamInviteDispatcher = JoinAnytimeHub.GetSteamInviteDispatcher(); if ((Object)(object)steamInviteDispatcher == (Object)null) { return; } bool flag = JoinAnytimeHub.IsHostLobbyPublic(steamInviteDispatcher); SyncHostPreference(flag, "lobby data refresh"); try { if (flag) { steamInviteDispatcher.SetLobbyPublic(true); ModLog.Debug("JoinAnytime", "Lobby refresh — keeping lobby public for join-anytime."); } else { steamInviteDispatcher.SetLobbyPublic(false); ModLog.Debug("JoinAnytime", "Lobby refresh — keeping lobby private."); } } catch (Exception ex) { ModLog.Debug("JoinAnytime", "Lobby visibility refresh failed — " + ex.Message); } } private static void ApplyPresence(SteamInviteDispatcher dispatcher, bool hostWantsPublic, bool requestedPublic) { if (hostWantsPublic) { dispatcher.SetPresenceInLobbyPublic(); } else if (!requestedPublic) { dispatcher.SetPresenceInLobby(); } } } } namespace MimesisPlayerEnhancement.Features.JoinAnytime.Patches { [HarmonyPatch(typeof(GameSessionInfo), "CanEnterSession")] internal static class GameSessionInfoCanEnterSessionPatch { [HarmonyPrefix] private static bool Prefix(ref bool __result) { if (!ModConfig.EnableJoinAnytime.Value) { return true; } __result = true; return false; } } [HarmonyPatch(typeof(SessionContext), "Login")] internal static class SessionContextLoginPatch { [HarmonyPostfix] private static void Postfix(SessionContext __instance) { HostStatusCache.Invalidate(); LateJoinManager.OnServerLogin(__instance); } } [HarmonyPatch(typeof(VRoomManager), "InitWaitingRoom")] internal static class VRoomManagerInitWaitingRoomPatch { [HarmonyPostfix] private static void Postfix(VRoomManager __instance) { JoinAnytimeRoomTools.RefreshWaitingRoomDisplaysForOccupants(__instance); } } [HarmonyPatch(typeof(VRoomManager), "EnterWaitingRoom")] internal static class VRoomManagerEnterWaitingRoomPatch { [HarmonyPrefix] private static void Prefix(SessionContext context) { LateJoinManager.OnServerEnterWaitingRoom(context); } } [HarmonyPatch(typeof(VRoomManager), "EnterMaintenenceRoom")] internal static class VRoomManagerEnterMaintenenceRoomPatch { [HarmonyPrefix] private static void Prefix(SessionContext context, int hashCode) { LateJoinManager.OnServerEnterMaintenance(context); } } [HarmonyPatch(typeof(IVroom), "RunEventActionInternal")] internal static class IVroomRunEventActionInternalPatch { [HarmonyPrefix] private static bool Prefix(IVroom __instance, IGameAction action, List paramList, ref bool __result) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Invalid comparison between Unknown and I4 if (!ModConfig.EnableJoinAnytime.Value) { return true; } GameAction val = (GameAction)(object)((action is GameAction) ? action : null); if (val == null || (int)val.ActionType != 28) { return true; } VWaitingRoom val2 = (VWaitingRoom)(object)((__instance is VWaitingRoom) ? __instance : null); if (val2 == null || val2.BackToMaintenance) { return true; } if (!JoinAnytimeRoomTools.ShouldBlockWaitingRoomStartGame()) { return true; } GameActionParamActor obj = GameActionParamHelper.FindParam(paramList); int actorId = ((obj != null) ? obj.ActorID : 0); ModLog.Info("JoinAnytime", "Blocked tram lever — players still split between dungeon and waiting room"); JoinAnytimeUserMessages.OnWaitingRoomStartBlocked(__instance, actorId); __result = false; return false; } } [HarmonyPatch(typeof(NewTramLeverLevelObject), "OnChangeLevelObjectStateSig")] internal static class NewTramLeverLevelObjectOnChangeLevelObjectStateSigPatch { [HarmonyPostfix] private static void Postfix(int actorId, int prevState, int currentState) { if (ModConfig.EnableJoinAnytime.Value && currentState == 2) { JoinAnytimeUserMessages.OnLocalTramLeverOpened(actorId); } } } [HarmonyPatch(typeof(SteamInviteDispatcher), "CreateLobby")] internal static class SteamInviteDispatcherCreateLobbyPatch { [HarmonyPostfix] private static void Postfix(SteamInviteDispatcher __instance, bool isOpenForRandomMatch) { LobbyVisibilityHelper.OnLobbyCreated(__instance, isOpenForRandomMatch); } } [HarmonyPatch(typeof(SteamInviteDispatcher), "SetLobbyPublic")] internal static class SteamInviteDispatcherSetLobbyPublicPatch { [HarmonyPostfix] private static void Postfix(SteamInviteDispatcher __instance, bool isPublic) { LobbyVisibilityHelper.OnSetLobbyPublicCompleted(__instance, isPublic); } } [HarmonyPatch(typeof(SteamInviteDispatcher), "SetPresenceInLobby")] internal static class SteamInviteDispatcherSetLobbyPublicPresencePatch { [HarmonyPrefix] private static bool Prefix(SteamInviteDispatcher __instance) { if (!ModConfig.EnableJoinAnytime.Value) { return true; } if (JoinAnytimeHub.IsHostLobbyPublic(__instance)) { __instance.SetPresenceInLobbyPublic(); return false; } return true; } } [HarmonyPatch] internal static class GameMainBaseCorRefreshSteamLobbyDataPatch { private static MethodBase? TargetMethod() { return AccessTools.Method(typeof(GameMainBase), "CorRefreshSteamLobbyData", new Type[1] { typeof(Action) }, (Type[])null); } [HarmonyPostfix] private static void Postfix() { LateJoinManager.RefreshLobbyVisibilityAfterSteamUpdate(); } } [HarmonyPatch] internal static class VPlayerCtorPatch { private static MethodBase? TargetMethod() { return AccessTools.Constructor(typeof(VPlayer), new Type[10] { typeof(SessionContext), typeof(int), typeof(int), typeof(bool), typeof(string), typeof(string), typeof(PosWithRot), typeof(bool), typeof(IVroom), typeof(ReasonOfSpawn) }, false); } [HarmonyPostfix] private static void Postfix(VPlayer __instance) { LateJoinManager.OnServerPlayerCreated(__instance); } } } namespace MimesisPlayerEnhancement.Features.DungeonTime { internal static class DungeonTimeApplier { private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly FieldInfo SessionEndTimeField = AccessToolsField(typeof(DungeonRoom), "_sessionEndTime"); private static readonly HashSet AppliedRooms = new HashSet(); internal static void EnsureApplied(DungeonRoom room) { if (AppliedRooms.Contains(room)) { return; } if (!ModConfig.EnableDungeonTime.Value) { AppliedRooms.Add(room); DungeonTimeLog.DebugSkipped("EnableDungeonTime is off"); return; } if (!HostApplyGate.ShouldApplyHostOnlyFeature()) { AppliedRooms.Add(room); DungeonTimeLog.DebugSkipped("not host"); return; } int num = SessionPlayerCountHelper.ResolveFromRoom((IVroom?)(object)room); long bonusMilliseconds = DungeonTimeResolver.GetBonusMilliseconds(num); if (bonusMilliseconds <= 0) { AppliedRooms.Add(room); DungeonTimeLog.DebugSkipped($"no bonus for players={num}"); return; } long num2 = (long)SessionEndTimeField.GetValue(room) + bonusMilliseconds; SessionEndTimeField.SetValue(room, num2); AppliedRooms.Add(room); DungeonTimeLog.InfoApplied(num, bonusMilliseconds, num2); } private static FieldInfo AccessToolsField(Type type, string name) { return type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new InvalidOperationException(type.Name + "." + name + " not found"); } } internal static class DungeonTimeLog { private const string Feature = "DungeonTime"; internal static void InfoApplied(int playerCount, long bonusMs, long newSessionEndTime) { double num = (double)bonusMs / 1000.0; ModLog.Info("DungeonTime", $"Shift extended — players={playerCount}, baseline={ModConfig.DungeonTimeBaselinePlayerCount.Value}, " + $"+{num:0.##}s ({ModConfig.ExtraShiftSecondsPerPlayerAboveBaseline.Value:0.##}s/player above baseline), " + $"newSessionEndTime={newSessionEndTime}"); } internal static void DebugSkipped(string reason) { ModLog.Debug("DungeonTime", "Shift extension skipped — " + reason); } } public static class DungeonTimePatches { [HarmonyPatch(typeof(DungeonRoom), "OnAllMemberEntered")] public static class DungeonRoomOnAllMemberEnteredPatch { [HarmonyPostfix] public static void Postfix(DungeonRoom __instance) { try { DungeonTimeApplier.EnsureApplied(__instance); } catch (Exception ex) { ModLog.Warn("DungeonTime", "OnAllMemberEntered postfix failed — " + ex.Message); } } } private const string Feature = "DungeonTime"; public static void Apply(Harmony harmony) { GameNetworkApi.GetGameAssembly(); HarmonyPatchHelper.PatchApplyResult result = HarmonyPatchHelper.ApplyPatchTypes(harmony, "DungeonTime", HarmonyPatchHelper.GetNestedPatchTypes(typeof(DungeonTimePatches))); LogPatchAudit(harmony); HarmonyPatchHelper.LogPatchSummary("DungeonTime", result); } private static void LogPatchAudit(Harmony harmony) { HarmonyPatchHelper.LogPatchAudit("DungeonTime", harmony, new <>z__ReadOnlySingleElementList<(string, MethodBase)>(("OnAllMemberEntered/DungeonRoom", AccessTools.Method(typeof(DungeonRoom), "OnAllMemberEntered", (Type[])null, (Type[])null)))); } } internal static class DungeonTimeResolver { internal static double GetBonusSeconds(int playerCount) { if (!ModConfig.EnableDungeonTime.Value) { return 0.0; } int value = ModConfig.DungeonTimeBaselinePlayerCount.Value; if (playerCount > value) { return (float)(playerCount - value) * ModConfig.ExtraShiftSecondsPerPlayerAboveBaseline.Value; } return 0.0; } internal static long GetBonusMilliseconds(int playerCount) { double bonusSeconds = GetBonusSeconds(playerCount); if (!(bonusSeconds <= 0.0)) { return (long)(bonusSeconds * 1000.0); } return 0L; } } } namespace MimesisPlayerEnhancement.Features.DungeonRandomizer { internal static class DungeonDataAccess { private const BindingFlags InstanceFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private static readonly PropertyInfo? HubDatamanProperty = typeof(Hub).GetProperty("dataman", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); internal static bool TryGetExcelDataManager(out ExcelDataManager excel) { excel = null; if ((Object)(object)Hub.s == (Object)null) { return false; } object? obj = HubDatamanProperty?.GetValue(Hub.s); DataManager val = (DataManager)((obj is DataManager) ? obj : null); if (val == null) { return false; } excel = val.ExcelDataManager; return excel != null; } internal static List GetFilteredActiveDungeonIds(HashSet allowlist, HashSet blocklist) { List list = new List(); if (!TryGetExcelDataManager(out ExcelDataManager excel)) { return list; } foreach (KeyValuePair item in excel.DungeonInfoDict) { DungeonMasterInfo value = item.Value; if (value != null && value.IsActive) { int iD = value.ID; if ((allowlist.Count <= 0 || allowlist.Contains(iD)) && !blocklist.Contains(iD)) { list.Add(iD); } } } return list; } internal static bool IsExcluded(int dungeonId, IReadOnlyList excludeIds) { if (excludeIds == null || excludeIds.Count == 0) { return false; } for (int i = 0; i < excludeIds.Count; i++) { if (excludeIds[i] == dungeonId) { return true; } } return false; } internal static List FilterExcluded(IReadOnlyList pool, IReadOnlyList excludeIds) { if (excludeIds == null || excludeIds.Count == 0) { return pool.ToList(); } List list = new List(pool.Count); for (int i = 0; i < pool.Count; i++) { int num = pool[i]; if (!IsExcluded(num, excludeIds)) { list.Add(num); } } return list; } internal static bool TryPickUniform(IReadOnlyList pool, out int dungeonId) { dungeonId = 0; if (pool.Count == 0) { return false; } int index = Random.Range(0, pool.Count); dungeonId = pool[index]; return true; } internal static bool TryPickUniformLayoutFlow(DungeonMasterInfo info, out string flowName) { flowName = string.Empty; ImmutableDictionary dungenCandidates = info.DungenCandidates; if (dungenCandidates == null || dungenCandidates.Count == 0) { return false; } int num = Random.Range(0, dungenCandidates.Count); foreach (string key in dungenCandidates.Keys) { if (num-- == 0) { flowName = key; return true; } } return false; } internal static bool TryPickUniformMapId(DungeonMasterInfo info, out int mapId) { mapId = 0; ImmutableArray mapIDs = info.MapIDs; if (mapIDs.IsDefaultOrEmpty) { return false; } int index = Random.Range(0, mapIDs.Length); mapId = mapIDs[index]; return true; } } internal static class DungeonIdListParser { internal static HashSet Parse(string? csv) { HashSet hashSet = new HashSet(); if (string.IsNullOrWhiteSpace(csv)) { return hashSet; } string[] array = csv.Split(','); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length != 0) { if (int.TryParse(text, out var result) && result > 0) { hashSet.Add(result); } else { DungeonRandomizerLog.Debug("Ignoring invalid dungeon ID token: '" + text + "'"); } } } return hashSet; } internal static DungeonPickPoolMode ParsePoolMode(string? value) { if (!string.Equals(value, "AllActiveUniform", StringComparison.OrdinalIgnoreCase)) { return DungeonPickPoolMode.WidenVanilla; } return DungeonPickPoolMode.AllActiveUniform; } } internal enum DungeonPickPoolMode { WidenVanilla, AllActiveUniform } internal static class DungeonPickResolver { private static HashSet? _cachedAllowlist; private static HashSet? _cachedBlocklist; private static string _cachedAllowlistRaw = ""; private static string _cachedBlocklistRaw = ""; internal static bool ShouldIgnoreDungeonExcludeList() { if (HostApplyGate.ShouldApplyHostOnlyFeature(() => ModConfig.EnableDungeonRandomizer.Value) && ModConfig.RandomizeDungeonPick.Value && DungeonIdListParser.ParsePoolMode(ModConfig.DungeonPickPoolMode.Value) == DungeonPickPoolMode.WidenVanilla) { return ModConfig.IgnoreDungeonExcludeList.Value; } return false; } internal static int ResolvePick(int vanillaResult, IReadOnlyList excludeDungeonIds) { GetFilters(out HashSet allowlist, out HashSet blocklist); if (DungeonIdListParser.ParsePoolMode(ModConfig.DungeonPickPoolMode.Value) == DungeonPickPoolMode.AllActiveUniform) { List filteredActiveDungeonIds = DungeonDataAccess.GetFilteredActiveDungeonIds(allowlist, blocklist); List list = DungeonDataAccess.FilterExcluded(filteredActiveDungeonIds, excludeDungeonIds); if (list.Count == 0 && excludeDungeonIds.Count > 0) { DungeonRandomizerLog.Warn("AllActiveUniform pool empty after tram excludes; falling back to full filtered pool."); list = filteredActiveDungeonIds; } if (DungeonDataAccess.TryPickUniform(list, out var dungeonId)) { DungeonRandomizerLog.Debug($"Dungeon pick (AllActiveUniform): {dungeonId} from pool of {list.Count}"); return dungeonId; } DungeonRandomizerLog.Warn("AllActiveUniform pool empty after filters; keeping vanilla pick."); return vanillaResult; } if (IsEligible(vanillaResult, allowlist, blocklist) && !DungeonDataAccess.IsExcluded(vanillaResult, excludeDungeonIds)) { DungeonRandomizerLog.Debug($"Dungeon pick (WidenVanilla): keeping vanilla result {vanillaResult}"); return vanillaResult; } List filteredActiveDungeonIds2 = DungeonDataAccess.GetFilteredActiveDungeonIds(allowlist, blocklist); List list2 = DungeonDataAccess.FilterExcluded(filteredActiveDungeonIds2, excludeDungeonIds); if (list2.Count == 0 && excludeDungeonIds.Count > 0) { DungeonRandomizerLog.Warn("WidenVanilla fallback pool empty after tram excludes; falling back to full filtered pool."); list2 = filteredActiveDungeonIds2; } if (DungeonDataAccess.TryPickUniform(list2, out var dungeonId2)) { DungeonRandomizerLog.Debug($"Dungeon pick (WidenVanilla): vanilla {vanillaResult} filtered out; fallback {dungeonId2}"); return dungeonId2; } DungeonRandomizerLog.Warn($"Dungeon pick fallback pool empty; keeping vanilla result {vanillaResult}"); return vanillaResult; } private static bool IsEligible(int dungeonId, HashSet allowlist, HashSet blocklist) { if (dungeonId > 0 && (allowlist.Count <= 0 || allowlist.Contains(dungeonId))) { return !blocklist.Contains(dungeonId); } return false; } private static void GetFilters(out HashSet allowlist, out HashSet blocklist) { string text = ModConfig.DungeonAllowlist.Value ?? ""; string text2 = ModConfig.DungeonBlocklist.Value ?? ""; if (_cachedAllowlist == null || !string.Equals(_cachedAllowlistRaw, text, StringComparison.Ordinal)) { _cachedAllowlistRaw = text; _cachedAllowlist = DungeonIdListParser.Parse(text); } if (_cachedBlocklist == null || !string.Equals(_cachedBlocklistRaw, text2, StringComparison.Ordinal)) { _cachedBlocklistRaw = text2; _cachedBlocklist = DungeonIdListParser.Parse(text2); } allowlist = _cachedAllowlist; blocklist = _cachedBlocklist; } } internal static class DungeonPickRollContext { internal static bool InRollDiceDungeon { get; private set; } internal static bool IsReroll { get; private set; } internal static int PickIndex { get; private set; } internal static void BeginRoll(bool reroll) { InRollDiceDungeon = true; IsReroll = reroll; PickIndex = 0; } internal static void EndRoll() { InRollDiceDungeon = false; IsReroll = false; PickIndex = 0; } internal static void AdvancePick() { PickIndex++; } internal static bool ShouldClearRerollExcludes() { if (InRollDiceDungeon && IsReroll && PickIndex == 0) { return DungeonPickResolver.ShouldIgnoreDungeonExcludeList(); } return false; } } internal static class DungeonRandomizerLog { private const string Feature = "DungeonRandomizer"; internal static void Debug(string message) { if (ModConfig.EnableDebugLogging.Value) { ModLog.Debug("DungeonRandomizer", message); } } internal static void Warn(string message) { ModLog.Warn("DungeonRandomizer", message); } } public static class DungeonRandomizerPatches { [HarmonyPatch(typeof(VWaitingRoom), "RollDiceDungeon")] public static class VWaitingRoomRollDiceDungeonPatch { [HarmonyPrefix] public static void Prefix(bool reroll) { DungeonPickRollContext.BeginRoll(reroll); } [HarmonyPostfix] public static void Postfix() { DungeonPickRollContext.EndRoll(); } } [HarmonyPatch(typeof(ExcelDataManager), "PickDungeon")] public static class ExcelDataManagerPickDungeonPatch { private static IReadOnlyList _excludeIdsForResolve = Array.Empty(); [HarmonyPrefix] public static void Prefix(ref List excludeDungeonIDs) { try { bool num = DungeonPickRollContext.ShouldClearRerollExcludes(); IReadOnlyList excludeIdsForResolve; if (!num) { IReadOnlyList readOnlyList = excludeDungeonIDs; excludeIdsForResolve = readOnlyList; } else { IReadOnlyList readOnlyList = Array.Empty(); excludeIdsForResolve = readOnlyList; } _excludeIdsForResolve = excludeIdsForResolve; if (num) { excludeDungeonIDs = new List(); } } catch (Exception ex) { DungeonRandomizerLog.Warn("PickDungeon prefix failed — " + ex.Message); } } [HarmonyPostfix] public static void Postfix(ref int __result) { try { if (DungeonPickRollContext.InRollDiceDungeon) { DungeonPickRollContext.AdvancePick(); } if (HostApplyGate.ShouldApplyHostOnlyFeature(() => ModConfig.EnableDungeonRandomizer.Value) && ModConfig.RandomizeDungeonPick.Value) { __result = DungeonPickResolver.ResolvePick(__result, _excludeIdsForResolve); } } catch (Exception ex) { DungeonRandomizerLog.Warn("PickDungeon postfix failed — " + ex.Message); } finally { _excludeIdsForResolve = Array.Empty(); } } } [HarmonyPatch(typeof(DungeonMasterInfo), "GetRandomDungenName")] public static class DungeonMasterInfoGetRandomDungenNamePatch { [HarmonyPostfix] public static void Postfix(DungeonMasterInfo __instance, ref string __result) { try { if (HostApplyGate.ShouldApplyHostOnlyFeature(() => ModConfig.EnableDungeonRandomizer.Value)) { string text = DungeonVariantResolver.ResolveLayoutFlow(__instance, __result); if (text != null) { __result = text; } } } catch (Exception ex) { DungeonRandomizerLog.Warn("GetRandomDungenName postfix failed — " + ex.Message); } } } [HarmonyPatch(typeof(DungeonMasterInfo), "PickMapID")] public static class DungeonMasterInfoPickMapIdPatch { [HarmonyPostfix] public static void Postfix(DungeonMasterInfo __instance, ref int __result) { try { if (HostApplyGate.ShouldApplyHostOnlyFeature(() => ModConfig.EnableDungeonRandomizer.Value)) { int? num = DungeonVariantResolver.ResolveMapId(__instance, __result); if (num.HasValue) { __result = num.Value; } } } catch (Exception ex) { DungeonRandomizerLog.Warn("PickMapID postfix failed — " + ex.Message); } } } [HarmonyPatch(typeof(GameSessionInfo), "SetNextDungeonMasterID")] public static class GameSessionInfoSetNextDungeonMasterIdPatch { [HarmonyPrefix] public static void Prefix(ref int randomDungeonSeed) { try { if (HostApplyGate.ShouldApplyHostOnlyFeature(() => ModConfig.EnableDungeonRandomizer.Value)) { randomDungeonSeed = DungeonSeedResolver.RollSeed(randomDungeonSeed); } } catch (Exception ex) { DungeonRandomizerLog.Warn("SetNextDungeonMasterID prefix failed — " + ex.Message); } } } private const string Feature = "DungeonRandomizer"; public static void Apply(Harmony harmony) { GameNetworkApi.GetGameAssembly(); HarmonyPatchHelper.PatchApplyResult result = HarmonyPatchHelper.ApplyPatchTypes(harmony, "DungeonRandomizer", HarmonyPatchHelper.GetNestedPatchTypes(typeof(DungeonRandomizerPatches))); LogPatchAudit(harmony); HarmonyPatchHelper.LogPatchSummary("DungeonRandomizer", result); } private static void LogPatchAudit(Harmony harmony) { HarmonyPatchHelper.LogPatchAudit("DungeonRandomizer", harmony, new <>z__ReadOnlyArray<(string, MethodBase)>(new(string, MethodBase)[5] { ("PickDungeon/ExcelDataManager", AccessTools.Method(typeof(ExcelDataManager), "PickDungeon", (Type[])null, (Type[])null)), ("RollDiceDungeon/VWaitingRoom", AccessTools.Method(typeof(VWaitingRoom), "RollDiceDungeon", (Type[])null, (Type[])null)), ("GetRandomDungenName/DungeonMasterInfo", AccessTools.Method(typeof(DungeonMasterInfo), "GetRandomDungenName", (Type[])null, (Type[])null)), ("PickMapID/DungeonMasterInfo", AccessTools.Method(typeof(DungeonMasterInfo), "PickMapID", (Type[])null, (Type[])null)), ("SetNextDungeonMasterID/GameSessionInfo", AccessTools.Method(typeof(GameSessionInfo), "SetNextDungeonMasterID", (Type[])null, (Type[])null)) })); } } internal static class DungeonSeedResolver { internal static int RollSeed(int vanillaSeed) { if (!ModConfig.RandomizeDungeonSeed.Value) { return vanillaSeed; } int num = Random.Range(1, int.MaxValue); DungeonRandomizerLog.Debug($"Dungeon seed: {vanillaSeed} -> {num}"); return num; } } internal static class DungeonVariantResolver { internal static string? ResolveLayoutFlow(DungeonMasterInfo info, string vanillaFlow) { if (!ModConfig.RandomizeLayoutFlow.Value) { return null; } if (!DungeonDataAccess.TryPickUniformLayoutFlow(info, out string flowName)) { DungeonRandomizerLog.Debug($"Layout flow: no candidates for dungeon {info.ID}; keeping '{vanillaFlow}'"); return null; } DungeonRandomizerLog.Debug($"Layout flow: dungeon {info.ID} '{vanillaFlow}' -> '{flowName}'"); return flowName; } internal static int? ResolveMapId(DungeonMasterInfo info, int vanillaMapId) { if (!ModConfig.RandomizeMapVariant.Value) { return null; } if (!DungeonDataAccess.TryPickUniformMapId(info, out var mapId)) { DungeonRandomizerLog.Debug($"Map variant: no MapIDs for dungeon {info.ID}; keeping {vanillaMapId}"); return null; } DungeonRandomizerLog.Debug($"Map variant: dungeon {info.ID} {vanillaMapId} -> {mapId}"); return mapId; } } } [CompilerGenerated] internal sealed class <>z__ReadOnlyArray : IEnumerable, ICollection, IList, IEnumerable, IReadOnlyCollection, IReadOnlyList, ICollection, IList { int ICollection.Count => _items.Length; bool ICollection.IsSynchronized => false; object ICollection.SyncRoot => this; object IList.this[int index] { get { return _items[index]; } set { throw new NotSupportedException(); } } bool IList.IsFixedSize => true; bool IList.IsReadOnly => true; int IReadOnlyCollection.Count => _items.Length; T IReadOnlyList.this[int index] => _items[index]; int ICollection.Count => _items.Length; bool ICollection.IsReadOnly => true; T IList.this[int index] { get { return _items[index]; } set { throw new NotSupportedException(); } } public <>z__ReadOnlyArray(T[] items) { _items = items; } IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_items).GetEnumerator(); } void ICollection.CopyTo(Array array, int index) { ((ICollection)_items).CopyTo(array, index); } int IList.Add(object value) { throw new NotSupportedException(); } void IList.Clear() { throw new NotSupportedException(); } bool IList.Contains(object value) { return ((IList)_items).Contains(value); } int IList.IndexOf(object value) { return ((IList)_items).IndexOf(value); } void IList.Insert(int index, object value) { throw new NotSupportedException(); } void IList.Remove(object value) { throw new NotSupportedException(); } void IList.RemoveAt(int index) { throw new NotSupportedException(); } IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)_items).GetEnumerator(); } void ICollection.Add(T item) { throw new NotSupportedException(); } void ICollection.Clear() { throw new NotSupportedException(); } bool ICollection.Contains(T item) { return ((ICollection)_items).Contains(item); } void ICollection.CopyTo(T[] array, int arrayIndex) { ((ICollection)_items).CopyTo(array, arrayIndex); } bool ICollection.Remove(T item) { throw new NotSupportedException(); } int IList.IndexOf(T item) { return ((IList)_items).IndexOf(item); } void IList.Insert(int index, T item) { throw new NotSupportedException(); } void IList.RemoveAt(int index) { throw new NotSupportedException(); } } [CompilerGenerated] internal sealed class <>z__ReadOnlySingleElementList : IEnumerable, ICollection, IList, IEnumerable, IReadOnlyCollection, IReadOnlyList, ICollection, IList { private sealed class Enumerator : IDisposable, IEnumerator, IEnumerator { object IEnumerator.Current => _item; T IEnumerator.Current => _item; public Enumerator(T item) { _item = item; } bool IEnumerator.MoveNext() { if (!_moveNextCalled) { return _moveNextCalled = true; } return false; } void IEnumerator.Reset() { _moveNextCalled = false; } void IDisposable.Dispose() { } } int ICollection.Count => 1; bool ICollection.IsSynchronized => false; object ICollection.SyncRoot => this; object IList.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } set { throw new NotSupportedException(); } } bool IList.IsFixedSize => true; bool IList.IsReadOnly => true; int IReadOnlyCollection.Count => 1; T IReadOnlyList.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } } int ICollection.Count => 1; bool ICollection.IsReadOnly => true; T IList.this[int index] { get { if (index != 0) { throw new IndexOutOfRangeException(); } return _item; } set { throw new NotSupportedException(); } } public <>z__ReadOnlySingleElementList(T item) { _item = item; } IEnumerator IEnumerable.GetEnumerator() { return new Enumerator(_item); } void ICollection.CopyTo(Array array, int index) { array.SetValue(_item, index); } int IList.Add(object value) { throw new NotSupportedException(); } void IList.Clear() { throw new NotSupportedException(); } bool IList.Contains(object value) { return EqualityComparer.Default.Equals(_item, (T)value); } int IList.IndexOf(object value) { if (!EqualityComparer.Default.Equals(_item, (T)value)) { return -1; } return 0; } void IList.Insert(int index, object value) { throw new NotSupportedException(); } void IList.Remove(object value) { throw new NotSupportedException(); } void IList.RemoveAt(int index) { throw new NotSupportedException(); } IEnumerator IEnumerable.GetEnumerator() { return new Enumerator(_item); } void ICollection.Add(T item) { throw new NotSupportedException(); } void ICollection.Clear() { throw new NotSupportedException(); } bool ICollection.Contains(T item) { return EqualityComparer.Default.Equals(_item, item); } void ICollection.CopyTo(T[] array, int arrayIndex) { array[arrayIndex] = _item; } bool ICollection.Remove(T item) { throw new NotSupportedException(); } int IList.IndexOf(T item) { if (!EqualityComparer.Default.Equals(_item, item)) { return -1; } return 0; } void IList.Insert(int index, T item) { throw new NotSupportedException(); } void IList.RemoveAt(int index) { throw new NotSupportedException(); } }