diff --git a/Directory.Build.props b/Directory.Build.props index 0c4adc03..47e63f7d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -72,6 +72,6 @@ true false - 4.13.0.0 + 4.14.0.0 diff --git a/FastTrack/PathPatches/DeferredTriggers.cs b/FastTrack/PathPatches/DeferredTriggers.cs index 9726d0a7..e4266624 100644 --- a/FastTrack/PathPatches/DeferredTriggers.cs +++ b/FastTrack/PathPatches/DeferredTriggers.cs @@ -121,20 +121,22 @@ internal void Process() { src.DestroySelf(); } } - while (cacheCellPending.TryDequeue(out var item)) { - int cell = Grid.PosToCell(item.transform.position), oldCell = item.cachedCell; - if (cell != oldCell) { + while (cacheCellPending.TryDequeue(out var item)) + // Pickupables can be destroyed mid-frame + if (item != null) { + int cell = Grid.PosToCell(item.transform.position), oldCell = item.cachedCell; + if (cell != oldCell) { #if DEBUG - PUtil.LogDebug("Adjusted bugged item {0} from {1:D} to {2:D}".F( - item.name, oldCell, cell)); + PUtil.LogDebug("Adjusted bugged item {0} from {1:D} to {2:D}".F( + item.name, oldCell, cell)); #endif - item.UpdateCachedCell(cell); - gsp.UpdatePosition(item.solidPartitionerEntry, cell); - gsp.UpdatePosition(item.partitionerEntry, cell); - item.NotifyChanged(oldCell); - item.NotifyChanged(cell); + item.UpdateCachedCell(cell); + gsp.UpdatePosition(item.solidPartitionerEntry, cell); + gsp.UpdatePosition(item.partitionerEntry, cell); + item.NotifyChanged(oldCell); + item.NotifyChanged(cell); + } } - } while (offsetPending.TryDequeue(out var offset)) offset.offsets.GetOffsets(offset.newCell); } diff --git a/FastTrack/VisualPatches/BuildWatermarkPatches.cs b/FastTrack/VisualPatches/BuildWatermarkPatches.cs new file mode 100644 index 00000000..8d18219c --- /dev/null +++ b/FastTrack/VisualPatches/BuildWatermarkPatches.cs @@ -0,0 +1,55 @@ +/* + * Copyright 2024 Peter Han + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +using HarmonyLib; + +namespace PeterHan.FastTrack.VisualPatches { + /// + /// Applied to BuildWatermark to modify the text if the Fast Track version should be shown. + /// + [HarmonyPatch(typeof(BuildWatermark), nameof(BuildWatermark.GetBuildText))] + public static class BuildWatermark_GetBuildText_Patch { + internal static bool Prepare() => FastTrackOptions.Instance.VersionInWatermark == + FastTrackOptions.WatermarkMessage.Show; + + /// + /// Applied after GetBuildText runs. + /// + internal static void Postfix(ref string __result) { + __result = __result + " (FT-" + ModVersion.FILE_VERSION + ")"; + } + } + + /// + /// Applied to BuildWatermark to hide the text if the option is set to remove it + /// completely. Only applies in game. + /// + [HarmonyPatch(typeof(BuildWatermark), nameof(BuildWatermark.RefreshText))] + public static class BuildWatermark_RefreshText_Patch { + internal static bool Prepare() => FastTrackOptions.Instance.VersionInWatermark == + FastTrackOptions.WatermarkMessage.Off; + + /// + /// Applied after RefreshText runs. + /// + internal static void Postfix(BuildWatermark __instance) { + if (Game.Instance != null) + __instance.Show(false); + } + } +} diff --git a/LICENSE b/LICENSE index ce6a3663..36b97c0b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ Code in this repository is licensed under the MIT License: -Copyright (c) 2023 Peter Han +Copyright (c) 2024 Peter Han Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/PLib/PLib.csproj b/PLib/PLib.csproj index e5e11f7f..2794048b 100644 --- a/PLib/PLib.csproj +++ b/PLib/PLib.csproj @@ -11,13 +11,13 @@ icon.png Game, OxygenNotIncluded, Oxygen, PLib, Library true - Copyright 2023 Peter Han + Copyright 2024 Peter Han PLib - Peter Han's library used for creating mods for Oxygen Not Included, a simulation game by Klei Entertainment. Contains methods aimed at improving cross-mod compatibility, in-game user interfaces, and game-wide functions such as Actions and Lighting. An easy-to-use Mod Options menu is also included. - Fix a potential crash when running PLib AVC on the Epic Game Store or WeGame editions. + PLib AVC will not warn for installed mods which are newer than the online version, if the mod version uses a standard semantic version. -Silence a warning emitted when buildings are registered with PLib. +PLib AVC now displays a main menu warning if mods are outdated. PeterHan.PLib $(PLibVersion) false diff --git a/PLibAVC/JsonURLVersionChecker.cs b/PLibAVC/JsonURLVersionChecker.cs index 50105388..43599ef7 100644 --- a/PLibAVC/JsonURLVersionChecker.cs +++ b/PLibAVC/JsonURLVersionChecker.cs @@ -101,7 +101,7 @@ private ModVersionCheckResults ParseModVersion(Mod mod, ModVersions versions) { if (string.IsNullOrEmpty(newVersion)) result = new ModVersionCheckResults(id, true); else - result = new ModVersionCheckResults(id, newVersion != + result = new ModVersionCheckResults(id, newVersion == PVersionCheck.GetCurrentVersion(mod), newVersion); break; } diff --git a/PLibAVC/ModOutdatedWarning.cs b/PLibAVC/ModOutdatedWarning.cs new file mode 100644 index 00000000..774c63c3 --- /dev/null +++ b/PLibAVC/ModOutdatedWarning.cs @@ -0,0 +1,112 @@ +/* + * Copyright 2024 Peter Han + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software + * and associated documentation files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or + * substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +using PeterHan.PLib.Core; +using PeterHan.PLib.Detours; +using UnityEngine; + +namespace PeterHan.PLib.AVC { + /// + /// Added to the main menu to warn users if mods are out of date. + /// + internal sealed class ModOutdatedWarning : KMonoBehaviour { + /// + /// The singleton (should be!) instance of this class. + /// + internal static ModOutdatedWarning Instance { get; private set; } + + // Detour the "Resume Game" button + private static readonly IDetouredField RESUME_GAME = PDetours. + DetourFieldLazy(nameof(MainMenu.Button_ResumeGame)); + + /// + /// The button used to open the Mods screen. + /// + private GameObject modsButton; + + internal ModOutdatedWarning() { + modsButton = null; + } + + /// + /// Finds the "Mods" button and stores it in the modsButton field. + /// + /// The parent of all the main menu buttons. + private void FindModsButton(Transform buttonParent) { + int n = buttonParent.childCount; + for (int i = 0; i < n; i++) { + var button = buttonParent.GetChild(i).gameObject; + // Match by text... sucks but unlikely to have changed this early + if (button != null && button.GetComponentInChildren()?.text == + STRINGS.UI.FRONTEND.MODS.TITLE) { + modsButton = button; + break; + } + } + if (modsButton == null) + PUtil.LogWarning("Unable to find Mods menu button, main menu update warning will not be functional"); + } + + protected override void OnCleanUp() { + Instance = null; + base.OnCleanUp(); + } + + protected override void OnPrefabInit() { + base.OnPrefabInit(); + var mm = GetComponent(); + Instance = this; + try { + Transform buttonParent; + KButton resumeButton; + // "Resume game" is in the same panel as "Mods" + if (mm != null && (resumeButton = RESUME_GAME.Get(mm)) != null && + (buttonParent = resumeButton.transform.parent) != null) + FindModsButton(buttonParent); + } catch (DetourException) { } + UpdateText(); + } + + /// + /// Updates the Mods button text. + /// + private void UpdateText() { + var inst = PVersionCheck.Instance; + if (modsButton != null && inst != null) { + var modsText = modsButton.GetComponentInChildren(); + int outdated = inst.OutdatedMods; + if (outdated > 0 && modsText != null) { + string text = STRINGS.UI.FRONTEND.MODS.TITLE; + if (outdated == 1) + text += PLibStrings.MAINMENU_UPDATE_1; + else + text += string.Format(PLibStrings.MAINMENU_UPDATE, outdated); + modsText.text = text; + } + } + } + + /// + /// Updates the button text in a coroutine after one frame. + /// + public System.Collections.IEnumerator UpdateTextThreaded() { + yield return null; + UpdateText(); + } + } +} diff --git a/PLibAVC/PVersionCheck.cs b/PLibAVC/PVersionCheck.cs index 1ecd652c..09f7132f 100644 --- a/PLibAVC/PVersionCheck.cs +++ b/PLibAVC/PVersionCheck.cs @@ -19,7 +19,6 @@ using HarmonyLib; using PeterHan.PLib.Core; using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Reflection; using UnityEngine; @@ -39,6 +38,12 @@ public sealed class PVersionCheck : PForwardedComponent { /// and the next version should be tried. public delegate void OnVersionCheckComplete(ModVersionCheckResults result); + /// + /// Versions of PVersionCheck older than this version are broken and do not pass the + /// linked list correctly. + /// + private static readonly Version WORKING_VERSION = new Version(4, 14, 0, 0); + /// /// The instantiated copy of this class. /// @@ -59,7 +64,7 @@ private static string GetCurrentVersion(Assembly assembly) { if (assembly != null) { version = assembly.GetFileVersion(); if (string.IsNullOrEmpty(version)) - version = assembly.GetName()?.Version?.ToString(); + version = assembly.GetName().Version?.ToString(); } return version; } @@ -103,8 +108,10 @@ public static string GetCurrentVersion(KMod.Mod mod) { return version; } - private static void MainMenu_OnSpawn_Postfix() { + private static void MainMenu_OnSpawn_Postfix(MainMenu __instance) { Instance?.RunVersionCheck(); + if (__instance != null) + __instance.gameObject.AddOrGet(); } private static void ModsScreen_BuildDisplay_Postfix(System.Collections.IEnumerable @@ -120,17 +127,35 @@ private static void ModsScreen_BuildDisplay_Postfix(System.Collections.IEnumerab /// private readonly IDictionary checkVersions; + /// + /// The number of mods that appear to be outdated. + /// + public int OutdatedMods { + get { + int outdated = 0; + foreach (var mod in results) + if (!mod.IsUpToDate) outdated++; + return outdated; + } + } + /// /// The location where the outcome of mod version checking will be stored. /// - private readonly ConcurrentDictionary results; + private readonly ICollection results; + + /// + /// Stores results by the mod static ID. Only populated in the active instance. + /// + private readonly IDictionary resultsByMod; public override Version Version => VERSION; public PVersionCheck() { checkVersions = new Dictionary(8); - results = new ConcurrentDictionary(2, 16); - InstanceData = results.Values; + results = new List(8); + resultsByMod = new Dictionary(32); + InstanceData = results; } /// @@ -149,7 +174,7 @@ private void AddWarningIfOutdated(object modEntry) { string id; if (rowInstance != null && mods != null && index >= 0 && index < mods.Count && !string.IsNullOrEmpty(id = mods[index]?.staticID) && rowInstance. - TryGetComponent(out HierarchyReferences hr) && results.TryGetValue(id, + TryGetComponent(out HierarchyReferences hr) && resultsByMod.TryGetValue(id, out ModVersionCheckResults data) && data != null) // Version text is thankfully known, even if other mods have added buttons AddWarningIfOutdated(data, hr.GetReference("Version")); @@ -188,9 +213,9 @@ public override void Process(uint operation, object args) { VersionCheckTask first = null, previous = null; results.Clear(); foreach (var pair in checkVersions) { - string staticID = pair.Key; var mod = pair.Value; #if DEBUG + string staticID = pair.Key; PUtil.LogDebug("Checking version for mod {0}".F(staticID)); #endif foreach (var checker in mod.Methods) { @@ -198,9 +223,10 @@ public override void Process(uint operation, object args) { Next = runNext }; if (previous != null) - previous.Next = runNext; + previous.Next = node.Run; if (first == null) first = node; + previous = node; } } first?.Run(); @@ -220,14 +246,12 @@ public override void Process(uint operation, object args) { /// The mod instance to check. /// The method to use for checking the mod version. public void Register(KMod.UserMod2 mod, IModVersionChecker checker) { - var kmod = mod?.mod; - if (kmod == null) - throw new ArgumentNullException(nameof(mod)); + var kmod = mod?.mod ?? throw new ArgumentNullException(nameof(mod)); if (checker == null) throw new ArgumentNullException(nameof(checker)); RegisterForForwarding(); string staticID = kmod.staticID; - if (!checkVersions.TryGetValue(staticID, out VersionCheckMethods checkers)) + if (!checkVersions.TryGetValue(staticID, out var checkers)) checkVersions.Add(staticID, checkers = new VersionCheckMethods(kmod)); checkers.Methods.Add(checker); } @@ -237,16 +261,27 @@ public void Register(KMod.UserMod2 mod, IModVersionChecker checker) { /// private void ReportResults() { var allMods = PRegistry.Instance.GetAllComponents(ID); - results.Clear(); - if (allMods != null) + if (allMods != null) { + var inst = ModOutdatedWarning.Instance; // Consolidate them through JSON roundtrip into results dictionary + resultsByMod.Clear(); foreach (var mod in allMods) { var modResults = mod.GetInstanceDataSerialized>(); if (modResults != null) - foreach (var result in modResults) - results.TryAdd(result.ModChecked, result); + foreach (var result in modResults) { + string id = result.ModChecked; + if (!resultsByMod.ContainsKey(id)) + resultsByMod[id] = result; + } } + results.Clear(); + foreach (var pair in resultsByMod) + results.Add(pair.Value); + // Attempt to update the main menu + if (inst != null) + inst.StartCoroutine(inst.UpdateTextThreaded()); + } } /// @@ -284,7 +319,11 @@ internal AllVersionCheckTask(IEnumerable allMods, PVersionCheck parent) { if (allMods == null) throw new ArgumentNullException(nameof(allMods)); - checkAllVersions = new List(allMods); + var modList = new List(allMods); + // Due to bugs in older versions of PVersionCheck, try to run all of the + // working ones first + modList.Sort(new PComponentComparator()); + checkAllVersions = modList; index = 0; this.parent = parent ?? throw new ArgumentNullException(nameof(parent)); } @@ -294,16 +333,29 @@ internal AllVersionCheckTask(IEnumerable allMods, /// internal void Run() { int n = checkAllVersions.Count; - if (index >= n) - parent.ReportResults(); + bool allowReport = true; while (index < n) { var doCheck = checkAllVersions[index++]; - if (doCheck != null) { + if (doCheck == null) + PUtil.LogDebug("Invalid version checker reported by PForwardedComponent!"); + else if (doCheck.Version.CompareTo(WORKING_VERSION) < 0) { +#if DEBUG + if (index < n) + PUtil.LogWarning("Skipped {0:D} broken PLib version checks". + F(n - index)); +#endif + // All remaining versions are bad + parent.ReportResults(); + allowReport = false; + break; + } else { doCheck.Process(0, new System.Action(Run)); + allowReport = false; break; - } else if (index >= n) - parent.ReportResults(); + } } + if (index >= n && allowReport) + parent.ReportResults(); } public override string ToString() { @@ -320,7 +372,7 @@ private sealed class VersionCheckMethods { /// internal IList Methods { get; } - // + /// /// The mod whose version will be checked. /// internal KMod.Mod ModToCheck { get; } diff --git a/PLibAVC/SteamVersionChecker.cs b/PLibAVC/SteamVersionChecker.cs index 90dd9f53..72791b6e 100644 --- a/PLibAVC/SteamVersionChecker.cs +++ b/PLibAVC/SteamVersionChecker.cs @@ -60,7 +60,7 @@ public sealed class SteamVersionChecker : IModVersionChecker { private static readonly ConstructorInfo NEW_PUBLISHED_FILE_ID = PUBLISHED_FILE_ID?. GetConstructor(PPatchTools.BASE_FLAGS | BindingFlags.Instance, null, - new Type[] { typeof(ulong) }, null); + new[] { typeof(ulong) }, null); /// /// The number of minutes allowed before a mod is considered out of date. @@ -87,9 +87,9 @@ private static ModVersionCheckResults CheckSteamInit(ulong id, object[] boxedID, ModVersionCheckResults results = null; // Mod takes time to be populated in the list if (inst != null && FIND_MOD.Invoke(inst, boxedID) is - SteamUGCService.Mod steamMod) { + SteamUGCService.Mod steamMod) { ulong ticks = steamMod.lastUpdateTime; - var steamUpdate = (ticks == 0U) ? System.DateTime.MinValue : + var steamUpdate = ticks == 0U ? System.DateTime.MinValue : UnixEpochToDateTime(ticks); bool updated = steamUpdate <= GetLocalLastModified(id).AddMinutes( UPDATE_JITTER); @@ -109,7 +109,7 @@ private static System.DateTime GetLocalLastModified(ulong id) { // Create a published file object, leave it boxed if (GET_ITEM_INSTALL_INFO != null) { // 260 = MAX_PATH - var methodArgs = new object[] { + var methodArgs = new[] { NEW_PUBLISHED_FILE_ID.Invoke(new object[] { id }), 0UL, "", 260U, 0U }; if (GET_ITEM_INSTALL_INFO.Invoke(null, methodArgs) is bool success && diff --git a/PLibAVC/VersionCheckTask.cs b/PLibAVC/VersionCheckTask.cs index 172244e2..ebf77c9f 100644 --- a/PLibAVC/VersionCheckTask.cs +++ b/PLibAVC/VersionCheckTask.cs @@ -18,7 +18,7 @@ using PeterHan.PLib.Core; using System; -using System.Collections.Concurrent; +using System.Collections.Generic; namespace PeterHan.PLib.AVC { /// @@ -43,10 +43,10 @@ internal sealed class VersionCheckTask { /// /// The location where the outcome of mod version checking will be stored. /// - private readonly ConcurrentDictionary results; + private readonly ICollection results; internal VersionCheckTask(KMod.Mod mod, IModVersionChecker method, - ConcurrentDictionary results) { + ICollection results) { this.mod = mod ?? throw new ArgumentNullException(nameof(mod)); this.method = method ?? throw new ArgumentNullException(nameof(method)); this.results = results ?? throw new ArgumentNullException(nameof(results)); @@ -61,7 +61,7 @@ internal VersionCheckTask(KMod.Mod mod, IModVersionChecker method, private void OnComplete(ModVersionCheckResults result) { method.OnVersionCheckCompleted -= OnComplete; if (result != null) { - results.TryAdd(result.ModChecked, result); + results.Add(result); if (!result.IsUpToDate) PUtil.LogWarning("Mod {0} is out of date! New version: {1}".F(result. ModChecked, result.NewVersion ?? "unknown")); @@ -70,8 +70,8 @@ private void OnComplete(ModVersionCheckResults result) { PUtil.LogDebug("Mod {0} is up to date".F(result.ModChecked)); #endif } - } else - RunNext(); + } + RunNext(); } /// @@ -79,10 +79,16 @@ private void OnComplete(ModVersionCheckResults result) { /// it is not null. /// internal void Run() { - if (results.ContainsKey(mod.staticID)) - RunNext(); - else { - bool run = false; + bool found = false; + // Usually there are few results, and using a dictionary has problems with + // the Values list being a snapshot at the time + foreach (var result in results) + if (result.ModChecked == mod.staticID) { + found = true; + break; + } + if (!found) { + bool run; method.OnVersionCheckCompleted += OnComplete; // Version check errors should not crash the game try { diff --git a/PLibAVC/YamlURLVersionChecker.cs b/PLibAVC/YamlURLVersionChecker.cs index f0f5bf72..3048c028 100644 --- a/PLibAVC/YamlURLVersionChecker.cs +++ b/PLibAVC/YamlURLVersionChecker.cs @@ -18,8 +18,8 @@ using Klei; using KMod; -using PeterHan.PLib.Core; using System; +using PeterHan.PLib.Core; using UnityEngine.Networking; namespace PeterHan.PLib.AVC { @@ -71,7 +71,7 @@ private void OnRequestFinished(UnityWebRequest request, Mod mod) { PUtil.LogDebug("Current version: {0} New YAML version: {1}".F( curVersion, newVersion)); #endif - result = new ModVersionCheckResults(mod.staticID, newVersion != + result = new ModVersionCheckResults(mod.staticID, newVersion == curVersion, newVersion); } } diff --git a/PLibCore/PForwardedComponent.cs b/PLibCore/PForwardedComponent.cs index f91084ec..5a8b7b0f 100644 --- a/PLibCore/PForwardedComponent.cs +++ b/PLibCore/PForwardedComponent.cs @@ -19,6 +19,7 @@ using HarmonyLib; using Newtonsoft.Json; using System; +using System.Collections.Generic; using System.IO; using System.Reflection; using System.Text; @@ -47,11 +48,7 @@ public abstract class PForwardedComponent : IComparable { /// This method is non-virtual for a reason, as the ID is sometimes only available /// on methods of type object, so GetType().FullName is used directly there. /// - public string ID { - get { - return GetType().FullName; - } - } + public string ID => GetType().FullName; /// /// The JSON serialization settings to be used if the Data is marshaled across @@ -288,5 +285,16 @@ protected bool RegisterForForwarding() { public void SetSharedData(object value) { PRegistry.Instance.SetSharedData(ID, value); } + + /// + /// Compares two forwarded components to each other. The latest versions will be + /// sorted first. + /// + public sealed class PComponentComparator : IComparer { + public int Compare(PForwardedComponent a, PForwardedComponent b) { + return b == null ? (a == null ? 0 : -1) : (a == null ? 1 : b.Version. + CompareTo(a.Version)); + } + } } } diff --git a/PLibCore/PLibStrings.cs b/PLibCore/PLibStrings.cs index b1e60ffc..258f66b0 100644 --- a/PLibCore/PLibStrings.cs +++ b/PLibCore/PLibStrings.cs @@ -109,6 +109,16 @@ public static class PLibStrings { /// public static LocString RESTART_OK = STRINGS.UI.FRONTEND.MOD_DIALOGS.RESTART.OK; + /// + /// Displayed in the menu when mods report as being outdated. + /// + public static LocString MAINMENU_UPDATE = "\n\n{0:D} mods may be out of date"; + + /// + /// Displayed in the menu when a mod reports as being outdated. + /// + public static LocString MAINMENU_UPDATE_1 = "\n\n1 mod may be out of date"; + /// /// The details tooltip when AVC detects a mod to be outdated. /// diff --git a/PLibCore/PRegistryComponent.cs b/PLibCore/PRegistryComponent.cs index 361a4200..a5452bb1 100644 --- a/PLibCore/PRegistryComponent.cs +++ b/PLibCore/PRegistryComponent.cs @@ -119,8 +119,8 @@ private void AddCandidateVersion(string id, PForwardedComponent instance) { bool first = list.Count < 1; list.Add(instance); #if DEBUG - PRegistry.LogPatchDebug("Candidate version of {0} from {1}".F(id, instance. - GetOwningAssembly())); + PRegistry.LogPatchDebug("Candidate version {2} of {0} from {1}".F(id, instance. + GetOwningAssembly(), instance.Version)); #endif if (first) instance.Bootstrap(PLibInstance); diff --git a/PLibCore/PVersion.cs b/PLibCore/PVersion.cs index 38cf4de8..89ad0b62 100644 --- a/PLibCore/PVersion.cs +++ b/PLibCore/PVersion.cs @@ -25,6 +25,6 @@ public static class PVersion { /// /// The PLib version. /// - public const string VERSION = "4.13.0.0"; + public const string VERSION = "4.14.0.0"; } } diff --git a/PLibCore/translations/fr.po b/PLibCore/translations/fr.po index 854c85c3..dfb0ec3d 100644 --- a/PLibCore/translations/fr.po +++ b/PLibCore/translations/fr.po @@ -23,6 +23,28 @@ msgctxt "PeterHan.PLib.Core.PLibStrings.DIALOG_TITLE" msgid "Options for {0}" msgstr "Options pour {0}" +#. PeterHan.PLib.Core.PLibStrings.MAINMENU_UPDATE +msgctxt "PeterHan.PLib.Core.PLibStrings.MAINMENU_UPDATE" +msgid "" +"\n" +"\n" +"{0:D} mods may be out of date" +msgstr "" +"\n" +"\n" +"{0:D} mods peuvent être obsolètes" + +#. PeterHan.PLib.Core.PLibStrings.MAINMENU_UPDATE_1 +msgctxt "PeterHan.PLib.Core.PLibStrings.MAINMENU_UPDATE_1" +msgid "" +"\n" +"\n" +"1 mod may be out of date" +msgstr "" +"\n" +"\n" +"1 mod peut être obsolète" + #. PeterHan.PLib.Core.PLibStrings.MOD_ASSEMBLY_VERSION msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_ASSEMBLY_VERSION" msgid "Assembly Version: {0}" diff --git a/PLibCore/translations/ru.po b/PLibCore/translations/ru.po index 2b03cd5b..653ec217 100644 --- a/PLibCore/translations/ru.po +++ b/PLibCore/translations/ru.po @@ -56,6 +56,16 @@ msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_CATEGORY_TITLE" msgid "Mods" msgstr "Моды" +#. PeterHan.PLib.Core.PLibStrings.MAINMENU_UPDATE +msgctxt "PeterHan.PLib.Core.PLibStrings.MAINMENU_UPDATE" +msgid "\n\n{0:D} mods may be out of date" +msgstr "\n\n{0:D} мода(-ов) могут быть устаревшими" + +#. PeterHan.PLib.Core.PLibStrings.MAINMENU_UPDATE_1 +msgctxt "PeterHan.PLib.Core.PLibStrings.MAINMENU_UPDATE_1" +msgid "\n\n1 mod may be out of date" +msgstr "\n\n1 мод может быть устаревшим" + #. PeterHan.PLib.Core.PLibStrings.MOD_ASSEMBLY_VERSION msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_ASSEMBLY_VERSION" msgid "Assembly Version: {0}" diff --git a/PLibCore/translations/zh.po b/PLibCore/translations/zh.po index feab65c4..0f00c8ad 100644 --- a/PLibCore/translations/zh.po +++ b/PLibCore/translations/zh.po @@ -103,6 +103,16 @@ msgctxt "PeterHan.PLib.Core.PLibStrings.KEY_SYSRQ" msgid "SysRq" msgstr "SysRq" +#. PeterHan.PLib.Core.PLibStrings.MAINMENU_UPDATE +msgctxt "PeterHan.PLib.Core.PLibStrings.MAINMENU_UPDATE" +msgid "\n\n{0:D} mods may be out of date" +msgstr "\n\n{0:D}个模组已经过时" + +#. PeterHan.PLib.Core.PLibStrings.MAINMENU_UPDATE_1 +msgctxt "PeterHan.PLib.Core.PLibStrings.MAINMENU_UPDATE_1" +msgid "\n\n1 mod may be out of date" +msgstr "\n\n一个模组已经过时" + #. PeterHan.PLib.Core.PLibStrings.MOD_ASSEMBLY_VERSION msgctxt "PeterHan.PLib.Core.PLibStrings.MOD_ASSEMBLY_VERSION" msgid "Assembly Version: {0}" diff --git a/StockBugFix/QueuedModReporter.cs b/StockBugFix/QueuedModReporter.cs index c202e349..61a37ed6 100644 --- a/StockBugFix/QueuedModReporter.cs +++ b/StockBugFix/QueuedModReporter.cs @@ -16,14 +16,97 @@ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +using PeterHan.PLib.Core; +using PeterHan.PLib.Detours; +using UnityEngine; + namespace PeterHan.StockBugFix { /// /// A component to report queued mod updates, replacing MainMenu.Update. /// public sealed class QueuedModReporter : KMonoBehaviour { + private const float MAX_LOCKOUT = 10.0f; + + // Detour the "Resume Game" button + private static readonly IDetouredField RESUME_GAME = PDetours. + DetourFieldLazy(nameof(MainMenu.Button_ResumeGame)); + + /// + /// Is the mods button already enabled? + /// + private bool buttonEnabled; + + /// + /// The button used to open the Mods screen. + /// + private KButton modsButton; + + /// + /// The time when the menu was first opened. + /// + private float startupTime; + + public QueuedModReporter() { + buttonEnabled = true; + modsButton = null; + startupTime = 0.0f; + } + + /// + /// Finds the "Mods" button and stores it in the modsButton field. + /// + /// The parent of all the main menu buttons. + private void FindModsButton(Transform buttonParent) { + int n = buttonParent.childCount; + for (int i = 0; i < n; i++) { + var button = buttonParent.GetChild(i).gameObject; + // Match by text... sucks but only Mod Updater and AVC change it this early + if (button != null && button.TryGetComponent(out KButton kb)) { + string text = button.GetComponentInChildren()?.text; + if (text != null && text.StartsWith(STRINGS.UI.FRONTEND.MODS.TITLE)) { + modsButton = kb; + break; + } + } + } + } + + protected override void OnSpawn() { + base.OnSpawn(); + startupTime = 0.0f; + if (StockBugFixOptions.Instance.DelayModsMenu && TryGetComponent(out MainMenu mm)) + { + try { + Transform buttonParent; + KButton resumeButton; + // "Resume game" is in the same panel as "Mods" + if (mm != null && (resumeButton = RESUME_GAME.Get(mm)) != null && + (buttonParent = resumeButton.transform.parent) != null) + FindModsButton(buttonParent); + } catch (DetourException) { } + if (modsButton != null) { + modsButton.isInteractable = false; + buttonEnabled = false; + } else + PUtil.LogWarning("Unable to find Mods menu button, mods lockout will not be functional"); + } + } + public void Update() { - if (isActiveAndEnabled) - QueuedReportManager.Instance.CheckQueuedReport(gameObject); + float now = Time.unscaledTime; + var inst = QueuedReportManager.Instance; + if (isActiveAndEnabled && inst != null) { + if (startupTime <= 0.0f) + startupTime = now; + else { + if (!buttonEnabled && (inst.ReadyToReport || now - MAX_LOCKOUT > + startupTime)) { + modsButton.isInteractable = true; + buttonEnabled = true; + } + inst.CheckQueuedReport(gameObject); + } + } } } } diff --git a/StockBugFix/QueuedReportManager.cs b/StockBugFix/QueuedReportManager.cs index a4a813a7..90dcaf33 100644 --- a/StockBugFix/QueuedReportManager.cs +++ b/StockBugFix/QueuedReportManager.cs @@ -52,27 +52,47 @@ internal static void QueueDelayedReport(KMod.Manager manager, GameObject _) { internal static void QueueDelayedSanitize(KMod.Manager manager, GameObject _) { Instance.QueueReport(true); } + + /// + /// Returns true if a report can (probably) be safely requested (by opening the Mods + /// menu). + /// + /// true if a report is unlikely to reinstall all mods, or false if you should + /// wait longer. + internal bool ReadyToReport { + get { + bool ready = false; + lock (reportLock) { + float time = lastSanitizeTime; + // Due for a report? + if (time > 0.0f && Time.unscaledTime - time > QUEUED_REPORT_DELAY) + ready = true; + } + return ready; + } + } /// /// The last Time.unscaledTime value when a report was generated. 0.0 if no /// report is pending. /// - private float lastReportTime; - + private volatile float lastReportTime; + /// - /// Manages multithreaded access to this object. + /// The last Time.unscaledTime value when a sanitize request was made. 0.0 if no + /// sanitize is pending. /// - private readonly object reportLock; + private volatile float lastSanitizeTime; /// - /// Whether a sanitization pass was requested by Steam. + /// Manages multithreaded access to this object. /// - private bool sanitizeRequested; + private readonly object reportLock; private QueuedReportManager() { lastReportTime = 0.0f; + lastSanitizeTime = 0.0f; reportLock = new object(); - sanitizeRequested = false; } /// @@ -85,12 +105,11 @@ internal void CheckQueuedReport(GameObject parent) { lock (reportLock) { float time = lastReportTime; // Due for a report? - if (time > 0.0f && (Time.unscaledTime - lastReportTime) > - QUEUED_REPORT_DELAY) { - sanitize = sanitizeRequested; + if (time > 0.0f && Time.unscaledTime - time > QUEUED_REPORT_DELAY) { + sanitize = lastSanitizeTime > 0.0f; report = true; lastReportTime = 0.0f; - sanitizeRequested = false; + lastSanitizeTime = 0.0f; } } if (report && parent != null) { @@ -107,9 +126,10 @@ internal void CheckQueuedReport(GameObject parent) { /// internal void QueueReport(bool sanitize) { lock (reportLock) { - lastReportTime = Time.unscaledTime; + float now = Time.unscaledTime; + lastReportTime = now; if (sanitize) - sanitizeRequested = true; + lastSanitizeTime = now; } } } diff --git a/StockBugFix/StockBugFix.csproj b/StockBugFix/StockBugFix.csproj index d7f8400f..9c71a232 100644 --- a/StockBugFix/StockBugFix.csproj +++ b/StockBugFix/StockBugFix.csproj @@ -2,7 +2,7 @@ Stock Bug Fix - 3.24.0.0 + 3.25.0.0 PeterHan.StockBugFix Fixes bugs and annoyances in the stock game. 3.2.0.0 diff --git a/StockBugFix/StockBugFixOptions.cs b/StockBugFix/StockBugFixOptions.cs index 89b17871..a476b586 100644 --- a/StockBugFix/StockBugFixOptions.cs +++ b/StockBugFix/StockBugFixOptions.cs @@ -57,6 +57,14 @@ public sealed class StockBugFixOptions : SingletonOptions { [Option("Fix Trait Conflicts", "Prevents nonsensical combinations of traits and interests that contradict each other from appearing.")] [JsonProperty] public bool FixTraits { get; set; } + + /// + /// If true, locks out the Mods button until the race condition which reinstalls all + /// mods clears out, or until the timeout passes. + /// + [Option("Prevent Mod Reinstalls", "Disable the Mods menu until the vanilla game race condition that could reinstall all mods resolves.")] + [JsonProperty] + public bool DelayModsMenu { get; set; } /// /// Allows changing food storage to a store errand. Does not affect cooking supply. @@ -67,6 +75,7 @@ public sealed class StockBugFixOptions : SingletonOptions { public StockBugFixOptions() { AllowTepidizerPulsing = false; + DelayModsMenu = false; FixMultipleAttributes = true; FixOverheat = true; FixTraits = true; @@ -74,8 +83,9 @@ public StockBugFixOptions() { } public override string ToString() { - return "StockBugFixOptions[allowTepidizer={1},fixOverheat={0},foodChoreType={2},fixAttributes={3}]".F( - FixOverheat, AllowTepidizerPulsing, StoreFoodChoreType, FixMultipleAttributes); + return "StockBugFixOptions[allowTepidizer={1},fixOverheat={0},foodChoreType={2},fixAttributes={3},fixTraits={4},delayModsMenu={5}]".F( + FixOverheat, AllowTepidizerPulsing, StoreFoodChoreType, FixMultipleAttributes, + FixTraits, DelayModsMenu); } }