From 3fa28d5f107030f51b6725f43541d3fddc7b5605 Mon Sep 17 00:00:00 2001 From: slxdy Date: Wed, 8 Jan 2025 18:06:27 +0100 Subject: [PATCH 1/9] Fix negative CI build numbers --- MelonLoader.Installer/MLVersion.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MelonLoader.Installer/MLVersion.cs b/MelonLoader.Installer/MLVersion.cs index f6e9900..d13e244 100644 --- a/MelonLoader.Installer/MLVersion.cs +++ b/MelonLoader.Installer/MLVersion.cs @@ -45,7 +45,7 @@ public class MLVersion var fileVersionRaw = FileVersionInfo.GetVersionInfo(mlAssemblyPath).FileVersion!; var fileVersion = System.Version.Parse(fileVersionRaw); version = SemVersion.ParsedFrom(fileVersion.Major, fileVersion.Minor, fileVersion.Build, - fileVersion.Revision == 0 ? string.Empty : $"ci.{fileVersion.Revision}"); + fileVersion.Revision <= 0 ? string.Empty : $"ci.{fileVersion.Revision}"); } catch { From 822944749a87146ab2223b2f934ea9148e80e95a Mon Sep 17 00:00:00 2001 From: slxdy Date: Wed, 8 Jan 2025 18:14:52 +0100 Subject: [PATCH 2/9] Cleanup --- .../GameLaunchers/EgsLauncher.cs | 2 +- MelonLoader.Installer/GameManager.cs | 2 +- MelonLoader.Installer/MLManager.cs | 5 ++--- MelonLoader.Installer/MLVersion.cs | 2 +- MelonLoader.Installer/Program.cs | 7 +++---- MelonLoader.Installer/Updater.cs | 9 ++++----- MelonLoader.Installer/ViewModels/GameModel.cs | 10 +++++----- .../Views/DetailsView.axaml.cs | 17 ++++++++--------- MelonLoader.Installer/Views/DialogBox.axaml.cs | 2 +- MelonLoader.Installer/Views/MainView.axaml.cs | 2 +- MelonLoader.Installer/Views/MainWindow.axaml.cs | 3 +-- 11 files changed, 28 insertions(+), 33 deletions(-) diff --git a/MelonLoader.Installer/GameLaunchers/EgsLauncher.cs b/MelonLoader.Installer/GameLaunchers/EgsLauncher.cs index f9bfb9a..80c822d 100644 --- a/MelonLoader.Installer/GameLaunchers/EgsLauncher.cs +++ b/MelonLoader.Installer/GameLaunchers/EgsLauncher.cs @@ -35,7 +35,7 @@ public override void AddGames() { continue; } - + if (json == null || (bool?)json["bIsExecutable"] != true) continue; diff --git a/MelonLoader.Installer/GameManager.cs b/MelonLoader.Installer/GameManager.cs index 28d5065..b4c1f3b 100644 --- a/MelonLoader.Installer/GameManager.cs +++ b/MelonLoader.Installer/GameManager.cs @@ -117,7 +117,7 @@ public static void RemoveGame(GameModel game) return null; } } - + if (dataDirs.Length > 1) { errorMessage = "The selected directory contains multiple Unity games?"; diff --git a/MelonLoader.Installer/MLManager.cs b/MelonLoader.Installer/MLManager.cs index e7c9500..074094e 100644 --- a/MelonLoader.Installer/MLManager.cs +++ b/MelonLoader.Installer/MLManager.cs @@ -1,5 +1,4 @@ using Semver; -using System.Diagnostics.CodeAnalysis; using System.Text.Json.Nodes; #if WINDOWS @@ -11,7 +10,7 @@ namespace MelonLoader.Installer; internal static class MLManager { private static bool inited; - internal static readonly string[] proxyNames = + internal static readonly string[] proxyNames = [ "version.dll", "winmm.dll", @@ -90,7 +89,7 @@ private static async Task GetVersionsAsync(List versions) if (!SemVersion.TryParse(runName[..runVerEnd], SemVersionStyles.Any, out var runVersion)) continue; - + var version = new MLVersion { Version = runVersion, diff --git a/MelonLoader.Installer/MLVersion.cs b/MelonLoader.Installer/MLVersion.cs index d13e244..c7bad92 100644 --- a/MelonLoader.Installer/MLVersion.cs +++ b/MelonLoader.Installer/MLVersion.cs @@ -16,7 +16,7 @@ public class MLVersion { x86 = false; linux = false; - + var mlDir = Path.Combine(gameDir, "MelonLoader"); if (!Directory.Exists(mlDir)) return null; diff --git a/MelonLoader.Installer/Program.cs b/MelonLoader.Installer/Program.cs index 728cc61..424d3bc 100644 --- a/MelonLoader.Installer/Program.cs +++ b/MelonLoader.Installer/Program.cs @@ -1,6 +1,5 @@ using Avalonia; using System.Diagnostics; -using System.Runtime.InteropServices; namespace MelonLoader.Installer; @@ -60,7 +59,7 @@ private static void Main(string[] args) } Exiting?.Invoke(); - + processLock.Dispose(); File.Delete(processLockPath); } @@ -99,9 +98,9 @@ private static bool CheckProcessLock() GrabAttention(proc); return false; } - catch + catch { - return false; + return false; } } #else diff --git a/MelonLoader.Installer/Updater.cs b/MelonLoader.Installer/Updater.cs index 046257e..ab03679 100644 --- a/MelonLoader.Installer/Updater.cs +++ b/MelonLoader.Installer/Updater.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using System.Runtime.InteropServices; using System.Text.Json.Nodes; namespace MelonLoader.Installer; @@ -13,7 +12,7 @@ public static partial class Updater { if (State != UpdateState.None) return null; - + // Don't auto-update on CI builds if (Program.Version.Revision > 0) { @@ -92,7 +91,7 @@ private static async Task UpdateAsync(string downloadUrl) throw new Exception("Failed to download the latest installer version: " + result); } } - + #if LINUX // Make the file executable on Unix Chmod(newPath, S_IRUSR | S_IXUSR | S_IWUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); @@ -161,10 +160,10 @@ public static bool CheckLegacyUpdate() ".Linux" #endif )); - + return asset?["browser_download_url"]?.ToString(); } - + #if LINUX // user permissions const int S_IRUSR = 0x100; diff --git a/MelonLoader.Installer/ViewModels/GameModel.cs b/MelonLoader.Installer/ViewModels/GameModel.cs index 1bfd0b3..670336c 100644 --- a/MelonLoader.Installer/ViewModels/GameModel.cs +++ b/MelonLoader.Installer/ViewModels/GameModel.cs @@ -38,9 +38,9 @@ public SemVersion? MLVersion public bool ValidateGame() { var exeExtIdx = path.LastIndexOf('.'); - + var pathNoExt = exeExtIdx != -1 ? path[..exeExtIdx] : path; - + if (!File.Exists(path) || !Directory.Exists(pathNoExt + "_Data")) { GameManager.RemoveGame(this); @@ -50,10 +50,10 @@ public bool ValidateGame() var newMlVersion = Installer.MLVersion.GetMelonLoaderVersion(Dir, out var ml86, out var mlLinux); if (newMlVersion != null && (ml86 != Is32Bit || mlLinux != IsLinux)) newMlVersion = null; - - if (newMlVersion == MLVersion) + + if (newMlVersion == MLVersion) return true; - + MLVersion = newMlVersion; GameManager.ResortGame(this); diff --git a/MelonLoader.Installer/Views/DetailsView.axaml.cs b/MelonLoader.Installer/Views/DetailsView.axaml.cs index e3e9320..ea980bb 100644 --- a/MelonLoader.Installer/Views/DetailsView.axaml.cs +++ b/MelonLoader.Installer/Views/DetailsView.axaml.cs @@ -5,7 +5,6 @@ using Avalonia.Threading; using MelonLoader.Installer.ViewModels; using System.ComponentModel; -using System.Runtime.InteropServices; namespace MelonLoader.Installer.Views; @@ -35,14 +34,14 @@ private void PropertyChangedHandler(object? sender, PropertyChangedEventArgs cha UpdateVersionInfo(); } } - + protected override async void OnDataContextChanged(EventArgs e) { base.OnDataContextChanged(e); if (Model == null) return; - + #if LINUX if (Model.Game.IsLinux) { @@ -86,13 +85,13 @@ private void BackClickHandler(object sender, RoutedEventArgs args) { if (Model == null) return; - + if (Model.LinuxInstructions) { Model.LinuxInstructions = false; return; } - + if (Model.Installing) return; @@ -176,9 +175,9 @@ private void OnOperationFinished(string? errorMessage, bool addedLocalBuild = fa if (addedLocalBuild) return; - bool isInstall = true; - string operationType = Model.Game.MLInstalled ? "Installed" : "Uninstalled"; - if (Model.Game.MLInstalled + var isInstall = true; + var operationType = Model.Game.MLInstalled ? "Installed" : "Uninstalled"; + if (Model.Game.MLInstalled && (Model.Game.MLVersion != null) && (currentMLVersion != null)) { @@ -268,7 +267,7 @@ private void ShowLinuxInstructionsHandler(object sender, TappedEventArgs args) { if (Model == null) return; - + Model.LinuxInstructions = true; } } \ No newline at end of file diff --git a/MelonLoader.Installer/Views/DialogBox.axaml.cs b/MelonLoader.Installer/Views/DialogBox.axaml.cs index a843710..9406115 100644 --- a/MelonLoader.Installer/Views/DialogBox.axaml.cs +++ b/MelonLoader.Installer/Views/DialogBox.axaml.cs @@ -15,7 +15,7 @@ public DialogBox() protected override void OnClosed(EventArgs e) { base.OnClosed(e); - + OnCancel?.Invoke(); } diff --git a/MelonLoader.Installer/Views/MainView.axaml.cs b/MelonLoader.Installer/Views/MainView.axaml.cs index 570326b..1e32eb2 100644 --- a/MelonLoader.Installer/Views/MainView.axaml.cs +++ b/MelonLoader.Installer/Views/MainView.axaml.cs @@ -8,7 +8,7 @@ namespace MelonLoader.Installer.Views; public partial class MainView : UserControl { private static bool showedNotice; - + public MainViewModel? Model => (MainViewModel?)DataContext; public MainView() diff --git a/MelonLoader.Installer/Views/MainWindow.axaml.cs b/MelonLoader.Installer/Views/MainWindow.axaml.cs index 63eddcb..8aa7553 100644 --- a/MelonLoader.Installer/Views/MainWindow.axaml.cs +++ b/MelonLoader.Installer/Views/MainWindow.axaml.cs @@ -1,6 +1,5 @@ using Avalonia; using Avalonia.Controls; -using Avalonia.Threading; using MelonLoader.Installer.ViewModels; namespace MelonLoader.Installer.Views; @@ -39,7 +38,7 @@ protected override void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e) if (!IsVisible) return; - + Topmost = true; Topmost = false; #if WINDOWS From ec5f06c0da91bfef59f91209dfe9472bcc184089 Mon Sep 17 00:00:00 2001 From: slxdy Date: Mon, 13 Jan 2025 13:44:41 +0100 Subject: [PATCH 3/9] Fix cleanup --- MelonLoader.Installer/Updater.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/MelonLoader.Installer/Updater.cs b/MelonLoader.Installer/Updater.cs index ab03679..c9dd413 100644 --- a/MelonLoader.Installer/Updater.cs +++ b/MelonLoader.Installer/Updater.cs @@ -1,6 +1,10 @@ using System.Diagnostics; using System.Text.Json.Nodes; +#if LINUX +using System.Runtime.InteropServices; +#endif + namespace MelonLoader.Installer; public static partial class Updater From 4a4c173f2ef28b56b3709598714e65fea136e34e Mon Sep 17 00:00:00 2001 From: slxdy Date: Wed, 15 Jan 2025 18:21:12 +0100 Subject: [PATCH 4/9] Add a global loading bar, optimize startup --- .github/workflows/build.yml | 4 +- Directory.Build.props | 2 +- MelonLoader.Installer/Config.cs | 5 + .../GameLaunchers/EgsLauncher.cs | 54 +++---- .../GameLaunchers/GameLauncher.cs | 2 +- .../GameLaunchers/GogLauncher.cs | 4 +- .../GameLaunchers/SteamLauncher.cs | 56 ++++---- MelonLoader.Installer/GameManager.cs | 55 +++++--- MelonLoader.Installer/MLManager.cs | 52 ++++--- MelonLoader.Installer/Program.cs | 8 -- MelonLoader.Installer/Updater.cs | 96 ++++--------- .../ViewModels/MainViewModel.cs | 3 - .../Views/DetailsView.axaml.cs | 15 +- .../Views/DialogBox.axaml.cs | 7 + MelonLoader.Installer/Views/LoadView.axaml | 12 ++ MelonLoader.Installer/Views/LoadView.axaml.cs | 19 +++ MelonLoader.Installer/Views/MainView.axaml | 8 +- MelonLoader.Installer/Views/MainView.axaml.cs | 45 +++--- MelonLoader.Installer/Views/MainWindow.axaml | 2 +- .../Views/MainWindow.axaml.cs | 133 +++++++++++++++--- MelonLoader.Installer/Views/UpdaterView.axaml | 14 -- .../Views/UpdaterView.axaml.cs | 19 --- 22 files changed, 341 insertions(+), 274 deletions(-) create mode 100644 MelonLoader.Installer/Views/LoadView.axaml create mode 100644 MelonLoader.Installer/Views/LoadView.axaml.cs delete mode 100644 MelonLoader.Installer/Views/UpdaterView.axaml delete mode 100644 MelonLoader.Installer/Views/UpdaterView.axaml.cs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 77b5a9e..1484828 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,9 +1,9 @@ name: Build Installer -run-name: 4.1.1-ci.${{ github.run_number }} | ${{ github.event_name != 'workflow_dispatch' && (github.event.head_commit.message || format('`[PR]` {0}', github.event.pull_request.title)) || 'Manual Build' }} +run-name: 5.0.0-ci.${{ github.run_number }} | ${{ github.event_name != 'workflow_dispatch' && (github.event.head_commit.message || format('`[PR]` {0}', github.event.pull_request.title)) || 'Manual Build' }} env: - DEVVERSION: "4.1.1" + DEVVERSION: "5.0.0" on: push: diff --git a/Directory.Build.props b/Directory.Build.props index c9613f8..2c07f4c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 4.1.1 + 5.0.0 net8.0 diff --git a/MelonLoader.Installer/Config.cs b/MelonLoader.Installer/Config.cs index e8b059a..6d1c289 100644 --- a/MelonLoader.Installer/Config.cs +++ b/MelonLoader.Installer/Config.cs @@ -13,6 +13,11 @@ internal static class Config public static string LocalZipCache { get; private set; } = Path.Combine(CacheDir, "Local Build"); public static string GameListPath { get; private set; } = Path.Combine(CacheDir, "games.txt"); + static Config() + { + Directory.CreateDirectory(CacheDir); + } + public static string[] LoadGameList() { if (!File.Exists(GameListPath)) diff --git a/MelonLoader.Installer/GameLaunchers/EgsLauncher.cs b/MelonLoader.Installer/GameLaunchers/EgsLauncher.cs index 80c822d..adeb4c1 100644 --- a/MelonLoader.Installer/GameLaunchers/EgsLauncher.cs +++ b/MelonLoader.Installer/GameLaunchers/EgsLauncher.cs @@ -19,36 +19,42 @@ static EgsLauncher() internal EgsLauncher() : base("/Assets/egs.png") { } - public override void AddGames() + public override void GetAddGameTasks(List tasks) { if (manifestDir == null) return; - foreach (var item in Directory.EnumerateFiles(manifestDir, "*.item")) + foreach (var file in Directory.GetFiles(manifestDir, "*.item")) { - JsonNode? json; - try - { - json = JsonNode.Parse(File.ReadAllText(item)); - } - catch - { - continue; - } - - if (json == null || (bool?)json["bIsExecutable"] != true) - continue; - - var dir = (string?)json["InstallLocation"]; - if (dir == null || !Directory.Exists(dir)) - continue; - - var name = (string?)json["DisplayName"]; - if (name == null) - continue; - - GameManager.TryAddGame(dir, name, this, null, out _); + tasks.Add(AddGameAsync(file)); } } + + private async Task AddGameAsync(string item) + { + JsonNode? json; + try + { + using var str = File.OpenRead(item); + json = await JsonNode.ParseAsync(str); + } + catch + { + return; + } + + if (json == null || (bool?)json["bIsExecutable"] != true) + return; + + var dir = (string?)json["InstallLocation"]; + if (dir == null || !Directory.Exists(dir)) + return; + + var name = (string?)json["DisplayName"]; + if (name == null) + return; + + GameManager.TryAddGame(dir, name, this, null, out _); + } } #endif \ No newline at end of file diff --git a/MelonLoader.Installer/GameLaunchers/GameLauncher.cs b/MelonLoader.Installer/GameLaunchers/GameLauncher.cs index c8f5cea..2c7b81e 100644 --- a/MelonLoader.Installer/GameLaunchers/GameLauncher.cs +++ b/MelonLoader.Installer/GameLaunchers/GameLauncher.cs @@ -13,5 +13,5 @@ public abstract class GameLauncher(string iconPath) public string IconPath => iconPath; - public abstract void AddGames(); + public abstract void GetAddGameTasks(List tasks); } diff --git a/MelonLoader.Installer/GameLaunchers/GogLauncher.cs b/MelonLoader.Installer/GameLaunchers/GogLauncher.cs index 77ef37e..b04d2dd 100644 --- a/MelonLoader.Installer/GameLaunchers/GogLauncher.cs +++ b/MelonLoader.Installer/GameLaunchers/GogLauncher.cs @@ -16,7 +16,7 @@ static GogLauncher() gamesKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\WOW6432Node\GOG.com\Games"); } - public override void AddGames() + public override void GetAddGameTasks(List tasks) { if (gamesKey == null) return; @@ -34,7 +34,7 @@ public override void AddGames() if (name == null) continue; - GameManager.TryAddGame(path, name, this, null, out _); + tasks.Add(Task.Run(() => GameManager.TryAddGame(path, name, this, null, out _))); } } } diff --git a/MelonLoader.Installer/GameLaunchers/SteamLauncher.cs b/MelonLoader.Installer/GameLaunchers/SteamLauncher.cs index cc3b880..ee186f8 100644 --- a/MelonLoader.Installer/GameLaunchers/SteamLauncher.cs +++ b/MelonLoader.Installer/GameLaunchers/SteamLauncher.cs @@ -28,7 +28,7 @@ static SteamLauncher() internal SteamLauncher() : base("/Assets/steam.png") { } - public override void AddGames() + public override void GetAddGameTasks(List tasks) { if (steamPath == null) return; @@ -49,30 +49,38 @@ public override void AddGames() var acfs = Directory.EnumerateFiles(steamapps, "*.acf"); foreach (var acfPath in acfs) { - VToken acf; - try - { - acf = VdfConvert.Deserialize(File.ReadAllText(acfPath)).Value; - } - catch - { - continue; - } - - var id = ((VProperty?)acf.FirstOrDefault(x => ((VProperty)x).Key == "appid"))?.Value?.ToString(); - var name = ((VProperty?)acf.FirstOrDefault(x => ((VProperty)x).Key == "name"))?.Value?.ToString(); - var dirName = ((VProperty?)acf.FirstOrDefault(x => ((VProperty)x).Key == "installdir"))?.Value?.ToString(); - - if (id == null || name == null || dirName == null) - continue; - - var appDir = Path.Combine(steamapps, "common", dirName); - if (!Directory.Exists(appDir)) - continue; - - var iconPath = Path.Combine(steamPath, "appcache", "librarycache", id + "_icon.jpg"); - GameManager.TryAddGame(appDir, name, this, iconPath, out _); + tasks.Add(AddGameAsync(acfPath, steamapps)); } } } + + private async Task AddGameAsync(string acfPath, string steamapps) + { + if (steamPath == null) + return; + + VToken acf; + try + { + acf = VdfConvert.Deserialize(await File.ReadAllTextAsync(acfPath)).Value; + } + catch + { + return; + } + + var id = ((VProperty?)acf.FirstOrDefault(x => ((VProperty)x).Key == "appid"))?.Value?.ToString(); + var name = ((VProperty?)acf.FirstOrDefault(x => ((VProperty)x).Key == "name"))?.Value?.ToString(); + var dirName = ((VProperty?)acf.FirstOrDefault(x => ((VProperty)x).Key == "installdir"))?.Value?.ToString(); + + if (id == null || name == null || dirName == null) + return; + + var appDir = Path.Combine(steamapps, "common", dirName); + if (!Directory.Exists(appDir)) + return; + + var iconPath = Path.Combine(steamPath, "appcache", "librarycache", id + "_icon.jpg"); + GameManager.TryAddGame(appDir, name, this, iconPath, out _); + } } diff --git a/MelonLoader.Installer/GameManager.cs b/MelonLoader.Installer/GameManager.cs index b4c1f3b..232ed5e 100644 --- a/MelonLoader.Installer/GameManager.cs +++ b/MelonLoader.Installer/GameManager.cs @@ -9,34 +9,44 @@ namespace MelonLoader.Installer; internal static class GameManager { - private static bool inited; + public static bool Initialized { get; private set; } public static ObservableCollection Games { get; } = []; - public static void Init() + public static Task InitAsync(InstallProgressEventHandler onProgress) { - if (inited) - return; + if (Initialized) + return Task.CompletedTask; + + Initialized = true; - inited = true; + onProgress?.Invoke(0, "Loading game library"); + var finishedTasks = 0; + var tasks = new List(); foreach (var launcher in GameLauncher.Launchers) { - launcher.AddGames(); + launcher.GetAddGameTasks(tasks); } - LoadSavedGames(); + GetLoadSavedGamesTasks(tasks); + + var contTasks = new Task[tasks.Count]; + + for (var i = 0; i < tasks.Count; i++) + { + contTasks[i] = tasks[i].ContinueWith((t) => onProgress?.Invoke(finishedTasks++ / tasks.Count, null)); + } + + return Task.WhenAll(contTasks); } - private static void LoadSavedGames() + private static void GetLoadSavedGamesTasks(List tasks) { foreach (var gamePath in Config.LoadGameList()) { - TryAddGame(gamePath, null, null, null, out _); + tasks.Add(Task.Run(() => TryAddGame(gamePath, null, null, null, out _))); } - - // In case it was manually edited or if any games were removed - SaveManualGameList(); } public static void SaveManualGameList() @@ -126,10 +136,13 @@ public static void RemoveGame(GameModel game) var exe = dataDirs[0][..^5] + (linux ? ".x86_64" : ".exe"); - if (Games.Any(x => x.Path.Equals(exe, StringComparison.OrdinalIgnoreCase))) + lock (Games) { - errorMessage = "Game is already listed."; - return null; + if (Games.Any(x => x.Path.Equals(exe, StringComparison.OrdinalIgnoreCase))) + { + errorMessage = "Game is already listed."; + return null; + } } var is64 = true; @@ -172,7 +185,17 @@ public static void RemoveGame(GameModel game) var result = new GameModel(exe, customName ?? Path.GetFileNameWithoutExtension(exe), !is64, linux, launcher, icon, mlVersion, isProtected); errorMessage = null; - AddGameSorted(result); + lock (Games) + { + // We're doing it again, in case another Task managed to already add a copy of the same game at this point + if (Games.Any(x => x.Path.Equals(exe, StringComparison.OrdinalIgnoreCase))) + { + errorMessage = "Game is already listed."; + return null; + } + + AddGameSorted(result); + } return result; } diff --git a/MelonLoader.Installer/MLManager.cs b/MelonLoader.Installer/MLManager.cs index 074094e..e997a88 100644 --- a/MelonLoader.Installer/MLManager.cs +++ b/MelonLoader.Installer/MLManager.cs @@ -9,7 +9,6 @@ namespace MelonLoader.Installer; internal static class MLManager { - private static bool inited; internal static readonly string[] proxyNames = [ "version.dll", @@ -25,6 +24,8 @@ internal static class MLManager public static List Versions { get; } = []; + public static bool RefreshedOnce { get; private set; } + static MLManager() { Program.Exiting += HandleExit; @@ -42,41 +43,31 @@ private static void HandleExit() } } - public static async Task Init() - { - if (inited) - return true; - - inited = await RefreshVersions(); - return inited; - } - - private static Task RefreshVersions() + public static async Task RefreshVersionsAsync(InstallProgressEventHandler onProgress) { Versions.Clear(); if (localBuild != null) Versions.Add(localBuild); - return GetVersionsAsync(Versions); - } + onProgress?.Invoke(0, "Fetching MelonLoader workflow builds"); - private static async Task GetVersionsAsync(List versions) - { HttpResponseMessage resp; try { - resp = await InstallerUtils.Http.GetAsync(Config.MelonLoaderBuildWorkflowApi).ConfigureAwait(false); + resp = await InstallerUtils.Http.GetAsync(Config.MelonLoaderBuildWorkflowApi); } - catch + catch (Exception ex) { - return false; + return $"Failed to fetch MelonLoader workflows: {ex.Message}"; } if (!resp.IsSuccessStatusCode) - return false; + return $"Failed to fetch MelonLoader CI versions: Status code '{resp.StatusCode}'"; + + onProgress?.Invoke(0.4, "Adding MelonLoader workflow builds"); - var relStr = await resp.Content.ReadAsStringAsync().ConfigureAwait(false); + var relStr = await resp.Content.ReadAsStringAsync(); var runsJson = JsonNode.Parse(relStr)!["workflow_runs"]!.AsArray(); // All run names must follow the following format: "{SemVersion} Remaining name" @@ -101,22 +92,26 @@ private static async Task GetVersionsAsync(List versions) if (version.DownloadUrlWin == null && version.DownloadUrlWinX86 == null && version.DownloadUrlLinux == null) continue; - versions.Add(version); + Versions.Add(version); } + onProgress?.Invoke(0.5, "Fetching MelonLoader releases"); + try { - resp = await InstallerUtils.Http.GetAsync(Config.MelonLoaderReleasesApi).ConfigureAwait(false); + resp = await InstallerUtils.Http.GetAsync(Config.MelonLoaderReleasesApi); } - catch + catch (Exception ex) { - return false; + return $"Failed to fetch MelonLoader releases: {ex.Message}"; } if (!resp.IsSuccessStatusCode) - return false; + return $"Failed to fetch MelonLoader releases: Status code '{resp.StatusCode}'"; - relStr = await resp.Content.ReadAsStringAsync().ConfigureAwait(false); + onProgress?.Invoke(0.9, "Adding MelonLoader releases"); + + relStr = await resp.Content.ReadAsStringAsync(); var releasesJson = JsonNode.Parse(relStr)!.AsArray(); foreach (var release in releasesJson) @@ -142,10 +137,11 @@ private static async Task GetVersionsAsync(List versions) if (version.DownloadUrlWin == null && version.DownloadUrlWinX86 == null && version.DownloadUrlLinux == null) continue; - versions.Add(version); + Versions.Add(version); } - return true; + RefreshedOnce = true; + return null; } public static string? Uninstall(string gameDir, bool removeUserFiles) diff --git a/MelonLoader.Installer/Program.cs b/MelonLoader.Installer/Program.cs index 424d3bc..2f5ba53 100644 --- a/MelonLoader.Installer/Program.cs +++ b/MelonLoader.Installer/Program.cs @@ -21,9 +21,6 @@ internal static class Program [STAThread] private static void Main(string[] args) { - if (!Directory.Exists(Config.CacheDir)) - Directory.CreateDirectory(Config.CacheDir); - if (args.Length >= 3) { if (!int.TryParse(args[2], out var pid)) @@ -41,11 +38,6 @@ private static void Main(string[] args) } } -#if WINDOWS - if (Updater.CheckLegacyUpdate()) - return; -#endif - if (!CheckProcessLock()) return; diff --git a/MelonLoader.Installer/Updater.cs b/MelonLoader.Installer/Updater.cs index c9dd413..447b01a 100644 --- a/MelonLoader.Installer/Updater.cs +++ b/MelonLoader.Installer/Updater.cs @@ -9,31 +9,43 @@ namespace MelonLoader.Installer; public static partial class Updater { - public static volatile UpdateState State; - public static event InstallProgressEventHandler? Progress; - - public static async Task UpdateIfPossible() + public static async Task UpdateIfPossibleAsync(InstallProgressEventHandler onProgress) { - if (State != UpdateState.None) - return null; - // Don't auto-update on CI builds if (Program.Version.Revision > 0) - { - State = UpdateState.AlreadyChecked; - return null; - } + return false; + + onProgress?.Invoke(0, "Checking for updates"); var downloadUrl = await CheckForUpdateAsync(); if (downloadUrl == null) + return false; + + var newPath = Path.GetTempFileName() +#if WINDOWS + + ".exe" +#endif + ; + + onProgress?.Invoke(0, "Updating"); + + await using (var newStr = File.OpenWrite(newPath)) { - State = UpdateState.AlreadyChecked; - return null; + var result = await InstallerUtils.DownloadFileAsync(downloadUrl, newStr, (progress, newStatus) => onProgress?.Invoke(progress, newStatus)); + if (result != null) + { + throw new Exception("Failed to download the latest installer version: " + result); + } } - State = UpdateState.Updating; +#if LINUX + // Make the file executable on Unix + Chmod(newPath, S_IRUSR | S_IXUSR | S_IWUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); +#endif + + Process.Start(newPath, ["-handleupdate", Environment.ProcessPath!, Environment.ProcessId.ToString()]); - return Task.Run(() => UpdateAsync(downloadUrl)); + return true; } public static bool WaitAndRemoveApp(string originalPath, int prevPID) @@ -79,52 +91,6 @@ public static void HandleUpdate(string originalPath, int prevPID) Process.Start(originalPath, ["-cleanup", Environment.ProcessPath!, Environment.ProcessId.ToString()]); } - private static async Task UpdateAsync(string downloadUrl) - { - var newPath = Path.GetTempFileName() -#if WINDOWS - + ".exe" -#endif - ; - - await using (var newStr = File.OpenWrite(newPath)) - { - var result = await InstallerUtils.DownloadFileAsync(downloadUrl, newStr, (progress, newStatus) => Progress?.Invoke(progress, newStatus)); - if (result != null) - { - throw new Exception("Failed to download the latest installer version: " + result); - } - } - -#if LINUX - // Make the file executable on Unix - Chmod(newPath, S_IRUSR | S_IXUSR | S_IWUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); -#endif - - Process.Start(newPath, ["-handleupdate", Environment.ProcessPath!, Environment.ProcessId.ToString()]); - - State = UpdateState.Done; - } - -#if WINDOWS - public static bool CheckLegacyUpdate() - { - if (!Environment.ProcessPath!.EndsWith(".tmp.exe", StringComparison.OrdinalIgnoreCase)) - return false; - - var dir = Path.GetDirectoryName(Environment.ProcessPath)!; - var name = Path.GetFileNameWithoutExtension(Environment.ProcessPath); - name = name.Remove(name.Length - 4) + ".exe"; - - var final = Path.Combine(dir, name); - - var prevProc = Process.GetProcessesByName(name); - - HandleUpdate(final, prevProc.FirstOrDefault()?.Id ?? 0); - return true; - } -#endif - private static async Task CheckForUpdateAsync() { HttpResponseMessage response; @@ -185,12 +151,4 @@ public static bool CheckLegacyUpdate() [LibraryImport("libc", EntryPoint = "chmod", StringMarshalling = StringMarshalling.Utf8)] private static partial int Chmod(string pathname, int mode); #endif - - public enum UpdateState - { - None, - Updating, - Done, - AlreadyChecked - } } diff --git a/MelonLoader.Installer/ViewModels/MainViewModel.cs b/MelonLoader.Installer/ViewModels/MainViewModel.cs index f2ae8bd..bd3973f 100644 --- a/MelonLoader.Installer/ViewModels/MainViewModel.cs +++ b/MelonLoader.Installer/ViewModels/MainViewModel.cs @@ -5,9 +5,6 @@ namespace MelonLoader.Installer.ViewModels; public partial class MainViewModel : ViewModelBase { - [ObservableProperty] - private bool ready; - [ObservableProperty] private string? search; diff --git a/MelonLoader.Installer/Views/DetailsView.axaml.cs b/MelonLoader.Installer/Views/DetailsView.axaml.cs index ea980bb..cdf89eb 100644 --- a/MelonLoader.Installer/Views/DetailsView.axaml.cs +++ b/MelonLoader.Installer/Views/DetailsView.axaml.cs @@ -10,7 +10,7 @@ namespace MelonLoader.Installer.Views; public partial class DetailsView : UserControl { - public DetailsViewModel? Model => (DetailsViewModel?)DataContext; + public DetailsViewModel? Model => DataContext as DetailsViewModel; public DetailsView() { @@ -35,7 +35,7 @@ private void PropertyChangedHandler(object? sender, PropertyChangedEventArgs cha } } - protected override async void OnDataContextChanged(EventArgs e) + protected override void OnDataContextChanged(EventArgs e) { base.OnDataContextChanged(e); @@ -55,12 +55,6 @@ protected override async void OnDataContextChanged(EventArgs e) Model.Game.PropertyChanged += PropertyChangedHandler; UpdateVersionList(); - - if (!await MLManager.Init()) - { - Model.Offline = true; - DialogBox.ShowError("Failed to fetch MelonLoader releases. Ensure you're online."); - } } private void NightlyToggleHandler(object sender, RoutedEventArgs args) @@ -77,6 +71,11 @@ public void UpdateVersionList() if (NightlyCheck.IsChecked != true) en = en.Where(x => !x.Version.IsPrerelease || x.IsLocalPath); + if (!en.Any()) + { + Model.Offline = true; + } + VersionCombobox.ItemsSource = en; VersionCombobox.SelectedIndex = 0; } diff --git a/MelonLoader.Installer/Views/DialogBox.axaml.cs b/MelonLoader.Installer/Views/DialogBox.axaml.cs index 9406115..98ab7ae 100644 --- a/MelonLoader.Installer/Views/DialogBox.axaml.cs +++ b/MelonLoader.Installer/Views/DialogBox.axaml.cs @@ -1,5 +1,6 @@ using Avalonia.Controls; using Avalonia.Interactivity; +using Avalonia.Threading; using MelonLoader.Installer.ViewModels; namespace MelonLoader.Installer.Views; @@ -89,6 +90,12 @@ public static void ShowConfirmation( private void Open() { + if (!Dispatcher.UIThread.CheckAccess()) + { + Dispatcher.UIThread.Invoke(Open); + return; + } + BringToFront(); if (MainWindow.Instance.IsVisible) diff --git a/MelonLoader.Installer/Views/LoadView.axaml b/MelonLoader.Installer/Views/LoadView.axaml new file mode 100644 index 0000000..fee4629 --- /dev/null +++ b/MelonLoader.Installer/Views/LoadView.axaml @@ -0,0 +1,12 @@ + + + + Text + + + diff --git a/MelonLoader.Installer/Views/LoadView.axaml.cs b/MelonLoader.Installer/Views/LoadView.axaml.cs new file mode 100644 index 0000000..79572ee --- /dev/null +++ b/MelonLoader.Installer/Views/LoadView.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia.Controls; + +namespace MelonLoader.Installer.Views; + +public partial class LoadView : UserControl +{ + public LoadView() + { + InitializeComponent(); + } + + public void SetProgress(double progress, string? newStatus) + { + Progress.Value = progress * 100; + + if (newStatus != null) + Text.Text = newStatus; + } +} \ No newline at end of file diff --git a/MelonLoader.Installer/Views/MainView.axaml b/MelonLoader.Installer/Views/MainView.axaml index e3e497c..da80ebb 100644 --- a/MelonLoader.Installer/Views/MainView.axaml +++ b/MelonLoader.Installer/Views/MainView.axaml @@ -30,7 +30,7 @@ If you can't find your game on the list, add it manually. - + @@ -43,13 +43,9 @@ If you can't find your game on the list, add it manually. No Unity games have been found on your device. Please add one manually using the button below. - - Loading... - - + - - - - Select a version - - Or select a - zipped version - - Enable Nightly builds - Keep mods and settings - - - - - + + + + + + + - - - + + + Select a version + + + Or select a + zipped version + + + Enable Nightly builds + Keep mods and settings + + + + + + + + + + + + + How do I start MelonLoader? + - - - How do I start MelonLoader? - - Linux Launch Instructions - - In order to start MelonLoader under Wine, you'll need to export the following variable: - WINEDLLOVERRIDES="version=n,b" - On Steam, you can set the launch options to: - WINEDLLOVERRIDES="version=n,b" %command% - - - In order to start MelonLoader, you'll need to export the following variables: - - LD_PRELOAD="libversion.so" - On Steam, you can set the launch options to: - - + + In order to start MelonLoader under Wine, you'll need to export the following variable: + WINEDLLOVERRIDES="version=n,b" + On Steam, you can set the launch options to: + WINEDLLOVERRIDES="version=n,b" %command% + + + In order to start MelonLoader, you'll need to export the following variables: + + LD_PRELOAD="libversion.so" + On Steam, you can set the launch options to: + + \ No newline at end of file diff --git a/MelonLoader.Installer/Views/DialogBox.axaml b/MelonLoader.Installer/Views/DialogBox.axaml index a042d9c..9e2cf78 100644 --- a/MelonLoader.Installer/Views/DialogBox.axaml +++ b/MelonLoader.Installer/Views/DialogBox.axaml @@ -11,7 +11,7 @@ WindowStartupLocation="CenterScreen" Background="#121217" CanResize="False" - Padding="15" + Padding="40 15" Height="100" SizeToContent="WidthAndHeight" RenderOptions.BitmapInterpolationMode="HighQuality"> @@ -22,7 +22,7 @@ - + - - - - - - - + + + + + + + + \ No newline at end of file diff --git a/MelonLoader.Installer/Views/MainView.axaml.cs b/MelonLoader.Installer/Views/MainView.axaml.cs index fb1faf6..9712fed 100644 --- a/MelonLoader.Installer/Views/MainView.axaml.cs +++ b/MelonLoader.Installer/Views/MainView.axaml.cs @@ -22,7 +22,7 @@ protected override async void OnDataContextChanged(EventArgs e) { base.OnDataContextChanged(e); - if (Model == null) + if (Model == null || MainWindow.Instance == null) return; if (!GameManager.Initialized) @@ -30,9 +30,9 @@ protected override async void OnDataContextChanged(EventArgs e) await GameManager.InitAsync(MainWindow.Instance.SetLoadStatus); } - if (DateTime.Now - _lastTimeCheckedVersions > TimeSpan.FromMinutes(3)) + if (DateTime.UtcNow - _lastTimeCheckedVersions > TimeSpan.FromMinutes(3)) { - _lastTimeCheckedVersions = DateTime.Now; + _lastTimeCheckedVersions = DateTime.UtcNow; var error = await MLManager.RefreshVersionsAsync(MainWindow.Instance.SetLoadStatus); if (error != null) @@ -54,17 +54,6 @@ protected override async void OnDataContextChanged(EventArgs e) } } - private static void CrashException(Exception ex) - { - Program.LogCrashException(ex); - - DialogBox.ShowError(""" - An error has occurred while loading the game library! - Please report this issue in the official Discord server in the #ml-support channel. - Include the crash log named 'melonloader-installer-crash.log', located next to the executable. - """, () => MainWindow.Instance.Close()); - } - protected override void OnUnloaded(RoutedEventArgs e) { base.OnUnloaded(e); diff --git a/MelonLoader.Installer/Views/MainWindow.axaml b/MelonLoader.Installer/Views/MainWindow.axaml index 4528a37..3d86c14 100644 --- a/MelonLoader.Installer/Views/MainWindow.axaml +++ b/MelonLoader.Installer/Views/MainWindow.axaml @@ -3,12 +3,12 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" - Width="900" Height="700" + MinWidth="700" MinHeight="700" + Width="1000" Height="700" x:Class="MelonLoader.Installer.Views.MainWindow" Icon="/Assets/icon.ico" Title="MelonLoader Installer" WindowStartupLocation="CenterScreen" - CanResize="False" RenderOptions.BitmapInterpolationMode="HighQuality"> From 08bcbbc6025f1f4242d928fd5125de04e9335c9a Mon Sep 17 00:00:00 2001 From: slxdy Date: Thu, 16 Jan 2025 00:02:33 +0100 Subject: [PATCH 6/9] Cleanup --- MelonLoader.Installer/Views/DetailsView.axaml | 2 +- MelonLoader.Installer/Views/DetailsView.axaml.cs | 5 +---- MelonLoader.Installer/Views/GameControl.axaml | 8 ++++---- MelonLoader.Installer/Views/GameControl.axaml.cs | 1 + MelonLoader.Installer/Views/MainView.axaml | 6 ++++-- MelonLoader.Installer/Views/MainWindow.axaml | 3 ++- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/MelonLoader.Installer/Views/DetailsView.axaml b/MelonLoader.Installer/Views/DetailsView.axaml index 74264bb..0b2d662 100644 --- a/MelonLoader.Installer/Views/DetailsView.axaml +++ b/MelonLoader.Installer/Views/DetailsView.axaml @@ -29,7 +29,7 @@ - Or select a zipped version diff --git a/MelonLoader.Installer/Views/DetailsView.axaml.cs b/MelonLoader.Installer/Views/DetailsView.axaml.cs index cdf89eb..485e59c 100644 --- a/MelonLoader.Installer/Views/DetailsView.axaml.cs +++ b/MelonLoader.Installer/Views/DetailsView.axaml.cs @@ -71,10 +71,7 @@ public void UpdateVersionList() if (NightlyCheck.IsChecked != true) en = en.Where(x => !x.Version.IsPrerelease || x.IsLocalPath); - if (!en.Any()) - { - Model.Offline = true; - } + Model.Offline = !en.Any(); VersionCombobox.ItemsSource = en; VersionCombobox.SelectedIndex = 0; diff --git a/MelonLoader.Installer/Views/GameControl.axaml b/MelonLoader.Installer/Views/GameControl.axaml index 6736258..2b5455c 100644 --- a/MelonLoader.Installer/Views/GameControl.axaml +++ b/MelonLoader.Installer/Views/GameControl.axaml @@ -7,12 +7,12 @@ x:Class="MelonLoader.Installer.Views.GameControl" x:DataType="vm:GameModel" Margin="3"> - + + @@ -50,7 +53,6 @@ Please add one manually using the button below. -