Some mods target the Mono version of the game, which is available by opting into the Steam beta branch "alternate"
Decompiled source of TimeNeverStops Mono v1.5.1
TimeNeverStops_Mono.dll
Decompiled a month agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Resources; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using FishNet; using FishNet.Managing.Transporting; using FishNet.Object; using FishNet.Transporting; using FishNet.Transporting.Multipass; using FishNet.Transporting.Tugboat; using HarmonyLib; using MelonLoader; using MelonLoader.Preferences; using MelonLoader.Utils; using Microsoft.CodeAnalysis; using S1API.PhoneApp; using S1API.UI; using ScheduleOne; using ScheduleOne.Core.Items.Framework; using ScheduleOne.DevUtilities; using ScheduleOne.GameTime; using ScheduleOne.ItemFramework; using ScheduleOne.Levelling; using ScheduleOne.Map; using ScheduleOne.Money; using ScheduleOne.Networking; using ScheduleOne.Persistence; using ScheduleOne.PlayerScripts; using ScheduleOne.SpecialCustomers; using ScheduleOne.SpecialCustomers.UI; using ScheduleOne.State; using ScheduleOne.UI; using ScheduleOne.UI.SleepMessage; using SteamNetworkLib; using SteamNetworkLib.Events; using Time_Never_Stops; using UnityEngine; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: MelonInfo(typeof(Core), "Time Never Stops", "1.5.1", "DropDaDeuce", null)] [assembly: MelonGame("TVGS", "Schedule I")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: IgnoresAccessChecksTo("Assembly-CSharp")] [assembly: AssemblyCompany("TimeNeverStops_Mono")] [assembly: AssemblyConfiguration("MONO")] [assembly: AssemblyFileVersion("1.5.1.0")] [assembly: AssemblyInformationalVersion("1.5.1+357f6a976a74cd70b0cabda56d9af79e4414cc50")] [assembly: AssemblyProduct("TimeNeverStops_Mono")] [assembly: AssemblyTitle("TimeNeverStops_Mono")] [assembly: NeutralResourcesLanguage("en-US")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.5.1.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace Time_Never_Stops { internal static class TNSLog { private static bool DebugEnabled => Core.cfgDebugLogging?.Value ?? false; public static void Msg(string message) { MelonLogger.Msg(message); } public static void Msg(string format, params object[] args) { MelonLogger.Msg(string.Format(format, args)); } public static void Debug(string message) { if (DebugEnabled) { MelonLogger.Msg("[Debug] " + message); } } public static void Debug(string format, params object[] args) { if (DebugEnabled) { MelonLogger.Msg("[Debug] " + string.Format(format, args)); } } public static void Warning(string message) { MelonLogger.Warning(message); } public static void Warning(string format, params object[] args) { MelonLogger.Warning(string.Format(format, args)); } public static void Error(string message) { MelonLogger.Error(message); } public static void Error(string format, params object[] args) { MelonLogger.Error(string.Format(format, args)); } } public class Core : MelonMod { private static MelonPreferences_Category cfgCategory; public static MelonPreferences_Entry<string> cfgDaySpeedStr; public static MelonPreferences_Entry<bool> cfgEnableSummaryAwake; public static MelonPreferences_Entry<bool> cfgEnableSummaryApp; public static MelonPreferences_Entry<bool> cfgSilentSleep; public static MelonPreferences_Entry<bool> cfgLocalMultiplayer; public static MelonPreferences_Entry<float> cfgLegacyDaySpeed; public static MelonPreferences_Entry<bool> cfgMultiplierOnly; public static MelonPreferences_Entry<bool> cfgDebugLogging; public static MelonPreferences_Entry<int> cfgDailySummaryCutoffHHMM; private bool _updatingPref; private bool _lastEnableSummaryAwake; public const float DefaultSpeed = 1f; private const float MinSpeed = 0.1f; private const float MaxSpeed = 100f; private FileSystemWatcher _cfgWatcher; private volatile bool _cfgFileChanged; private TimeSyncManager? _timeSyncManager; private bool _timeSyncInitialized; private bool _timeSyncRetrying; private object? _timeSyncUpdateHandle; internal static bool IsHost => (Object)(object)Player.Local != (Object)null && ((NetworkBehaviour)Player.Local).IsHost; internal static bool SilentSleepActive { get { MelonPreferences_Entry<bool> obj = cfgMultiplierOnly; int result; if (obj == null || !obj.Value) { MelonPreferences_Entry<bool> obj2 = cfgSilentSleep; if (obj2 != null && obj2.Value) { result = (DailySummaryPatch.ShouldSuppressSummaryUI() ? 1 : 0); goto IL_0031; } } result = 0; goto IL_0031; IL_0031: return (byte)result != 0; } } public static float CurrentDaySpeed => Sanitize(ParseFloatOrDefault(cfgDaySpeedStr?.Value, 1f)); internal static int DailySummaryCutoffHHMM => SanitizeHHMM(cfgDailySummaryCutoffHHMM?.Value ?? 800); public override void OnInitializeMelon() { MelonPreferences_Category val = MelonPreferences.CreateCategory("TimeNeverStops"); cfgLegacyDaySpeed = val.GetEntry<float>("DaySpeedMultiplier") ?? val.CreateEntry<float>("DaySpeedMultiplier", 1f, "Day Speed Multiplier", "Sets the in-game time speed multiplier. 1.0 = normal speed, 0.5 = half speed, 2.0 = double speed.Minimum 0.1, Maximum 3.0. Default = 1.0 (Vanilla Game Speed)", false, false, (ValueValidator)null, (string)null); val.DeleteEntry("DaySpeedMultiplier"); if (cfgLegacyDaySpeed.Value != 1f) { TNSLog.Warning($"Legacy DaySpeedMultiplier found with value {cfgLegacyDaySpeed.Value}. Migrating to new config system."); } string text = Math.Clamp(cfgLegacyDaySpeed.Value, 0.1f, 100f).ToString("R", CultureInfo.InvariantCulture); string text2 = Path.Combine(MelonEnvironment.UserDataDirectory, "DropDaDeuce-TimeNeverStops"); string text3 = Path.Combine(text2, "TimeNeverStops.cfg"); Directory.CreateDirectory(text2); cfgCategory = val; val = null; cfgCategory.SetFilePath(text3, File.Exists(text3), true); cfgMultiplierOnly = cfgCategory.GetEntry<bool>("TimeMultiplierOnlyMode") ?? cfgCategory.CreateEntry<bool>("TimeMultiplierOnlyMode", false, "Multiplier Only Mode", "If true the mod ONLY changes the day speed multiplier.\nDisables:\n- 4AM freeze bypass\n- Daily summary while awake\n- Sleep prompt hiding.\nChange at runtime is applied but a restart is safest.", false, false, (ValueValidator)null, (string)null); cfgDaySpeedStr = cfgCategory.GetEntry<string>("DaySpeedMultiplier") ?? cfgCategory.CreateEntry<string>("DaySpeedMultiplier", text, "Day Speed Multiplier", "String parsed to float by the mod.\n1.0 = normal, 0.5 = half, 2.0 = double.\nMin 0.1, Max 100.0, Default 1.0.", false, false, (ValueValidator)null, (string)null); cfgEnableSummaryAwake = cfgCategory.GetEntry<bool>("EnableDailySummaryAwake") ?? cfgCategory.CreateEntry<bool>("EnableDailySummaryAwake", true, "Enable Daily Summary While Awake", "If enabled, the daily summary popup will be shown while awake (unless Multiplier Only Mode is ON).\nIgnored while the Daily Summary phone app is active — the app replaces the popup.", false, false, (ValueValidator)null, (string)null); cfgEnableSummaryApp = cfgCategory.GetEntry<bool>("EnableDailySummaryApp") ?? cfgCategory.CreateEntry<bool>("EnableDailySummaryApp", true, "Enable Daily Summary Phone App", "If enabled, yesterday's summary is available on demand from a phone app instead of\ninterrupting you with the popup. Requires S1API. If S1API is missing or the app fails\nto load, the popup is used instead so the summary is never lost.", false, false, (ValueValidator)null, (string)null); cfgSilentSleep = cfgCategory.GetEntry<bool>("EnableSilentSleep") ?? cfgCategory.CreateEntry<bool>("EnableSilentSleep", true, "Enable Silent Sleep", "If enabled, the nightly sleep sequence runs with no screen takeover at all:\nno fade to black, no HUD drop, no cursor, no waiting.\nEverything the game does overnight still happens, including the daily save,\nso expect a brief hitch while it writes. Turn this off to get the vanilla\nsleep screen back (ignored while Multiplier Only Mode is ON).", false, false, (ValueValidator)null, (string)null); cfgLocalMultiplayer = cfgCategory.GetEntry<bool>("EnableLocalMultiplayer") ?? cfgCategory.CreateEntry<bool>("EnableLocalMultiplayer", false, "Enable Local Multiplayer (Mono only)", "MONO BUILD ONLY. Lets a SECOND copy of the game on the SAME PC join your session\nover loopback, with no second Steam account. Host a normal single-player save,\nthen press F6 in the other instance. Same machine only - this is not LAN.\nTime sync over Steam lobby data does not run in a local session; keep\nDaySpeedMultiplier identical in both instances. Off by default.", false, false, (ValueValidator)null, (string)null); cfgDebugLogging = cfgCategory.GetEntry<bool>("EnableDebugLogging") ?? cfgCategory.CreateEntry<bool>("EnableDebugLogging", false, "Enable Debug Logging", "If true, prints additional debug messages from the mod to help diagnose issues.", false, false, (ValueValidator)null, (string)null); cfgDailySummaryCutoffHHMM = cfgCategory.GetEntry<int>("ForceDailySummaryWhileInUITime") ?? cfgCategory.CreateEntry<int>("ForceDailySummaryWhileInUITime", 800, "Force Daily Summary While In UI Time", "The time the mod will force the daily summary to show if the user is still in a UI.", false, false, (ValueValidator)null, (string)null); ((MelonEventBase<LemonAction<int, int>>)(object)cfgDailySummaryCutoffHHMM.OnEntryValueChanged).Subscribe((LemonAction<int, int>)delegate(int _, int newVal) { if (!_updatingPref) { int num2 = SanitizeHHMM(newVal); if (num2 != newVal) { _updatingPref = true; cfgDailySummaryCutoffHHMM.Value = num2; cfgCategory.SaveToFile(false); _updatingPref = false; } TNSLog.Msg($"Daily summary cutoff set to {FormatHHMM(num2)} ({num2:D4})."); } }, 0, false); if (!cfgMultiplierOnly.Value) { _lastEnableSummaryAwake = cfgEnableSummaryAwake.Value; MelonCoroutines.Start(SleepStateWatcher()); } else { TNSLog.Msg("TimeMultiplierOnlyMode enabled: all non-multiplier features disabled."); } float num = Sanitize(ParseFloatOrDefault(cfgDaySpeedStr.Value, 1f)); if (!IsStringValidAndInRange(cfgDaySpeedStr.Value)) { cfgDaySpeedStr.Value = num.ToString(CultureInfo.InvariantCulture); } _updatingPref = true; cfgCategory.SaveToFile(true); _updatingPref = false; ((MelonEventBase<LemonAction<string, string>>)(object)cfgDaySpeedStr.OnEntryValueChanged).Subscribe((LemonAction<string, string>)delegate(string _, string newV) { if (!_updatingPref) { float v = ParseFloatOrDefault(newV, 1f); float newSpeed = Sanitize(v); if (!IsStringValidAndInRange(newV)) { _updatingPref = true; cfgDaySpeedStr.Value = newSpeed.ToString(CultureInfo.InvariantCulture); cfgCategory.SaveToFile(false); _updatingPref = false; } ApplyDaySpeed(newSpeed); if (_timeSyncManager != null) { try { _timeSyncManager.SyncHostMultiplierNow(); } catch (Exception ex2) { TNSLog.Warning("Error syncing multiplier change to lobby: " + ex2.Message); } } } }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)cfgEnableSummaryAwake.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate(bool _, bool newVal) { if (!_updatingPref) { _lastEnableSummaryAwake = newVal; TNSLog.Msg("EnableDailySummaryAwake: " + (newVal ? "ON" : "OFF")); _updatingPref = true; cfgCategory.SaveToFile(false); _updatingPref = false; } }, 0, false); ((MelonEventBase<LemonAction<bool, bool>>)(object)cfgDebugLogging.OnEntryValueChanged).Subscribe((LemonAction<bool, bool>)delegate(bool _, bool newVal) { if (!_updatingPref) { cfgDebugLogging.Value = newVal; TNSLog.Msg("DebugLogging: " + (newVal ? "Enabled" : "Disabled")); _updatingPref = true; cfgCategory.SaveToFile(false); _updatingPref = false; } }, 0, false); try { _cfgWatcher = new FileSystemWatcher(text2, Path.GetFileName(text3)) { NotifyFilter = (NotifyFilters.FileName | NotifyFilters.Size | NotifyFilters.LastWrite), IncludeSubdirectories = false, EnableRaisingEvents = true }; _cfgWatcher.Changed += markDirty; _cfgWatcher.Created += markDirty; _cfgWatcher.Renamed += markDirtyRename; } catch (Exception ex) { TNSLog.Warning("Config watcher init failed: " + ex.Message + ". Falling back to in-process updates only."); } MelonCoroutines.Start(SetDaySpeedLoop()); TNSLog.Debug($"Init complete. DaySpeed={cfgDaySpeedStr.Value}, MultiplierOnly={cfgMultiplierOnly.Value}, " + $"SummaryAwake={cfgEnableSummaryAwake.Value}, DebugLogging={cfgDebugLogging.Value}, " + $"DailySummaryCutoff={FormatHHMM(cfgDailySummaryCutoffHHMM.Value)} ({cfgDailySummaryCutoffHHMM.Value:D4})."); if (cfgDebugLogging.Value) { LogPatchedMethods(); } void markDirty(object _, FileSystemEventArgs __) { _cfgFileChanged = true; } void markDirtyRename(object _, RenamedEventArgs __) { _cfgFileChanged = true; } } private void LogPatchedMethods() { try { List<string> list = new List<string>(); foreach (MethodBase patchedMethod in ((MelonBase)this).HarmonyInstance.GetPatchedMethods()) { list.Add(patchedMethod.DeclaringType?.Name + "." + patchedMethod.Name); } list.Sort(StringComparer.Ordinal); TNSLog.Debug(string.Format("[Harmony] {0} game method(s) patched: {1}", list.Count, string.Join(", ", list))); } catch (Exception ex) { TNSLog.Debug("[Harmony] Could not list patched methods: " + ex.Message); } } public override void OnUpdate() { LocalMultiplayer.Tick(); } public override void OnSceneWasInitialized(int buildIndex, string sceneName) { TNSLog.Debug($"Scene initialized: '{sceneName}' (buildIndex={buildIndex})."); if (sceneName == "Menu") { _timeSyncRetrying = false; _timeSyncInitialized = false; StopTimeSyncUpdateLoop(); if (_timeSyncManager != null) { try { _timeSyncManager.Dispose(); } catch { } _timeSyncManager = null; } DailySummaryStore.Reset(); S1APIBridge.Reset(); LocalMultiplayer.Reset(); } else if (sceneName == "Main" && !_timeSyncInitialized && !_timeSyncRetrying) { _timeSyncRetrying = true; MelonCoroutines.Start(InitTimeSyncWithRetry()); LocalMultiplayer.OnGameSceneLoaded(); } } private IEnumerator InitTimeSyncWithRetry() { for (int attempt = 1; attempt <= 3; attempt++) { if (TryInitTimeSync()) { TNSLog.Msg("Time synchronization enabled."); yield break; } if (attempt < 3) { TNSLog.Debug($"Steamworks not ready — retrying in {2f}s..."); yield return (object)new WaitForSecondsRealtime(2f); } } TNSLog.Debug($"Fast attempts exhausted. Polling every {15f}s " + "in case a multiplayer session starts later..."); WaitForSecondsRealtime pollWait = new WaitForSecondsRealtime(15f); int polls = 0; while ((Object)(object)NetworkSingleton<TimeManager>.Instance != (Object)null && !_timeSyncInitialized) { yield return pollWait; polls++; TNSLog.Debug($"TimeSyncManager poll {polls}: attempting init..."); if (TryInitTimeSync()) { TNSLog.Msg($"Time synchronization enabled (connected after {polls} " + "poll" + ((polls == 1) ? "" : "s") + ")."); yield break; } } if (!_timeSyncInitialized) { TNSLog.Debug("TimeSyncManager polling stopped (session ended or scene changed)."); } _timeSyncRetrying = false; } private void StopTimeSyncUpdateLoop() { if (_timeSyncUpdateHandle != null) { try { MelonCoroutines.Stop(_timeSyncUpdateHandle); } catch { } _timeSyncUpdateHandle = null; } } private bool TryInitTimeSync() { StopTimeSyncUpdateLoop(); if (_timeSyncManager != null) { try { _timeSyncManager.Dispose(); } catch { } _timeSyncManager = null; } try { _timeSyncManager = new TimeSyncManager(); _timeSyncManager.SetHostMultiplierCallback(() => Sanitize(ParseFloatOrDefault(cfgDaySpeedStr.Value, 1f))); _timeSyncManager.Initialize(); if (_timeSyncManager.IsInitialized) { _timeSyncUpdateHandle = MelonCoroutines.Start(TimeSyncUpdateLoop()); _timeSyncInitialized = true; _timeSyncRetrying = false; return true; } TNSLog.Debug("TryInitTimeSync: Initialize() returned but IsInitialized is false."); } catch (Exception ex) { TNSLog.Debug("TryInitTimeSync failed: " + ex.Message); } return false; } public override void OnDeinitializeMelon() { try { _cfgWatcher?.Dispose(); } catch { } _cfgWatcher = null; StopTimeSyncUpdateLoop(); try { _timeSyncManager?.Dispose(); } catch { } _timeSyncManager = null; } private IEnumerator SleepStateWatcher() { bool last = false; SleepController lastSc = null; while (true) { SleepController sc = NetworkSingleton<SleepController>.Instance; if ((Object)(object)sc == (Object)null) { if (last) { SleepFlow.EndNight("SleepController gone"); } lastSc = null; last = false; yield return null; continue; } if ((Object)(object)sc != (Object)(object)lastSc) { lastSc = sc; last = sc.IsSleepInProgress; } bool cur = sc.IsSleepInProgress; if (cur && !last) { SleepFlow.EnsureNightStarted(); } else if (!cur && last) { SleepFlow.EndNight("watcher"); } last = cur; yield return null; } } public static bool TryParseFloatInvariant(string s, out float value) { return float.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out value); } public static float ParseFloatOrDefault(string s, float fallback) { float value; return (TryParseFloatInvariant(s, out value) && float.IsFinite(value)) ? value : fallback; } public static float Sanitize(float v) { return Math.Clamp(float.IsFinite(v) ? v : 1f, 0.1f, 100f); } private static bool IsStringValidAndInRange(string s) { float value; return TryParseFloatInvariant(s, out value) && float.IsFinite(value) && value >= 0.1f && value <= 100f; } private IEnumerator SetDaySpeedLoop() { WaitForSecondsRealtime tick = new WaitForSecondsRealtime(1f); while (true) { if ((Object)(object)NetworkSingleton<TimeManager>.Instance == (Object)null) { yield return null; continue; } TimeManager tm = NetworkSingleton<TimeManager>.Instance; float last = ReadPrefAndNormalize(); TNSLog.Debug($"TimeManager acquired. IsHost={IsHost}, speed={last:0.########}x."); if (IsHost) { ApplyDaySpeed(last); } while ((Object)(object)NetworkSingleton<TimeManager>.Instance == (Object)(object)tm) { if (_cfgFileChanged) { _cfgFileChanged = false; cfgCategory.LoadFromFile(false); } float desired = ReadPrefAndNormalize(); if (IsHost && (Differs(desired, last) || Differs(desired, tm.TimeSpeedMultiplier))) { last = desired; ApplyDaySpeed(last); } yield return tick; } } static bool Differs(float a, float b, float eps = 0.0001f) { return Mathf.Abs(a - b) > eps; } float ReadPrefAndNormalize() { float num = ParseFloatOrDefault(cfgDaySpeedStr.Value, 1f); float result = Sanitize(float.IsFinite(num) ? num : 1f); string text = result.ToString(CultureInfo.InvariantCulture); if (cfgDaySpeedStr.Value != text) { _updatingPref = true; cfgDaySpeedStr.Value = text; cfgCategory.SaveToFile(false); _updatingPref = false; } return result; } } private IEnumerator TimeSyncUpdateLoop() { WaitForSecondsRealtime tick = new WaitForSecondsRealtime(0.1f); while (true) { try { _timeSyncManager?.Update(); } catch (Exception ex) { Exception ex2 = ex; TNSLog.Warning("Error in time sync update loop: " + ex2.Message); } yield return tick; } } public static void ApplyDaySpeed(float newSpeed) { TimeManager instance = NetworkSingleton<TimeManager>.Instance; if (!((Object)(object)instance == (Object)null)) { float num = Sanitize(newSpeed); if (Mathf.Abs(instance.TimeSpeedMultiplier - num) > 0.0001f) { instance.SetTimeSpeedMultiplier(num); TNSLog.Msg($"Day speed set to {num:0.########}x"); } } } internal static int SanitizeHHMM(int v) { if (v < 0) { v = 0; } int num = v / 100; int num2 = v % 100; if (num < 0) { num = 0; } else if (num > 23) { num = 23; } if (num2 < 0) { num2 = 0; } else if (num2 > 59) { num2 = 59; } return num * 100 + num2; } internal static string FormatHHMM(int hhmm) { try { return TimeManager.Get12HourTime((float)hhmm, true); } catch { int num = Mathf.Clamp(hhmm / 100, 0, 23); int num2 = Mathf.Clamp(hhmm % 100, 0, 59); return $"{num:00}:{num2:00}"; } } } public static class PatchLogger { public static void LogPatchLoad(string patchName) { TNSLog.Msg("[Harmony] " + patchName + " loaded."); } } [HarmonyPatch(typeof(TimeManager), "RpcLogic___PassMinute_Client_3316948804")] [HarmonyPriority(600)] public static class Patch_Tick_XPMenu { private static bool firedToday; private static int lastHHmm; private static int lastHH; private static bool suppressForSleep; public static bool startupSkip; private static int lastElapsedDays; private static bool wouldFreeze; static Patch_Tick_XPMenu() { lastHH = -1; startupSkip = true; lastElapsedDays = -1; wouldFreeze = false; PatchLogger.LogPatchLoad("Patch_Tick_XPMenu"); startupSkip = true; } private static bool IsDailySummaryOpen() { try { DailySummary instance = NetworkSingleton<DailySummary>.Instance; return (Object)(object)instance != (Object)null && instance.IsInProgress; } catch { return false; } } private static void LogState(string tag, TimeManager tm) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) TNSLog.Debug($"{tag}: t={tm.CurrentTime:D4}, day={tm.CurrentDay}, elapsed={tm.ElapsedDays}, " + $"firedToday={firedToday}, startupSkip={startupSkip}, suppress={suppressForSleep}, " + $"sleep={SleepFlow.IsSleepInProgress}, dsOpen={IsDailySummaryOpen()}, lastHHmm={lastHHmm}, " + $"lastElapsedDays={lastElapsedDays}, wouldFreeze={wouldFreeze}"); } public static void SuppressDuringSleep(bool on) { suppressForSleep = on; } public static void MarkHandledForToday() { firedToday = true; lastHHmm = 700; TNSLog.Debug($"MarkHandledForToday: firedToday=true, lastHHmm={700:D4}"); } [HarmonyPrefix] public static bool Prefix(TimeManager __instance, int oldTime) { //IL_018c: Unknown result type (might be due to invalid IL or missing references) //IL_0192: Invalid comparison between Unknown and I4 if ((Object)(object)__instance == (Object)null || !((Behaviour)__instance).enabled) { return true; } if (Core.IsHost) { float currentDaySpeed = Core.CurrentDaySpeed; if (Mathf.Abs(__instance.TimeSpeedMultiplier - currentDaySpeed) > 0.0001f) { Core.ApplyDaySpeed(currentDaySpeed); } } MelonPreferences_Entry<bool> cfgMultiplierOnly = Core.cfgMultiplierOnly; if (cfgMultiplierOnly != null && cfgMultiplierOnly.Value) { return true; } float num = TimeManager.MinuteDuration / (Time.timeScale * 0.9f); if (SleepFlow.ShouldHoldClock(__instance, oldTime)) { SleepFlow.OnClockHeld(oldTime); ActionList onUncappedMinutePass = __instance.onUncappedMinutePass; if (onUncappedMinutePass != null) { onUncappedMinutePass.InvokeAllStaggered(num); } return false; } wouldFreeze = __instance.CurrentTime == 400 || (__instance.IsCurrentTimeWithinRange(400, 600) && !GameManager.IS_TUTORIAL); if (!wouldFreeze) { return true; } if ((Object)(object)Player.Local == (Object)null) { TNSLog.Warning("Local player does not exist. Waiting for player spawn."); return false; } TMAccess.SetCurrentTime(__instance, oldTime); TMAccess.SetTimeOnCurrentMinute(__instance, 0f); if (__instance.CurrentTime == 2359) { TMAccess.SetElapsedDays(__instance, __instance.ElapsedDays + 1); TMAccess.SetCurrentTime(__instance, 0); TMAccess.SetDailyMinSum(__instance, 0); __instance.onHourPass?.Invoke(); __instance.onDayPass?.Invoke(); if ((int)__instance.CurrentDay == 0) { __instance.onWeekPass?.Invoke(); } } else if (__instance.CurrentTime % 100 >= 59) { TMAccess.SetCurrentTime(__instance, __instance.CurrentTime + 41); __instance.onHourPass?.Invoke(); } else { TMAccess.SetCurrentTime(__instance, __instance.CurrentTime + 1); } TMAccess.SetDailyMinSum(__instance, TimeManager.GetMinSumFrom24HourTime(__instance.CurrentTime)); __instance.HasChanged = true; ActionList onMinutePass = __instance.onMinutePass; if (onMinutePass != null) { onMinutePass.InvokeAllStaggered(num); } ActionList onUncappedMinutePass2 = __instance.onUncappedMinutePass; if (onUncappedMinutePass2 != null) { onUncappedMinutePass2.InvokeAllStaggered(num); } __instance.onTimeChanged?.Invoke(); return false; } [HarmonyPostfix] public static void Postfix(TimeManager __instance) { MelonPreferences_Entry<bool> cfgMultiplierOnly = Core.cfgMultiplierOnly; if (cfgMultiplierOnly != null && cfgMultiplierOnly.Value) { return; } lastElapsedDays = __instance.ElapsedDays; if (lastHH != __instance.CurrentTime / 100) { LogState("PassMinute/Postfix/start", __instance); } lastHH = __instance.CurrentTime / 100; bool isSleepInProgress = SleepFlow.IsSleepInProgress; if (suppressForSleep && !isSleepInProgress) { SleepFlow.EndNight("PassMinute"); } if (isSleepInProgress) { return; } if (IsDailySummaryOpen()) { TNSLog.Debug("Skip: DailySummary is open."); return; } if (startupSkip) { startupSkip = false; lastHHmm = __instance.CurrentTime; firedToday = __instance.CurrentTime > 658; TNSLog.Debug($"Startup init: lastHHmm={lastHHmm}, firedToday={firedToday}"); } int currentTime = __instance.CurrentTime; if (lastHHmm > currentTime) { firedToday = false; TNSLog.Debug("Rearm: midnight wrap detected (lastHHmm > hhmm)."); } if (!firedToday && currentTime > 658 && __instance.ElapsedDays > 0) { TriggerDayRollover(__instance); } lastHHmm = currentTime; } private static void TriggerDayRollover(TimeManager tm) { if (!Core.IsHost) { firedToday = true; TNSLog.Debug($"Skipping the day rollover: not host (t={tm.CurrentTime:D4}). " + "Waiting for the host's ObserversRpc to drive the night."); return; } SleepController instance = NetworkSingleton<SleepController>.Instance; if ((Object)(object)instance == (Object)null) { TNSLog.Debug($"Day rollover due at t={tm.CurrentTime:D4} but there is no SleepController yet; retrying next minute."); return; } firedToday = true; TNSLog.Debug($"Triggering the day rollover (SleepController.StartSleep): t={tm.CurrentTime:D4}, " + $"elapsed={tm.ElapsedDays}, silent={Core.SilentSleepActive}."); try { instance.StartSleep(); } catch (Exception arg) { TNSLog.Error($"Day rollover: StartSleep threw: {arg}"); } } internal static IEnumerator ClosePostSleepCanvases() { try { DailySummary ds = NetworkSingleton<DailySummary>.Instance; if ((Object)(object)ds != (Object)null && ds.IsInProgress) { ds.Close(); TNSLog.Debug("Release valve: closed DailySummary."); } } catch (Exception ex) { Exception ex2 = ex; TNSLog.Warning("Release valve: closing DailySummary failed: " + ex2.Message); } yield return (object)new WaitForSecondsRealtime(2f); try { RankUpCanvas[] ranks = Resources.FindObjectsOfTypeAll<RankUpCanvas>(); if (ranks != null) { RankUpCanvas[] array = ranks; foreach (RankUpCanvas r in array) { if (!((Object)(object)r == (Object)null) && r.IsInProgress) { r.EndEvent(); TNSLog.Debug("Release valve: ended RankUpCanvas."); } } } } catch (Exception ex) { Exception ex3 = ex; TNSLog.Warning("Release valve: ending RankUpCanvas failed: " + ex3.Message); } yield return (object)new WaitForSecondsRealtime(2f); try { RegionUnlockedCanvas region = Singleton<RegionUnlockedCanvas>.Instance; if ((Object)(object)region != (Object)null && region.IsInProgress) { region.EndEvent(); TNSLog.Debug("Release valve: ended RegionUnlockedCanvas."); } } catch (Exception ex) { Exception ex4 = ex; TNSLog.Warning("Release valve: ending RegionUnlockedCanvas failed: " + ex4.Message); } } } [HarmonyPatch(typeof(HUD), "Update")] [HarmonyPriority(200)] public static class Patch_HUD_HideSleepPrompt { private static void Postfix(HUD __instance) { MelonPreferences_Entry<bool> cfgMultiplierOnly = Core.cfgMultiplierOnly; if ((cfgMultiplierOnly == null || !cfgMultiplierOnly.Value) && (Object)(object)__instance?.SleepPrompt != (Object)null) { ((Component)__instance.SleepPrompt).gameObject.SetActive(false); } } } [HarmonyPatch(typeof(RankUpCanvas), "StartEvent")] public static class Patch_RankUpCanvas_StartEvent { static Patch_RankUpCanvas_StartEvent() { PatchLogger.LogPatchLoad("Patch_RankUpCanvas_StartEvent"); } [HarmonyPrefix] public static bool Prefix(RankUpCanvas __instance) { if (DailySummaryPatch.ShouldSuppressSleepPopups()) { TNSLog.Debug("Rank Up Canvas Skipping"); if ((Object)(object)__instance.Canvas != (Object)null) { ((Behaviour)__instance.Canvas).enabled = false; } __instance.EndEvent(); return false; } TNSLog.Debug("Rank Up Canvas Running"); return true; } } [HarmonyPatch(typeof(RegionUnlockedCanvas), "StartEvent")] public static class Patch_RegionUnlockedCanvas_StartEvent { static Patch_RegionUnlockedCanvas_StartEvent() { PatchLogger.LogPatchLoad("Patch_RegionUnlockedCanvas_StartEvent"); } [HarmonyPrefix] public static bool Prefix(RegionUnlockedCanvas __instance) { if (DailySummaryPatch.ShouldSuppressSleepPopups()) { TNSLog.Debug("Region Unlocked Canvas Skipping"); __instance.EndEvent(); return false; } TNSLog.Debug("Region Unlocked Canvas Running"); return true; } } [HarmonyPatch(typeof(IncomingSpecialCustomerInfoPopup), "StartEvent")] public static class Patch_IncomingSpecialCustomerInfoPopup_StartEvent { static Patch_IncomingSpecialCustomerInfoPopup_StartEvent() { PatchLogger.LogPatchLoad("Patch_IncomingSpecialCustomerInfoPopup_StartEvent"); } [HarmonyPrefix] public static bool Prefix(IncomingSpecialCustomerInfoPopup __instance) { if (!DailySummaryPatch.ShouldSuppressSleepPopups()) { TNSLog.Debug("Incoming special customers popup running."); return true; } DailySummaryStore.NoteIncomingSpecialCustomers(__instance); TNSLog.Debug("Incoming special customers popup skipped; recorded for the summary app."); return false; } } [HarmonyPatch(typeof(RegionUnlockedCanvas), "QueueUnlocked")] public static class Patch_RegionUnlockedCanvas_QueueUnlocked { static Patch_RegionUnlockedCanvas_QueueUnlocked() { PatchLogger.LogPatchLoad("Patch_RegionUnlockedCanvas_QueueUnlocked"); } [HarmonyPostfix] public static void Postfix(EMapRegion _region) { //IL_0018: Unknown result type (might be due to invalid IL or missing references) MelonPreferences_Entry<bool> cfgMultiplierOnly = Core.cfgMultiplierOnly; if (cfgMultiplierOnly == null || !cfgMultiplierOnly.Value) { DailySummaryStore.NoteRegionUnlocked(_region); } } } [HarmonyPatch(typeof(DailySummary))] public static class DailySummaryPatch { static DailySummaryPatch() { PatchLogger.LogPatchLoad("DailySummaryPatch"); } internal static bool ShouldSuppressSummaryUI() { MelonPreferences_Entry<bool> cfgMultiplierOnly = Core.cfgMultiplierOnly; if (cfgMultiplierOnly != null && cfgMultiplierOnly.Value) { return false; } MelonPreferences_Entry<bool> cfgEnableSummaryAwake = Core.cfgEnableSummaryAwake; if (cfgEnableSummaryAwake != null && !cfgEnableSummaryAwake.Value) { return true; } return S1APIBridge.ShouldGateSummaryBehindApp(); } internal static bool ShouldSuppressSleepPopups() { return (SleepFlow.NightOpen && SleepFlow.SilentSleepEngaged) || ShouldSuppressSummaryUI(); } [HarmonyPrefix] [HarmonyPatch("StartEvent")] public static bool StartEventPrefix() { if (ShouldSuppressSleepPopups()) { TNSLog.Debug("Daily Summary popup skipped (never started, so the night does not wait on it)."); return false; } TNSLog.Debug("Daily Summary popup running."); return true; } [HarmonyPrefix] [HarmonyPatch("SleepEnd")] public static void SleepEndPrefix(DailySummary __instance) { MelonPreferences_Entry<bool> cfgMultiplierOnly = Core.cfgMultiplierOnly; if (cfgMultiplierOnly == null || !cfgMultiplierOnly.Value) { SleepFlow.SleepEndReached = true; TNSLog.Debug("DailySummary.SleepEnd reached — capturing the night's summary before ClearStats wipes it."); DailySummaryStore.Capture(__instance); } } } internal static class SleepFlow { internal const int WakeTime = 700; internal static bool SleepingStatePushSkipped; internal static bool SleepMessageOpenSkipped; internal static bool CameraTakenBySleep; internal static bool SleepEndReached; private static int _startDay = -1; private static int _startTime = -1; private static int _wakeDay = -1; private static int _heldMinutes; private static int _heldAt = -1; private static bool _valveDone; internal static bool NightOpen { get; private set; } internal static bool SilentSleepEngaged { get; private set; } internal static bool IsSleepInProgress { get { try { SleepController instance = NetworkSingleton<SleepController>.Instance; return (Object)(object)instance != (Object)null && instance.IsSleepInProgress; } catch { return false; } } } internal static bool IsInStartingPhase { get { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Invalid comparison between Unknown and I4 try { SleepController instance = NetworkSingleton<SleepController>.Instance; return (Object)(object)instance != (Object)null && (int)instance.CurrentPhase == 1; } catch { return false; } } } internal static bool IsSleepingState(MonoState state) { try { SleepController instance = NetworkSingleton<SleepController>.Instance; return (Object)(object)instance != (Object)null && (Object)(object)state != (Object)null && (Object)(object)instance.SleepingState == (Object)(object)state; } catch { return false; } } internal static void BeginNight(bool allowSilent) { NightOpen = true; SilentSleepEngaged = allowSilent && Core.SilentSleepActive; SleepingStatePushSkipped = false; SleepMessageOpenSkipped = false; CameraTakenBySleep = false; SleepEndReached = false; _heldMinutes = 0; _heldAt = -1; _valveDone = false; TimeManager instance = NetworkSingleton<TimeManager>.Instance; _startDay = (((Object)(object)instance != (Object)null) ? instance.ElapsedDays : (-1)); _startTime = (((Object)(object)instance != (Object)null) ? instance.CurrentTime : (-1)); _wakeDay = ((_startDay < 0) ? (-1) : ((_startTime >= 1200) ? (_startDay + 1) : _startDay)); Patch_Tick_XPMenu.SuppressDuringSleep(on: true); Patch_Tick_XPMenu.MarkHandledForToday(); DailySummaryStore.BeginNight(); TNSLog.Debug($"Night starting: t={_startTime:D4}, elapsed={_startDay}, wakes on day {_wakeDay} at {700:D4}, " + $"silent={SilentSleepEngaged}, IsHost={Core.IsHost}."); } internal static void EnsureNightStarted() { if (!NightOpen) { TNSLog.Debug("A night started without the StartSleep prefix having run — opening it late, not silent."); BeginNight(allowSilent: false); } } internal static void EndNight(string source) { if (NightOpen) { NightOpen = false; TNSLog.Debug($"Night over ({source}): silent={SilentSleepEngaged}, clock held {_heldMinutes} min, " + $"cutscene={CameraTakenBySleep}, sleepEnd={SleepEndReached}."); if (!SleepEndReached && (Object)(object)NetworkSingleton<SleepController>.Instance != (Object)null && !GameManager.IS_TUTORIAL) { TNSLog.Warning("A night ended without DailySummary.SleepEnd being seen — the capture hook did not fire, so the summary app is out of date. Please report this with your MelonLoader log."); } SilentSleepEngaged = false; SleepingStatePushSkipped = false; SleepMessageOpenSkipped = false; CameraTakenBySleep = false; SleepEndReached = false; _wakeDay = -1; Patch_Tick_XPMenu.SuppressDuringSleep(on: false); Patch_Tick_XPMenu.MarkHandledForToday(); DailySummaryStore.EndNight(); } } internal static bool ShouldHoldClock(TimeManager tm, int oldTime) { if (_wakeDay < 0 || GameManager.IS_TUTORIAL) { return false; } if (!IsSleepInProgress) { return false; } int elapsedDays = tm.ElapsedDays; return elapsedDays > _wakeDay || (elapsedDays == _wakeDay && oldTime >= 700); } internal static void OnClockHeld(int oldTime) { if (_heldMinutes == 0) { _heldAt = oldTime; TNSLog.Debug($"Wake hold: the night is still running at {oldTime:D4}; holding the clock until the game's wake skip."); } _heldMinutes++; if (!_valveDone) { int num = TimeManager.AddMinutesTo24HourTime(Core.DailySummaryCutoffHHMM, 30); int num2 = TimeManager.GetMinSumFrom24HourTime(num) - TimeManager.GetMinSumFrom24HourTime(_heldAt); if (num2 <= 0) { _valveDone = true; } else if (_heldMinutes >= num2) { _valveDone = true; TNSLog.Debug("Release valve: " + Core.FormatHHMM(num) + " reached on the held clock; closing post-sleep screens."); MelonCoroutines.Start(Patch_Tick_XPMenu.ClosePostSleepCanvases()); } } } } [HarmonyPatch(typeof(SleepController), "RpcLogic___StartSleep_2166136261")] public static class Patch_SleepController_StartSleep { static Patch_SleepController_StartSleep() { PatchLogger.LogPatchLoad("Patch_SleepController_StartSleep"); } [HarmonyPrefix] public static void Prefix(SleepController __instance) { MelonPreferences_Entry<bool> cfgMultiplierOnly = Core.cfgMultiplierOnly; if ((cfgMultiplierOnly == null || !cfgMultiplierOnly.Value) && !((Object)(object)__instance == (Object)null) && !__instance.IsSleepInProgress) { SleepFlow.BeginNight(allowSilent: true); } } } [HarmonyPatch(typeof(MonoState), "PushToDefaultParent")] public static class Patch_MonoState_PushToDefaultParent { [HarmonyPrefix] public static bool Prefix(MonoState __instance) { if (!SleepFlow.SilentSleepEngaged) { return true; } if (!SleepFlow.IsSleepingState(__instance)) { return true; } SleepFlow.SleepingStatePushSkipped = true; TNSLog.Debug("Silent sleep: SleepingState push suppressed — no screen takeover."); return false; } } [HarmonyPatch(typeof(MonoState), "PopFromDefaultParent")] public static class Patch_MonoState_PopFromDefaultParent { [HarmonyPrefix] public static bool Prefix(MonoState __instance) { if (!SleepFlow.SleepingStatePushSkipped) { return true; } if (!SleepFlow.IsSleepingState(__instance)) { return true; } SleepFlow.SleepingStatePushSkipped = false; TNSLog.Debug("Silent sleep: the matching SleepingState pop suppressed."); return false; } } [HarmonyPatch(typeof(BlackOverlay), "Open")] public static class Patch_BlackOverlay_Open { [HarmonyPrefix] public static bool Prefix(Action onComplete) { if (!SleepFlow.SilentSleepEngaged) { return true; } if (onComplete != null) { return true; } if (!SleepFlow.IsInStartingPhase) { return true; } TNSLog.Debug("Silent sleep: fade to black suppressed."); return false; } } [HarmonyPatch(typeof(SleepMessageInterface), "Open")] public static class Patch_SleepMessageInterface_Open { [HarmonyPrefix] public static bool Prefix(string message) { if (!SleepFlow.SilentSleepEngaged || !SleepFlow.IsSleepInProgress) { return true; } SleepFlow.SleepMessageOpenSkipped = true; TNSLog.Debug("Silent sleep: sleep-screen text suppressed (\"" + message + "\")."); return false; } } [HarmonyPatch(typeof(SleepMessageInterface), "Close")] public static class Patch_SleepMessageInterface_Close { [HarmonyPrefix] public static bool Prefix() { if (!SleepFlow.SleepMessageOpenSkipped) { return true; } SleepFlow.SleepMessageOpenSkipped = false; return false; } } [HarmonyPatch(typeof(SleepMessage), "StartEvent")] public static class Patch_SleepMessage_StartEvent { static Patch_SleepMessage_StartEvent() { PatchLogger.LogPatchLoad("Patch_SleepMessage_StartEvent"); } [HarmonyPrefix] public static bool Prefix(SleepMessage __instance) { MelonPreferences_Entry<bool> cfgMultiplierOnly = Core.cfgMultiplierOnly; if (cfgMultiplierOnly != null && cfgMultiplierOnly.Value) { return true; } DailySummaryStore.NoteSleepMessage(__instance); if (!SleepFlow.NightOpen || !SleepFlow.SilentSleepEngaged) { return true; } TNSLog.Debug("Silent sleep: overnight message kept for the summary app instead of the sleep screen."); return false; } } [HarmonyPatch(typeof(SpecialCustomerCutscene), "StartEvent")] public static class Patch_SpecialCustomerCutscene_StartEvent { static Patch_SpecialCustomerCutscene_StartEvent() { PatchLogger.LogPatchLoad("Patch_SpecialCustomerCutscene_StartEvent"); } [HarmonyPrefix] public static void Prefix() { if (!SleepFlow.NightOpen || !SleepFlow.SilentSleepEngaged) { return; } SleepFlow.CameraTakenBySleep = true; try { BlackOverlay instance = Singleton<BlackOverlay>.Instance; if ((Object)(object)instance != (Object)null) { instance.Open(0f, (Action)null); } } catch (Exception ex) { TNSLog.Warning("Silent sleep: could not black out before the special-customer cutscene: " + ex.Message); } TNSLog.Debug("Silent sleep: special-customer cutscene playing; cut to black first, as the sleep screen would have."); } } [HarmonyPatch(typeof(PlayerCamera), "StopTransformOverride")] public static class Patch_PlayerCamera_StopTransformOverride { [HarmonyPrefix] public static bool Prefix(float lerpTime, bool reenableCameraLook, bool returnToOriginalRotation) { if (!SleepFlow.SilentSleepEngaged || !SleepFlow.SleepEndReached || SleepFlow.CameraTakenBySleep) { return true; } if (lerpTime != 0f || reenableCameraLook || !returnToOriginalRotation) { return true; } if (!SleepFlow.IsSleepInProgress) { return true; } TNSLog.Debug("Silent sleep: end-of-night camera reset suppressed."); return false; } } internal static class TMAccess { private static readonly Action<TimeManager, int> _setCurrentTime; private static readonly Action<TimeManager, int> _setElapsedDays; private static readonly Action<TimeManager, int> _setDailyMinSum; private static readonly FieldInfo _secondsOnCurrentMinute; private static readonly FieldInfo _timeSpeedMultiplierField; static TMAccess() { _setCurrentTime = BuildSetter<int>("CurrentTime"); _setElapsedDays = BuildSetter<int>("ElapsedDays"); _setDailyMinSum = BuildSetter<int>("DailyMinSum"); _secondsOnCurrentMinute = AccessTools.Field(typeof(TimeManager), "_secondsOnCurrentMinute"); _timeSpeedMultiplierField = AccessTools.Field(typeof(TimeManager), "<TimeSpeedMultiplier>k__BackingField"); } private static Action<TimeManager, T> BuildSetter<T>(string propName) { MethodInfo methodInfo = AccessTools.PropertySetter(typeof(TimeManager), propName); if (methodInfo == null) { return delegate { }; } return (Action<TimeManager, T>)Delegate.CreateDelegate(typeof(Action<TimeManager, T>), null, methodInfo); } public static void SetCurrentTime(TimeManager tm, int v) { _setCurrentTime(tm, v); } public static void SetElapsedDays(TimeManager tm, int v) { _setElapsedDays(tm, v); } public static void SetDailyMinSum(TimeManager tm, int v) { _setDailyMinSum(tm, v); } public static void SetTimeOnCurrentMinute(TimeManager tm, float v) { _secondsOnCurrentMinute?.SetValue(tm, v); } public static void SetTimeSpeedMultiplierDirect(TimeManager tm, float v) { _timeSpeedMultiplierField?.SetValue(tm, Mathf.Max(v, 0f)); } } public class TimeSyncManager : IDisposable { private SteamNetworkClient? _steamClient; private bool _disposed = false; private bool _isInitialized = false; private object? _syncCoroutine; private object? _readCoroutine; private const string LOBBY_DATA_KEY_CURRENT_TIME = "__tns_current_time"; private const string LOBBY_DATA_KEY_ELAPSED_DAYS = "__tns_elapsed_days"; private const string LOBBY_DATA_KEY_TIME_UPDATE_TIMESTAMP = "__tns_time_update_ts"; private const string LOBBY_DATA_KEY_TIME_MULTIPLIER = "__tns_time_multiplier"; private const float HOST_CHECK_INTERVAL = 5f; private const float CLIENT_SYNC_INTERVAL = 10f; private const float MULTIPLIER_SYNC_INTERVAL = 5f; private const float MAX_TIME_DIFF_THRESHOLD = 5f; private int _lastSyncedCurrentTime = -1; private int _lastSyncedElapsedDays = -1; private int _lastSyncedHour = -1; private float _lastSyncTimestamp = 0f; private float _lastSyncedMultiplier = float.NaN; private float _lastMultiplierCheckTime = 0f; private int _lastSyncedHostTime = -1; private int _lastSyncedHostDays = -1; private float _lastSyncedHostMultiplier = float.NaN; private Func<float>? _getHostMultiplierCallback; private float? _clientOriginalMultiplier = null; public bool IsInitialized => _isInitialized && !_disposed; public bool IsInLobby { get { SteamNetworkClient? steamClient = _steamClient; return steamClient != null && steamClient.IsInLobby; } } public bool IsHost { get { SteamNetworkClient? steamClient = _steamClient; return steamClient != null && steamClient.IsHost; } } public void SetHostMultiplierCallback(Func<float> callback) { _getHostMultiplierCallback = callback; } public void SyncHostMultiplierNow() { if (IsHost && IsInLobby) { _lastSyncedMultiplier = float.NaN; SyncHostMultiplierToLobby(); } } public void Initialize() { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Expected O, but got Unknown if (_isInitialized || _disposed) { return; } try { _steamClient = new SteamNetworkClient(); if (!_steamClient.Initialize()) { TNSLog.Warning("Failed to initialize SteamNetworkClient for time sync"); return; } _steamClient.OnLobbyJoined += OnLobbyJoined; _steamClient.OnLobbyCreated += OnLobbyCreated; _steamClient.OnLobbyLeft += OnLobbyLeft; _steamClient.OnLobbyDataChanged += OnLobbyDataChanged; _isInitialized = true; TNSLog.Debug("TimeSyncManager initialized"); if (_steamClient.IsInLobby) { StartSyncCoroutines(); } } catch (Exception ex) { TNSLog.Error("Failed to initialize TimeSyncManager: " + ex.Message); } } public void Update() { if (!_isInitialized || _disposed) { return; } try { SteamNetworkClient? steamClient = _steamClient; if (steamClient != null) { steamClient.ProcessIncomingMessages(); } } catch (Exception ex) { TNSLog.Warning("Error processing Steam messages in TimeSyncManager: " + ex.Message); } } private void StartSyncCoroutines() { if ((Object)(object)NetworkSingleton<TimeManager>.Instance == (Object)null) { TNSLog.Debug("TimeManager.Instance is null, waiting..."); MelonCoroutines.Start(WaitForTimeManagerAndStartSync()); } else if (IsHost) { if (_syncCoroutine != null) { MelonCoroutines.Stop(_syncCoroutine); } _syncCoroutine = MelonCoroutines.Start(HostUpdateTimeLoop()); TNSLog.Debug("Started host time sync loop"); } else { if (_readCoroutine != null) { MelonCoroutines.Stop(_readCoroutine); } _readCoroutine = MelonCoroutines.Start(ClientSyncTimeLoop()); TNSLog.Debug("Started client time sync loop"); } } private IEnumerator WaitForTimeManagerAndStartSync() { TNSLog.Debug("TimeSyncManager: waiting for TimeManager before starting sync coroutines..."); while ((Object)(object)NetworkSingleton<TimeManager>.Instance == (Object)null) { yield return null; } TNSLog.Debug("TimeSyncManager: TimeManager ready, starting sync coroutines."); StartSyncCoroutines(); } private IEnumerator HostUpdateTimeLoop() { WaitForSecondsRealtime waitTime = new WaitForSecondsRealtime(5f); while (IsInLobby && IsHost && (Object)(object)NetworkSingleton<TimeManager>.Instance != (Object)null) { try { TimeManager tm = NetworkSingleton<TimeManager>.Instance; int currentTime = tm.CurrentTime; int elapsedDays = tm.ElapsedDays; int currentHour = currentTime / 100; bool hourChanged = _lastSyncedHour != currentHour; bool daysChanged = _lastSyncedElapsedDays != elapsedDays; bool isFirstSync = _lastSyncedCurrentTime == -1; if (isFirstSync || hourChanged || daysChanged) { string currentTimeStr = currentTime.ToString(); string elapsedDaysStr = elapsedDays.ToString(); string timestampStr = Time.realtimeSinceStartup.ToString("F2"); SteamNetworkClient? steamClient = _steamClient; if (steamClient != null) { steamClient.SetLobbyData("__tns_current_time", currentTimeStr); } SteamNetworkClient? steamClient2 = _steamClient; if (steamClient2 != null) { steamClient2.SetLobbyData("__tns_elapsed_days", elapsedDaysStr); } SteamNetworkClient? steamClient3 = _steamClient; if (steamClient3 != null) { steamClient3.SetLobbyData("__tns_time_update_ts", timestampStr); } _lastSyncedCurrentTime = currentTime; _lastSyncedElapsedDays = elapsedDays; _lastSyncedHour = currentHour; _lastSyncTimestamp = Time.realtimeSinceStartup; TNSLog.Debug($"Host updated lobby time: {currentTime} (Hour: {currentHour}), Days: {elapsedDays}"); } } catch (Exception ex) { TNSLog.Warning("Error in host time update loop: " + ex.Message); } float currentTimeRealtime = Time.realtimeSinceStartup; if (currentTimeRealtime - _lastMultiplierCheckTime >= 5f) { _lastMultiplierCheckTime = currentTimeRealtime; try { SyncHostMultiplierToLobby(); } catch (Exception ex2) { Exception ex3 = ex2; TNSLog.Warning("Error syncing host multiplier: " + ex3.Message); } } yield return waitTime; } } private void SyncHostMultiplierToLobby() { if (!IsHost || _steamClient == null || (Object)(object)NetworkSingleton<TimeManager>.Instance == (Object)null) { return; } try { float num = _getHostMultiplierCallback?.Invoke() ?? NetworkSingleton<TimeManager>.Instance.TimeSpeedMultiplier; if (float.IsNaN(_lastSyncedMultiplier) || Mathf.Abs(_lastSyncedMultiplier - num) > 0.0001f) { string text = num.ToString("R", CultureInfo.InvariantCulture); _steamClient.SetLobbyData("__tns_time_multiplier", text); _lastSyncedMultiplier = num; TNSLog.Debug($"Host synced multiplier to lobby: {num:0.########}x"); } } catch (Exception ex) { TNSLog.Warning("Error syncing host multiplier to lobby: " + ex.Message); } } private void SyncToHostMultiplier() { if (IsHost || _steamClient == null || (Object)(object)NetworkSingleton<TimeManager>.Instance == (Object)null) { return; } try { string lobbyData = _steamClient.GetLobbyData("__tns_time_multiplier"); if (!string.IsNullOrEmpty(lobbyData) && float.TryParse(lobbyData, NumberStyles.Float, CultureInfo.InvariantCulture, out var result) && float.IsFinite(result) && result > 0f && (float.IsNaN(_lastSyncedHostMultiplier) || Mathf.Abs(_lastSyncedHostMultiplier - result) > 0.0001f)) { TimeManager instance = NetworkSingleton<TimeManager>.Instance; if (!_clientOriginalMultiplier.HasValue) { _clientOriginalMultiplier = instance.TimeSpeedMultiplier; } TMAccess.SetTimeSpeedMultiplierDirect(instance, result); _lastSyncedHostMultiplier = result; TNSLog.Msg($"Synced time multiplier to host: {result:0.########}x (was {_clientOriginalMultiplier.Value:0.########}x)"); TNSLog.Debug($"[Config Sync] Client received and applied host time multiplier config: {result:0.########}x"); } } catch (Exception ex) { TNSLog.Warning("Error syncing to host multiplier: " + ex.Message); } } private IEnumerator ClientSyncTimeLoop() { WaitForSecondsRealtime waitTime = new WaitForSecondsRealtime(10f); while (IsInLobby && !IsHost && (Object)(object)NetworkSingleton<TimeManager>.Instance != (Object)null) { try { TimeManager tm = NetworkSingleton<TimeManager>.Instance; SteamNetworkClient? steamClient = _steamClient; string currentTimeStr = ((steamClient != null) ? steamClient.GetLobbyData("__tns_current_time") : null); SteamNetworkClient? steamClient2 = _steamClient; string elapsedDaysStr = ((steamClient2 != null) ? steamClient2.GetLobbyData("__tns_elapsed_days") : null); if (!string.IsNullOrEmpty(currentTimeStr) && !string.IsNullOrEmpty(elapsedDaysStr) && int.TryParse(currentTimeStr, out var hostCurrentTime) && int.TryParse(elapsedDaysStr, out var hostElapsedDays)) { bool hostDataChanged = _lastSyncedHostTime != hostCurrentTime || _lastSyncedHostDays != hostElapsedDays; bool isFirstSync = _lastSyncedHostTime == -1; if (hostDataChanged) { int localCurrentTime = tm.CurrentTime; int localElapsedDays = tm.ElapsedDays; int localMinutes = localCurrentTime / 100 * 60 + localCurrentTime % 100; int hostMinutes = hostCurrentTime / 100 * 60 + hostCurrentTime % 100; int timeDiff = Mathf.Abs(localMinutes - hostMinutes); int daysDiff = Mathf.Abs(localElapsedDays - hostElapsedDays); if (isFirstSync || (float)timeDiff > 5f || daysDiff > 0) { TMAccess.SetCurrentTime(tm, hostCurrentTime); if (daysDiff > 0 || isFirstSync) { TMAccess.SetElapsedDays(tm, hostElapsedDays); } TMAccess.SetDailyMinSum(tm, TimeManager.GetMinSumFrom24HourTime(hostCurrentTime)); _lastSyncedHostTime = hostCurrentTime; _lastSyncedHostDays = hostElapsedDays; TNSLog.Debug($"Client synced time to host: {localCurrentTime} -> {hostCurrentTime}, Days: {localElapsedDays} -> {hostElapsedDays}"); } else { TNSLog.Debug($"Client skipped sync: timeDiff={timeDiff}min <= {5f}, daysDiff={daysDiff}. (local={localCurrentTime:D4}, host={hostCurrentTime:D4})"); _lastSyncedHostTime = hostCurrentTime; _lastSyncedHostDays = hostElapsedDays; } } } } catch (Exception ex) { TNSLog.Warning("Error in client time sync loop: " + ex.Message); } yield return waitTime; } } private void OnLobbyDataChanged(object? sender, LobbyDataChangedEventArgs e) { if (_disposed || !IsInitialized || IsHost || !((Object)(object)NetworkSingleton<TimeManager>.Instance != (Object)null)) { return; } if (e.Key == "__tns_current_time" || e.Key == "__tns_elapsed_days" || e.Key == "__tns_time_update_ts") { try { TimeManager instance = NetworkSingleton<TimeManager>.Instance; SteamNetworkClient? steamClient = _steamClient; string text = ((steamClient != null) ? steamClient.GetLobbyData("__tns_current_time") : null); SteamNetworkClient? steamClient2 = _steamClient; string text2 = ((steamClient2 != null) ? steamClient2.GetLobbyData("__tns_elapsed_days") : null); if (!string.IsNullOrEmpty(text) && !string.IsNullOrEmpty(text2) && int.TryParse(text, out var result) && int.TryParse(text2, out var result2) && (_lastSyncedHostTime != result || _lastSyncedHostDays != result2)) { TMAccess.SetCurrentTime(instance, result); TMAccess.SetElapsedDays(instance, result2); TMAccess.SetDailyMinSum(instance, TimeManager.GetMinSumFrom24HourTime(result)); _lastSyncedHostTime = result; _lastSyncedHostDays = result2; TNSLog.Debug($"Client synced time immediately on lobby data change: {result}, Days: {result2}"); } return; } catch (Exception ex) { TNSLog.Warning("Error syncing time on lobby data change: " + ex.Message); return; } } if (e.Key == "__tns_time_multiplier") { SyncToHostMultiplier(); } } private void OnLobbyJoined(object? sender, LobbyJoinedEventArgs e) { TNSLog.Debug($"Lobby joined. IsHost={IsHost}, TM ready={(Object)(object)NetworkSingleton<TimeManager>.Instance != (Object)null}."); if (!IsHost && (Object)(object)NetworkSingleton<TimeManager>.Instance != (Object)null) { SteamNetworkClient? steamClient = _steamClient; string text = ((steamClient != null) ? steamClient.GetLobbyData("__tns_time_multiplier") : null); TNSLog.Debug("Client joined lobby. Host multiplier in lobby data: '" + text + "'."); SyncToHostMultiplier(); } else if (IsHost) { TNSLog.Debug("Host joined/created lobby. Broadcasting multiplier."); SyncHostMultiplierToLobby(); } StartSyncCoroutines(); } private void OnLobbyCreated(object? sender, LobbyCreatedEventArgs e) { TNSLog.Debug($"Lobby created. IsHost={IsHost}."); if (IsHost) { SyncHostMultiplierToLobby(); } StartSyncCoroutines(); } private void OnLobbyLeft(object? sender, LobbyLeftEventArgs e) { if (sender != null) { TNSLog.Debug("Lobby left, stopping time sync"); } if (_syncCoroutine != null) { MelonCoroutines.Stop(_syncCoroutine); _syncCoroutine = null; } if (_readCoroutine != null) { MelonCoroutines.Stop(_readCoroutine); _readCoroutine = null; } if (_clientOriginalMultiplier.HasValue && (Object)(object)NetworkSingleton<TimeManager>.Instance != (Object)null) { try { TMAccess.SetTimeSpeedMultiplierDirect(NetworkSingleton<TimeManager>.Instance, _clientOriginalMultiplier.Value); TNSLog.Msg($"Restored original time multiplier: {_clientOriginalMultiplier.Value:0.########}x"); } catch (Exception ex) { TNSLog.Warning("Error restoring original multiplier: " + ex.Message); } _clientOriginalMultiplier = null; } _lastSyncedCurrentTime = -1; _lastSyncedElapsedDays = -1; _lastSyncedHour = -1; _lastSyncTimestamp = 0f; _lastSyncedMultiplier = float.NaN; _lastMultiplierCheckTime = 0f; _lastSyncedHostTime = -1; _lastSyncedHostDays = -1; _lastSyncedHostMultiplier = float.NaN; } public void Dispose() { if (_disposed) { return; } try { OnLobbyLeft(null, null); if (_steamClient != null) { _steamClient.OnLobbyJoined -= OnLobbyJoined; _steamClient.OnLobbyCreated -= OnLobbyCreated; _steamClient.OnLobbyLeft -= OnLobbyLeft; _steamClient.OnLobbyDataChanged -= OnLobbyDataChanged; _steamClient.Dispose(); _steamClient = null; } } catch (Exception ex) { TNSLog.Warning("Error disposing TimeSyncManager: " + ex.Message); } _disposed = true; _isInitialized = false; } } internal static class DailySummaryStore { internal readonly struct SoldItem { internal string Id { get; } internal int Count { get; } internal SoldItem(string id, int count) { Id = id; Count = count; } } private static readonly List<string> _unlocks; private static readonly List<string> _pendingRegions; private static readonly List<string> _regions; private static readonly List<string> _pendingStory; private static readonly List<string> _story; private static bool _capturedThisNight; private static readonly List<SoldItem> _items; private static readonly FieldInfo _fiItemsSold; private static readonly FieldInfo _fiMoneyPlayer; private static readonly FieldInfo _fiMoneyDealers; private static readonly FieldInfo _fiSleepMessageText; private static readonly FieldInfo _fiIncomingGroupData; internal static bool HasSummary { get; private set; } internal static string CaptureError { get; private set; } internal static string DayLabel { get; private set; } internal static float MoneyEarnedByPlayer { get; private set; } internal static float MoneyEarnedByDealers { get; private set; } internal static int XpGained { get; private set; } internal static string RankBefore { get; private set; } internal static string RankAfter { get; private set; } internal static IReadOnlyList<string> Unlocks => _unlocks; internal static IReadOnlyList<string> RegionsUnlocked => _regions; internal static IReadOnlyList<string> StoryLines => _story; internal static IReadOnlyList<SoldItem> Items => _items; internal static event Action OnSummaryChanged; static DailySummaryStore() { DayLabel = string.Empty; _unlocks = new List<string>(); _pendingRegions = new List<string>(); _regions = new List<string>(); _pendingStory = new List<string>(); _story = new List<string>(); _items = new List<SoldItem>(); _fiItemsSold = AccessTools.Field(typeof(DailySummary), "itemsSoldByPlayer"); _fiMoneyPlayer = AccessTools.Field(typeof(DailySummary), "moneyEarnedByPlayer"); _fiMoneyDealers = AccessTools.Field(typeof(DailySummary), "moneyEarnedByDealers"); _fiSleepMessageText = AccessTools.Field(typeof(SleepMessage), "_message"); _fiIncomingGroupData = AccessTools.Field(typeof(IncomingSpecialCustomerInfoPopup), "_incomingGroupData"); if (_fiItemsSold == null || _fiMoneyPlayer == null || _fiMoneyDealers == null) { TNSLog.Warning("DailySummaryStore: one or more DailySummary fields were not found — the phone app will show an incomplete summary. A game update likely renamed them; check References\\MONO\\Assembly-CSharp\\ScheduleOne\\UI\\DailySummary.cs."); } if (_fiSleepMessageText == null || _fiIncomingGroupData == null) { TNSLog.Warning("DailySummaryStore: the overnight-message fields were not found — the phone app will not show special-customer messages. A game update likely renamed them; check References\\MONO\\Assembly-CSharp\\ScheduleOne\\UI\\SleepMessage\\SleepMessage.cs and SpecialCustomers\\UI\\IncomingSpecialCustomerInfoPopup.cs."); } } internal static void Capture(DailySummary ds) { _capturedThisNight = true; try { if ((Object)(object)ds == (Object)null) { Fail("DailySummary instance was null at capture time."); return; } _items.Clear(); if (_fiItemsSold?.GetValue(ds) is Dictionary<string, int> dictionary) { foreach (KeyValuePair<string, int> item in dictionary) { _items.Add(new SoldItem(item.Key, item.Value)); } } MoneyEarnedByPlayer = ((_fiMoneyPlayer?.GetValue(ds) is float num) ? num : 0f); MoneyEarnedByDealers = ((_fiMoneyDealers?.GetValue(ds) is float num2) ? num2 : 0f); XpGained = ds.xpGained; DayLabel = BuildDayLabel(); CaptureRankUp(XpGained); _regions.Clear(); _regions.AddRange(_pendingRegions); _pendingRegions.Clear(); _story.Clear(); _story.AddRange(_pendingStory); _pendingStory.Clear(); HasSummary = true; CaptureError = null; TNSLog.Debug($"Daily summary captured: {_items.Count} item type(s), " + $"player={MoneyEarnedByPlayer}, dealers={MoneyEarnedByDealers}, xp={XpGained}, " + "rankUp=" + ((RankAfter != null) ? (RankBefore + " -> " + RankAfter) : "none") + ", " + $"regions={_regions.Count}."); RaiseChanged(); } catch (Exception ex) { Fail(ex.GetType().Name + ": " + ex.Message); TNSLog.Error($"Daily summary capture failed, sleep is unaffected: {ex}"); } } private static void RaiseChanged() { try { DailySummaryStore.OnSummaryChanged?.Invoke(); } catch (Exception arg) { TNSLog.Error($"A summary-changed subscriber threw; sleep is unaffected: {arg}"); } } private static void CaptureRankUp(int xpGained) { //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) RankBefore = null; RankAfter = null; _unlocks.Clear(); try { LevelManager instance = NetworkSingleton<LevelManager>.Instance; if ((Object)(object)instance == (Object)null || xpGained <= 0) { return; } int totalXP = instance.TotalXP; FullRank fullRank = instance.GetFullRank(totalXP - xpGained); FullRank fullRank2 = instance.GetFullRank(totalXP); if (fullRank.Rank == fullRank2.Rank && fullRank.Tier == fullRank2.Tier) { return; } RankBefore = FullRank.GetString(fullRank); RankAfter = FullRank.GetString(fullRank2); try { if (instance.Unlockables == null || !instance.Unlockables.ContainsKey(fullRank2)) { return; } List<Unlockable> list = instance.Unlockables[fullRank2]; if (list == null) { return; } foreach (Unlockable item in list) { if (item != null && !string.IsNullOrEmpty(item.Title)) { _unlocks.Add(item.Title); } } } catch (Exception ex) { TNSLog.Debug("Could not read unlockables for the new rank: " + ex.Message); } } catch (Exception ex2) { TNSLog.Debug("Rank-up detection failed: " + ex2.Message); RankBefore = null; RankAfter = null; _unlocks.Clear(); } } internal unsafe static void NoteRegionUnlocked(EMapRegion region) { //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) try { string text = ((object)(*(EMapRegion*)(®ion))/*cast due to .constrained prefix*/).ToString(); try { Map instance = Singleton<Map>.Instance; MapRegionData val = ((instance != null) ? instance.GetRegionData(region) : null); if (val != null && !string.IsNullOrEmpty(val.Name)) { text = val.Name; } } catch (Exception ex) { TNSLog.Debug($"Could not resolve region name for {region}: {ex.Message}"); } if (!_pendingRegions.Contains(text)) { _pendingRegions.Add(text); } TNSLog.Debug("Region unlock recorded for the summary: " + text); } catch (Exception ex2) { TNSLog.Debug("NoteRegionUnlocked failed: " + ex2.Message); } } internal static void BeginNight() { _capturedThisNight = false; } internal static void EndNight() { _capturedThisNight = false; } internal static void NoteSleepMessage(SleepMessage message) { try { if (message != null) { string text = _fiSleepMessageText?.GetValue(message) as string; NoteStoryLine(text); } } catch (Exception ex) { TNSLog.Debug("NoteSleepMessage failed: " + ex.Message); } } internal static void NoteIncomingSpecialCustomers(IncomingSpecialCustomerInfoPopup popup) { try { if (!((Object)(object)popup == (Object)null)) { object? obj = _fiIncomingGroupData?.GetValue(popup); SpecialCustomerData val = (SpecialCustomerData)((obj is SpecialCustomerData) ? obj : null); if (!((Object)(object)val == (Object)null)) { NoteStoryLine(val.GetIncomingMessage(true)); } } } catch (Exception ex) { TNSLog.Debug("NoteIncomingSpecialCustomers failed: " + ex.Message); } } private static void NoteStoryLine(string text) { if (string.IsNullOrEmpty(text)) { return; } if (_capturedThisNight) { if (_story.Contains(text)) { return; } _story.Add(text); RaiseChanged(); } else { if (_pendingStory.Contains(text)) { return; } _pendingStory.Add(text); } TNSLog.Debug("Overnight line recorded for the summary: " + text); } private static void Fail(string reason) { HasSummary = false; CaptureError = reason; ClearData(); RaiseChanged(); } internal static void Reset() { HasSummary = false; CaptureError = null; ClearData(); _pendingRegions.Clear(); _pendingStory.Clear(); _capturedThisNight = false; DailySummaryStore.OnSummaryChanged = null; } private static void ClearData() { DayLabel = string.Empty; _items.Clear(); _regions.Clear(); _unlocks.Clear(); _story.Clear(); RankBefore = null; RankAfter = null; MoneyEarnedByPlayer = (MoneyEarnedByDealers = 0f); XpGained = 0; } private static string BuildDayLabel() { //IL_0021: Unknown result type (might be due to invalid IL or missing references) try { TimeManager instance = NetworkSingleton<TimeManager>.Instance; if ((Object)(object)instance == (Object)null) { return string.Empty; } return $"{instance.CurrentDay}, Day {instance.ElapsedDays + 1}"; } catch (Exception ex) { TNSLog.Debug("Could not build day label: " + ex.Message); return string.Empty; } } internal static bool TryGetItemDisplay(string id, out string displayName, out Sprite icon) { displayName = id; icon = null; try { ItemDefinition item = Registry.GetItem(id); if ((Object)(object)item == (Object)null) { return false; } if (!string.IsNullOrEmpty(((BaseItemDefinition)item).Name)) { displayName = ((BaseItemDefinition)item).Name; } icon = ((BaseItemDefinition)item).Icon; return true; } catch (Exception ex) { TNSLog.Debug("Could not resolve item '" + id + "': " + ex.Message); return false; } } internal static string FormatMoney(float amount) { try { return MoneyManager.FormatAmount(amount, false, false); } catch (Exception ex) { TNSLog.Debug("MoneyManager.FormatAmount failed: " + ex.Message); return "$" + amount.ToString("N2"); } } } internal static class S1APIBridge { private const string S1ApiAssemblyName = "S1API"; private const string PhoneAppTypeName = "S1API.PhoneApp.PhoneApp"; private static bool _probed; private static bool _present; private static bool _registered; private static string _failure; private static bool _loggedAbsence; internal static bool IsS1APIPresent { get { if (_probed) { return _present; } _probed = true; try { Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { if (string.Equals(assembly.GetName().Name, "S1API", StringComparison.OrdinalIgnoreCase)) { _present = assembly.GetType("S1API.PhoneApp.PhoneApp", throwOnError: false) != null; break; } } } catch (Exception ex) { _present = false; TNSLog.Debug("S1API probe failed, treating as absent: " + ex.Message); } return _present; } } internal static bool AppRegistered => _registered && _failure == null; internal static string AppFailure => _failure; internal static void MarkAppRegistered() { _registered = true; _failure = null; } internal static void MarkAppFailed(string reason) { _failure = reason ?? "unknown"; TNSLog.Error("Daily Summary phone app is unavailable (" + _failure + "). Falling back to the vanilla popup so the summary is not lost."); } internal static bool ShouldGateSummaryBehindApp() { MelonPreferences_Entry<bool> cfgEnableSummaryApp = Core.cfgEnableSummaryApp; if (cfgEnableSummaryApp == null || !cfgEnableSummaryApp.Value) { return false; } if (!IsS1APIPresent) { if (!_loggedAbsence) { _loggedAbsence = true; TNSLog.Error("EnableDailySummaryApp is on but S1API is not installed, so there is no phone app to show the summary in. Install ifBars-S1API_Forked, or set EnableDailySummaryApp=false to silence this. The vanilla popup stays enabled in the meantime."); } return false; } return AppRegistered; } internal static void Reset() { _registered = false; _failure = null; } } public class DailySummaryApp : PhoneApp { internal const string AppKey = "TNSDailySummary"; private const string IconResourceName = "Time_Never_Stops.icon.png"; private static readonly Color PanelColor = new Color(0.11f, 0.11f, 0.13f, 1f); private static readonly Color HeaderColor = new Color(0.16f, 0.16f, 0.19f, 1f); private static readonly Color MutedColor = new Color(0.68f, 0.68f, 0.72f, 1f); private static readonly Color AccentColor = new Color(0.45f, 0.8f, 0.55f, 1f); private static readonly Color StoryColor = new Color(0.92f, 0.8f, 0.45f, 1f); private GameObject _container; protected override string AppName => "TNSDailySummary"; protected override string AppTitle => "Daily Summary"; protected override string IconLabel => "Summary"; protected override string IconFileName => "TimeNeverStops_Summary.png"; protected override void OnCreated() { try { TrySetEmbeddedIcon(); ((PhoneApp)this).OnCreated(); S1APIBridge.MarkAppRegistered(); TNSLog.Msg("Daily Summary phone app registered."); } catch (Exception ex) { S1APIBridge.MarkAppFailed("OnCreated failed: " + ex.Message); TNSLog.Error($"Daily Summary app failed during OnCreated: {ex}"); } } protected override void OnCreatedUI(GameObject container) { _container = container; DailySummaryStore.OnSummaryChanged -= Rebuild; DailySummaryStore.OnSummaryChanged += Rebuild; Rebuild(); } protected override void OnDestroyed() { DailySummaryStore.OnSummaryChanged -= Rebuild; _container = null; ((PhoneApp)this).OnDestroyed(); } private void Rebuild() { GameObject container = _container; if ((Object)(object)container == (Object)null) { return; } try { UIFactory.ClearChildren(container.transform); BuildUI(container); } catch (Exception ex) { S1APIBridge.MarkAppFailed("UI build failed: " + ex.Message); TNSLog.Error($"Daily Summary app UI failed to build; the mod's time features are unaffected: {ex}"); TryShowFatalPanel(container, ex); } } private void BuildUI(GameObject container) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_015f: Unknown result type (might be due to invalid IL or missing references) //IL_0169: Unknown result type (might be due to invalid IL or missing references) //IL_017d: Unknown result type (might be due to invalid IL or missing references) //IL_0187: Unknown result type (might be due to invalid IL or missing references) //IL_01ee: Unknown result type (might be due to invalid IL or missing references) //IL_01f8: Expected O, but got Unknown //IL_021b: Unknown result type (might be due to invalid IL or missing references) //IL_0250: Unknown result type (might be due to invalid IL or missing references) //IL_02a6: Unknown result type (might be due to invalid IL or missing references) //IL_033c: Unknown result type (might be due to invalid IL or missing references) //IL_02e5: Unknown result type (might be due to invalid IL or missing references) //IL_0384: Unknown result type (might be due to invalid IL or missing references) //IL_03e0: Unknown result type (might be due to invalid IL or missing references) //IL_0408: Unknown result type (might be due to invalid IL or missing references) //IL_0430: Unknown result type (might be due to invalid IL or missing references) GameObject val = UIFactory.Panel("TNSSummaryPanel", container.transform, PanelColor, (Vector2?)null, (Vector2?)null, true); GameObject val2 = UIFactory.Panel("TNSSummaryHeader", val.transform, HeaderColor, (Vector2?)null, (Vector2?)null, false); SetRect(val2, 0f, 0.86f, 1f, 1f); Text val3 = UIFactory.Text("TNSSummaryTitle", "Daily Summary", val2.transform, 24, (TextAnchor)3, (FontStyle)1); SetRect(((Component)val3).gameObject, 0.03f, 0f, 0.97f, 1f); GameObject val4 = UIFactory.Panel("TNSSummaryBody", val.transform, PanelColor, (Vector2?)null, (Vector2?)null, false); SetRect(val4, 0f, 0f, 1f, 0.86f); if (!DailySummaryStore.HasSummary) { Text val5 = UIFactory.Text("TNSSummaryEmpty", EmptyMessage(), val4.transform, 18, (TextAnchor)4, (FontStyle)0); SetRect(((Component)val5).gameObject, 0.06f, 0.1f, 0.94f, 0.9f); return; } ScrollRect val7 = default(ScrollRect); RectTransform val6 = UIFactory.ScrollableVerticalList("TNSSummaryList", val4.transform, ref val7); val6.sizeDelta = new Vector2(0f, val6.sizeDelta.y); val6.anchoredPosition = new Vector2(0f, val6.anchoredPosition.y); VerticalLayoutGroup component = ((Component)val6).GetComponent<VerticalLayoutGroup>(); if ((Object)(object)component != (Object)null) { ((HorizontalOrVerticalLayoutGroup)component).childControlWidth = true; ((HorizontalOrVerticalLayoutGroup)component).childForceExpandWidth = true; ((HorizontalOrVerticalLayoutGroup)component).childControlHeight = true; ((HorizontalOrVerticalLayoutGroup)component).childForceExpandHeight = false; ((LayoutGroup)component).childAlignment = (TextAnchor)0; ((HorizontalOrVerticalLayoutGroup)component).spacing = 6f; ((LayoutGroup)component).padding = new RectOffset(16, 16, 12, 12); } if (!string.IsNullOrEmpty(DailySummaryStore.DayLabel)) { AddLine((Transform)(object)val6, "TNSSummaryDay", DailySummaryStore.DayLabel, 20, MutedColor); } IReadOnlyList<string> storyLines = DailySummaryStore.StoryLines; for (int i = 0; i < storyLines.Count; i++) { AddLine((Transform)(object)val6, $"TNSSummaryStory{i}", storyLines[i], 18, StoryColor); } if (DailySummaryStore.RankAfter != null) { AddLine((Transform)(object)val6, "TNSSummaryRank", "Rank up: " + DailySummaryStore.RankBefore + " -> " + DailySummaryStore.RankAfter, 20, AccentColor); IReadOnlyList<string> unlocks = DailySummaryStore.Unlocks; for (int j = 0; j < unlocks.Count; j++) { AddLine((Transform)(object)val6, $"TNSSummaryUnlock{j}", " Unlocked: " + unlocks[j], 16, MutedColor); } } IReadOnlyList<string> regionsUnlocked = DailySummaryStore.RegionsUnlocked; for (int k = 0; k < regionsUnlocked.Count; k++) { AddLine((Transform)(object)val6, $"TNSSummaryRegion{k}", "New region: " + regionsUnlocked[k], 20, AccentColor); } IReadOnlyList<DailySummaryStore.SoldItem> items = DailySummaryStore.Items; if (items.Count == 0) { AddLine((Transform)(object)val6, "TNSSummaryNoSales", "No sales recorded.", 18, MutedColor); } else { for (int l = 0; l < items.Count; l++) { AddItemRow((Transform)(object)val6, l, items[l]); } } AddLine((Transform)(object)val6, "TNSSummaryPlayer", "You earned " + DailySummaryStore.FormatMoney(DailySummaryStore.MoneyEarnedByPlayer), 20, AccentColor); AddLine((Transform)(object)val6, "TNSSummaryDealers", "Dealers earned " + DailySummaryStore.FormatMoney(DailySummaryStore.MoneyEarnedByDealers), 20, AccentColor); AddLine((Transform)(object)val6, "TNSSummaryXP", $"{DailySummaryStore.XpGained} XP", 20, AccentColor); } private static void SetRect(GameObject go, float xMin, float yMin, float xMax, float yMax) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)go == (Object)null)) { RectTransform component = go.GetComponent<RectTransform>(); if (!((Object)(object)component == (Object)null)) { component.anchorMin = new Vector2(xMin, yMin); component.anchorMax = new Vector2(xMax, yMax); component.pivot = new Vector2(0.5f, 0.5f); component.offsetMin = Vector2.zero; component.offsetMax = Vector2.zero; ((Transform)component).localScale = Vector3.one; } } } private static string EmptyMessage() { string captureError = DailySummaryStore.CaptureError; if (!string.IsNullOrEmpty(captureError)) { return "Yesterday's summary could not be read.\n\n" + captureError; } return "No summary yet.\n\nIt will appear here after your first night."; } private static void AddLine(Transform parent, string name, string text, int size, Color color) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) Text val = UIFactory.Text(name, text, parent, size, (TextAnchor)3, (FontStyle)0); if (!((Object)(object)val == (Object)null)) { ((Graphic)val).color = color; LayoutElement val2 = ((Component)val).gameObject.AddComponent<LayoutElement>(); val2.minHeight = size + 8; } } private static void AddItemRow(Transform parent, int index, DailySummaryStore.SoldItem item) { //IL_003c: Unknown result type (might be due to invalid IL or missing references) DailySummaryStore.TryGetItemDisplay(item.Id, out string displayName, out Sprite _); AddLine(parent, $"TNSSummaryItem{index}", $" {item.Count}x {displayName}", 18, Color.white); } private void TrySetEmbeddedIcon() { //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Expected O, but got Unknown try { Assembly assembly = typeof(DailySummaryApp).Assembly; using Stream stream = assembly.GetManifestResourceStream("Time_Never_Stops.icon.png"); if (stream == null) { TNSLog.Debug("Embedded icon 'Time_Never_Stops.icon.png' not found; using the default app icon."); return; } byte[] array = new byte[stream.Length]; int num; for (int i = 0; i < array.Length; i += num) { num = stream.Read(array, i, array.Length - i); if (num <= 0) { break; } } Texture2D val = new Texture2D(2, 2); if (ImageConversion.LoadImage(val, array)) { ((PhoneApp)this).SetIconTexture(val); } else { TNSLog.Debug("Embedded icon failed to decode; using the default app icon."); } } catch (Exception ex) { TNSLog.Debug("Could not apply the embedded app icon: " + ex.Message); } } private static void TryShowFatalPanel(GameObject container, Exception cause) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) try { GameObject val = UIFactory.Panel("TNSSummaryError", container.transform, PanelColor, (Vector2?)null, (Vector2?)null, true); Text val2 = UIFactory.Text("TNSSummaryErrorText", "Daily Summary could not be displayed.\n\n" + cause.GetType().Name + ": " + cause.Message + "\n\nThe rest of Time Never Stops is unaffected.\nCheck MelonLoader\\Latest.log for detail.", val.transform, 16, (TextAnchor)4, (FontStyle)0); SetRect(((Component)val2).gameObject, 0.06f, 0.1f, 0.94f, 0.9f); } catch { } } } internal static class LocalMultiplayer { internal const ushort LoopbackPort = 38465; internal const KeyCode JoinKey = (KeyCode)287; private static bool _serverOpened; private static bool _syncNoticeLogged; private static object _openRoutine; internal static bool Enabled => Core.cfgLocalMultiplayer?.Value ?? false; internal static void Reset() { if (_openRoutine != null) { try { MelonCoroutines.Stop(_openRoutine); } catch { } _openRoutine = null; } _serverOpened = false; _syncNoticeLogged = false; } internal static void OnGameSceneLoaded() { if (Enabled) { _openRoutine = MelonCoroutines.Start(OpenLoopbackServerWhenReady()); } } internal static void Tick() { if (Enabled && Input.GetKeyDown((KeyCode)287)) { TryJoinLocalHost(); } } private static IEnumerator OpenLoopbackServerWhenReady() { float start = Time.realtimeSinceStartup; while (!InstanceFinder.IsServer && Time.realtimeSinceStartup - start < 180f) { yield return (object)new WaitForSecondsRealtime(0.5f); } if (!InstanceFinder.IsServer) { TNSLog.Debug("Local multiplayer: this instance is not the server, so there is no loopback port to open."); _openRoutine = null; } else if (Singleton<Lobby>.InstanceExists && (Object)(object)Singleton<Lobby>.Instance != (Object)null && Singleton<Lobby>.Instance.IsInLobby) { TNSLog.Msg("Local multiplayer: skipped — this session is in a Steam lobby, which already has real networking."); _openRoutine = null; } else { OpenLoopbackServer(); _openRoutine = null; } } private static void OpenLoopbackServer() { //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Invalid comparison between Unknown and I4 try { TransportManager transportManager = InstanceFinder.TransportManager; Multipass val = ((transportManager != null) ? transportManager.GetTransport<Multipass>() : null); if ((Object)(object)val == (Object)null) { TNSLog.Warning("Local multiplayer: no Multipass transport on this build — cannot open the loopback port."); return; } Tugboat transport = val.GetTransport<Tugboat>(); if ((Object)(object)transport == (Object)null) { TNSLog.Error("Local multiplayer: Tugboat is not in this build's Multipass transport list, so a second instance cannot connect. Nothing else about the mod is affected."); return; } if ((int)((Transport)transport).GetConnectionState(true) == 2) { TNSLog.Debug("Local multiplayer: the loopback port is already open."); _serverOpened = true; return; } ((Transport)transport).SetPort((ushort)38465); if (!((Transport)transport).StartConnection(true)) { TNSLog.Error($"Local multiplayer: Tugboat refused to listen on port {(ushort)38465}. " + "Something else may already hold it."); return; } _serverOpened = true; TNSLog.Msg($"Local multiplayer: listening on 127.0.0.1:{(ushort)38465}. " + $"Press {(object)(KeyCode)287} in the OTHER instance to join this session."); LogSyncNoticeOnce(); } catch (Exception arg) { TNSLog.Error($"Local multiplayer: could not open the loopback port; the session is unaffected: {arg}"); } } private static void TryJoinLocalHost() { try { LoadManager val = (Singleton<LoadManager>.InstanceExists ? Singleton<LoadManager>.Instance : null); if ((Object)(object)val == (Object)null) { TNSLog.Warning("Local multiplayer: no LoadManager yet — wait for the main menu and try again."); return; } if (val.IsLoading) { TNSLog.Debug("Local multiplayer: already loading; ignoring the join key."); return; } if (InstanceFinder.IsServer) { TNSLog.Warning($"Local multiplayer: this instance IS the host — press {(object)(KeyCode)287} in the other one. Ignoring."); return; } TNSLog.Msg($"Local multiplayer: joining 127.0.0.1:{(ushort)38465} as a client..."); LogSyncNoticeOnce(); val.LoadAsClient("localhost"); } catch (Exception arg) { TNSLog.Error($"Local multiplayer: join failed; the session is unaffected: {arg}"); } } private static void LogSyncNoticeOnce() { if (!_syncNoticeLogged) { _syncNoticeLogged = true; TNSLog.Msg("Local multiplayer: there is no Steam lobby here, so TimeSyncManager's lobby-data sync is INACTIVE. The clock still tracks the host through the game's own PassMinute_Client RPC. Keep DaySpeedMultiplier identical in both instances — the host cannot push it without a lobby."); } } } internal static class BuildVersion { internal const string Value = "1.5.1"; } } namespace Time_Never_Stops.TemplateUtils { public static class AssetBundleUtils { private static Core mod = MelonAssembly.FindMelonInstance<Core>(); private static MelonAssembly melonAssembly = ((MelonBase)mod).MelonAssembly; public static AssetBundle LoadAssetBundle(string bundleFileName) { try { string text = typeof(Core).Namespace + ".Assets." + bundleFileName; Stream manifestResourceStream = melonAssembly.Assembly.GetManifestResourceStream(text ?? ""); if (manifestResourceStream == null) { ((MelonBase)mod).Unregister("AssetBundle: '" + text + "' not found. \nOpen .csproj file and search for '" + bundleFileName + "'.\nIf it doesn't exist,\nCopy your asset to Assets/ folder then look for 'your.assetbundle' in .csproj file.", false); return null; } return AssetBundle.LoadFromStream(manifestResourceStream); } catch (Exception arg) { ((MelonBase)mod).Unregister($"Failed to load AssetBundle. Please report to dev: {arg}", false); return null; } } public static AssetBundle GetLoadedAssetBundle(string asset_name_flag) { AssetBundle[] array = AssetBundle.GetAllLoadedAssetBundles().ToArray(); try { AssetBundle[] array2 = array; foreach (AssetBundle val in array2) { if (val.Contains(asset_name_flag)) { return val; } } string text = ""; AssetBundle[] array3 = array; foreach (AssetBundle val2 in array3) { string[] allAssetNames = val2.GetAllAssetNames(); string text2 = string.Join("\n\r -", allAssetNames); text = text + ((Object)val2).name + $"({allAssetNames.Length} assets):" + text2; } throw new Exception($"Asset '{asset_name_flag}' not found in {array.Length} bundle(s).\n{text}"); } catch (Exception arg) { ((MelonBase)mod).Unregister($"Failed to get loaded AssetBundle. Please report to dev: \n{arg}", false); return null; } } public static GameObject LoadAssetFromBundle(string asset_name) { AssetBundle loadedAssetBundle = GetLoadedAssetBundle(asset_name); return loadedAssetBundle.LoadAsset<GameObject>(asset_name); } } } namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] internal sealed class IgnoresAccessChecksToAttribute : Attribute { internal IgnoresAccessChecksToAttribute(string assemblyName) { } } }