/* * Copyright 2022 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; using PeterHan.PLib.Core; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Reflection; using UnityEngine; namespace PeterHan.PLib.AVC { /// /// Implements a basic automatic version check, using either Steam or an external website. /// /// The version of the current mod is taken from the mod version attribute of the provided /// mod. /// public sealed class PVersionCheck : PForwardedComponent { /// /// The delegate type used when a background version check completes. /// /// The results of the check. If null, the check has failed, /// and the next version should be tried. public delegate void OnVersionCheckComplete(ModVersionCheckResults result); /// /// The instantiated copy of this class. /// internal static PVersionCheck Instance { get; private set; } /// /// The version of this component. Uses the running PLib version. /// internal static readonly Version VERSION = new Version(PVersion.VERSION); /// /// Gets the reported version of the specified assembly. /// /// The assembly to check. /// The assembly's file version, or if that is unset, its assembly version. private static string GetCurrentVersion(Assembly assembly) { string version = null; if (assembly != null) { version = assembly.GetFileVersion(); if (string.IsNullOrEmpty(version)) version = assembly.GetName()?.Version?.ToString(); } return version; } /// /// Gets the current version of the mod. If the version is specified in mod_info.yaml, /// that version is reported. Otherwise, the assembly file version (and failing that, /// the assembly version) of the assembly defining the mod's first UserMod2 instance /// is reported. /// /// This method will only work after mods have loaded. /// /// The mod to check. /// The current version of that mod. public static string GetCurrentVersion(KMod.Mod mod) { if (mod == null) throw new ArgumentNullException(nameof(mod)); string version = mod.packagedModInfo?.version; if (string.IsNullOrEmpty(version)) { // Does it have UM2 instances? var instances = mod.loaded_mod_data?.userMod2Instances; var dlls = mod.loaded_mod_data?.dlls; if (instances != null) // Use first UserMod2 foreach (var um2 in instances) { version = GetCurrentVersion(um2.Key); if (!string.IsNullOrEmpty(version)) break; } else if (dlls != null && dlls.Count > 0) // Use first DLL foreach (var assembly in dlls) { version = GetCurrentVersion(assembly); if (!string.IsNullOrEmpty(version)) break; } else // All methods of determining the version have failed version = ""; } return version; } private static void MainMenu_OnSpawn_Postfix() { Instance?.RunVersionCheck(); } private static void ModsScreen_BuildDisplay_Postfix(System.Collections.IEnumerable ___displayedMods) { // Must cast the type because ModsScreen.DisplayedMod is private if (Instance != null && ___displayedMods != null) foreach (var modEntry in ___displayedMods) Instance.AddWarningIfOutdated(modEntry); } /// /// The mods whose version will be checked. /// private readonly IDictionary checkVersions; /// /// The location where the outcome of mod version checking will be stored. /// private readonly ConcurrentDictionary results; public override Version Version => VERSION; public PVersionCheck() { checkVersions = new Dictionary(8); results = new ConcurrentDictionary(2, 16); InstanceData = results.Values; } /// /// Adds a warning to the mods screen if a mod is outdated. /// /// The mod entry to modify. private void AddWarningIfOutdated(object modEntry) { int index = -1; var type = modEntry.GetType(); var indexVal = type.GetFieldSafe("mod_index", false)?.GetValue(modEntry); if (indexVal is int intVal) index = intVal; var rowInstance = type.GetFieldSafe("rect_transform", false)?.GetValue( modEntry) as RectTransform; var mods = Global.Instance.modManager?.mods; 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, out ModVersionCheckResults data) && data != null) // Version text is thankfully known, even if other mods have added buttons AddWarningIfOutdated(data, hr.GetReference("Version")); } /// /// Adds a warning to a mod version label if it is outdated. /// /// The updated mod version. /// The current mod version label. private void AddWarningIfOutdated(ModVersionCheckResults data, LocText versionText) { GameObject go; if (versionText != null && (go = versionText.gameObject) != null && !data. IsUpToDate) { string text = versionText.text; if (string.IsNullOrEmpty(text)) text = PLibStrings.OUTDATED_WARNING; else text = text + " " + PLibStrings.OUTDATED_WARNING; versionText.text = text; go.AddOrGet().toolTip = string.Format(PLibStrings.OUTDATED_TOOLTIP, data.NewVersion ?? ""); } } public override void Initialize(Harmony plibInstance) { Instance = this; plibInstance.Patch(typeof(MainMenu), "OnSpawn", postfix: PatchMethod(nameof( MainMenu_OnSpawn_Postfix))); plibInstance.Patch(typeof(ModsScreen), "BuildDisplay", postfix: PatchMethod( nameof(ModsScreen_BuildDisplay_Postfix))); } public override void Process(uint operation, object args) { if (operation == 0 && args is System.Action runNext) { VersionCheckTask first = null, previous = null; results.Clear(); foreach (var pair in checkVersions) { string staticID = pair.Key; var mod = pair.Value; #if DEBUG PUtil.LogDebug("Checking version for mod {0}".F(staticID)); #endif foreach (var checker in mod.Methods) { var node = new VersionCheckTask(mod.ModToCheck, checker, results) { Next = runNext }; if (previous != null) previous.Next = runNext; if (first == null) first = node; } } first?.Run(); } } /// /// Registers the specified mod for automatic version checking. Mods will be registered /// using their static ID, so to avoid the default ID from being used instead, set this /// attribute in mod.yaml. /// /// The same mod can be registered multiple times with different methods to check the /// mod versions. The methods will be attempted in order from first registered to last. /// However, the same mod must not be registered multiple times in different instances /// of PVersionCheck. /// /// 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)); if (checker == null) throw new ArgumentNullException(nameof(checker)); RegisterForForwarding(); string staticID = kmod.staticID; if (!checkVersions.TryGetValue(staticID, out VersionCheckMethods checkers)) checkVersions.Add(staticID, checkers = new VersionCheckMethods(kmod)); checkers.Methods.Add(checker); } /// /// Reports the results of the version check. /// private void ReportResults() { var allMods = PRegistry.Instance.GetAllComponents(ID); results.Clear(); if (allMods != null) // Consolidate them through JSON roundtrip into results dictionary foreach (var mod in allMods) { var modResults = mod.GetInstanceDataSerialized>(); if (modResults != null) foreach (var result in modResults) results.TryAdd(result.ModChecked, result); } } /// /// Starts the automatic version check for all mods. /// internal void RunVersionCheck() { var allMods = PRegistry.Instance.GetAllComponents(ID); // See if Mod Updater triggered master disable if (!PRegistry.GetData("PLib.VersionCheck.ModUpdaterActive") && allMods != null) new AllVersionCheckTask(allMods, this).Run(); } /// /// Checks each mod's version one at a time to avoid saturating the network with /// generally nonessential traffic (in the case of yaml/json checkers). /// private sealed class AllVersionCheckTask { /// /// A list of actions that will check each version in turn. /// private readonly IList checkAllVersions; /// /// The current location in the list. /// private int index; /// /// Handles version check result reporting when complete. /// private readonly PVersionCheck parent; internal AllVersionCheckTask(IEnumerable allMods, PVersionCheck parent) { if (allMods == null) throw new ArgumentNullException(nameof(allMods)); checkAllVersions = new List(allMods); index = 0; this.parent = parent ?? throw new ArgumentNullException(nameof(parent)); } /// /// Runs all checks and fires the callback when complete. /// internal void Run() { int n = checkAllVersions.Count; if (index >= n) parent.ReportResults(); while (index < n) { var doCheck = checkAllVersions[index++]; if (doCheck != null) { doCheck.Process(0, new System.Action(Run)); break; } else if (index >= n) parent.ReportResults(); } } public override string ToString() { return "AllVersionCheckTask for {0:D} mods".F(checkAllVersions.Count); } } /// /// A placeholder class which stores all methods used to check a single mod. /// private sealed class VersionCheckMethods { /// /// The methods which will be used to check. /// internal IList Methods { get; } // /// The mod whose version will be checked. /// internal KMod.Mod ModToCheck { get; } internal VersionCheckMethods(KMod.Mod mod) { Methods = new List(8); ModToCheck = mod ?? throw new ArgumentNullException(nameof(mod)); PUtil.LogDebug("Registered mod ID {0} for automatic version checking".F( ModToCheck.staticID)); } public override string ToString() { return ModToCheck.staticID; } } } }