oni-priority-ux/mod/PLibAVC/SteamVersionChecker.cs

169 lines
6.8 KiB
C#

/*
* 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;
}
}
}