Initial commit

This commit is contained in:
2022-12-30 00:58:33 +01:00
commit 3f1075e67f
176 changed files with 25715 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
/*
* 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.
*/
namespace PeterHan.PLib.AVC {
/// <summary>
/// Implemented by classes which can check the current mod version and detect if it is out
/// of date.
/// </summary>
public interface IModVersionChecker {
/// <summary>
/// The event to subscribe for when the check completes.
/// </summary>
event PVersionCheck.OnVersionCheckComplete OnVersionCheckCompleted;
/// <summary>
/// Checks the mod and reports if it is out of date. The mod's current version as
/// reported by its mod_info.yaml file is available on the packagedModInfo member.
///
/// This method might not be run on the foreground thread. Do not create new behaviors
/// or components without a coroutine to an existing GameObject.
/// </summary>
/// <param name="mod">The mod whose version is being checked.</param>
/// <returns>true if the version check has started, or false if it could not be
/// started, which will trigger the next version checker in line.</returns>
bool CheckVersion(KMod.Mod mod);
}
}

View File

@@ -0,0 +1,146 @@
/*
* 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 KMod;
using Newtonsoft.Json;
using PeterHan.PLib.Core;
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine.Networking;
namespace PeterHan.PLib.AVC {
/// <summary>
/// Checks the mod version using a URL to a JSON file. The file at this URL must resolve
/// to a JSON file which can deserialize to the JsonURLVersionChecker.ModVersions class.
/// </summary>
public sealed class JsonURLVersionChecker : IModVersionChecker {
/// <summary>
/// The timeout in seconds for the web request before declaring the check as failed.
/// </summary>
public const int REQUEST_TIMEOUT = 8;
/// <summary>
/// The URL to query for checking the mod version.
/// </summary>
public string JsonVersionURL { get; }
public event PVersionCheck.OnVersionCheckComplete OnVersionCheckCompleted;
public JsonURLVersionChecker(string url) {
if (string.IsNullOrEmpty(url))
throw new ArgumentNullException(nameof(url));
JsonVersionURL = url;
}
public bool CheckVersion(Mod mod) {
if (mod == null)
throw new ArgumentNullException(nameof(mod));
var request = UnityWebRequest.Get(JsonVersionURL);
request.SetRequestHeader("Content-Type", "application/json");
request.SetRequestHeader("User-Agent", "PLib AVC");
request.timeout = REQUEST_TIMEOUT;
var operation = request.SendWebRequest();
operation.completed += (_) => OnRequestFinished(request, mod);
return true;
}
/// <summary>
/// When a web request completes, triggers the handler for the next updater.
/// </summary>
/// <param name="request">The JSON web request data.</param>
/// <param name="mod">The mod that needs to be checked.</param>
private void OnRequestFinished(UnityWebRequest request, Mod mod) {
ModVersionCheckResults result = null;
if (request.result == UnityWebRequest.Result.Success) {
// Parse the text
ModVersions versions;
using (var reader = new StreamReader(new MemoryStream(request.
downloadHandler.data))) {
versions = new JsonSerializer() {
MaxDepth = 4, DateTimeZoneHandling = DateTimeZoneHandling.Utc,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
}.Deserialize<ModVersions>(new JsonTextReader(reader));
}
if (versions != null)
result = ParseModVersion(mod, versions);
}
request.Dispose();
OnVersionCheckCompleted?.Invoke(result);
}
/// <summary>
/// Parses the JSON file and looks up the version for the specified mod.
/// </summary>
/// <param name="mod">The mod's static ID.</param>
/// <param name="versions">The data from the web JSON file.</param>
/// <returns>The results of the update, or null if the mod could not be found in the
/// JSON.</returns>
private ModVersionCheckResults ParseModVersion(Mod mod, ModVersions versions) {
ModVersionCheckResults result = null;
string id = mod.staticID;
if (versions.mods != null)
foreach (var modVersion in versions.mods)
if (modVersion != null && modVersion.staticID == id) {
string newVersion = modVersion.version?.Trim();
if (string.IsNullOrEmpty(newVersion))
result = new ModVersionCheckResults(id, true);
else
result = new ModVersionCheckResults(id, newVersion !=
PVersionCheck.GetCurrentVersion(mod), newVersion);
break;
}
return result;
}
/// <summary>
/// The serialization type for JSONURLVersionChecker. Allows multiple mods to query
/// the same URL.
/// </summary>
[JsonObject(MemberSerialization.OptIn)]
public sealed class ModVersions {
[JsonProperty]
public List<ModVersion> mods;
public ModVersions() {
mods = new List<ModVersion>(16);
}
}
/// <summary>
/// Represents the current version of each mod.
/// </summary>
public sealed class ModVersion {
/// <summary>
/// The mod's static ID, as reported by its mod.yaml. If a mod does not specify its
/// static ID, it gets the default ID mod.label.id + "_" + mod.label.
/// distribution_platform.
/// </summary>
public string staticID { get; set; }
/// <summary>
/// The mod's current version.
/// </summary>
public string version { get; set; }
public override string ToString() {
return "{0}: version={1}".F(staticID, version);
}
}
}
}

View File

@@ -0,0 +1,70 @@
/*
* 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 Newtonsoft.Json;
using PeterHan.PLib.Core;
namespace PeterHan.PLib.AVC {
/// <summary>
/// The results of checking the mod version.
/// </summary>
[JsonObject(MemberSerialization.OptIn)]
public sealed class ModVersionCheckResults {
/// <summary>
/// true if the mod is up to date, or false if it is out of date.
/// </summary>
[JsonProperty]
public bool IsUpToDate { get; set; }
/// <summary>
/// The mod whose version was queried. The current mod version is available on this
/// mod through its packagedModInfo.
/// </summary>
[JsonProperty]
public string ModChecked { get; set; }
/// <summary>
/// The new version of this mod. If it is not available, it can be null, even if
/// IsUpdated is false. Not relevant if IsUpToDate reports true.
/// </summary>
[JsonProperty]
public string NewVersion { get; set; }
public ModVersionCheckResults() : this("", false) { }
public ModVersionCheckResults(string id, bool updated, string newVersion = null) {
IsUpToDate = updated;
ModChecked = id;
NewVersion = newVersion;
}
public override bool Equals(object obj) {
return obj is ModVersionCheckResults other && other.ModChecked == ModChecked &&
IsUpToDate == other.IsUpToDate && NewVersion == other.NewVersion;
}
public override int GetHashCode() {
return ModChecked.GetHashCode();
}
public override string ToString() {
return "ModVersionCheckResults[{0},updated={1},newVersion={2}]".F(
ModChecked, IsUpToDate, NewVersion ?? "");
}
}
}

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Title>PLib Automatic Version Check</Title>
<AssemblyTitle>PLib.AVC</AssemblyTitle>
<Version>4.11.0.0</Version>
<UsesPLib>false</UsesPLib>
<RootNamespace>PeterHan.PLib.AVC</RootNamespace>
<AssemblyVersion>4.11.0.0</AssemblyVersion>
<DistributeMod>false</DistributeMod>
<PLibCore>true</PLibCore>
<Platforms>Vanilla;Mergedown</Platforms>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<DocumentationFile>bin\$(Platform)\Release\PLibAVC.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../PLibCore/PLibCore.csproj" />
<ProjectReference Include="../PLibUI/PLibUI.csproj" />
<Reference Include="UnityEngine.UnityWebRequestModule" HintPath="$(GameFolderActive)/UnityEngine.UnityWebRequestModule.dll" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,340 @@
/*
* 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 {
/// <summary>
/// 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.
/// </summary>
public sealed class PVersionCheck : PForwardedComponent {
/// <summary>
/// The delegate type used when a background version check completes.
/// </summary>
/// <param name="result">The results of the check. If null, the check has failed,
/// and the next version should be tried.</param>
public delegate void OnVersionCheckComplete(ModVersionCheckResults result);
/// <summary>
/// The instantiated copy of this class.
/// </summary>
internal static PVersionCheck Instance { get; private set; }
/// <summary>
/// The version of this component. Uses the running PLib version.
/// </summary>
internal static readonly Version VERSION = new Version(PVersion.VERSION);
/// <summary>
/// Gets the reported version of the specified assembly.
/// </summary>
/// <param name="assembly">The assembly to check.</param>
/// <returns>The assembly's file version, or if that is unset, its assembly version.</returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="mod">The mod to check.</param>
/// <returns>The current version of that mod.</returns>
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);
}
/// <summary>
/// The mods whose version will be checked.
/// </summary>
private readonly IDictionary<string, VersionCheckMethods> checkVersions;
/// <summary>
/// The location where the outcome of mod version checking will be stored.
/// </summary>
private readonly ConcurrentDictionary<string, ModVersionCheckResults> results;
public override Version Version => VERSION;
public PVersionCheck() {
checkVersions = new Dictionary<string, VersionCheckMethods>(8);
results = new ConcurrentDictionary<string, ModVersionCheckResults>(2, 16);
InstanceData = results.Values;
}
/// <summary>
/// Adds a warning to the mods screen if a mod is outdated.
/// </summary>
/// <param name="modEntry">The mod entry to modify.</param>
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<LocText>("Version"));
}
/// <summary>
/// Adds a warning to a mod version label if it is outdated.
/// </summary>
/// <param name="data">The updated mod version.</param>
/// <param name="versionText">The current mod version label.</param>
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>().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();
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="mod">The mod instance to check.</param>
/// <param name="checker">The method to use for checking the mod version.</param>
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);
}
/// <summary>
/// Reports the results of the version check.
/// </summary>
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<ICollection<
ModVersionCheckResults>>();
if (modResults != null)
foreach (var result in modResults)
results.TryAdd(result.ModChecked, result);
}
}
/// <summary>
/// Starts the automatic version check for all mods.
/// </summary>
internal void RunVersionCheck() {
var allMods = PRegistry.Instance.GetAllComponents(ID);
// See if Mod Updater triggered master disable
if (!PRegistry.GetData<bool>("PLib.VersionCheck.ModUpdaterActive") &&
allMods != null)
new AllVersionCheckTask(allMods, this).Run();
}
/// <summary>
/// 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).
/// </summary>
private sealed class AllVersionCheckTask {
/// <summary>
/// A list of actions that will check each version in turn.
/// </summary>
private readonly IList<PForwardedComponent> checkAllVersions;
/// <summary>
/// The current location in the list.
/// </summary>
private int index;
/// <summary>
/// Handles version check result reporting when complete.
/// </summary>
private readonly PVersionCheck parent;
internal AllVersionCheckTask(IEnumerable<PForwardedComponent> allMods,
PVersionCheck parent) {
if (allMods == null)
throw new ArgumentNullException(nameof(allMods));
checkAllVersions = new List<PForwardedComponent>(allMods);
index = 0;
this.parent = parent ?? throw new ArgumentNullException(nameof(parent));
}
/// <summary>
/// Runs all checks and fires the callback when complete.
/// </summary>
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);
}
}
/// <summary>
/// A placeholder class which stores all methods used to check a single mod.
/// </summary>
private sealed class VersionCheckMethods {
/// <summary>
/// The methods which will be used to check.
/// </summary>
internal IList<IModVersionChecker> Methods { get; }
// <summary>
/// The mod whose version will be checked.
/// </summary>
internal KMod.Mod ModToCheck { get; }
internal VersionCheckMethods(KMod.Mod mod) {
Methods = new List<IModVersionChecker>(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;
}
}
}
}

View File

@@ -0,0 +1,168 @@
/*
* 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 KMod;
using PeterHan.PLib.Core;
using System;
using System.Reflection;
namespace PeterHan.PLib.AVC {
/// <summary>
/// Checks Steam to see if mods are out of date.
/// </summary>
public sealed class SteamVersionChecker : IModVersionChecker {
/// <summary>
/// A reference to the PublishedFileId_t type, or null if running on the EGS/WeGame
/// version.
/// </summary>
private static readonly Type PUBLISHED_FILE_ID = PPatchTools.GetTypeSafe(
"Steamworks.PublishedFileId_t");
/// <summary>
/// A reference to the SteamUGC type, or null if running on the EGS/WeGame version.
/// </summary>
private static readonly Type STEAM_UGC = PPatchTools.GetTypeSafe(
"Steamworks.SteamUGC");
/// <summary>
/// A reference to the game's version of SteamUGCService, or null if running on the
/// EGS/WeGame version.
/// </summary>
private static readonly Type STEAM_UGC_SERVICE = PPatchTools.GetTypeSafe(
nameof(SteamUGCService), "Assembly-CSharp");
/// <summary>
/// Detours requires knowing the types at compile time, which might not be available,
/// and these methods are only called once at startup.
/// </summary>
private static readonly MethodInfo FIND_MOD = STEAM_UGC_SERVICE?.GetMethodSafe(
nameof(SteamUGCService.FindMod), false, PUBLISHED_FILE_ID);
private static readonly MethodInfo GET_ITEM_INSTALL_INFO = STEAM_UGC?.GetMethodSafe(
"GetItemInstallInfo", true, PUBLISHED_FILE_ID, typeof(ulong).
MakeByRefType(), typeof(string).MakeByRefType(), typeof(uint), typeof(uint).
MakeByRefType());
private static readonly ConstructorInfo NEW_PUBLISHED_FILE_ID = PUBLISHED_FILE_ID?.
GetConstructor(PPatchTools.BASE_FLAGS | BindingFlags.Instance, null,
new Type[] { typeof(ulong) }, null);
/// <summary>
/// The number of minutes allowed before a mod is considered out of date.
/// </summary>
public const double UPDATE_JITTER = 10.0;
/// <summary>
/// The epoch time for Steam time stamps.
/// </summary>
private static readonly System.DateTime UNIX_EPOCH = new System.DateTime(1970, 1, 1,
0, 0, 0, DateTimeKind.Utc);
/// <summary>
/// Gets the last modified date of a mod's local files. The time is returned in UTC.
/// </summary>
/// <param name="id">The mod ID to check.</param>
/// <returns>The date and time of its last modification.</returns>
private static System.DateTime GetLocalLastModified(ulong id) {
var result = System.DateTime.UtcNow;
// Create a published file object, leave it boxed
if (GET_ITEM_INSTALL_INFO != null) {
// 260 = MAX_PATH
var methodArgs = new object[] {
NEW_PUBLISHED_FILE_ID.Invoke(new object[] { id }), 0UL, "", 260U, 0U
};
if (GET_ITEM_INSTALL_INFO.Invoke(null, methodArgs) is bool success &&
success && methodArgs.Length == 5 && methodArgs[4] is uint timestamp &&
timestamp > 0U)
result = UnixEpochToDateTime(timestamp);
else
PUtil.LogDebug("Unable to determine last modified date for: " + id);
}
return result;
}
/// <summary>
/// Converts a time from Steam (seconds since Unix epoch) to a C# DateTime.
/// </summary>
/// <param name="timeSeconds">The timestamp since the epoch.</param>
/// <returns>The UTC date and time that it represents.</returns>
public static System.DateTime UnixEpochToDateTime(ulong timeSeconds) {
return UNIX_EPOCH.AddSeconds(timeSeconds);
}
public event PVersionCheck.OnVersionCheckComplete OnVersionCheckCompleted;
public bool CheckVersion(Mod mod) {
// Epic editions of the game do not even have SteamUGCService
return FIND_MOD != null && NEW_PUBLISHED_FILE_ID != null && DoCheckVersion(mod);
}
/// <summary>
/// Checks the mod on Steam and reports if it is out of date. This helper method
/// avoids a type load error if a non-Steam version of the game is used to load this
/// mod.
/// </summary>
/// <param name="mod">The mod whose version is being checked.</param>
/// <returns>true if the version check has started, or false if it could not be
/// started.</returns>
private bool DoCheckVersion(Mod mod) {
bool check = false;
if (mod.label.distribution_platform == Label.DistributionPlatform.Steam && ulong.
TryParse(mod.label.id, out ulong id)) {
// Jump into a coroutine and wait for it to be initialized
Global.Instance.StartCoroutine(WaitForSteamInit(id, mod));
check = true;
} else
PUtil.LogWarning("SteamVersionChecker cannot check version for non-Steam mod {0}".
F(mod.staticID));
return check;
}
/// <summary>
/// To avoid blowing the stack, waits for Steam to initialize in a coroutine.
/// </summary>
/// <param name="id">The Steam file ID of the mod.</param>
/// <param name="mod">The mod to check for updates.</param>
private System.Collections.IEnumerator WaitForSteamInit(ulong id, Mod mod) {
var boxedID = NEW_PUBLISHED_FILE_ID.Invoke(new object[] { id });
ModVersionCheckResults results = null;
int timeout = 0;
do {
yield return null;
var inst = SteamUGCService.Instance;
// Mod takes time to be populated in the list
if (inst != null && FIND_MOD.Invoke(inst, new object[] { boxedID }) is
SteamUGCService.Mod steamMod) {
ulong ticks = steamMod.lastUpdateTime;
var steamUpdate = (ticks == 0U) ? System.DateTime.MinValue :
UnixEpochToDateTime(ticks);
bool updated = steamUpdate <= GetLocalLastModified(id).AddMinutes(
UPDATE_JITTER);
results = new ModVersionCheckResults(mod.staticID,
updated, updated ? null : steamUpdate.ToString("f"));
}
// 2 seconds at 60 FPS
} while (results == null && ++timeout < 120);
if (results == null)
PUtil.LogWarning("Unable to check version for mod {0} (SteamUGCService timeout)".
F(mod.label.title));
OnVersionCheckCompleted?.Invoke(results);
yield break;
}
}
}

View File

@@ -0,0 +1,109 @@
/*
* 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 PeterHan.PLib.Core;
using System;
using System.Collections.Concurrent;
namespace PeterHan.PLib.AVC {
/// <summary>
/// Represents a "task" to check a particular mod for updates.
/// </summary>
internal sealed class VersionCheckTask {
/// <summary>
/// The method which will be used to check.
/// </summary>
private readonly IModVersionChecker method;
/// <summary>
/// The mod whose version will be checked.
/// </summary>
private readonly KMod.Mod mod;
/// <summary>
/// The next task to run when the check completes, or null to not run any task.
/// </summary>
internal System.Action Next { get; set; }
/// <summary>
/// The location where the outcome of mod version checking will be stored.
/// </summary>
private readonly ConcurrentDictionary<string, ModVersionCheckResults> results;
internal VersionCheckTask(KMod.Mod mod, IModVersionChecker method,
ConcurrentDictionary<string, ModVersionCheckResults> 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));
Next = null;
}
/// <summary>
/// Records the result of the mod version check, and runs the next checker in
/// line, from this mod or a different one.
/// </summary>
/// <param name="result">The results from the version check.</param>
private void OnComplete(ModVersionCheckResults result) {
method.OnVersionCheckCompleted -= OnComplete;
if (result != null) {
results.TryAdd(result.ModChecked, result);
if (!result.IsUpToDate)
PUtil.LogWarning("Mod {0} is out of date! New version: {1}".F(result.
ModChecked, result.NewVersion ?? "unknown"));
else {
#if DEBUG
PUtil.LogDebug("Mod {0} is up to date".F(result.ModChecked));
#endif
}
} else
RunNext();
}
/// <summary>
/// Runs the version check, and registers a callback to run the next one if
/// it is not null.
/// </summary>
internal void Run() {
if (results.ContainsKey(mod.staticID))
RunNext();
else {
bool run = false;
method.OnVersionCheckCompleted += OnComplete;
// Version check errors should not crash the game
try {
run = method.CheckVersion(mod);
} catch (Exception e) {
PUtil.LogWarning("Unable to check version for mod " + mod.label.title + ":");
PUtil.LogExcWarn(e);
run = false;
}
if (!run) {
method.OnVersionCheckCompleted -= OnComplete;
RunNext();
}
}
}
/// <summary>
/// Runs the next version check.
/// </summary>
private void RunNext() {
Next?.Invoke();
}
}
}

View File

@@ -0,0 +1,82 @@
/*
* 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 Klei;
using KMod;
using PeterHan.PLib.Core;
using System;
using UnityEngine.Networking;
namespace PeterHan.PLib.AVC {
/// <summary>
/// Checks the mod version using a URL to a YAML file. The file at this URL must resolve
/// to a YAML file of the same format as the mod_info.yaml class.
/// </summary>
public sealed class YamlURLVersionChecker : IModVersionChecker {
/// <summary>
/// The URL to query for checking the mod version.
/// </summary>
public string YamlVersionURL { get; }
public event PVersionCheck.OnVersionCheckComplete OnVersionCheckCompleted;
public YamlURLVersionChecker(string url) {
if (string.IsNullOrEmpty(url))
throw new ArgumentNullException(nameof(url));
YamlVersionURL = url;
}
public bool CheckVersion(Mod mod) {
if (mod == null)
throw new ArgumentNullException(nameof(mod));
var request = UnityWebRequest.Get(YamlVersionURL);
request.SetRequestHeader("Content-Type", "application/x-yaml");
request.SetRequestHeader("User-Agent", "PLib AVC");
request.timeout = JsonURLVersionChecker.REQUEST_TIMEOUT;
var operation = request.SendWebRequest();
operation.completed += (_) => OnRequestFinished(request, mod);
return true;
}
/// <summary>
/// When a web request completes, triggers the handler for the next updater.
/// </summary>
/// <param name="request">The YAML web request data.</param>
/// <param name="mod">The mod that needs to be checked.</param>
private void OnRequestFinished(UnityWebRequest request, Mod mod) {
ModVersionCheckResults result = null;
if (request.result == UnityWebRequest.Result.Success) {
// Parse the text
var modInfo = YamlIO.Parse<Mod.PackagedModInfo>(request.downloadHandler.
text, default);
string newVersion = modInfo?.version;
if (modInfo != null && !string.IsNullOrEmpty(newVersion)) {
string curVersion = PVersionCheck.GetCurrentVersion(mod);
#if DEBUG
PUtil.LogDebug("Current version: {0} New YAML version: {1}".F(
curVersion, newVersion));
#endif
result = new ModVersionCheckResults(mod.staticID, newVersion !=
curVersion, newVersion);
}
}
request.Dispose();
OnVersionCheckCompleted?.Invoke(result);
}
}
}