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 TimeConfig.Configuration;
using TimeConfig.Models;
using TimeConfig.Network;
using TimeConfig.Runtime;
using UnityEngine;
[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 TimeConfig
{
[BepInPlugin("com.lncinteractive", "TimeConfig", "0.2.0")]
public sealed class PluginMain : BaseUnityPlugin
{
public const string PluginGuid = "com.lncinteractive";
public const string PluginName = "TimeConfig";
public const string PluginVersion = "0.2.0";
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)"TimeConfig 0.2.0 initialized.");
}
private void OnDestroy()
{
Harmony? harmony = _harmony;
if (harmony != null)
{
harmony.UnpatchSelf();
}
}
}
}
namespace TimeConfig.Runtime
{
public static class NativeLobbySettingsMenu
{
private const int MaxEditableQuotaMultipliers = 6;
private static readonly List<string> QuotaScalingModeOptions = new List<string> { "Vanilla scaling", "Custom pattern" };
public const string SectionKey = "timeconfig.section";
public const string TimeSectionKey = "timeconfig.time.section";
public const string QuotaSectionKey = "timeconfig.quota.section";
public const string DayDurationMinutesKey = "timeconfig.day-duration-minutes";
public const string StartingQuotaKey = "timeconfig.starting-quota";
public const string CatchUpFactorKey = "timeconfig.catch-up-factor";
public const string QuotaScalingModeKey = "timeconfig.quota-scaling-mode";
public const string QuotaPatternLengthKey = "timeconfig.quota-pattern-length";
private static SettingsLayout? _lobbySettingsLayout;
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 bool EnsureInjected(SettingsLayout layout, string source)
{
if ((Object)(object)layout == (Object)null)
{
return false;
}
if (!IsLobbySettingsLayout(layout))
{
return false;
}
RegisterLobbyLayout(layout, source);
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<SettingItemBase>();
}
int num = 0;
num += EnsureTitleEntry(val.entries, "timeconfig.section", "TimeConfig");
num += EnsureTitleEntry(val.entries, "timeconfig.time.section", "Time");
num += EnsureSliderEntry(val.entries, "timeconfig.day-duration-minutes", "Day duration (minutes)", 1f, 1440f, wholeNumbers: true);
num += EnsureTitleEntry(val.entries, "timeconfig.quota.section", "Quota");
num += EnsureSliderEntry(val.entries, "timeconfig.starting-quota", "Starting quota", 0f, 100000f, wholeNumbers: true);
num += EnsureSliderEntry(val.entries, "timeconfig.catch-up-factor", "Catch-up factor", 0f, 5f, wholeNumbers: false);
num += EnsureDropdownEntry(val.entries, "timeconfig.quota-scaling-mode", "Quota scaling", QuotaScalingModeOptions, 0);
num += EnsureSliderEntry(val.entries, "timeconfig.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();
}
public static void HandleSettingChanged(SettingItemBase entry)
{
if (!_isSynchronizing && !((Object)(object)entry == (Object)null) && IsTimeConfigKey(entry.key) && !((Object)(object)_lobbySettingsLayout == (Object)null) && TryGetCurrentProfileForEditing(out TimingProfile profile) && profile != null && TryGetEditableEntries(_lobbySettingsLayout, out SliderSettingItem dayDurationEntry, out SliderSettingItem startingQuotaEntry, out SliderSettingItem catchUpFactorEntry, out DropdownSettingItem quotaScalingModeEntry, out SliderSettingItem quotaPatternLengthEntry, out SliderSettingItem[] quotaMultiplierEntries))
{
QuotaScalingMode quotaScalingMode = ResolveQuotaScalingMode(quotaScalingModeEntry);
float[] array = QuotaPatternEditor.BuildPattern(quotaMultiplierEntries.Select((SliderSettingItem multiplierEntry) => multiplierEntry.value).ToArray(), Mathf.RoundToInt(quotaPatternLengthEntry.value));
float dayDurationSeconds = dayDurationEntry.value * 60f;
int daysBeforeQuota = profile.DaysBeforeQuota;
long startingQuota = (long)Mathf.Round(startingQuotaEntry.value);
float value = catchUpFactorEntry.value;
IReadOnlyList<float> quotaMultipliers;
if (quotaScalingMode != QuotaScalingMode.CustomPattern)
{
quotaMultipliers = profile.QuotaMultipliers;
}
else
{
IReadOnlyList<float> readOnlyList = array;
quotaMultipliers = readOnlyList;
}
TimingProfile updatedProfile = new TimingProfile("ActiveConfig", isVanillaProfile: false, dayDurationSeconds, daysBeforeQuota, startingQuota, value, quotaScalingMode, quotaMultipliers);
if (!TimingCoordinator.TryApplyManualOverrides(profile, updatedProfile, "NativeLobbySettingsMenu.NotifyChanged", out IReadOnlyList<ValidationOutcome> _))
{
SyncFromCurrentState();
return;
}
LobbyVisibility.BroadcastCurrentState();
SyncFromCurrentState();
}
}
private static void SyncFromCurrentState()
{
if ((Object)(object)_lobbySettingsLayout == (Object)null || !TryGetCurrentProfileForEditing(out TimingProfile profile) || profile == null || !TryGetEditableEntries(_lobbySettingsLayout, out SliderSettingItem dayDurationEntry, 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;
startingQuotaEntry.max = Mathf.Max(100000f, Mathf.Ceil((float)profile.StartingQuota / 5000f) * 5000f);
startingQuotaEntry.value = Mathf.Clamp((float)profile.StartingQuota, 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;
}
}
private static bool TryGetCurrentProfileForEditing(out TimingProfile? profile)
{
if (!TimingCoordinator.TryGetResolvedProfile("NativeLobbySettingsMenu.Sync", out profile) || profile == null)
{
return false;
}
return true;
}
private static bool IsTimeConfigKey(string? key)
{
switch (key)
{
default:
return IsQuotaMultiplierKey(key);
case "timeconfig.day-duration-minutes":
case "timeconfig.starting-quota":
case "timeconfig.catch-up-factor":
case "timeconfig.quota-scaling-mode":
case "timeconfig.quota-pattern-length":
return true;
}
}
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, "timeconfig.section", StringComparison.OrdinalIgnoreCase))
{
return false;
}
return string.Equals(entry.label?.Trim(), "Lobby Mode", StringComparison.OrdinalIgnoreCase);
}
private static int EnsureTitleEntry(ICollection<SettingItemBase> entries, string key, string label)
{
string key2 = key;
if (entries.Any((SettingItemBase entry) => entry.key == key2))
{
return 0;
}
TitleSettingItem val = ScriptableObject.CreateInstance<TitleSettingItem>();
((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<SettingItemBase> entries, string key, string label, IReadOnlyCollection<string> options, int defaultIndex)
{
string key2 = key;
if (entries.Any((SettingItemBase entry) => entry.key == key2))
{
return 0;
}
DropdownSettingItem val = ScriptableObject.CreateInstance<DropdownSettingItem>();
((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<SettingItemBase> 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<SliderSettingItem>();
((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<T>(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<T>().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? startingQuotaEntry, out SliderSettingItem? catchUpFactorEntry, out DropdownSettingItem? quotaScalingModeEntry, out SliderSettingItem? quotaPatternLengthEntry, out SliderSettingItem[]? quotaMultiplierEntries)
{
dayDurationEntry = FindEntry<SliderSettingItem>(layout, "timeconfig.day-duration-minutes");
startingQuotaEntry = FindEntry<SliderSettingItem>(layout, "timeconfig.starting-quota");
catchUpFactorEntry = FindEntry<SliderSettingItem>(layout, "timeconfig.catch-up-factor");
quotaScalingModeEntry = FindEntry<DropdownSettingItem>(layout, "timeconfig.quota-scaling-mode");
quotaPatternLengthEntry = FindEntry<SliderSettingItem>(layout, "timeconfig.quota-pattern-length");
quotaMultiplierEntries = GetQuotaMultiplierEntries(layout);
if ((Object)(object)dayDurationEntry != (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<SliderSettingItem>(layout, GetQuotaMultiplierKey(i));
if ((Object)(object)val == (Object)null)
{
return Array.Empty<SliderSettingItem>();
}
array[i] = val;
}
return array;
}
private static QuotaScalingMode ResolveQuotaScalingMode(DropdownSettingItem entry)
{
if (entry.index != 1)
{
return QuotaScalingMode.Vanilla;
}
return QuotaScalingMode.CustomPattern;
}
private static string GetQuotaMultiplierKey(int index)
{
return $"timeconfig.quota-multiplier-{index + 1}";
}
private static bool IsQuotaMultiplierKey(string? key)
{
if (!string.IsNullOrWhiteSpace(key))
{
return key.StartsWith("timeconfig.quota-multiplier-", StringComparison.OrdinalIgnoreCase);
}
return false;
}
}
public static class QuotaPatternEditor
{
public static float[] BuildEditableValues(IReadOnlyList<float> 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<float> editableValues, int desiredLength)
{
if (editableValues == null)
{
throw new ArgumentNullException("editableValues");
}
if (editableValues.Count == 0)
{
return Array.Empty<float>();
}
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 class TimingCoordinator
{
private static readonly FieldRef<GameManager, GameSettings> GameSettingsRef = AccessTools.FieldRefAccess<GameManager, GameSettings>("_gs");
private static readonly FieldRef<SaveManager, SaveData> CurrentSaveDataRef = AccessTools.FieldRefAccess<SaveManager, SaveData>("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.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 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<ValidationOutcome> 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<ValidationOutcome> 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<GameManager>();
if ((Object)(object)val != (Object)null)
{
ApplyManualProfileToGameManager(val, previousProfile, profile, context);
}
SaveManager val2 = Object.FindFirstObjectByType<SaveManager>();
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<ValidationOutcome> 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.catchUpFactor, QuotaScalingMode.Vanilla, (gameSettings.quotas ?? Array.Empty<float>()).ToArray());
}
}
private static TimingProfile GetBaseProfile(GameSettings gameSettings)
{
CaptureVanillaProfile(gameSettings);
return _vanillaProfile ?? new TimingProfile("Vanilla", isVanillaProfile: true, gameSettings.dayDuration, gameSettings.daysBeforeQuota, gameSettings.startingQuota, gameSettings.catchUpFactor, QuotaScalingMode.Vanilla, (gameSettings.quotas ?? Array.Empty<float>()).ToArray());
}
private static bool TryGetActiveGameSettings(string context, out GameSettings? gameSettings, bool includeSceneInstance = true)
{
if (includeSceneInstance)
{
GameManager val = Object.FindFirstObjectByType<GameManager>();
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>("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.catchUpFactor = profile.CatchUpFactor;
gameSettings.quotas = profile.QuotaMultipliers.ToArray();
CurrentState = BuildState(profile.Name, profile.IsVanillaProfile, profile.DayDurationSeconds, profile.DaysBeforeQuota, profile.StartingQuota, 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}, 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;
}
_log.LogInfo((object)("[" + context + "] Applied save timing state " + $"(quotaReset={resetQuotaState}, currentQuota={saveData.currentQuota}, " + $"requiredQuota={saveData.requiredQuotaToNextFloor})."));
}
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);
if (flag)
{
saveData.currentQuota = updatedProfile.StartingQuota;
saveData.requiredQuotaToNextFloor = updatedProfile.StartingQuota;
}
_log.LogInfo((object)("[" + context + "] Updated active save state " + $"(preservedQuota={!flag})."));
}
private static bool CanUpdatePreDayRuntimeState(GameManager gameManager)
{
return !(HasDayStartedProperty?.GetValue(gameManager) as bool?).GetValueOrDefault();
}
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, float catchUpFactor, QuotaScalingMode quotaScalingMode, int quotaMultiplierCount, string context)
{
return new SessionTimingState(context, profileName, isVanilla, dayDurationSeconds, daysBeforeQuota, startingQuota, 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 TimeConfig.Patches
{
[HarmonyPatch(typeof(GameManager), "OnAwake")]
internal static class DayTimerPatches
{
private static readonly FieldRef<GameManager, GameSettings> GameSettingsRef = AccessTools.FieldRefAccess<GameManager, GameSettings>("_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");
LobbyVisibility.BroadcastCurrentState();
}
}
}
[HarmonyPatch]
internal static class NativeLobbySettingsRuntimeUIPatches
{
private static readonly FieldRef<SettingsLayoutRuntimeUI, SettingsLayout> LayoutRef = AccessTools.FieldRefAccess<SettingsLayoutRuntimeUI, SettingsLayout>("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.EnsureInjected(val, "SettingsLayoutRuntimeUI.Awake");
}
}
}
}
[HarmonyPatch]
internal static class NativeLobbySettingsButtonPatches
{
private static readonly FieldRef<LobbyModeDropdownButton, SettingsLayout> LayoutRef = AccessTools.FieldRefAccess<LobbyModeDropdownButton, SettingsLayout>("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(typeof(LocalSaveManager), "CreateNewSave")]
internal static class QuotaSaveInitializationPatches
{
private static readonly FieldRef<SaveManager, SaveData> CurrentSaveDataRef = AccessTools.FieldRefAccess<SaveManager, SaveData>("currentSaveData");
private static void Prefix()
{
if (NetworkServer.active)
{
TimingCoordinator.TryApplyFromResources("LocalSaveManager.CreateNewSave");
}
}
private static void Postfix()
{
if (!NetworkServer.active)
{
return;
}
SaveManager val = Object.FindFirstObjectByType<SaveManager>();
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<SaveManager, SaveData> CurrentSaveDataRef = AccessTools.FieldRefAccess<SaveManager, SaveData>("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 TimeConfig.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<TimingConfigMessage>.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.WriteFloat(writer, msg.CatchUpFactor);
NetworkWriterExtensions.WriteInt(writer, msg.QuotaScalingModeValue);
NetworkWriterExtensions.WriteInt(writer, msg.QuotaMultiplierCount);
};
Reader<TimingConfigMessage>.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.CatchUpFactor = NetworkReaderExtensions.ReadFloat(reader);
result.QuotaScalingModeValue = NetworkReaderExtensions.ReadInt(reader);
result.QuotaMultiplierCount = NetworkReaderExtensions.ReadInt(reader);
return result;
};
NetworkClient.RegisterHandler<TimingConfigMessage>((Action<TimingConfigMessage>)OnTimingConfigReceived, true);
}
public static void BroadcastCurrentState()
{
if (!NetworkServer.active)
{
return;
}
SessionTimingState currentState = TimingCoordinator.CurrentState;
if (currentState != null)
{
NetworkServer.SendToAll<TimingConfigMessage>(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<TimingConfigMessage>(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.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.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}, 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 float CatchUpFactor;
public int QuotaScalingModeValue;
public int QuotaMultiplierCount;
}
}
namespace TimeConfig.Models
{
public enum QuotaScalingMode
{
Vanilla,
CustomPattern
}
public static class QuotaScalingModeParser
{
public static bool TryParse(string? rawValue, out QuotaScalingMode mode)
{
return Enum.TryParse<QuotaScalingMode>(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 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, float catchUpFactor, QuotaScalingMode quotaScalingMode, int quotaMultiplierCount, DateTimeOffset appliedAtUtc)
{
Source = source;
ProfileName = profileName;
IsVanilla = isVanilla;
DayDurationSeconds = dayDurationSeconds;
DaysBeforeQuota = daysBeforeQuota;
StartingQuota = startingQuota;
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 float CatchUpFactor { get; }
public QuotaScalingMode QuotaScalingMode { get; }
public IReadOnlyList<float> QuotaMultipliers { get; }
public TimingProfile(string name, bool isVanillaProfile, float dayDurationSeconds, int daysBeforeQuota, long startingQuota, float catchUpFactor, QuotaScalingMode quotaScalingMode, IReadOnlyList<float> quotaMultipliers)
{
Name = name ?? throw new ArgumentNullException("name");
IsVanillaProfile = isVanillaProfile;
DayDurationSeconds = dayDurationSeconds;
DaysBeforeQuota = daysBeforeQuota;
StartingQuota = startingQuota;
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 TimeConfig.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<bool> _enableCustomTiming;
private readonly ConfigEntry<float> _dayDurationSeconds;
private readonly ConfigEntry<int> _daysBeforeQuota;
private readonly ConfigEntry<long> _startingQuota;
private readonly ConfigEntry<float> _catchUpFactor;
private readonly ConfigEntry<string> _quotaScalingMode;
private readonly ConfigEntry<string> _quotaMultipliersCsv;
private readonly ConfigEntry<string> _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<bool> enableCustomTiming, ConfigEntry<float> dayDurationSeconds, ConfigEntry<int> daysBeforeQuota, ConfigEntry<long> startingQuota, ConfigEntry<float> catchUpFactor, ConfigEntry<string> quotaScalingMode, ConfigEntry<string> quotaMultipliersCsv, ConfigEntry<string> activeProfileName, ProfileStore profileStore)
{
_config = config;
_enableCustomTiming = enableCustomTiming;
_dayDurationSeconds = dayDurationSeconds;
_daysBeforeQuota = daysBeforeQuota;
_startingQuota = startingQuota;
_catchUpFactor = catchUpFactor;
_quotaScalingMode = quotaScalingMode;
_quotaMultipliersCsv = quotaMultipliersCsv;
_activeProfileName = activeProfileName;
_profileStore = profileStore;
}
public static ActiveSettings Bind(ConfigFile config)
{
ConfigEntry<bool> enableCustomTiming = config.Bind<bool>("General", "EnableCustomTiming", false, "Enable host-authoritative timing overrides for new sessions.");
ConfigEntry<float> dayDurationSeconds = config.Bind<float>("Timing", "DayDurationSeconds", -1f, "Override the vanilla day duration in seconds. Use -1 to preserve the loaded value.");
ConfigEntry<int> daysBeforeQuota = config.Bind<int>("Timing", "DaysBeforeQuota", -1, "Deprecated. Multi-day quota overrides are ignored; use -1 to preserve the loaded value.");
ConfigEntry<long> startingQuota = config.Bind<long>("Quota", "StartingQuota", -1L, "Override the starting quota for new sessions. Use -1 to preserve the loaded value.");
ConfigEntry<float> catchUpFactor = config.Bind<float>("Quota", "CatchUpFactor", -1f, "Override the quota catch-up factor. Use -1 to preserve the loaded value.");
ConfigEntry<string> quotaScalingMode = config.Bind<string>("Quota", "QuotaScalingMode", string.Empty, "Set the quota scaling mode to Vanilla or CustomPattern. Leave blank to keep legacy behavior based on whether a custom multiplier pattern is present.");
ConfigEntry<string> quotaMultipliersCsv = config.Bind<string>("Quota", "QuotaMultipliersCsv", string.Empty, "Comma-separated quota multipliers. Leave blank to preserve the loaded values.");
ConfigEntry<string> activeProfileName = config.Bind<string>("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/TimeConfig/profiles/.");
ProfileStore profileStore = new ProfileStore(Path.GetDirectoryName(config.ConfigFilePath) ?? AppDomain.CurrentDomain.BaseDirectory);
return new ActiveSettings(config, enableCustomTiming, dayDurationSeconds, daysBeforeQuota, startingQuota, catchUpFactor, quotaScalingMode, quotaMultipliersCsv, activeProfileName, profileStore);
}
public void SetManualOverrides(TimingProfile profile)
{
_enableCustomTiming.Value = true;
_activeProfileName.Value = string.Empty;
_dayDurationSeconds.Value = profile.DayDurationSeconds;
_daysBeforeQuota.Value = -1;
_startingQuota.Value = profile.StartingQuota;
_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<ValidationOutcome> outcomes)
{
List<ValidationOutcome> list = new List<ValidationOutcome>();
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);
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_0173;
}
}
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_0173;
IL_0173:
string text3 = ((!string.IsNullOrEmpty(text)) ? text : "ActiveConfig");
profile = new TimingProfile(IsEnabled ? text3 : "Vanilla", !IsEnabled, dayDurationSeconds, daysBeforeQuota, startingQuota, 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, "TimeConfig", "profiles");
}
public IReadOnlyList<string> ListProfiles()
{
if (!Directory.Exists(_profilesDir))
{
return Array.Empty<string>();
}
return (from f in Directory.GetFiles(_profilesDir, "*.profile")
select Path.GetFileNameWithoutExtension(f) ?? Path.GetFileName(f)).OrderBy<string, string>((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<string, string> dictionary = new Dictionary<string, string>(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), ParseFloat(dictionary, "CatchUpFactor", -1f), ParseQuotaScalingMode(dictionary), ParseMultipliers(dictionary));
return true;
}
public void Save(string name, TimingProfile profile)
{
Directory.CreateDirectory(_profilesDir);
List<string> list = new List<string>
{
"# TimeConfig profile: " + name,
"DayDurationSeconds=" + profile.DayDurationSeconds.ToString(CultureInfo.InvariantCulture),
$"DaysBeforeQuota={profile.DaysBeforeQuota}",
$"StartingQuota={profile.StartingQuota}",
"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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> props)
{
if (!props.TryGetValue("QuotaMultipliers", out string value))
{
return Array.Empty<float>();
}
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<float>();
}
}
return array2;
}
}
public sealed class StoredProfile
{
public float DayDurationSeconds { get; }
public int DaysBeforeQuota { get; }
public long StartingQuota { get; }
public float CatchUpFactor { get; }
public QuotaScalingMode? QuotaScalingMode { get; }
public float[] QuotaMultipliers { get; }
public StoredProfile(float dayDurationSeconds, int daysBeforeQuota, long startingQuota, float catchUpFactor, QuotaScalingMode? quotaScalingMode, float[] quotaMultipliers)
{
DayDurationSeconds = dayDurationSeconds;
DaysBeforeQuota = daysBeforeQuota;
StartingQuota = startingQuota;
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<ValidationOutcome> Validate(TimingProfile profile)
{
List<ValidationOutcome> list = new List<ValidationOutcome>();
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.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<float> list = new List<float>(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;
}
}
}