Files
crtr/Assets/Cryville/Crtr/Network/UpdateChecker.cs

191 lines
8.1 KiB
C#

using Cryville.Common;
using Cryville.Common.Network.Http11;
using Cryville.Common.Unity;
using Cryville.Crtr.UI;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
#if UNITY_STANDALONE_WIN && !UNITY_EDITOR
using System.Runtime.InteropServices;
#endif
using System.Text;
using System.Threading;
using UnityEngine;
using ThreadPriority = System.Threading.ThreadPriority;
namespace Cryville.Crtr.Network {
public class UpdateChecker : MonoBehaviour {
string _currentVersion;
Thread _thread;
#pragma warning disable IDE0044
bool _shutdown;
#pragma warning restore IDE0044
void Start() {
_currentVersion = Application.version;
_thread = new Thread(ThreadLogic) { IsBackground = true, Priority = ThreadPriority.BelowNormal };
_thread.Start();
}
void Update() {
if (!_thread.IsAlive) {
enabled = false;
if (_shutdown) Application.Quit();
}
}
static readonly Uri BaseUri = new("https://www.cryville.world/api/crtr/index");
List<VersionInfo> _versions;
void ThreadLogic() {
try {
CheckVersion();
Game.MainLogger.Log(0, "Network", "Update checker exited normally");
}
catch (Exception ex) {
Game.MainLogger.Log(4, "Network", "An error occurred while checking for update: {0}", ex);
Dialog.Show(null, "Failed to check for update.");
}
}
void CheckVersion() {
using (var client = new Https11Client(BaseUri)) {
client.Connect();
using var response = client.Request("GET", new Uri(BaseUri, "versions"));
var data = Encoding.UTF8.GetString(response.MessageBody.ReadToEnd());
_versions = JsonConvert.DeserializeObject<List<VersionInfo>>(data, Game.GlobalJsonSerializerSettings);
}
var availableVersions = _versions.Where(v => v.platforms.ContainsKey(PlatformConfig.Name)).ToArray();
var versionIndex = new Dictionary<string, int>(availableVersions.Length);
for (int i = 0; i < availableVersions.Length; i++) versionIndex.Add(availableVersions[i].name, i);
var currentVersion = availableVersions.Where(v => v.name == _currentVersion).SingleOrDefault();
var latestVersion = availableVersions.Last();
if (currentVersion == null) {
Dialog.Show(null, string.Format("You are playing an unknown version of Cosmo Resona: {0}\nThe latest version is: {1}", _currentVersion, latestVersion.name));
return;
}
if (latestVersion.name == _currentVersion) return;
var latestResources = latestVersion.platforms[PlatformConfig.Name].resources;
VersionResourceInfo fullPackage = null;
if (latestResources != null) {
fullPackage = (from r in latestResources where r.upstream == null select r).SingleOrDefault();
}
if (fullPackage == null) {
Dialog.Show(null, string.Format("A new version is present: {0}\nUpdate is not available.", latestVersion.name));
return;
}
long totalDiffSize = 0;
int searchIndex = versionIndex[latestVersion.name], targetIndex = versionIndex[currentVersion.name];
var stream = new List<VersionResourceInfo>();
while (searchIndex != targetIndex) {
var searchVersion = availableVersions[searchIndex];
VersionResourceInfo matchedUpstream = null;
var resources = searchVersion.platforms[PlatformConfig.Name].resources;
if (resources == null) {
totalDiffSize = 0;
break;
}
foreach (var r in resources) {
if (r.upstream == null) continue;
var upstreamIndex = versionIndex[r.upstream];
if (upstreamIndex >= targetIndex && upstreamIndex < searchIndex) {
matchedUpstream = r;
searchIndex = upstreamIndex;
}
}
if (matchedUpstream != null) {
if (matchedUpstream.external) {
totalDiffSize = 0;
Dialog.ShowAndWait("An error occurred while checking for update.\nPlease report this to the developers.");
throw new InvalidOperationException("Diff package is external, which is not expected");
}
stream.Add(matchedUpstream);
totalDiffSize += matchedUpstream.size;
if (totalDiffSize >= fullPackage.size) {
totalDiffSize = 0;
break;
}
}
else {
totalDiffSize = 0;
break;
}
}
if (totalDiffSize == 0) {
// TODO Check if external
if (Dialog.ShowAndWait(string.Format("A new version is available: {0}\nYou have to download the full package.\nOpen the download link now?", latestVersion.name), "Yes", "No") == 0) {
UrlOpener.OpenThreaded(fullPackage.url);
}
}
else {
if (Dialog.ShowAndWait(string.Format("A new version is available: {0}\nDo you want to update?", latestVersion.name), "Yes", "No") == 0) {
var diffPaths = new List<string>();
var updateDir = Directory.CreateDirectory(Path.Combine(Game.GameDataPath, "update")).FullName;
foreach (var diff in stream) {
string path = Path.Combine(updateDir, StringUtils.EscapeFileName(diff.upstream));
diffPaths.Add(path);
Download(diff, path);
}
while (Dialog.ShowAndWait("The new version has been downloaded.\nUpdate now?\n(The game will be shut down)", "OK", "Later") == 1)
Thread.Sleep(60000);
ExecuteUpdate(diffPaths);
}
}
}
void Download(VersionResourceInfo diff, string path) {
var uri = new Uri(diff.url);
using var client = new Https11Client(uri);
client.Connect();
using var response = client.Request("GET", uri);
var data = response.MessageBody.ReadToEnd();
using var file = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
file.Write(data);
}
void ExecuteUpdate(List<string> diffPaths) {
#if UNITY_EDITOR
Dialog.Show(null, "Could not update the game in editor");
#elif UNITY_STANDALONE_WIN
var gameDir = new DirectoryInfo(Game.UnityDataPath).Parent;
var exe = new FileInfo(Path.Combine(gameDir.FullName, System.Diagnostics.Process.GetCurrentProcess().ProcessName + ".exe"));
var updateDir = Path.Combine(Game.GameDataPath, "update");
File.Copy(Path.Combine(Game.UnityDataPath, "Plugins", "x86_64", "hpatchz.dll"), Path.Combine(updateDir, "hpatchz.exe"), true);
var batchPath = Path.Combine(updateDir, "update.bat");
var oldPath = Path.Combine(updateDir, "old");
var newPath = Path.Combine(updateDir, "new");
using (var batch = new StreamWriter(batchPath, false, new UTF8Encoding(false))) {
batch.WriteLine("@echo off\r\ntitle [Updater] Cosmo Resona\r\necho Waiting for the game to shut down...\r\n:pending\r\ntasklist /fi \"ImageName eq {0}\" | find /i \"{0}\" >nul\r\nif \"%ERRORLEVEL%\" == \"0\" goto pending\r\necho Copying old data...\r\nrmdir /s /q {2}\r\nxcopy \"{1}\" \"{2}\" /s /e /h /i /q /y\r\nif not \"%ERRORLEVEL%\" == \"0\" goto error", exe.Name, gameDir, oldPath);
for (int i = 0; i < diffPaths.Count; i++) {
string path = diffPaths[i];
batch.WriteLine("echo Applying patch... ({0}/{1})\r\nhpatchz -f \"{2}\" \"{3}\" \"{4}\"\r\nif not \"%ERRORLEVEL%\" == \"0\" goto error\r\nrmdir /s /q \"{2}\"\r\nmove /y \"{4}\" \"{2}\"\r\nif not \"%ERRORLEVEL%\" == \"0\" goto error", i + 1, diffPaths.Count, oldPath, path, newPath);
}
batch.WriteLine("echo Copying new data...\r\nrmdir /s /q {1}\r\nmove /y \"{0}\" \"{1}\"\r\nif not \"%ERRORLEVEL%\" == \"0\" goto error\r\nrmdir /s /q \"{2}\"\r\necho Update succeeded\r\npause\r\nexit\r\n:error\r\necho Update failed\r\npause\r\nexit", oldPath, gameDir, updateDir);
}
ShellExecute(IntPtr.Zero, "open", batchPath, null, Path.Combine(updateDir), 1);
_shutdown = true;
#elif UNITY_ANDROID
// TODO
#else
#error No update logic for the selected platform.
#endif
}
#if UNITY_STANDALONE_WIN && !UNITY_EDITOR
[DllImport("shell32.dll")]
static extern int ShellExecute(IntPtr hwnd, [MarshalAs(UnmanagedType.LPStr)] string lpOperation, [MarshalAs(UnmanagedType.LPStr)] string lpFile, [MarshalAs(UnmanagedType.LPStr)] string lpParameters, [MarshalAs(UnmanagedType.LPStr)] string lpDirectory, int nShowCmd);
#endif
class VersionInfo {
[JsonRequired]
public string name;
public string type;
public Dictionary<string, PlatformVersionInfo> platforms;
}
class PlatformVersionInfo {
public List<VersionResourceInfo> resources;
}
class VersionResourceInfo {
public bool external;
public string upstream;
[JsonRequired]
public string url;
[JsonRequired]
public long size;
}
}
}