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