using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Mirror; using MoreSettings.Configuration; using MoreSettings.Models; using MoreSettings.Network; using MoreSettings.Runtime; using TMPro; using UnityEngine; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyVersion("0.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.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] [Microsoft.CodeAnalysis.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] [Microsoft.CodeAnalysis.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 MoreSettings { [BepInPlugin("com.lncinteractive", "MoreSettings", "0.2.5")] public sealed class PluginMain : BaseUnityPlugin { public const string PluginGuid = "com.lncinteractive"; public const string PluginName = "MoreSettings"; public const string PluginVersion = "0.2.5"; private Harmony? _harmony; internal static ManualLogSource Log { get; private set; } private void Awake() { //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; TimingCoordinator.Initialize(ActiveSettings.Bind(((BaseUnityPlugin)this).Config), Log); TimingCoordinator.TryApplyFromResources("PluginMain.Awake"); LobbyVisibility.Initialize(Log); LobbyVisibility.RegisterMessageDelegates(); _harmony = new Harmony("com.lncinteractive"); _harmony.PatchAll(); Log.LogInfo((object)"MoreSettings 0.2.5 initialized."); } private void OnDestroy() { Harmony? harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } } namespace MoreSettings.Runtime { public static class NativeLobbySettingsMenu { private sealed class SliderRuntimeBinding { public string Key { get; } public Transform Root { get; } public SliderRuntimeBinding(string key, Transform root) { Key = key; Root = root; } } private const int MaxEditableQuotaMultipliers = 6; private const float DefaultCurrencySliderMax = 100000f; private const float CurrencySliderStep = 5000f; private const float SliderValueFieldWidth = 120f; private static readonly List QuotaScalingModeOptions = new List { "Vanilla scaling", "Custom pattern" }; private static readonly List SliderRuntimeBindings = new List(); public const string SectionKey = "moresettings.section"; public const string TimeSectionKey = "moresettings.time.section"; public const string QuotaSectionKey = "moresettings.quota.section"; public const string DayDurationMinutesKey = "moresettings.day-duration-minutes"; public const string StartingMoneyKey = "moresettings.starting-money"; public const string StartingQuotaKey = "moresettings.starting-quota"; public const string CatchUpFactorKey = "moresettings.catch-up-factor"; public const string QuotaScalingModeKey = "moresettings.quota-scaling-mode"; public const string QuotaPatternLengthKey = "moresettings.quota-pattern-length"; private static SettingsLayout? _lobbySettingsLayout; private static SettingsLayout? _runtimeLobbySettingsLayout; private static bool _isSynchronizing; public static void RegisterLobbyLayout(SettingsLayout layout, string source) { if (!((Object)(object)layout == (Object)null)) { _lobbySettingsLayout = layout; PluginMain.Log.LogDebug((object)("[NativeLobbySettingsMenu] Registered lobby settings layout from " + source + ".")); } } public static void RegisterRuntimeLobbyLayout(SettingsLayout layout, string source) { if (!((Object)(object)layout == (Object)null)) { _runtimeLobbySettingsLayout = layout; PluginMain.Log.LogDebug((object)("[NativeLobbySettingsMenu] Registered runtime lobby settings layout from " + source + ".")); } } public static bool EnsureInjected(SettingsLayout layout, string source) { if ((Object)(object)layout == (Object)null) { return false; } if (!IsLobbySettingsLayout(layout)) { return false; } if (layout.tabs == null || layout.tabs.Count == 0) { PluginMain.Log.LogDebug((object)("[NativeLobbySettingsMenu] Skipped injection from " + source + " because the layout had no tabs.")); return false; } Tab val = FindTargetTab(layout); if (val == null) { PluginMain.Log.LogDebug((object)("[NativeLobbySettingsMenu] Skipped injection from " + source + " because no lobby settings tab was found.")); return false; } Tab val2 = val; if (val2.entries == null) { val2.entries = new List(); } int num = 0; num += EnsureTitleEntry(val.entries, "moresettings.section", "MoreSettings"); num += EnsureTitleEntry(val.entries, "moresettings.time.section", "Time"); num += EnsureSliderEntry(val.entries, "moresettings.day-duration-minutes", "Day duration (minutes)", 1f, 1440f, wholeNumbers: true); num += EnsureTitleEntry(val.entries, "moresettings.quota.section", "Quota"); num += EnsureSliderEntry(val.entries, "moresettings.starting-money", "Starting money", 0f, 100000f, wholeNumbers: true); num += EnsureSliderEntry(val.entries, "moresettings.starting-quota", "Starting quota", 0f, 100000f, wholeNumbers: true); num += EnsureSliderEntry(val.entries, "moresettings.catch-up-factor", "Catch-up factor", 0f, 5f, wholeNumbers: false); num += EnsureDropdownEntry(val.entries, "moresettings.quota-scaling-mode", "Quota scaling", QuotaScalingModeOptions, 0); num += EnsureSliderEntry(val.entries, "moresettings.quota-pattern-length", "Custom pattern length", 1f, 6f, wholeNumbers: true); for (int i = 0; i < 6; i++) { num += EnsureSliderEntry(val.entries, GetQuotaMultiplierKey(i), $"Pattern multiplier {i + 1}", 0.01f, 100f, wholeNumbers: false); } SyncFromCurrentState(); PluginMain.Log.LogInfo((object)string.Format("[NativeLobbySettingsMenu] {0} native lobby settings entries from {1}. Tab='{2}', added={3}.", (num > 0) ? "Injected" : "Reused", source, val.tabName, num)); return true; } public static void RefreshFromLobbyButton(SettingsLayout layout) { EnsureInjected(layout, "LobbyModeDropdownButton.OnClick"); SyncFromCurrentState(); RefreshRuntimeSliderBindings(); } public static void HandleSettingChanged(SettingItemBase entry) { SettingsLayout activeLobbySettingsLayout = GetActiveLobbySettingsLayout(); if (_isSynchronizing || (Object)(object)entry == (Object)null || !IsMoreSettingsKey(entry.key) || (Object)(object)activeLobbySettingsLayout == (Object)null) { return; } TimingProfile profile; SliderSettingItem dayDurationEntry; SliderSettingItem startingMoneyEntry; SliderSettingItem startingQuotaEntry; SliderSettingItem catchUpFactorEntry; DropdownSettingItem quotaScalingModeEntry; SliderSettingItem quotaPatternLengthEntry; SliderSettingItem[] quotaMultiplierEntries; if (string.Equals(entry.key, "moresettings.starting-money", StringComparison.OrdinalIgnoreCase) && !IsStartingMoneyEditable()) { SyncFromCurrentState(); } else if (TryGetCurrentProfileForEditing(out profile) && profile != null && TryGetEditableEntries(activeLobbySettingsLayout, out dayDurationEntry, out startingMoneyEntry, out startingQuotaEntry, out catchUpFactorEntry, out quotaScalingModeEntry, out quotaPatternLengthEntry, out quotaMultiplierEntries)) { QuotaScalingMode quotaScalingMode = ResolveQuotaScalingMode(quotaScalingModeEntry); float[] array = QuotaPatternEditor.BuildPattern(quotaMultiplierEntries.Select((SliderSettingItem multiplierEntry) => multiplierEntry.value).ToArray(), Mathf.RoundToInt(quotaPatternLengthEntry.value)); long startingMoney = (long)Mathf.Round(startingMoneyEntry.value); long startingQuota = EnsureLinkedStartingQuota(startingMoney, (long)Mathf.Round(startingQuotaEntry.value)); float dayDurationSeconds = dayDurationEntry.value * 60f; int daysBeforeQuota = profile.DaysBeforeQuota; float value = catchUpFactorEntry.value; IReadOnlyList quotaMultipliers; if (quotaScalingMode != QuotaScalingMode.CustomPattern) { quotaMultipliers = profile.QuotaMultipliers; } else { IReadOnlyList readOnlyList = array; quotaMultipliers = readOnlyList; } TimingProfile updatedProfile = new TimingProfile("ActiveConfig", isVanillaProfile: false, dayDurationSeconds, daysBeforeQuota, startingQuota, startingMoney, value, quotaScalingMode, quotaMultipliers); if (!TimingCoordinator.TryApplyManualOverrides(profile, updatedProfile, "NativeLobbySettingsMenu.NotifyChanged", out IReadOnlyList _)) { SyncFromCurrentState(); return; } LobbyVisibility.BroadcastCurrentState(); SyncFromCurrentState(); } } private static void SyncFromCurrentState() { SettingsLayout activeLobbySettingsLayout = GetActiveLobbySettingsLayout(); if ((Object)(object)activeLobbySettingsLayout == (Object)null || !TryGetCurrentProfileForEditing(out TimingProfile profile) || profile == null || !TryGetEditableEntries(activeLobbySettingsLayout, out SliderSettingItem dayDurationEntry, out SliderSettingItem startingMoneyEntry, out SliderSettingItem startingQuotaEntry, out SliderSettingItem catchUpFactorEntry, out DropdownSettingItem quotaScalingModeEntry, out SliderSettingItem quotaPatternLengthEntry, out SliderSettingItem[] quotaMultiplierEntries)) { return; } float[] array = QuotaPatternEditor.BuildEditableValues(profile.QuotaMultipliers, 6); _isSynchronizing = true; try { dayDurationEntry.value = Mathf.Clamp(profile.DayDurationSeconds / 60f, dayDurationEntry.min, dayDurationEntry.max); dayDurationEntry.defaultValue = dayDurationEntry.value; long startingMoney = profile.StartingMoney; long num = EnsureLinkedStartingQuota(profile.StartingMoney, profile.StartingQuota); ApplyCurrencyBounds(startingMoneyEntry, startingQuotaEntry, startingMoney, num); startingMoneyEntry.value = Mathf.Clamp((float)startingMoney, startingMoneyEntry.min, startingMoneyEntry.max); startingMoneyEntry.defaultValue = startingMoneyEntry.value; startingQuotaEntry.value = Mathf.Clamp((float)num, startingQuotaEntry.min, startingQuotaEntry.max); startingQuotaEntry.defaultValue = startingQuotaEntry.value; catchUpFactorEntry.value = Mathf.Clamp(profile.CatchUpFactor, catchUpFactorEntry.min, catchUpFactorEntry.max); catchUpFactorEntry.defaultValue = catchUpFactorEntry.value; quotaScalingModeEntry.index = Mathf.Clamp((int)profile.QuotaScalingMode, 0, QuotaScalingModeOptions.Count - 1); quotaPatternLengthEntry.value = Mathf.Clamp((float)Mathf.Max(1, Mathf.Min(profile.QuotaMultipliers.Count, 6)), quotaPatternLengthEntry.min, quotaPatternLengthEntry.max); quotaPatternLengthEntry.defaultValue = quotaPatternLengthEntry.value; for (int i = 0; i < quotaMultiplierEntries.Length; i++) { SliderSettingItem val = quotaMultiplierEntries[i]; val.max = Mathf.Max(100f, Mathf.Ceil(array[i])); val.value = Mathf.Clamp(array[i], val.min, val.max); val.defaultValue = val.value; } } finally { _isSynchronizing = false; } RefreshRuntimeSliderBindings(); } private static bool TryGetCurrentProfileForEditing(out TimingProfile? profile) { if (!TimingCoordinator.TryGetResolvedProfile("NativeLobbySettingsMenu.Sync", out profile) || profile == null) { return false; } return true; } private static bool IsMoreSettingsKey(string? key) { switch (key) { default: return IsQuotaMultiplierKey(key); case "moresettings.day-duration-minutes": case "moresettings.starting-money": case "moresettings.starting-quota": case "moresettings.catch-up-factor": case "moresettings.quota-scaling-mode": case "moresettings.quota-pattern-length": return true; } } public static bool IsManagedSliderKey(string? key) { switch (key) { default: return IsQuotaMultiplierKey(key); case "moresettings.day-duration-minutes": case "moresettings.starting-money": case "moresettings.starting-quota": case "moresettings.catch-up-factor": case "moresettings.quota-pattern-length": return true; } } public static bool IsStartingMoneyEditable() { if (!NetworkServer.active) { return !NetworkClient.active; } return false; } public static void RegisterSliderRuntimeBinding(string? key, Transform? root) { Transform root2 = root; if (!string.IsNullOrWhiteSpace(key) && !((Object)(object)root2 == (Object)null) && IsManagedSliderKey(key)) { SliderRuntimeBindings.RemoveAll((SliderRuntimeBinding binding) => (Object)(object)binding.Root == (Object)null); if (!SliderRuntimeBindings.Any((SliderRuntimeBinding binding) => (Object)(object)binding.Root == (Object)(object)root2)) { SliderRuntimeBindings.Add(new SliderRuntimeBinding(key, root2)); } ApplyRuntimeSliderPresentation(key, root2); } } public static void RefreshRuntimeSliderBindings() { SliderRuntimeBindings.RemoveAll((SliderRuntimeBinding binding) => (Object)(object)binding.Root == (Object)null); foreach (SliderRuntimeBinding sliderRuntimeBinding in SliderRuntimeBindings) { ApplyRuntimeSliderPresentation(sliderRuntimeBinding.Key, sliderRuntimeBinding.Root); } } private static SettingsLayout? GetActiveLobbySettingsLayout() { return _runtimeLobbySettingsLayout ?? _lobbySettingsLayout; } private static bool IsLobbySettingsLayout(SettingsLayout layout) { return FindTargetTab(layout) != null; } private static Tab? FindTargetTab(SettingsLayout layout) { foreach (Tab tab in layout.tabs) { if (tab.entries != null && tab.entries.Count != 0) { if (tab.entries.Any(IsLobbyModeEntry)) { return tab; } if (!string.IsNullOrWhiteSpace(tab.tabName) && string.Equals(tab.tabName.Trim(), "Settings", StringComparison.OrdinalIgnoreCase)) { return tab; } } } return null; } private static bool IsLobbyModeEntry(SettingItemBase entry) { if (!(entry is DropdownSettingItem)) { return false; } if (string.Equals(entry.key, "moresettings.section", StringComparison.OrdinalIgnoreCase)) { return false; } return string.Equals(entry.label?.Trim(), "Lobby Mode", StringComparison.OrdinalIgnoreCase); } private static int EnsureTitleEntry(ICollection entries, string key, string label) { string key2 = key; if (entries.Any((SettingItemBase entry) => entry.key == key2)) { return 0; } TitleSettingItem val = ScriptableObject.CreateInstance(); ((Object)val).hideFlags = (HideFlags)61; ((SettingItemBase)val).key = key2; ((SettingItemBase)val).label = label; entries.Add((SettingItemBase)(object)val); return 1; } private static int EnsureDropdownEntry(ICollection entries, string key, string label, IReadOnlyCollection options, int defaultIndex) { string key2 = key; if (entries.Any((SettingItemBase entry) => entry.key == key2)) { return 0; } DropdownSettingItem val = ScriptableObject.CreateInstance(); ((Object)val).hideFlags = (HideFlags)61; ((SettingItemBase)val).key = key2; ((SettingItemBase)val).label = label; val.options = options.ToList(); val.index = Mathf.Clamp(defaultIndex, 0, val.options.Count - 1); val.loadOnSceneStart = false; entries.Add((SettingItemBase)(object)val); return 1; } private static int EnsureSliderEntry(ICollection entries, string key, string label, float min, float max, bool wholeNumbers) { string key2 = key; if (entries.Any((SettingItemBase entry) => entry.key == key2)) { return 0; } SliderSettingItem val = ScriptableObject.CreateInstance(); ((Object)val).hideFlags = (HideFlags)61; ((SettingItemBase)val).key = key2; ((SettingItemBase)val).label = label; val.min = min; val.max = max; val.wholeNumbers = wholeNumbers; val.value = min; val.defaultValue = min; val.loadOnSceneStart = false; entries.Add((SettingItemBase)(object)val); return 1; } private static T? FindEntry(SettingsLayout layout, string key) where T : SettingItemBase { string key2 = key; foreach (Tab tab in layout.tabs) { if (tab.entries != null) { T val = tab.entries.OfType().FirstOrDefault((T entry) => ((SettingItemBase)entry).key == key2); if ((Object)(object)val != (Object)null) { return val; } } } return default(T); } private static bool TryGetEditableEntries(SettingsLayout layout, out SliderSettingItem? dayDurationEntry, out SliderSettingItem? startingMoneyEntry, out SliderSettingItem? startingQuotaEntry, out SliderSettingItem? catchUpFactorEntry, out DropdownSettingItem? quotaScalingModeEntry, out SliderSettingItem? quotaPatternLengthEntry, out SliderSettingItem[]? quotaMultiplierEntries) { dayDurationEntry = FindEntry(layout, "moresettings.day-duration-minutes"); startingMoneyEntry = FindEntry(layout, "moresettings.starting-money"); startingQuotaEntry = FindEntry(layout, "moresettings.starting-quota"); catchUpFactorEntry = FindEntry(layout, "moresettings.catch-up-factor"); quotaScalingModeEntry = FindEntry(layout, "moresettings.quota-scaling-mode"); quotaPatternLengthEntry = FindEntry(layout, "moresettings.quota-pattern-length"); quotaMultiplierEntries = GetQuotaMultiplierEntries(layout); if ((Object)(object)dayDurationEntry != (Object)null && (Object)(object)startingMoneyEntry != (Object)null && (Object)(object)startingQuotaEntry != (Object)null && (Object)(object)catchUpFactorEntry != (Object)null && (Object)(object)quotaScalingModeEntry != (Object)null && (Object)(object)quotaPatternLengthEntry != (Object)null) { return quotaMultiplierEntries.Length == 6; } return false; } private static SliderSettingItem[] GetQuotaMultiplierEntries(SettingsLayout layout) { SliderSettingItem[] array = (SliderSettingItem[])(object)new SliderSettingItem[6]; for (int i = 0; i < 6; i++) { SliderSettingItem val = FindEntry(layout, GetQuotaMultiplierKey(i)); if ((Object)(object)val == (Object)null) { return Array.Empty(); } array[i] = val; } return array; } private static QuotaScalingMode ResolveQuotaScalingMode(DropdownSettingItem entry) { if (entry.index != 1) { return QuotaScalingMode.Vanilla; } return QuotaScalingMode.CustomPattern; } private static float ComputeLinkedCurrencySliderMax(long startingMoney, long startingQuota) { float num = Mathf.Max(new float[3] { 100000f, startingMoney, startingQuota }); return Mathf.Max(100000f, Mathf.Ceil(num / 5000f) * 5000f); } private static long EnsureLinkedStartingQuota(long startingMoney, long startingQuota) { return Math.Max(startingMoney, startingQuota); } private static void ApplyCurrencyBounds(SliderSettingItem startingMoneyEntry, SliderSettingItem startingQuotaEntry, long startingMoney, long startingQuota) { float max = ComputeLinkedCurrencySliderMax(startingMoney, startingQuota); startingMoneyEntry.min = 0f; startingMoneyEntry.max = max; startingQuotaEntry.min = startingMoney; startingQuotaEntry.max = max; } private static void ApplyRuntimeSliderPresentation(string key, Transform root) { SettingsLayout activeLobbySettingsLayout = GetActiveLobbySettingsLayout(); SliderSettingItem val = (((Object)(object)activeLobbySettingsLayout != (Object)null) ? FindEntry(activeLobbySettingsLayout, key) : null); if ((Object)(object)val != (Object)null) { SyncRuntimeSlider(root, val); } TMP_InputField val2 = FindRuntimeValueInput(root); if ((Object)(object)val2 != (Object)null) { ApplyFixedWidth(((Component)val2).gameObject); } Transform val3 = root.Find("ValueText"); if ((Object)(object)val3 != (Object)null) { ApplyFixedWidth(((Component)val3).gameObject); } if (string.Equals(key, "moresettings.starting-money", StringComparison.OrdinalIgnoreCase)) { SetInteractable(root, IsStartingMoneyEditable()); } } private static void SyncRuntimeSlider(Transform root, SliderSettingItem entry) { Slider componentInChildren = ((Component)root).GetComponentInChildren(true); if ((Object)(object)componentInChildren != (Object)null) { componentInChildren.minValue = entry.min; componentInChildren.maxValue = entry.max; componentInChildren.wholeNumbers = entry.wholeNumbers; componentInChildren.SetValueWithoutNotify(Mathf.Clamp(entry.value, componentInChildren.minValue, componentInChildren.maxValue)); } string textWithoutNotify = (entry.wholeNumbers ? Mathf.Round(entry.value).ToString() : entry.value.ToString("F1")); TMP_InputField val = FindRuntimeValueInput(root); if ((Object)(object)val != (Object)null) { val.SetTextWithoutNotify(textWithoutNotify); } } private static TMP_InputField? FindRuntimeValueInput(Transform root) { TMP_InputField componentInChildren = ((Component)root).GetComponentInChildren(true); if ((Object)(object)componentInChildren != (Object)null) { return componentInChildren; } Transform val = root.Find("ValueText"); if (!((Object)(object)val != (Object)null)) { return null; } return ((Component)val).GetComponent(); } private static void ApplyFixedWidth(GameObject gameObject) { LayoutElement obj = gameObject.GetComponent() ?? gameObject.AddComponent(); obj.minWidth = 120f; obj.preferredWidth = 120f; obj.flexibleWidth = 0f; RectTransform component = gameObject.GetComponent(); if ((Object)(object)component != (Object)null) { component.SetSizeWithCurrentAnchors((Axis)0, 120f); } } private static void SetInteractable(Transform root, bool isInteractable) { Selectable[] componentsInChildren = ((Component)root).GetComponentsInChildren(true); for (int i = 0; i < componentsInChildren.Length; i++) { componentsInChildren[i].interactable = isInteractable; } } private static string GetQuotaMultiplierKey(int index) { return $"moresettings.quota-multiplier-{index + 1}"; } private static bool IsQuotaMultiplierKey(string? key) { if (!string.IsNullOrWhiteSpace(key)) { return key.StartsWith("moresettings.quota-multiplier-", StringComparison.OrdinalIgnoreCase); } return false; } } public static class QuotaPatternEditor { public static float[] BuildEditableValues(IReadOnlyList sourceValues, int slotCount) { if (slotCount <= 0) { throw new ArgumentOutOfRangeException("slotCount"); } float[] array = new float[slotCount]; float num = ((sourceValues != null && sourceValues.Count > 0) ? sourceValues[sourceValues.Count - 1] : 1f); for (int i = 0; i < slotCount; i++) { array[i] = ((i < sourceValues.Count) ? sourceValues[i] : num); } return array; } public static float[] BuildPattern(IReadOnlyList editableValues, int desiredLength) { if (editableValues == null) { throw new ArgumentNullException("editableValues"); } if (editableValues.Count == 0) { return Array.Empty(); } int num = Math.Max(1, Math.Min(desiredLength, editableValues.Count)); float[] array = new float[num]; for (int i = 0; i < num; i++) { array[i] = editableValues[i]; } return array; } } public static class QuotaRuntimeStatePlanner { public static int ComputeRemainingDays(int daysBeforeQuota, int daysPassed) { return Math.Max(0, daysBeforeQuota - daysPassed); } public static bool ShouldResetInitialQuota(long previousStartingQuota, long currentQuota, long requiredQuota, int daysPassed, int successfulQuota) { if (successfulQuota == 0 && daysPassed == 0 && currentQuota == previousStartingQuota) { return requiredQuota == previousStartingQuota; } return false; } public static bool ShouldResetInitialMoney(long previousStartingMoney, long currentMoney, int daysPassed, int successfulQuota) { if (successfulQuota == 0 && daysPassed == 0) { return currentMoney == previousStartingMoney; } return false; } } public static class TimingCoordinator { private static readonly FieldRef GameSettingsRef = AccessTools.FieldRefAccess("_gs"); private static readonly FieldRef CurrentSaveDataRef = AccessTools.FieldRefAccess("currentSaveData"); private static readonly PropertyInfo? HasDayStartedProperty = AccessTools.Property(typeof(GameManager), "HasDayStarted"); private static readonly PropertyInfo? NetworkTimerProperty = AccessTools.Property(typeof(GameManager), "Network_timer"); private static readonly PropertyInfo? NetworkCurrentQuotaProperty = AccessTools.Property(typeof(GameManager), "NetworkcurrentQuota"); private static readonly PropertyInfo? NetworkRequiredQuotaProperty = AccessTools.Property(typeof(GameManager), "NetworkrequiredQuotaToNextFloor"); private static readonly PropertyInfo? NetworkDaysLeftProperty = AccessTools.Property(typeof(GameManager), "NetworkdaysLeft"); private static readonly PropertyInfo? NetworkDaysPassedProperty = AccessTools.Property(typeof(GameManager), "NetworkdaysPassed"); private static readonly PropertyInfo? NetworkSuccessfulQuotaProperty = AccessTools.Property(typeof(GameManager), "NetworksuccessfulQuota"); private static ActiveSettings? _settings; private static ManualLogSource? _log; private static TimingProfile? _vanillaProfile; public static SessionTimingState? CurrentState { get; private set; } public static void Initialize(ActiveSettings settings, ManualLogSource log) { _settings = settings; _log = log; _vanillaProfile = null; CurrentState = null; } public static bool TryApplyFromResources(string context) { EnsureInitialized(); if (!TryGetActiveGameSettings(context, out GameSettings gameSettings, includeSceneInstance: false)) { return false; } return TryApplyToGameSettings(gameSettings, context); } public static bool TryApplyToGameSettings(GameSettings gameSettings, string context) { EnsureInitialized(); CaptureVanillaProfile(gameSettings); if (!_settings.IsEnabled) { TimingProfile baseProfile = GetBaseProfile(gameSettings); CurrentState = BuildState(baseProfile.Name, baseProfile.IsVanillaProfile, baseProfile.DayDurationSeconds, baseProfile.DaysBeforeQuota, baseProfile.StartingQuota, baseProfile.StartingMoney, baseProfile.CatchUpFactor, baseProfile.QuotaScalingMode, baseProfile.QuotaMultipliers.Count, context); return false; } if (!TryResolveProfile(gameSettings, context, out TimingProfile profile)) { return false; } TimingProfile profile2 = profile; ApplyResolvedProfileToGameSettings(gameSettings, profile2, context); return true; } public static bool TryApplyInitialQuotaToSaveData(SaveData saveData, string context) { EnsureInitialized(); if (saveData == null || !_settings.IsEnabled) { return false; } if (!TryGetActiveGameSettings(context, out GameSettings gameSettings)) { return false; } if (!TryResolveProfile(gameSettings, context, out TimingProfile profile)) { return false; } ApplyResolvedProfileToSaveData(saveData, profile, context, resetQuotaState: true); return true; } public static bool TryApplyToActiveSaveData(string context) { EnsureInitialized(); if (!_settings.IsEnabled) { return false; } SaveManager val = Object.FindFirstObjectByType(); if ((Object)(object)val == (Object)null) { return false; } SaveData val2 = CurrentSaveDataRef.Invoke(val); if (val2 == null) { return false; } GameManager val3 = Object.FindFirstObjectByType(); if ((Object)(object)val3 != (Object)null && !CanUpdatePreDayRuntimeState(val3)) { return false; } if (!TryGetActiveGameSettings(context, out GameSettings gameSettings)) { return false; } if (!TryResolveProfile(gameSettings, context, out TimingProfile profile)) { return false; } ApplyResolvedProfileToSaveData(val2, profile, context, resetQuotaState: true); return true; } public static bool TryGetResolvedProfile(string context, out TimingProfile? profile) { EnsureInitialized(); if (!TryGetActiveGameSettings(context, out GameSettings gameSettings)) { profile = null; return false; } if (!_settings.IsEnabled) { profile = GetBaseProfile(gameSettings); return true; } return TryResolveProfile(gameSettings, context, out profile); } public static bool TryApplyManualOverrides(TimingProfile previousProfile, TimingProfile updatedProfile, string context, out IReadOnlyList outcomes) { EnsureInitialized(); if (!TryGetActiveGameSettings(context, out GameSettings gameSettings)) { outcomes = new ValidationOutcome[1] { ValidationOutcome.Error("GameSettings", "No active GameSettings were available to apply manual overrides.") }; return false; } ValidationOutcome[] array = (outcomes = TimingProfileValidator.Validate(updatedProfile)).Where((ValidationOutcome outcome) => outcome.Status == ValidationStatus.Error).ToArray(); if (array.Length != 0) { ValidationOutcome[] array2 = array; foreach (ValidationOutcome validationOutcome in array2) { _log.LogError((object)("[" + context + "] " + validationOutcome.TargetField + ": " + validationOutcome.Message)); } return false; } CaptureVanillaProfile(gameSettings); _settings.SetManualOverrides(updatedProfile); if (!_settings.TryCreateResolvedProfile(GetBaseProfile(gameSettings), out TimingProfile profile, out IReadOnlyList outcomes2)) { outcomes = outcomes2; foreach (ValidationOutcome item in outcomes2.Where((ValidationOutcome outcome) => outcome.Status == ValidationStatus.Error)) { _log.LogError((object)("[" + context + "] " + item.TargetField + ": " + item.Message)); } return false; } outcomes = outcomes2; ApplyResolvedProfileToGameSettings(gameSettings, profile, context); GameManager val = Object.FindFirstObjectByType(); if ((Object)(object)val != (Object)null) { ApplyManualProfileToGameManager(val, previousProfile, profile, context); } SaveManager val2 = Object.FindFirstObjectByType(); if ((Object)(object)val2 != (Object)null) { SaveData val3 = CurrentSaveDataRef.Invoke(val2); if (val3 != null) { ApplyManualProfileToSaveData(val3, previousProfile, profile, context); } } return true; } public static bool TryApplyToGameManagerRuntime(GameManager gameManager, string context) { EnsureInitialized(); if ((Object)(object)gameManager == (Object)null || !_settings.IsEnabled) { return false; } GameSettings val = GameSettingsRef.Invoke(gameManager); if ((Object)(object)val == (Object)null) { return false; } if (!TryResolveProfile(val, context, out TimingProfile profile)) { return false; } ApplyResolvedProfileToGameManager(gameManager, profile, context); return true; } private static bool TryResolveProfile(GameSettings gameSettings, string context, out TimingProfile? profile) { CaptureVanillaProfile(gameSettings); if (!_settings.TryCreateResolvedProfile(GetBaseProfile(gameSettings), out TimingProfile profile2, out IReadOnlyList outcomes)) { foreach (ValidationOutcome item in outcomes.Where((ValidationOutcome o) => o.Status == ValidationStatus.Error)) { _log.LogError((object)("[" + context + "] " + item.TargetField + ": " + item.Message)); } profile = null; return false; } profile = profile2; return true; } private static void CaptureVanillaProfile(GameSettings gameSettings) { if (_vanillaProfile == null && !((Object)(object)gameSettings == (Object)null)) { _vanillaProfile = new TimingProfile("Vanilla", isVanillaProfile: true, gameSettings.dayDuration, gameSettings.daysBeforeQuota, gameSettings.startingQuota, gameSettings.startingMoney, gameSettings.catchUpFactor, QuotaScalingMode.Vanilla, (gameSettings.quotas ?? Array.Empty()).ToArray()); } } private static TimingProfile GetBaseProfile(GameSettings gameSettings) { CaptureVanillaProfile(gameSettings); return _vanillaProfile ?? new TimingProfile("Vanilla", isVanillaProfile: true, gameSettings.dayDuration, gameSettings.daysBeforeQuota, gameSettings.startingQuota, gameSettings.startingMoney, gameSettings.catchUpFactor, QuotaScalingMode.Vanilla, (gameSettings.quotas ?? Array.Empty()).ToArray()); } private static bool TryGetActiveGameSettings(string context, out GameSettings? gameSettings, bool includeSceneInstance = true) { if (includeSceneInstance) { GameManager val = Object.FindFirstObjectByType(); if ((Object)(object)val != (Object)null) { GameSettings val2 = GameSettingsRef.Invoke(val); if ((Object)(object)val2 != (Object)null) { gameSettings = val2; return true; } } } gameSettings = Resources.Load("GameSettings"); if ((Object)(object)gameSettings == (Object)null) { _log.LogDebug((object)("No GameSettings resource was available during " + context + ".")); return false; } return true; } private static void ApplyResolvedProfileToGameSettings(GameSettings gameSettings, TimingProfile profile, string context) { gameSettings.dayDuration = profile.DayDurationSeconds; gameSettings.startingQuota = profile.StartingQuota; gameSettings.startingMoney = profile.StartingMoney; gameSettings.catchUpFactor = profile.CatchUpFactor; gameSettings.quotas = profile.QuotaMultipliers.ToArray(); CurrentState = BuildState(profile.Name, profile.IsVanillaProfile, profile.DayDurationSeconds, profile.DaysBeforeQuota, profile.StartingQuota, profile.StartingMoney, profile.CatchUpFactor, profile.QuotaScalingMode, profile.QuotaMultipliers.Count, context); _log.LogInfo((object)("[" + context + "] Applied timing profile '" + profile.Name + "' " + $"(dayDuration={profile.DayDurationSeconds}, daysBeforeQuota={profile.DaysBeforeQuota}, " + $"startingQuota={profile.StartingQuota}, startingMoney={profile.StartingMoney}, catchUpFactor={profile.CatchUpFactor}, " + $"quotaScalingMode={profile.QuotaScalingMode}, " + $"quotaMultipliers={profile.QuotaMultipliers.Count}).")); } private static void ApplyResolvedProfileToGameManager(GameManager gameManager, TimingProfile profile, string context, bool resetQuotaState = true) { if (CanUpdatePreDayRuntimeState(gameManager)) { NetworkTimerProperty?.SetValue(gameManager, profile.DayDurationSeconds); if (resetQuotaState) { NetworkCurrentQuotaProperty?.SetValue(gameManager, profile.StartingQuota); NetworkRequiredQuotaProperty?.SetValue(gameManager, profile.StartingQuota); } _log.LogInfo((object)("[" + context + "] Applied pre-day runtime state to GameManager " + $"(timer={profile.DayDurationSeconds}, quotaReset={resetQuotaState}, " + $"currentQuota={profile.StartingQuota}, requiredQuota={profile.StartingQuota}).")); } } private static void ApplyManualProfileToGameManager(GameManager gameManager, TimingProfile previousProfile, TimingProfile updatedProfile, string context) { if (CanUpdatePreDayRuntimeState(gameManager)) { NetworkTimerProperty?.SetValue(gameManager, updatedProfile.DayDurationSeconds); int daysPassed = ReadIntProperty(NetworkDaysPassedProperty, gameManager); int successfulQuota = ReadIntProperty(NetworkSuccessfulQuotaProperty, gameManager); bool flag = QuotaRuntimeStatePlanner.ShouldResetInitialQuota(previousProfile.StartingQuota, ReadLongProperty(NetworkCurrentQuotaProperty, gameManager), ReadLongProperty(NetworkRequiredQuotaProperty, gameManager), daysPassed, successfulQuota); if (flag) { NetworkCurrentQuotaProperty?.SetValue(gameManager, updatedProfile.StartingQuota); NetworkRequiredQuotaProperty?.SetValue(gameManager, updatedProfile.StartingQuota); } _log.LogInfo((object)("[" + context + "] Applied manual pre-day runtime state to GameManager " + $"(timer={updatedProfile.DayDurationSeconds}, preservedQuota={!flag}).")); } } private static void ApplyResolvedProfileToSaveData(SaveData saveData, TimingProfile profile, string context, bool resetQuotaState) { if (resetQuotaState) { saveData.currentQuota = profile.StartingQuota; saveData.requiredQuotaToNextFloor = profile.StartingQuota; saveData.money = profile.StartingMoney; TrySyncMoneyManagerBalance(profile.StartingMoney); } _log.LogInfo((object)("[" + context + "] Applied save timing state " + $"(quotaReset={resetQuotaState}, currentQuota={saveData.currentQuota}, " + $"requiredQuota={saveData.requiredQuotaToNextFloor}, money={saveData.money}).")); } private static void ApplyManualProfileToSaveData(SaveData saveData, TimingProfile previousProfile, TimingProfile updatedProfile, string context) { bool flag = QuotaRuntimeStatePlanner.ShouldResetInitialQuota(previousProfile.StartingQuota, saveData.currentQuota, saveData.requiredQuotaToNextFloor, saveData.daysPassed, saveData.successfulQuota); bool flag2 = QuotaRuntimeStatePlanner.ShouldResetInitialMoney(previousProfile.StartingMoney, saveData.money, saveData.daysPassed, saveData.successfulQuota); if (flag) { saveData.currentQuota = updatedProfile.StartingQuota; saveData.requiredQuotaToNextFloor = updatedProfile.StartingQuota; } if (flag2) { saveData.money = updatedProfile.StartingMoney; } if (flag2) { TrySyncMoneyManagerBalance(updatedProfile.StartingMoney); } _log.LogInfo((object)("[" + context + "] Updated active save state " + $"(preservedQuota={!flag}, preservedMoney={!flag2}).")); } private static bool CanUpdatePreDayRuntimeState(GameManager gameManager) { return !(HasDayStartedProperty?.GetValue(gameManager) as bool?).GetValueOrDefault(); } private static void TrySyncMoneyManagerBalance(long balance) { MoneyManager val = Object.FindFirstObjectByType(); if (!((Object)(object)val == (Object)null)) { val.SetBalance(balance, (PlayerProfile)null, (ChangeType)3); } } private static int ReadIntProperty(PropertyInfo? property, object instance) { object obj = property?.GetValue(instance); if (!(obj is int result)) { if (obj is long num) { return checked((int)num); } return 0; } return result; } private static long ReadLongProperty(PropertyInfo? property, object instance) { object obj = property?.GetValue(instance); if (!(obj is long result)) { if (obj is int num) { return num; } return 0L; } return result; } private static SessionTimingState BuildState(string profileName, bool isVanilla, float dayDurationSeconds, int daysBeforeQuota, long startingQuota, long startingMoney, float catchUpFactor, QuotaScalingMode quotaScalingMode, int quotaMultiplierCount, string context) { return new SessionTimingState(context, profileName, isVanilla, dayDurationSeconds, daysBeforeQuota, startingQuota, startingMoney, catchUpFactor, quotaScalingMode, quotaMultiplierCount, DateTimeOffset.UtcNow); } private static void EnsureInitialized() { if (_settings == null || _log == null) { throw new InvalidOperationException("TimingCoordinator.Initialize must run before timing overrides are applied."); } } } } namespace MoreSettings.Patches { [HarmonyPatch(typeof(GameManager), "OnAwake")] internal static class DayTimerPatches { private static readonly FieldRef GameSettingsRef = AccessTools.FieldRefAccess("_gs"); private static void Postfix(GameManager __instance) { if ((Object)(object)__instance == (Object)null) { return; } GameSettings val = GameSettingsRef.Invoke(__instance); if (!((Object)(object)val == (Object)null)) { if (!NetworkServer.active) { LobbyVisibility.ClearClientState(); return; } TimingCoordinator.TryApplyToGameSettings(val, "GameManager.OnAwake"); TimingCoordinator.TryApplyToGameManagerRuntime(__instance, "GameManager.OnAwake"); LobbyVisibility.BroadcastCurrentState(); } } } [HarmonyPatch(typeof(GameManager))] internal static class LobbyPatches { [HarmonyPostfix] [HarmonyPatch("OnStartServer")] private static void OnStartServer_Postfix() { LobbyVisibility.BroadcastCurrentState(); } [HarmonyPostfix] [HarmonyPatch("ServerOnClientScenePlayReady")] private static void ServerOnClientScenePlayReady_Postfix(NetworkConnectionToClient conn) { LobbyVisibility.SendToClient(conn); } [HarmonyPostfix] [HarmonyPatch("RpcSetInLobbyPresence")] private static void RpcSetInLobbyPresence_Postfix(GameManager __instance) { if (!((Object)(object)__instance == (Object)null) && NetworkServer.active) { TimingCoordinator.TryApplyToGameManagerRuntime(__instance, "GameManager.RpcSetInLobbyPresence"); TimingCoordinator.TryApplyToActiveSaveData("GameManager.RpcSetInLobbyPresence"); NativeLobbySettingsMenu.RefreshRuntimeSliderBindings(); LobbyVisibility.BroadcastCurrentState(); } } } [HarmonyPatch] internal static class NativeLobbySettingsRuntimeUIPatches { private static readonly FieldRef LayoutRef = AccessTools.FieldRefAccess("layout"); [HarmonyPrefix] [HarmonyPatch(typeof(SettingsLayoutRuntimeUI), "Awake")] private static void SettingsLayoutRuntimeUI_Awake_Prefix(SettingsLayoutRuntimeUI __instance) { if (!((Object)(object)__instance == (Object)null)) { SettingsLayout val = LayoutRef.Invoke(__instance); if (!((Object)(object)val == (Object)null)) { NativeLobbySettingsMenu.RegisterRuntimeLobbyLayout(val, "SettingsLayoutRuntimeUI.Awake"); NativeLobbySettingsMenu.EnsureInjected(val, "SettingsLayoutRuntimeUI.Awake"); } } } } [HarmonyPatch] internal static class NativeLobbySettingsButtonPatches { private static readonly FieldRef LayoutRef = AccessTools.FieldRefAccess("settingsLayout"); [HarmonyPostfix] [HarmonyPatch(typeof(LobbyModeDropdownButton), "Awake")] private static void LobbyModeDropdownButton_Awake_Postfix(LobbyModeDropdownButton __instance) { if (!((Object)(object)__instance == (Object)null)) { SettingsLayout val = LayoutRef.Invoke(__instance); if (!((Object)(object)val == (Object)null)) { NativeLobbySettingsMenu.RegisterLobbyLayout(val, "LobbyModeDropdownButton.Awake"); NativeLobbySettingsMenu.EnsureInjected(val, "LobbyModeDropdownButton.Awake"); } } } [HarmonyPrefix] [HarmonyPatch(typeof(LobbyModeDropdownButton), "OnClick")] private static void LobbyModeDropdownButton_OnClick_Prefix(LobbyModeDropdownButton __instance) { if (!((Object)(object)__instance == (Object)null)) { SettingsLayout val = LayoutRef.Invoke(__instance); if (!((Object)(object)val == (Object)null)) { NativeLobbySettingsMenu.RefreshFromLobbyButton(val); } } } } [HarmonyPatch(typeof(SettingItemBase), "NotifyChanged")] internal static class NativeLobbySettingChangePatches { private static void Postfix(SettingItemBase __instance) { NativeLobbySettingsMenu.HandleSettingChanged(__instance); } } [HarmonyPatch] internal static class NativeLobbySliderPresentationPatches { [HarmonyPrefix] [HarmonyPatch(typeof(SettingsLayoutRuntimeUI), "CreateSliderEntry")] private static void CreateSliderEntry_Prefix(RectTransform parent, out int __state) { __state = (((Object)(object)parent != (Object)null) ? ((Transform)parent).childCount : 0); } [HarmonyPostfix] [HarmonyPatch(typeof(SettingsLayoutRuntimeUI), "CreateSliderEntry")] private static void CreateSliderEntry_Postfix(RectTransform parent, SliderSettingItem entry, int __state) { if (!((Object)(object)parent == (Object)null) && !((Object)(object)entry == (Object)null) && NativeLobbySettingsMenu.IsManagedSliderKey(((SettingItemBase)entry).key) && ((Transform)parent).childCount > __state) { Transform child = ((Transform)parent).GetChild(((Transform)parent).childCount - 1); Transform root = ((child.childCount > 0) ? child.GetChild(0) : child); NativeLobbySettingsMenu.RegisterSliderRuntimeBinding(((SettingItemBase)entry).key, root); } } } [HarmonyPatch(typeof(LocalSaveManager), "CreateNewSave")] internal static class QuotaSaveInitializationPatches { private static readonly FieldRef CurrentSaveDataRef = AccessTools.FieldRefAccess("currentSaveData"); private static void Prefix() { if (NetworkServer.active) { TimingCoordinator.TryApplyFromResources("LocalSaveManager.CreateNewSave"); } } private static void Postfix() { if (!NetworkServer.active) { return; } SaveManager val = Object.FindFirstObjectByType(); if (!((Object)(object)val == (Object)null)) { SaveData val2 = CurrentSaveDataRef.Invoke(val); if (val2 != null) { TimingCoordinator.TryApplyInitialQuotaToSaveData(val2, "LocalSaveManager.CreateNewSave"); } } } } [HarmonyPatch(typeof(SaveManager), "ResetCurrentSaveToDefaults")] internal static class QuotaSaveResetPatches { private static readonly FieldRef CurrentSaveDataRef = AccessTools.FieldRefAccess("currentSaveData"); private static void Prefix() { if (NetworkServer.active) { TimingCoordinator.TryApplyFromResources("SaveManager.ResetCurrentSaveToDefaults"); } } private static void Postfix(SaveManager __instance) { if (NetworkServer.active && !((Object)(object)__instance == (Object)null)) { SaveData val = CurrentSaveDataRef.Invoke(__instance); if (val != null) { TimingCoordinator.TryApplyInitialQuotaToSaveData(val, "SaveManager.ResetCurrentSaveToDefaults"); } } } } } namespace MoreSettings.Network { public static class LobbyVisibility { private static ManualLogSource? _log; private static SessionTimingState? _clientState; public static void Initialize(ManualLogSource log) { _log = log; } public static SessionTimingState? GetVisibleState() { if (NetworkServer.active) { return TimingCoordinator.CurrentState; } if (NetworkClient.active) { return _clientState; } return TimingCoordinator.CurrentState; } public static void ClearClientState() { _clientState = null; } public static void RegisterMessageDelegates() { Writer.write = delegate(NetworkWriter writer, TimingConfigMessage msg) { NetworkWriterExtensions.WriteBool(writer, msg.IsVanilla); NetworkWriterExtensions.WriteString(writer, msg.ProfileName ?? string.Empty); NetworkWriterExtensions.WriteFloat(writer, msg.DayDurationSeconds); NetworkWriterExtensions.WriteInt(writer, msg.DaysBeforeQuota); NetworkWriterExtensions.WriteLong(writer, msg.StartingQuota); NetworkWriterExtensions.WriteLong(writer, msg.StartingMoney); NetworkWriterExtensions.WriteFloat(writer, msg.CatchUpFactor); NetworkWriterExtensions.WriteInt(writer, msg.QuotaScalingModeValue); NetworkWriterExtensions.WriteInt(writer, msg.QuotaMultiplierCount); }; Reader.read = delegate(NetworkReader reader) { TimingConfigMessage result = default(TimingConfigMessage); result.IsVanilla = NetworkReaderExtensions.ReadBool(reader); result.ProfileName = NetworkReaderExtensions.ReadString(reader); result.DayDurationSeconds = NetworkReaderExtensions.ReadFloat(reader); result.DaysBeforeQuota = NetworkReaderExtensions.ReadInt(reader); result.StartingQuota = NetworkReaderExtensions.ReadLong(reader); result.StartingMoney = NetworkReaderExtensions.ReadLong(reader); result.CatchUpFactor = NetworkReaderExtensions.ReadFloat(reader); result.QuotaScalingModeValue = NetworkReaderExtensions.ReadInt(reader); result.QuotaMultiplierCount = NetworkReaderExtensions.ReadInt(reader); return result; }; NetworkClient.RegisterHandler((Action)OnTimingConfigReceived, true); } public static void BroadcastCurrentState() { if (!NetworkServer.active) { return; } SessionTimingState currentState = TimingCoordinator.CurrentState; if (currentState != null) { NetworkServer.SendToAll(BuildMessage(currentState), 0, false); ManualLogSource? log = _log; if (log != null) { log.LogDebug((object)("[LobbyVisibility] Broadcast timing profile '" + currentState.ProfileName + "' to all clients.")); } } } public static void SendToClient(NetworkConnectionToClient conn) { if (NetworkServer.active) { SessionTimingState currentState = TimingCoordinator.CurrentState; if (currentState != null) { ((NetworkConnection)conn).Send(BuildMessage(currentState), 0); } } } private static TimingConfigMessage BuildMessage(SessionTimingState state) { TimingConfigMessage result = default(TimingConfigMessage); result.IsVanilla = state.IsVanilla; result.ProfileName = state.ProfileName; result.DayDurationSeconds = state.DayDurationSeconds; result.DaysBeforeQuota = state.DaysBeforeQuota; result.StartingQuota = state.StartingQuota; result.StartingMoney = state.StartingMoney; result.CatchUpFactor = state.CatchUpFactor; result.QuotaScalingModeValue = (int)state.QuotaScalingMode; result.QuotaMultiplierCount = state.QuotaMultiplierCount; return result; } private static void OnTimingConfigReceived(TimingConfigMessage msg) { if (NetworkServer.activeHost) { return; } _clientState = new SessionTimingState("TimingConfigMessage", string.IsNullOrWhiteSpace(msg.ProfileName) ? "Vanilla" : msg.ProfileName, msg.IsVanilla, msg.DayDurationSeconds, msg.DaysBeforeQuota, msg.StartingQuota, msg.StartingMoney, msg.CatchUpFactor, ResolveQuotaScalingMode(msg.QuotaScalingModeValue), msg.QuotaMultiplierCount, DateTimeOffset.UtcNow); if (msg.IsVanilla) { ManualLogSource? log = _log; if (log != null) { log.LogInfo((object)"[LobbyVisibility] Host is using vanilla timing — no overrides active."); } return; } ManualLogSource? log2 = _log; if (log2 != null) { log2.LogInfo((object)("[LobbyVisibility] Host timing profile '" + msg.ProfileName + "': " + $"dayDuration={msg.DayDurationSeconds}s, daysBeforeQuota={msg.DaysBeforeQuota}, " + $"startingQuota={msg.StartingQuota}, startingMoney={msg.StartingMoney}, catchUpFactor={msg.CatchUpFactor}, " + $"quotaScalingMode={ResolveQuotaScalingMode(msg.QuotaScalingModeValue)}, " + $"quotaMultiplierCount={msg.QuotaMultiplierCount}")); } } private static QuotaScalingMode ResolveQuotaScalingMode(int rawValue) { if (!Enum.IsDefined(typeof(QuotaScalingMode), rawValue)) { return QuotaScalingMode.Vanilla; } return (QuotaScalingMode)rawValue; } } public struct TimingConfigMessage : NetworkMessage { public bool IsVanilla; public string ProfileName; public float DayDurationSeconds; public int DaysBeforeQuota; public long StartingQuota; public long StartingMoney; public float CatchUpFactor; public int QuotaScalingModeValue; public int QuotaMultiplierCount; } } namespace MoreSettings.Models { public enum QuotaScalingMode { Vanilla, CustomPattern } public static class QuotaScalingModeParser { public static bool TryParse(string? rawValue, out QuotaScalingMode mode) { return Enum.TryParse(rawValue?.Trim(), ignoreCase: true, out mode); } public static QuotaScalingMode ParseOrDefault(string? rawValue, QuotaScalingMode fallback = QuotaScalingMode.Vanilla) { if (!TryParse(rawValue, out var mode)) { return fallback; } return mode; } } public sealed class SessionTimingState { public string Source { get; } public string ProfileName { get; } public bool IsVanilla { get; } public float DayDurationSeconds { get; } public int DaysBeforeQuota { get; } public long StartingQuota { get; } public long StartingMoney { get; } public float CatchUpFactor { get; } public QuotaScalingMode QuotaScalingMode { get; } public int QuotaMultiplierCount { get; } public DateTimeOffset AppliedAtUtc { get; } public SessionTimingState(string source, string profileName, bool isVanilla, float dayDurationSeconds, int daysBeforeQuota, long startingQuota, long startingMoney, float catchUpFactor, QuotaScalingMode quotaScalingMode, int quotaMultiplierCount, DateTimeOffset appliedAtUtc) { Source = source; ProfileName = profileName; IsVanilla = isVanilla; DayDurationSeconds = dayDurationSeconds; DaysBeforeQuota = daysBeforeQuota; StartingQuota = startingQuota; StartingMoney = startingMoney; CatchUpFactor = catchUpFactor; QuotaScalingMode = quotaScalingMode; QuotaMultiplierCount = quotaMultiplierCount; AppliedAtUtc = appliedAtUtc; } } public sealed class TimingProfile { public string Name { get; } public bool IsVanillaProfile { get; } public float DayDurationSeconds { get; } public int DaysBeforeQuota { get; } public long StartingQuota { get; } public long StartingMoney { get; } public float CatchUpFactor { get; } public QuotaScalingMode QuotaScalingMode { get; } public IReadOnlyList QuotaMultipliers { get; } public TimingProfile(string name, bool isVanillaProfile, float dayDurationSeconds, int daysBeforeQuota, long startingQuota, long startingMoney, float catchUpFactor, QuotaScalingMode quotaScalingMode, IReadOnlyList quotaMultipliers) { Name = name ?? throw new ArgumentNullException("name"); IsVanillaProfile = isVanillaProfile; DayDurationSeconds = dayDurationSeconds; DaysBeforeQuota = daysBeforeQuota; StartingQuota = startingQuota; StartingMoney = startingMoney; CatchUpFactor = catchUpFactor; QuotaScalingMode = quotaScalingMode; QuotaMultipliers = quotaMultipliers ?? throw new ArgumentNullException("quotaMultipliers"); } } public enum ValidationStatus { Valid, Warning, Error } public sealed class ValidationOutcome { public string TargetField { get; } public ValidationStatus Status { get; } public string Message { get; } public ValidationOutcome(string targetField, ValidationStatus status, string message) { TargetField = targetField; Status = status; Message = message; } public static ValidationOutcome Valid(string targetField, string message) { return new ValidationOutcome(targetField, ValidationStatus.Valid, message); } public static ValidationOutcome Warning(string targetField, string message) { return new ValidationOutcome(targetField, ValidationStatus.Warning, message); } public static ValidationOutcome Error(string targetField, string message) { return new ValidationOutcome(targetField, ValidationStatus.Error, message); } } } namespace MoreSettings.Configuration { public sealed class ActiveSettings { private readonly ConfigFile _config; public const float PreserveFloat = -1f; public const int PreserveInt = -1; public const long PreserveLong = -1L; private readonly ConfigEntry _enableCustomTiming; private readonly ConfigEntry _dayDurationSeconds; private readonly ConfigEntry _daysBeforeQuota; private readonly ConfigEntry _startingQuota; private readonly ConfigEntry _startingMoney; private readonly ConfigEntry _catchUpFactor; private readonly ConfigEntry _quotaScalingMode; private readonly ConfigEntry _quotaMultipliersCsv; private readonly ConfigEntry _activeProfileName; private readonly ProfileStore _profileStore; public bool IsEnabled => _enableCustomTiming.Value; public ProfileStore Profiles => _profileStore; public string ActiveProfileName => _activeProfileName.Value?.Trim() ?? string.Empty; private ActiveSettings(ConfigFile config, ConfigEntry enableCustomTiming, ConfigEntry dayDurationSeconds, ConfigEntry daysBeforeQuota, ConfigEntry startingQuota, ConfigEntry startingMoney, ConfigEntry catchUpFactor, ConfigEntry quotaScalingMode, ConfigEntry quotaMultipliersCsv, ConfigEntry activeProfileName, ProfileStore profileStore) { _config = config; _enableCustomTiming = enableCustomTiming; _dayDurationSeconds = dayDurationSeconds; _daysBeforeQuota = daysBeforeQuota; _startingQuota = startingQuota; _startingMoney = startingMoney; _catchUpFactor = catchUpFactor; _quotaScalingMode = quotaScalingMode; _quotaMultipliersCsv = quotaMultipliersCsv; _activeProfileName = activeProfileName; _profileStore = profileStore; } public static ActiveSettings Bind(ConfigFile config) { return Bind(config, LoadVanillaDefaults()); } public static ActiveSettings Bind(ConfigFile config, TimingProfile vanillaDefaults) { ConfigEntry enableCustomTiming = config.Bind("General", "EnableCustomTiming", false, "Enable host-authoritative timing overrides for new sessions."); ConfigEntry dayDurationSeconds = config.Bind("Timing", "DayDurationSeconds", vanillaDefaults.DayDurationSeconds, "Override the vanilla day duration in seconds. Defaults to the current vanilla value. Set -1 to preserve the loaded value."); ConfigEntry daysBeforeQuota = config.Bind("Timing", "DaysBeforeQuota", vanillaDefaults.DaysBeforeQuota, "Deprecated. Multi-day quota overrides are ignored. Defaults to the current vanilla value; set -1 to preserve the loaded value."); ConfigEntry startingQuota = config.Bind("Quota", "StartingQuota", vanillaDefaults.StartingQuota, "Override the starting quota for new sessions. Defaults to the current vanilla value. Set -1 to preserve the loaded value."); ConfigEntry startingMoney = config.Bind("Quota", "StartingMoney", vanillaDefaults.StartingMoney, "Override the starting money for new sessions. Defaults to the current vanilla value. Set -1 to preserve the loaded value."); ConfigEntry catchUpFactor = config.Bind("Quota", "CatchUpFactor", vanillaDefaults.CatchUpFactor, "Override the quota catch-up factor. Defaults to the current vanilla value. Set -1 to preserve the loaded value."); ConfigEntry quotaScalingMode = config.Bind("Quota", "QuotaScalingMode", QuotaScalingMode.Vanilla.ToString(), "Set the quota scaling mode to Vanilla or CustomPattern. Defaults to Vanilla."); ConfigEntry quotaMultipliersCsv = config.Bind("Quota", "QuotaMultipliersCsv", string.Empty, "Comma-separated quota multipliers. Leave blank to preserve the loaded values."); ConfigEntry activeProfileName = config.Bind("Profiles", "ActiveProfileName", string.Empty, "Name of a saved profile to load on startup. Overrides all individual Timing/Quota entries when non-empty. Use the ProfileStore API or edit profile files under BepInEx/config/MoreSettings/profiles/."); ProfileStore profileStore = new ProfileStore(Path.GetDirectoryName(config.ConfigFilePath) ?? AppDomain.CurrentDomain.BaseDirectory); return new ActiveSettings(config, enableCustomTiming, dayDurationSeconds, daysBeforeQuota, startingQuota, startingMoney, catchUpFactor, quotaScalingMode, quotaMultipliersCsv, activeProfileName, profileStore); } private static TimingProfile LoadVanillaDefaults() { GameSettings val = Resources.Load("GameSettings"); if ((Object)(object)val == (Object)null) { throw new InvalidOperationException("Could not load the GameSettings resource."); } return new TimingProfile("Vanilla", isVanillaProfile: true, val.dayDuration, val.daysBeforeQuota, val.startingQuota, val.startingMoney, val.catchUpFactor, QuotaScalingMode.Vanilla, (val.quotas ?? Array.Empty()).ToArray()); } public void SetManualOverrides(TimingProfile profile) { _enableCustomTiming.Value = true; _activeProfileName.Value = string.Empty; _dayDurationSeconds.Value = profile.DayDurationSeconds; _daysBeforeQuota.Value = -1; _startingQuota.Value = profile.StartingQuota; _startingMoney.Value = profile.StartingMoney; _catchUpFactor.Value = profile.CatchUpFactor; _quotaScalingMode.Value = profile.QuotaScalingMode.ToString(); _quotaMultipliersCsv.Value = ((profile.QuotaScalingMode == QuotaScalingMode.CustomPattern) ? string.Join(",", profile.QuotaMultipliers.Select((float multiplier) => multiplier.ToString(CultureInfo.InvariantCulture))) : string.Empty); _config.Save(); } public bool TryCreateResolvedProfile(TimingProfile baseProfile, out TimingProfile profile, out IReadOnlyList outcomes) { List list = new List(); StoredProfile storedProfile = null; string text = _activeProfileName.Value?.Trim() ?? string.Empty; if (!string.IsNullOrEmpty(text) && _profileStore.TryLoad(text, out StoredProfile profile2)) { storedProfile = profile2; } float dayDurationSeconds = ResolveFloat(storedProfile?.DayDurationSeconds ?? _dayDurationSeconds.Value, baseProfile.DayDurationSeconds); int daysBeforeQuota = baseProfile.DaysBeforeQuota; long startingQuota = ResolveLong(storedProfile?.StartingQuota ?? _startingQuota.Value, baseProfile.StartingQuota); long startingMoney = ResolveLong(storedProfile?.StartingMoney ?? _startingMoney.Value, baseProfile.StartingMoney); float catchUpFactor = ResolveFloat(storedProfile?.CatchUpFactor ?? _catchUpFactor.Value, baseProfile.CatchUpFactor); string text2 = _quotaMultipliersCsv.Value?.Trim() ?? string.Empty; QuotaScalingMode quotaScalingMode = ResolveQuotaScalingMode(storedProfile?.QuotaScalingMode, _quotaScalingMode.Value, storedProfile?.QuotaMultipliers, text2); float[] source = baseProfile.QuotaMultipliers.ToArray(); if (quotaScalingMode == QuotaScalingMode.CustomPattern) { float[] array = storedProfile?.QuotaMultipliers; if (array != null && array.Length > 0) { source = array; goto IL_0196; } } if (quotaScalingMode == QuotaScalingMode.CustomPattern) { if (!string.IsNullOrWhiteSpace(text2)) { if (TimingProfileValidator.TryParseQuotaMultipliers(text2, out float[] parsedValues, out ValidationOutcome error)) { source = parsedValues; } else if (error != null) { list.Add(error); } } else { source = new float[0]; } } goto IL_0196; IL_0196: string text3 = ((!string.IsNullOrEmpty(text)) ? text : "ActiveConfig"); profile = new TimingProfile(IsEnabled ? text3 : "Vanilla", !IsEnabled, dayDurationSeconds, daysBeforeQuota, startingQuota, startingMoney, catchUpFactor, quotaScalingMode, source.ToArray()); list.AddRange(TimingProfileValidator.Validate(profile)); outcomes = list; return list.All((ValidationOutcome outcome) => outcome.Status != ValidationStatus.Error); } private static float ResolveFloat(float value, float vanilla) { if (!(value > -1f)) { return vanilla; } return value; } private static int ResolveInt(int value, int vanilla) { if (value <= -1) { return vanilla; } return value; } private static long ResolveLong(long value, long vanilla) { if (value <= -1) { return vanilla; } return value; } private static QuotaScalingMode ResolveQuotaScalingMode(QuotaScalingMode? storedMode, string configuredMode, float[]? storedMultipliers, string rawMultipliers) { if (storedMode.HasValue) { return storedMode.Value; } if (QuotaScalingModeParser.TryParse(configuredMode, out var mode)) { return mode; } if (((storedMultipliers != null && storedMultipliers.Length != 0) ? 1 : 0) <= (false ? 1 : 0) && string.IsNullOrWhiteSpace(rawMultipliers)) { return QuotaScalingMode.Vanilla; } return QuotaScalingMode.CustomPattern; } } public sealed class ProfileStore { private readonly string _profilesDir; public ProfileStore(string configDir) { _profilesDir = Path.Combine(configDir, "MoreSettings", "profiles"); string[] array = new string[3] { Path.Combine(configDir, "LobbySettings", "profiles"), Path.Combine(configDir, "ConfigManager", "profiles"), Path.Combine(configDir, "TimeConfig", "profiles") }; foreach (string text in array) { if (!Directory.Exists(_profilesDir) && Directory.Exists(text)) { Directory.CreateDirectory(Path.GetDirectoryName(_profilesDir)); Directory.Move(text, _profilesDir); string text2 = Directory.GetParent(text)?.FullName; if (!string.IsNullOrWhiteSpace(text2) && Directory.Exists(text2) && !Directory.EnumerateFileSystemEntries(text2).Any()) { Directory.Delete(text2); } break; } } } public IReadOnlyList ListProfiles() { if (!Directory.Exists(_profilesDir)) { return Array.Empty(); } return (from f in Directory.GetFiles(_profilesDir, "*.profile") select Path.GetFileNameWithoutExtension(f) ?? Path.GetFileName(f)).OrderBy((string n) => n, StringComparer.OrdinalIgnoreCase).ToList(); } public bool TryLoad(string name, out StoredProfile? profile) { profile = null; string path = ProfilePath(name); if (!File.Exists(path)) { return false; } Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); string[] array = File.ReadAllLines(path); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length != 0 && text[0] != '#') { int num = text.IndexOf('='); if (num >= 1) { string key = text.Substring(0, num).Trim(); string text2 = text; int num2 = num + 1; dictionary[key] = text2.Substring(num2, text2.Length - num2).Trim(); } } } profile = new StoredProfile(ParseFloat(dictionary, "DayDurationSeconds", -1f), ParseInt(dictionary, "DaysBeforeQuota", -1), ParseLong(dictionary, "StartingQuota", -1L), ParseLong(dictionary, "StartingMoney", -1L), ParseFloat(dictionary, "CatchUpFactor", -1f), ParseQuotaScalingMode(dictionary), ParseMultipliers(dictionary)); return true; } public void Save(string name, TimingProfile profile) { Directory.CreateDirectory(_profilesDir); List list = new List { "# MoreSettings profile: " + name, "DayDurationSeconds=" + profile.DayDurationSeconds.ToString(CultureInfo.InvariantCulture), $"DaysBeforeQuota={profile.DaysBeforeQuota}", $"StartingQuota={profile.StartingQuota}", $"StartingMoney={profile.StartingMoney}", "CatchUpFactor=" + profile.CatchUpFactor.ToString(CultureInfo.InvariantCulture), $"QuotaScalingMode={profile.QuotaScalingMode}" }; if (profile.QuotaScalingMode == QuotaScalingMode.CustomPattern && profile.QuotaMultipliers.Count > 0) { list.Add("QuotaMultipliers=" + string.Join(",", profile.QuotaMultipliers.Select((float m) => m.ToString(CultureInfo.InvariantCulture)))); } File.WriteAllLines(ProfilePath(name), list); } public bool Delete(string name) { string path = ProfilePath(name); if (!File.Exists(path)) { return false; } File.Delete(path); return true; } private string ProfilePath(string name) { return Path.Combine(_profilesDir, name + ".profile"); } private static float ParseFloat(Dictionary props, string key, float fallback) { if (!props.TryGetValue(key, out string value) || !float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { return fallback; } return result; } private static int ParseInt(Dictionary props, string key, int fallback) { if (!props.TryGetValue(key, out string value) || !int.TryParse(value, out var result)) { return fallback; } return result; } private static long ParseLong(Dictionary props, string key, long fallback) { if (!props.TryGetValue(key, out string value) || !long.TryParse(value, out var result)) { return fallback; } return result; } private static QuotaScalingMode? ParseQuotaScalingMode(Dictionary props) { if (!props.TryGetValue("QuotaScalingMode", out string value) || string.IsNullOrWhiteSpace(value)) { return null; } if (!QuotaScalingModeParser.TryParse(value, out var mode)) { return null; } return mode; } private static float[] ParseMultipliers(Dictionary props) { if (!props.TryGetValue("QuotaMultipliers", out string value)) { return Array.Empty(); } string[] array = value.Split(','); float[] array2 = new float[array.Length]; for (int i = 0; i < array.Length; i++) { if (!float.TryParse(array[i].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out array2[i])) { return Array.Empty(); } } return array2; } } public sealed class StoredProfile { public float DayDurationSeconds { get; } public int DaysBeforeQuota { get; } public long StartingQuota { get; } public long StartingMoney { get; } public float CatchUpFactor { get; } public QuotaScalingMode? QuotaScalingMode { get; } public float[] QuotaMultipliers { get; } public StoredProfile(float dayDurationSeconds, int daysBeforeQuota, long startingQuota, long startingMoney, float catchUpFactor, QuotaScalingMode? quotaScalingMode, float[] quotaMultipliers) { DayDurationSeconds = dayDurationSeconds; DaysBeforeQuota = daysBeforeQuota; StartingQuota = startingQuota; StartingMoney = startingMoney; CatchUpFactor = catchUpFactor; QuotaScalingMode = quotaScalingMode; QuotaMultipliers = quotaMultipliers; } } public static class TimingProfileValidator { public const float MinDayDurationSeconds = 10f; public const float MaxDayDurationSeconds = 86400f; public const int MinDaysBeforeQuota = 1; public const int MaxDaysBeforeQuota = 30; public const long MaxQuotaValue = 1000000000000000000L; public const float MinCatchUpFactor = 0f; public const float MaxCatchUpFactor = 5f; public const float MinQuotaMultiplier = 0.01f; public const float MaxQuotaMultiplier = 100f; public static IReadOnlyList Validate(TimingProfile profile) { List list = new List(); if (profile.DayDurationSeconds < 10f || profile.DayDurationSeconds > 86400f) { list.Add(ValidationOutcome.Error("DayDurationSeconds", $"Day duration must be between {10f} and {86400f} seconds.")); } if (profile.DaysBeforeQuota < 1 || profile.DaysBeforeQuota > 30) { list.Add(ValidationOutcome.Error("DaysBeforeQuota", $"Days before quota must be between {1} and {30}.")); } if (profile.StartingQuota < 0 || profile.StartingQuota > 1000000000000000000L) { list.Add(ValidationOutcome.Error("StartingQuota", $"Starting quota must be between 0 and {1000000000000000000L}.")); } if (profile.StartingMoney < 0 || profile.StartingMoney > 1000000000000000000L) { list.Add(ValidationOutcome.Error("StartingMoney", $"Starting money must be between 0 and {1000000000000000000L}.")); } if (profile.StartingQuota < profile.StartingMoney) { list.Add(ValidationOutcome.Error("StartingQuota", "Starting quota must be greater than or equal to starting money.")); } if (profile.CatchUpFactor < 0f || profile.CatchUpFactor > 5f) { list.Add(ValidationOutcome.Error("CatchUpFactor", $"Catch-up factor must be between {0f} and {5f}.")); } if (profile.QuotaScalingMode == QuotaScalingMode.CustomPattern) { if (profile.QuotaMultipliers.Count == 0) { list.Add(ValidationOutcome.Error("QuotaMultipliers", "At least one quota multiplier is required when custom quota scaling is enabled.")); } else if (profile.QuotaMultipliers.Any((float multiplier) => multiplier < 0.01f || multiplier > 100f)) { list.Add(ValidationOutcome.Error("QuotaMultipliers", $"Each quota multiplier must be between {0.01f} and {100f}.")); } } if (list.Count == 0) { list.Add(ValidationOutcome.Valid("Profile", "Timing profile values are valid.")); } return list; } public static bool TryParseQuotaMultipliers(string rawValue, out float[] parsedValues, out ValidationOutcome? error) { string[] array = (from token in rawValue.Split(new char[3] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries) select token.Trim()).ToArray(); if (array.Length == 0) { parsedValues = new float[0]; error = ValidationOutcome.Error("rawValue", "Quota multipliers cannot be empty when an override string is provided."); return false; } List list = new List(array.Length); string[] array2 = array; foreach (string text in array2) { if (!float.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { parsedValues = new float[0]; error = ValidationOutcome.Error("rawValue", "'" + text + "' is not a valid floating-point quota multiplier."); return false; } list.Add(result); } parsedValues = list.ToArray(); error = null; return true; } } }