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 _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>(data, Game.GlobalJsonSerializerSettings); } var availableVersions = _versions.Where(v => v.platforms.ContainsKey(PlatformConfig.Name)).ToArray(); var versionIndex = new Dictionary(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(); 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(); 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 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 platforms; } class PlatformVersionInfo { public List resources; } class VersionResourceInfo { public bool external; public string upstream; [JsonRequired] public string url; [JsonRequired] public long size; } } }