From 16d118c4a93c24d9ea787dabcf5d5cdb600a0686 Mon Sep 17 00:00:00 2001 From: seton Date: Fri, 11 Sep 2026 15:19:23 -0400 Subject: [PATCH] MiscTweaks: cancellable autoload of the most recent save From the main menu, a short countdown loads the newest save. Any input cancels so a misclick does not dump you into a session. --- README.md | 8 + src/Main.cs | 1 + src/Modules/MiscTweaks/MiscTweaksModule.cs | 44 +++++ src/Modules/MiscTweaks/MiscTweaksSettings.cs | 11 ++ .../MiscTweaks/MiscTweaksSettingsUI.cs | 42 +++++ .../MiscTweaks/RecentSaveAutoLoader.cs | 177 ++++++++++++++++++ 6 files changed, 283 insertions(+) create mode 100644 src/Modules/MiscTweaks/MiscTweaksModule.cs create mode 100644 src/Modules/MiscTweaks/MiscTweaksSettings.cs create mode 100644 src/Modules/MiscTweaks/MiscTweaksSettingsUI.cs create mode 100644 src/Modules/MiscTweaks/RecentSaveAutoLoader.cs diff --git a/README.md b/README.md index 9961642..4c53121 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ I originally planned on releasing individual mods, but considering my workflow o | Mesh LOD | Adds level-of-detail to rolling stock: distant cars progressively shed detail and finally collapse to a cheap proxy box, cutting triangle count on large saves. | | Base Game Performance | Smooths Unity's incremental garbage collector and Nature Renderer grass streaming to reduce camera-motion hitches without lowering visual quality. | | Profiler | Unified in-game performance overlay with hitch attribution captures. Console: `/rpf overlay`, `/s3bench` | +| Misc Tweaks | Small QoL: cancellable autoload of the most recent save from the main menu. | All modules are disabled by default; enable them per-module from the S³ settings page. A game restart is required for enable/disable to take effect. More modules will follow; S³ is designed to grow. @@ -197,6 +198,13 @@ Toggle the overlay with `/rpf overlay`, or from the Profiler settings page. Run --- +## Misc Tweaks + +After a short cancellable countdown on the main menu, load the most recent save. +Any key cancels. Enable it from the S³ settings page. + +--- + ## Migrating from the standalone mods S³ replaces the separate **Physics Optimizer** (`RailroaderPhysicsOverhaul`) and diff --git a/src/Main.cs b/src/Main.cs index 2244dcd..8aafcde 100644 --- a/src/Main.cs +++ b/src/Main.cs @@ -34,6 +34,7 @@ public static class Main _registry.Register(new Modules.MeshLod.MeshLodModule()); _registry.Register(new Modules.BaseGamePerf.BaseGamePerfModule()); _registry.Register(new Modules.Profiler.ProfilerModule()); + _registry.Register(new Modules.MiscTweaks.MiscTweaksModule()); _registry.Register(new Modules.Popout.PopoutModule()); _registry.EnableConfigured(); diff --git a/src/Modules/MiscTweaks/MiscTweaksModule.cs b/src/Modules/MiscTweaks/MiscTweaksModule.cs new file mode 100644 index 0000000..73d7c4e --- /dev/null +++ b/src/Modules/MiscTweaks/MiscTweaksModule.cs @@ -0,0 +1,44 @@ +using S3.Core; +using UnityEngine; + +namespace S3.Modules.MiscTweaks; + +public sealed class MiscTweaksModule : IModule +{ + const string SettingsFile = "S3.misctweaks.json"; + + public static MiscTweaksSettings Settings { get; private set; } = new(); + + static GameObject? _host; + + public MiscTweaksModule() => + Settings = SettingsStore.Load(SettingsFile); + + public string Id => "misctweaks"; + public string DisplayName => "Misc. Tweaks"; + public string Description => + "Small quality-of-life changes that do not belong to a larger module."; + + public bool Enabled + { + get => Settings.enabled; + set => Settings.enabled = value; + } + + public void OnEnable() + { + _host = new GameObject("[S3] MiscTweaksHost"); + Object.DontDestroyOnLoad(_host); + _host.AddComponent(); + } + + public void OnDisable() + { + if (_host != null) Object.Destroy(_host); + _host = null; + } + + public void SaveSettings() => Persist(); + internal static void Persist() => SettingsStore.Save(SettingsFile, Settings); + public void DrawSettings() => MiscTweaksSettingsUI.Draw(); +} diff --git a/src/Modules/MiscTweaks/MiscTweaksSettings.cs b/src/Modules/MiscTweaks/MiscTweaksSettings.cs new file mode 100644 index 0000000..e399df2 --- /dev/null +++ b/src/Modules/MiscTweaks/MiscTweaksSettings.cs @@ -0,0 +1,11 @@ +using System; + +namespace S3.Modules.MiscTweaks; + +[Serializable] +public class MiscTweaksSettings +{ + public bool enabled = false; + public bool autoLoadMostRecent = false; + public float autoLoadCountdownSeconds = 3f; +} diff --git a/src/Modules/MiscTweaks/MiscTweaksSettingsUI.cs b/src/Modules/MiscTweaks/MiscTweaksSettingsUI.cs new file mode 100644 index 0000000..abaaa48 --- /dev/null +++ b/src/Modules/MiscTweaks/MiscTweaksSettingsUI.cs @@ -0,0 +1,42 @@ +using UnityEngine; + +namespace S3.Modules.MiscTweaks; + +static class MiscTweaksSettingsUI +{ + public static void Draw() + { + MiscTweaksSettings settings = MiscTweaksModule.Settings; + bool changed = false; + + GUILayout.Label("Recent save autoload"); + GUILayout.Space(4f); + bool enabled = GUILayout.Toggle( + settings.autoLoadMostRecent, + " Automatically load the most recently modified save from the main menu"); + if (enabled != settings.autoLoadMostRecent) + { + settings.autoLoadMostRecent = enabled; + changed = true; + } + + GUILayout.BeginHorizontal(); + GUILayout.Label("Countdown (seconds)", GUILayout.Width(175f)); + float countdown = GUILayout.HorizontalSlider( + settings.autoLoadCountdownSeconds, 1f, 15f, GUILayout.Width(180f)); + GUILayout.Label(settings.autoLoadCountdownSeconds.ToString("0"), GUILayout.Width(48f)); + GUILayout.EndHorizontal(); + countdown = Mathf.Round(countdown); + if (Mathf.Abs(countdown - settings.autoLoadCountdownSeconds) >= 0.5f) + { + settings.autoLoadCountdownSeconds = countdown; + changed = true; + } + + GUILayout.Label( + " Press any keyboard, mouse, or controller button during the countdown to cancel."); + + if (changed) + MiscTweaksModule.Persist(); + } +} diff --git a/src/Modules/MiscTweaks/RecentSaveAutoLoader.cs b/src/Modules/MiscTweaks/RecentSaveAutoLoader.cs new file mode 100644 index 0000000..6aa60f8 --- /dev/null +++ b/src/Modules/MiscTweaks/RecentSaveAutoLoader.cs @@ -0,0 +1,177 @@ +using System; +using System.Reflection; +using Game; +using Game.Persistence; +using Game.State; +using HarmonyLib; +using S3.Core; +using UI.Menu; +using UnityEngine; + +namespace S3.Modules.MiscTweaks; + +sealed class RecentSaveAutoLoader : MonoBehaviour +{ + static readonly MethodInfo? StartSingleplayer = + AccessTools.Method( + typeof(MenuManager), + "StartGameSinglePlayer", + new[] { typeof(GameSetup) }); + + bool _wasMainMenu; + bool _attempted; + bool _counting; + bool _cancelled; + float _loadAt; + float _acceptCancelAt; + string _saveName = ""; + DateTime _saveDate; + string _status = ""; + + void Update() + { + bool mainMenu = false; + try { mainMenu = SceneDescriptor.MainMenu.IsLoaded; } + catch { } + + if (!mainMenu) + { + _wasMainMenu = false; + _attempted = false; + _counting = false; + _cancelled = false; + return; + } + + if (!_wasMainMenu) + { + _wasMainMenu = true; + _attempted = false; + _counting = false; + _cancelled = false; + _status = ""; + } + + MiscTweaksSettings settings = MiscTweaksModule.Settings; + if (!settings.autoLoadMostRecent || _attempted) return; + if (!_counting) + { + TryStartCountdown(settings); + return; + } + + if (Time.unscaledTime >= _acceptCancelAt && AnyButtonDown()) + { + _counting = false; + _cancelled = true; + _attempted = true; + _status = "Recent-save autoload cancelled."; + Log.Info("[misc] " + _status); + return; + } + + if (Time.unscaledTime < _loadAt) return; + _counting = false; + _attempted = true; + LoadRecentSave(); + } + + void TryStartCountdown(MiscTweaksSettings settings) + { + if (StartSingleplayer == null) + { + _attempted = true; + _status = "Recent-save autoload unavailable: game launch method not found."; + Log.Warn("[misc] " + _status); + return; + } + if (FindObjectOfType() == null) + return; + + var saves = WorldStore.FindSaveInfos(); + if (saves == null || saves.Count == 0) + { + _attempted = true; + _status = "No saves found."; + return; + } + + WorldStore.SaveInfo recent = saves[0]; + _saveName = recent.Name; + _saveDate = recent.Date; + float seconds = Mathf.Clamp(settings.autoLoadCountdownSeconds, 1f, 15f); + _loadAt = Time.unscaledTime + seconds; + _acceptCancelAt = Time.unscaledTime + 0.15f; + _counting = true; + _status = $"Loading {_saveName} in {seconds:0} seconds..."; + Log.Info($"[misc] {_status}"); + } + + void LoadRecentSave() + { + MenuManager? manager = FindObjectOfType(); + if (manager == null || StartSingleplayer == null) + { + _status = "Recent-save autoload cancelled: main menu is no longer ready."; + Log.Warn("[misc] " + _status); + return; + } + + try + { + _status = "Loading " + _saveName + "..."; + Log.Info("[misc] " + _status); + StartSingleplayer.Invoke(manager, new object[] { new GameSetup(_saveName) }); + } + catch (Exception e) + { + _status = "Recent-save autoload failed: " + (e.InnerException?.Message ?? e.Message); + Log.Error("[misc] " + _status); + } + } + + static bool AnyButtonDown() + { + try + { + if (Input.anyKeyDown) return true; + for (int i = 0; i < 7; i++) + if (Input.GetMouseButtonDown(i)) return true; + } + catch { } + return false; + } + + void OnGUI() + { + if (!_counting || _cancelled) return; + float remaining = Mathf.Max(0f, _loadAt - Time.unscaledTime); + const float width = 520f; + const float height = 104f; + Rect box = new Rect( + (Screen.width - width) * 0.5f, + Mathf.Max(24f, Screen.height * 0.13f), + width, + height); + GUI.Box(box, ""); + var title = new GUIStyle(GUI.skin.label) + { + alignment = TextAnchor.MiddleCenter, + fontSize = 20, + fontStyle = FontStyle.Bold, + }; + var detail = new GUIStyle(GUI.skin.label) + { + alignment = TextAnchor.MiddleCenter, + fontSize = 14, + }; + GUI.Label( + new Rect(box.x + 12f, box.y + 10f, box.width - 24f, 32f), + $"Loading most recent save in {Mathf.CeilToInt(remaining)}...", + title); + GUI.Label( + new Rect(box.x + 12f, box.y + 43f, box.width - 24f, 50f), + $"{_saveName} ({_saveDate:g})\nPress any keyboard, mouse, or controller button to cancel", + detail); + } +}