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.
This commit is contained in:
Seton Carmichael 2026-09-11 15:19:23 -04:00
parent 9f2097e579
commit 16d118c4a9
6 changed files with 283 additions and 0 deletions

View file

@ -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

View file

@ -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();

View file

@ -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<MiscTweaksSettings>(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<RecentSaveAutoLoader>();
}
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();
}

View file

@ -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;
}

View file

@ -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("<b>Recent save autoload</b>");
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();
}
}

View file

@ -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<MenuManager>() == 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<MenuManager>();
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);
}
}